Skip to content

Utils

encode(data)

Encode strings or integers into a raw byte format safely.

Parameters:

Name Type Description Default
data str | int | bytes

The payload to encode.

required

Returns:

Name Type Description
bytes bytes

The sanitized byte array.

Source code in src/pwninit/helpers/utils.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def encode(data: str | int | bytes) -> bytes:
    """Encode strings or integers into a raw byte format safely.

    Args:
        data: The payload to encode.

    Returns:
        bytes: The sanitized byte array.
    """
    if isinstance(data, int):
        data = str(data).encode()
    elif isinstance(data, str):
        data = data.encode()
    return data

getb(d, a, b)

Extract a substring between two delimiters.

Parameters:

Name Type Description Default
d bytes | str

The data to search in.

required
a bytes | str

The start delimiter.

required
b bytes | str

The end delimiter.

required

Returns:

Type Description
bytes | str

bytes | str: The isolated substring.

Example:

>>> getb(b"Here is the [secret] data", b"[", b"]")
b'secret'
Source code in src/pwninit/helpers/utils.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def getb(d: bytes | str, a: bytes | str, b: bytes | str) -> bytes | str:
    """Extract a substring between two delimiters.

    Args:
        d (bytes | str): The data to search in.
        a (bytes | str): The start delimiter.
        b (bytes | str): The end delimiter.

    Returns:
        bytes | str: The isolated substring.

    Example:

        >>> getb(b"Here is the [secret] data", b"[", b"]")
        b'secret'
    """
    a_ = d.find(a)
    if a_ == -1 or len(a) == 0:
        a_ = 0
    b_ = d.find(b, a_ + len(a))
    if b_ == -1 or len(b) == 0:
        b_ = len(d)
    return d[a_ + len(a) : b_]

getr(d, p)

Extract the first match of a regex pattern from the data.

Parameters:

Name Type Description Default
d bytes | str

The data to search in.

required
p str

The regex pattern.

required

Returns:

Type Description
bytes | str

bytes | str: The first match of the pattern.

Example:

>>> getr("Leak: 0x7ffff7e45000", r"0x[0-9a-f]+")
'0x7ffff7e45000'
Source code in src/pwninit/helpers/utils.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def getr(d: bytes | str, p: str) -> bytes | str:
    """Extract the first match of a regex pattern from the data.

    Args:
        d (bytes | str): The data to search in.
        p (str): The regex pattern.

    Returns:
        bytes | str: The first match of the pattern.

    Example:

        >>> getr("Leak: 0x7ffff7e45000", r"0x[0-9a-f]+")
        '0x7ffff7e45000'
    """
    return re.findall(p, d)[0]

hexdump(data, s=context.word_size // 8)

Print an aligned hexdump of the specified data payload.

Parameters:

Name Type Description Default
data bytes

The data to dump.

required
s int

The size of each chunk in bytes (default: machine word size).

word_size // 8
Source code in src/pwninit/helpers/utils.py
220
221
222
223
224
225
226
227
228
229
def hexdump(data: bytes, s: int = context.word_size // 8):
    """Print an aligned hexdump of the specified data payload.

    Args:
        data (bytes): The data to dump.
        s (int): The size of each chunk in bytes (default: machine word size).
    """
    idx_max = math.ceil(math.log(len(data), 16))
    for i in range(0, len(data), s):
        log.info(f"%0{idx_max}x: %#0{2 * s + 2}x" % (i, u64(data[i : i + s])))

jitspray(code, size=8, jmp=b'\xeb\x03')

Perform a jitspray with movabs on x64 by default.

Parameters:

Name Type Description Default
code str

Multi-line assembly code string.

required
size int

Maximum code part size (default 8 for movabs).

8
jmp bytes

Stub for jumping between code parts.

b'\xeb\x03'

Returns:

Type Description
list[int]

list[int]: Array of packed integer values representing the spray.

Source code in src/pwninit/helpers/utils.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
def jitspray(code: str, size: int = 8, jmp: bytes = b"\xeb\x03") -> list[int]:
    """Perform a jitspray with `movabs` on x64 by default.

    Args:
        code (str): Multi-line assembly code string.
        size (int): Maximum code part size (default 8 for movabs).
        jmp (bytes): Stub for jumping between code parts.

    Returns:
        list[int]: Array of packed integer values representing the spray.
    """
    code_parts = [asm(c) for c in code.splitlines()]
    size -= len(jmp)
    parts = [b""]
    for c in code_parts:
        if len(c) > size:
            log.error(f"jitspray(): code part {c.hex()} too long for maximum size {size}")

        p = parts[-1]
        if len(p) + len(c) > size:
            parts[-1] = p.ljust(size, b"\x90") + jmp
            parts.append(c)
        else:
            parts[-1] += c

    return [u64(p) for p in parts]

printx(**kwargs)

Print hex formatted values for debugging specified keyword arguments.

Parameters:

Name Type Description Default
**kwargs Any

Key-value pairs to print.

{}

Example:

>>> printx(libc_base=0x7ffff7e00000, heap=0x555555559000)
[+] libc_base: 0x7ffff7e00000
[+] heap: 0x555555559000
Source code in src/pwninit/helpers/utils.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def printx(**kwargs: Any):
    """Print hex formatted values for debugging specified keyword arguments.

    Args:
        **kwargs: Key-value pairs to print.

    Example:

        >>> printx(libc_base=0x7ffff7e00000, heap=0x555555559000)
        [+] libc_base: 0x7ffff7e00000
        [+] heap: 0x555555559000
    """
    for k, v in kwargs.items():
        log.success("%s: %#x" % (k, v))

Compute the thread-local pointer guard cookie given a known pointer pair.

Parameters:

Name Type Description Default
mangled int

The protected value found in memory.

required
demangled int

The expected/known original value.

required

Returns:

Name Type Description
int int

The underlying cookie guard value.

Source code in src/pwninit/helpers/utils.py
276
277
278
279
280
281
282
283
284
285
286
def ptr_cookie(mangled: int, demangled: int) -> int:
    """Compute the thread-local pointer guard cookie given a known pointer pair.

    Args:
        mangled (int): The protected value found in memory.
        demangled (int): The expected/known original value.

    Returns:
        int: The underlying cookie guard value.
    """
    return ptr_demangle(mangled, demangled)

ptr_demangle(addr, cookie=0)

Demangle a pointer previously protected by a TLS pointer guard.

Parameters:

Name Type Description Default
addr int

The mangled memory address.

required
cookie int

The thread-local pointer guard cookie.

0

Returns:

Name Type Description
int int

The resolved raw memory address.

Source code in src/pwninit/helpers/utils.py
263
264
265
266
267
268
269
270
271
272
273
def ptr_demangle(addr: int, cookie: int = 0) -> int:
    """Demangle a pointer previously protected by a TLS pointer guard.

    Args:
        addr (int): The mangled memory address.
        cookie (int): The thread-local pointer guard cookie.

    Returns:
        int: The resolved raw memory address.
    """
    return ror(addr, 17) ^ cookie

ptr_mangle(addr, cookie=0)

Mangle a pointer (e.g., setjmp/longjmp or exit handlers) with a TLS cookie.

Parameters:

Name Type Description Default
addr int

The raw memory address.

required
cookie int

The thread-local pointer guard cookie.

0

Returns:

Name Type Description
int int

The heavily mangled value.

Source code in src/pwninit/helpers/utils.py
250
251
252
253
254
255
256
257
258
259
260
def ptr_mangle(addr: int, cookie: int = 0) -> int:
    """Mangle a pointer (e.g., setjmp/longjmp or exit handlers) with a TLS cookie.

    Args:
        addr (int): The raw memory address.
        cookie (int): The thread-local pointer guard cookie.

    Returns:
        int: The heavily mangled value.
    """
    return rol(addr ^ cookie, 17)

Compute the glibc >= 2.32 safelink value for a fastbin/tcache pointer.

Parameters:

Name Type Description Default
addr int

The storage address of the pointer.

required
ptr int

The actual target pointer value.

required

Returns:

Name Type Description
int int

The masked value to write to memory.

Example:

>>> safelink(0x555555559010, 0x555555559050)
0x55555000c040
Source code in src/pwninit/helpers/utils.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def safelink(addr: int, ptr: int) -> int:
    """Compute the glibc >= 2.32 safelink value for a fastbin/tcache pointer.

    Args:
        addr (int): The storage address of the pointer.
        ptr (int): The actual target pointer value.

    Returns:
        int: The masked value to write to memory.

    Example:

        >>> safelink(0x555555559010, 0x555555559050)
        0x55555000c040
    """
    return (addr >> 12) ^ ptr

Recover a safelinked next pointer assuming both next & addr are in the same page.

Parameters:

Name Type Description Default
ptr int

The mangled/safelinked next value.

required

Returns:

Name Type Description
int int

The brute-forced original pointer.

Source code in src/pwninit/helpers/utils.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def safelink_bf64(ptr: int) -> int:
    """Recover a safelinked next pointer assuming both next & addr are in the same page.

    Args:
        ptr (int): The mangled/safelinked next value.

    Returns:
        int: The brute-forced original pointer.
    """
    fd = 0
    for i in range(36, -1, -12):
        tmp = fd
        fd <<= 12
        fd |= (tmp ^ (ptr >> i)) & 0xFFF
    if fd & 0xF != 0:
        log.warn("safelink_bf64(): page differs")
    return fd

solve_hashcash(data)

Solve hashcash proof of work.

Source code in src/pwninit/helpers/utils.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def solve_hashcash(data: bytes) -> bytes | None:
    """Solve hashcash proof of work."""
    binary = _get_binary("hashcash")
    if binary is None:
        return None

    cmd = re.findall(rb"hashcash (-m.*?b\d+) ([0-9A-Za-z+/]+)", data)
    if len(cmd) == 0:
        log.error(f"Proof of work failed (hashcash): {data}")

    cmd = cmd[0]
    p = run([binary, cmd[0], cmd[1]], stdout=PIPE, stderr=DEVNULL)
    if p.returncode != 0:
        log.error(f"Proof of work failed (hashcash): {data}")

    return p.stdout

solve_hxp(data)

Solve hxp proof of work.

Source code in src/pwninit/helpers/utils.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def solve_hxp(data: bytes) -> bytes | None:
    """Solve hxp proof of work."""
    binary = _get_binary("hxp")
    if binary is None:
        return None

    prefix = re.findall(rb"sha256\(unhex\(\"([0-9A-fa-f]+)\"", data)
    difficulty = re.findall(rb"ends with (\d+) zero", data)
    if len(prefix) == 0 or len(difficulty) == 0:
        log.error(f"Proof of work failed (hxp): {data}")

    prefix = prefix[0]
    difficulty = difficulty[0]
    p = run([binary, difficulty, prefix], stdout=PIPE, stderr=DEVNULL)
    if p.returncode != 0:
        log.error(f"Proof of work failed (hxp): {data}")

    return p.stdout

solve_kctf(data)

Solve kctf proof of work.

Source code in src/pwninit/helpers/utils.py
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def solve_kctf(data: bytes) -> bytes | None:
    """Solve kctf proof of work."""
    binary = _get_binary("kctf")  
    if binary is None:
        return None

    arg = re.findall(rb"\) solve (.+)\n", data)
    if len(arg) == 0:
        log.error(f"Proof of work failed (kctf): {data}")

    arg = arg[0]
    p = run([binary, arg], stdout=PIPE, stderr=DEVNULL)
    if p.returncode != 0:
        log.error(f"Proof of work failed (kctf): {data}")

    return p.stdout

solve_pow(data)

Take a buffer, detect the proof of work type, and solve it.

Parameters:

Name Type Description Default
data bytes

The buffer containing the PoW challenge string.

required
Source code in src/pwninit/helpers/utils.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def solve_pow(data: bytes) -> bytes | None:
    """Take a buffer, detect the proof of work type, and solve it.

    Args:
        data (bytes): The buffer containing the PoW challenge string.
    """
    functions = {
        b"Please provide an ASCII printable": solve_sossette,
        b"give S such that sha256": solve_hxp,
        b"https://pwn.red/pow": solve_redpwn,
        b"please solve a pow first": solve_kctf,
        b"hashcash": solve_hashcash,
    }

    for s, f in functions.items():
        if s in data:
            return f(data).strip()

    log.warn(f"Unknown proof of work: {data}")
    return None

solve_redpwn(data)

Solve redpwn proof of work.

Source code in src/pwninit/helpers/utils.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def solve_redpwn(data: bytes) -> bytes | None:
    """Solve redpwn proof of work."""
    binary = _get_binary("redpwn")
    if binary is None:
        return None

    arg = re.findall(rb"\| sh -s (.+)\n", data)
    if len(arg) == 0:
        log.error(f"Proof of work failed (redpwn): {data}")

    arg = arg[0]
    p = run([binary, "solve", arg], stdout=PIPE, stderr=DEVNULL)
    if p.returncode != 0:
        log.error(f"Proof of work failed (redpwn): {data}")

    return p.stdout

solve_sossette(data)

Solve sossette proof of work.

Source code in src/pwninit/helpers/utils.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def solve_sossette(data: bytes) -> bytes | None:
    """Solve sossette proof of work."""
    binary = _get_binary("sossette")
    if binary is None:
        return None

    prefix = re.findall(rb"SHA256\(([0-9A-Za-z]+) ", data)
    difficulty = re.findall(rb"starts with (\d+) bits", data)
    if len(prefix) == 0 or len(difficulty) == 0:
        log.error(f"Proof of work failed (sossette): {data}")

    prefix = prefix[0]
    difficulty = difficulty[0]
    p = run([binary, prefix, difficulty], stdout=PIPE, stderr=DEVNULL)
    if p.returncode != 0:
        log.error(f"Proof of work failed (sossette): {data}")

    return p.stdout

HouseOfMuney

Exploitation helper for the House of Muney technique.

This class facilitates dynamic symbol table (.dynsym) rewriting to hijack function resolution in dynamically linked ELF binaries.

Source code in src/pwninit/helpers/muney.py
  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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class HouseOfMuney:
    """Exploitation helper for the House of Muney technique.

    This class facilitates dynamic symbol table (`.dynsym`) rewriting 
    to hijack function resolution in dynamically linked ELF binaries.
    """

    def __init__(self, elf: ELF):
        """Initialize the HouseOfMuney context.

        Args:
            elf (ELF): The target ELF binary.
        """
        self.elf = elf
        self.payload = self.elf.get_segment_for_address(0).data()
        self.dynsym = self.elf.get_section_by_name(".dynsym")
        self.gnuhash = self.elf.get_section_by_name(".gnu.hash")

    def strtab(self) -> int:
        """Get the offset to the string table."""
        return self.dynsym.stringtable.header["sh_offset"]

    def symbol_offset(self, name: str | bytes) -> int:
        """Locate the exact binary offset of a specific symbol inside `.dynsym`.

        Args:
            name (str | bytes): The name of the symbol to find.

        Returns:
            int: The absolute offset, or -1 if not found.
        """
        sym = self.get_sym(name)
        if sym is None:
            return -1

        p = flat(self.dynsym.structs.Elf_Sym.build(sym))
        offset = self.dynsym.data().find(p)
        if offset == -1:
            return -1
        return self.dynsym.header["sh_offset"] + offset

    def get_sym(self, name: str | bytes):
        """Retrieve the raw symbol entry via the GNU hash table.

        Args:
            name (str | bytes): The target symbol name.
        """
        return self.gnuhash.get_symbol(name).entry

    def set_sym(self, name: str, sym: str):
        """Overwrite an existing symbol entry in the ELF data payload.

        Args:
            name (str | bytes): The symbol name.
            sym: The struct built entry to write back.
        """
        off = self.symbol_offset(name)
        self.write(off, self.dynsym.structs.Elf_Sym.build(sym))

    def set_call(self, name: str, offset: int, data: bytes = b"sh\0"):
        """Hijack a symbol to act as a GNU indirect function (IFUNC) call.

        Args:
            name (str | bytes): The symbol to hijack.
            offset (int): The target execution offset.
            data (bytes): Data to inject into the string table (default: `b"sh\\0"`).
        """
        sym = self.get_sym(name)
        idx = u32(data)
        sym["st_name"] = idx
        sym["st_value"] = offset
        # Note: STT_GNU_IFUNC must be available in your local scope/constants
        sym["st_info"]["type"] = STT_GNU_IFUNC 
        self.set_sym(name, sym)
        self.write(self.strtab() + idx, name)
        self.write(self.strtab() + idx + len(name), b"\0")

    def set_offset(self, name: str, offset: int):
        """Directly patch the virtual value offset of a specific symbol.

        Args:
            name (str | bytes): The symbol to patch.
            offset (int): The new offset value.
        """
        sym = self.get_sym(name)
        sym["st_value"] = offset
        self.set_sym(name, sym)

    def write(self, addr: int, data: bytes | str):
        """Patch the internal ELF payload memory representation.

        Args:
            addr (int): The starting address offset.
            data (bytes | str): The raw bytes to write.
        """
        if isinstance(data, str):
            data = data.encode()
        elif not isinstance(data, bytes):
            raise ValueError("Data must be bytes or str")

        if addr < 0 or addr + len(data) > len(self):
            raise IndexError("Write out of bounds")

        self.payload = self.payload[:addr] + data + self.payload[addr + len(data) :]

    def read(self, addr: int, size: int) -> bytes:
        """Read a chunk from the internal ELF payload memory representation.

        Args:
            addr (int): The starting address offset.
            size (int): Amount of bytes to read.
        """
        return self.payload[addr : addr + size]

    def __len__(self):
        return len(bytes(self))

    def __bytes__(self):
        return self.payload

__init__(elf)

Initialize the HouseOfMuney context.

Parameters:

Name Type Description Default
elf ELF

The target ELF binary.

required
Source code in src/pwninit/helpers/muney.py
12
13
14
15
16
17
18
19
20
21
def __init__(self, elf: ELF):
    """Initialize the HouseOfMuney context.

    Args:
        elf (ELF): The target ELF binary.
    """
    self.elf = elf
    self.payload = self.elf.get_segment_for_address(0).data()
    self.dynsym = self.elf.get_section_by_name(".dynsym")
    self.gnuhash = self.elf.get_section_by_name(".gnu.hash")

get_sym(name)

Retrieve the raw symbol entry via the GNU hash table.

Parameters:

Name Type Description Default
name str | bytes

The target symbol name.

required
Source code in src/pwninit/helpers/muney.py
46
47
48
49
50
51
52
def get_sym(self, name: str | bytes):
    """Retrieve the raw symbol entry via the GNU hash table.

    Args:
        name (str | bytes): The target symbol name.
    """
    return self.gnuhash.get_symbol(name).entry

read(addr, size)

Read a chunk from the internal ELF payload memory representation.

Parameters:

Name Type Description Default
addr int

The starting address offset.

required
size int

Amount of bytes to read.

required
Source code in src/pwninit/helpers/muney.py
110
111
112
113
114
115
116
117
def read(self, addr: int, size: int) -> bytes:
    """Read a chunk from the internal ELF payload memory representation.

    Args:
        addr (int): The starting address offset.
        size (int): Amount of bytes to read.
    """
    return self.payload[addr : addr + size]

set_call(name, offset, data=b'sh\x00')

Hijack a symbol to act as a GNU indirect function (IFUNC) call.

Parameters:

Name Type Description Default
name str | bytes

The symbol to hijack.

required
offset int

The target execution offset.

required
data bytes

Data to inject into the string table (default: b"sh\0").

b'sh\x00'
Source code in src/pwninit/helpers/muney.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def set_call(self, name: str, offset: int, data: bytes = b"sh\0"):
    """Hijack a symbol to act as a GNU indirect function (IFUNC) call.

    Args:
        name (str | bytes): The symbol to hijack.
        offset (int): The target execution offset.
        data (bytes): Data to inject into the string table (default: `b"sh\\0"`).
    """
    sym = self.get_sym(name)
    idx = u32(data)
    sym["st_name"] = idx
    sym["st_value"] = offset
    # Note: STT_GNU_IFUNC must be available in your local scope/constants
    sym["st_info"]["type"] = STT_GNU_IFUNC 
    self.set_sym(name, sym)
    self.write(self.strtab() + idx, name)
    self.write(self.strtab() + idx + len(name), b"\0")

set_offset(name, offset)

Directly patch the virtual value offset of a specific symbol.

Parameters:

Name Type Description Default
name str | bytes

The symbol to patch.

required
offset int

The new offset value.

required
Source code in src/pwninit/helpers/muney.py
82
83
84
85
86
87
88
89
90
91
def set_offset(self, name: str, offset: int):
    """Directly patch the virtual value offset of a specific symbol.

    Args:
        name (str | bytes): The symbol to patch.
        offset (int): The new offset value.
    """
    sym = self.get_sym(name)
    sym["st_value"] = offset
    self.set_sym(name, sym)

set_sym(name, sym)

Overwrite an existing symbol entry in the ELF data payload.

Parameters:

Name Type Description Default
name str | bytes

The symbol name.

required
sym str

The struct built entry to write back.

required
Source code in src/pwninit/helpers/muney.py
54
55
56
57
58
59
60
61
62
def set_sym(self, name: str, sym: str):
    """Overwrite an existing symbol entry in the ELF data payload.

    Args:
        name (str | bytes): The symbol name.
        sym: The struct built entry to write back.
    """
    off = self.symbol_offset(name)
    self.write(off, self.dynsym.structs.Elf_Sym.build(sym))

strtab()

Get the offset to the string table.

Source code in src/pwninit/helpers/muney.py
23
24
25
def strtab(self) -> int:
    """Get the offset to the string table."""
    return self.dynsym.stringtable.header["sh_offset"]

symbol_offset(name)

Locate the exact binary offset of a specific symbol inside .dynsym.

Parameters:

Name Type Description Default
name str | bytes

The name of the symbol to find.

required

Returns:

Name Type Description
int int

The absolute offset, or -1 if not found.

Source code in src/pwninit/helpers/muney.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def symbol_offset(self, name: str | bytes) -> int:
    """Locate the exact binary offset of a specific symbol inside `.dynsym`.

    Args:
        name (str | bytes): The name of the symbol to find.

    Returns:
        int: The absolute offset, or -1 if not found.
    """
    sym = self.get_sym(name)
    if sym is None:
        return -1

    p = flat(self.dynsym.structs.Elf_Sym.build(sym))
    offset = self.dynsym.data().find(p)
    if offset == -1:
        return -1
    return self.dynsym.header["sh_offset"] + offset

write(addr, data)

Patch the internal ELF payload memory representation.

Parameters:

Name Type Description Default
addr int

The starting address offset.

required
data bytes | str

The raw bytes to write.

required
Source code in src/pwninit/helpers/muney.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def write(self, addr: int, data: bytes | str):
    """Patch the internal ELF payload memory representation.

    Args:
        addr (int): The starting address offset.
        data (bytes | str): The raw bytes to write.
    """
    if isinstance(data, str):
        data = data.encode()
    elif not isinstance(data, bytes):
        raise ValueError("Data must be bytes or str")

    if addr < 0 or addr + len(data) > len(self):
        raise IndexError("Write out of bounds")

    self.payload = self.payload[:addr] + data + self.payload[addr + len(data) :]