bytemancy3

3 minute read

⚒️ Bytemancy 3

Category Author
⚒️ Binary Exploitation LT ‘syreal’ Jones

Challenge Prompt

Can you conjure the right bytes? The program’s source code can be downloaded here and the compiled spellbook binary can be downloaded here.

Additional details will be available after launching your challenge instance.

Problem Type

  • Python
  • pwntools

Solve

We are given the program source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import os
import random
import select
import sys
from typing import Optional
from pwn import ELF, p32

BANNER = "⊹──────[ BYTEMANCY-3 ]──────⊹"
BINARY_PATH = os.path.join(os.path.dirname(__file__), "spellbook")
QUESTION_COUNT = 3

SPELLBOOK_FUNCTIONS = [
    "ember_sigil",
    "glyph_conflux",
    "astral_spark",
    "binding_word",
]


def read_exact_bytes(expected_len: int) -> Optional[bytes]:
    """Read a fixed number of bytes from stdin, trimming a trailing newline."""
    stdin_buffer = sys.stdin.buffer
    buf = stdin_buffer.read(expected_len)
    if not buf or len(buf) < expected_len:
        return None

    # Discard trailing newlines only if more bytes are immediately available
    try:
        stdin_fileno = sys.stdin.fileno()
    except (AttributeError, ValueError, OSError):
        stdin_fileno = None

    if stdin_fileno is not None:
        while True:
            readable, _, _ = select.select([stdin_fileno], [], [], 0)
            if not readable:
                break

            peek = stdin_buffer.peek(1)[:1]
            if peek in (b"\n", b"\r"):
                stdin_buffer.read(1)
                continue
            break

    return buf


def main():
    try:
        elf = ELF(BINARY_PATH, checksec=False)
    except FileNotFoundError:
        print("The spellbook is missing!")
        return

    flag = open("./flag.txt", "r").read().strip()

    while True:
        try:
            print(BANNER)
            print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
            print()
            print("I will name four procedures hidden inside spellbook.")
            print(
                f"Each round, send me their *raw* 4-byte addresses "
                f"in little-endian form. {QUESTION_COUNT} correct answers unlock the flag."
            )
            print()
            print("☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐☉⟊☽☈⟁⧋⟡☍⟐")
            print('⊹─────────────⟡─────────────⊹')

            selections = random.sample(SPELLBOOK_FUNCTIONS, QUESTION_COUNT)
            success = True

            for idx, symbol in enumerate(selections, 1):
                target_addr = elf.symbols[symbol]
                expected_bytes = p32(target_addr)

                print(
                    f"[{idx}/{QUESTION_COUNT}] Send the 4-byte little-endian "
                    f"address for procedure '{symbol}'."
                )
                print("==> ", end='', flush=True)
                user_bytes = read_exact_bytes(len(expected_bytes))

                if user_bytes is None:
                    print("\nI needed four bytes, traveler.")
                    success = False
                    break

                if user_bytes != expected_bytes:
                    print("\nThose aren't the right runes.")
                    success = False
                    break

            if success:
                print(flag)
                break

            print()
            print("The aether rejects your incantation. Try again.\n")
        except EOFError:
            break
        except Exception as exc:
            print(exc)
            break


if __name__ == "__main__":
    main()

This time we need to send the program the exact raw 4 byte address in little-endian of 3 random spells from the spellbook app.
To get the locations we need to feed the app, we can use:

1
objdump -t spellbook

Near the bottom of the output we can see the 4 functions we need addresses for: image

To solve this, we will need to use pwntools.
If you don’t have pwntools you will need to install it, it doens’t come with Kali by default. You can install using:

1
2
3
sudo apt update
pipx install pwntools
pipx ensurepath

Connecting to the machine we can see it gives us a random spell from the spellbook, so we can write a pwntools script to look for that and then send the right location: image

For our script we will use the below. This will read in our prompt and look for the matching spellbook item before seding the location:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

HOST = 'green-hill.picoctf.net'
PORT = 49782

p = remote(HOST, PORT)

responses = {
    b"ember_sigil": p32(0x08049176),
    b"glyph_conflux": p32(0x0804919a),
    b"binding_word": p32(0x080491e3),
    b"astral_spark": p32(0x080491c1)
}

for _ in range(3):
    data = p.recvuntil(b'==> ')
    
    for prompt, payload in responses.items():
        if prompt in data:
            p.send(payload)
            break

print(p.recvall().decode(errors="ignore"))

p32 packs a 32-bit integer and since we didn’t specify, it uses little endian. If we needed big endian, we could specify that like this:
image

Make sure you use your host and port from the challenge instance you are given: image

We can run the program and when we do that, we are presented with the flag! image