一、前言

在基本了解了模糊測試以后,接下來就開始看看一直心心念念的符號執行吧。聽群友說這個東西的概念在九幾年就有了,算是個老東西,不過 Angr 本身倒是挺新的,看看這個工具能不能有什么收獲吧。

按照計劃,一方面是 Angr 的使用技巧,另一方面是 Angr 的源代碼閱讀。不過因為兩者的內容都挺多的,所以本篇只寫使用技巧部分,如果未來有這樣的預訂,或許還會有另外一篇。

二、Angr 的基本描述

首先在開始解釋 Angr 的各個模塊和使用之前,我們需要先對它是如何工作的有一個大概的認識。

我們一般用 Angr 的目的其實就是為了自動化的求解輸入,比如說逆向或是 PWN。而它的原理被稱之為“符號執行”。

Angr 其實并不是真正被運行起來的,它就向一個虛擬機,會讀取每一條命令并在虛擬機中模擬該命令的行為。我們類比到更加常用的 z3 庫中,每個寄存器都可以相當與 z3 中的一個變量,在模擬執行的過程中,這個變量會被延伸為一個表達式,而當我們成功找到了目標地址之后,通過表達式就可以求解對應的初值應該是什么了。

看著簡單,但是您或許聽說過,這類符號執行有一個現今仍為解決的麻煩問題:路徑爆炸。

Angr 被稱之為 IR-Based 類的符號執行引擎,他會對輸入的二進制重建對應的 CFG ,在完成重建后開始模擬執行。而對于分支語句,就需要分支出兩個不同的情況:跳轉 和 不跳轉 。在一般情況下,這不會引發問題,但是我們可以考慮如下的代碼:

num=xxxfor(int i=0;i<1000;i++)//<---- judge 1{    if(num==0x6666){//<----- judge 2        break;    }    else{    num+=1;    }}

當符號執行引起遇到循環語句,由于循環語句本身就需要判斷是否應該跳出循環,因此引擎會在這里開始分叉為兩個情況。

而如果這個循環里又嵌套了判斷條件,那么就需要再次分叉為兩條路徑。

也就是說,對于一個人為理解起來相當易懂的循環判斷,符號執行引擎卻會因此分叉出指數級別增長的分支數量。

但這還不是最簡單的情況,我們可以更極端一點考慮這么一個情況:

while(1){    if(condition)    {        break;    }}

循環本身是一個死循環,盡管我們靠自己的思維能夠理解,它會在未來的某一個跳出循環,但符號執行引擎卻不知道這件事,因此每一次遇到判斷跳轉都需要進行分叉,最后這個路徑就會無限增長,最后把內存擠爆,然后程序崩潰。

說了這么多,其實是為了將清楚一件事,“符號執行引擎是通過按行讀取的方式模擬執行每條機器碼,并更新對應變量,最后在通過約束求解的方式去逆推輸入初值的”。

三、Angr 基本模塊

一般來說,使用 Angr 的基本流程如下:

import angrproject = angr.Project(path_to_binary, auto_load_libs=False)state = project.factory.entry_state()sim = project.factory.simgr(state)sim.explore(find=target)if simulation.found:    res = simulation.found[0]    res = res.posix.dumps(0)    print("[+] Success! Solution is: {}".format(res.decode("utf-8")))

筆者一直以來都是套這個模板對二進制程序一把梭,但既然現在要開始正經思考一下怎么辦,總要對里面的各種模塊有所了解了。

Project 模塊

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

對于一個使用 angr.Project 加載的二進制程序,angr 會讀取它的一些基本屬性:

>>> project=angr.Project("02_angr_find_condition",auto_load_libs=False) >>> project.filename '02_angr_find_condition'>>> project.arch >>> hex(project.entry) '0x8048450'

這些信息會由 angr 自動分析,但是如果你有需要,可以通過 angr.Project 中的其他參數手動進行設定。

Loader 模塊

而對于一個 Project 對象,它擁有一個自己的 Loader ,提供如下信息:

>>> project.loader <Loaded 02_angr_find_condition, maps [0x8048000:0x8407fff]>>>> project.loader.main_object <ELF Object 02_angr_find_condition, maps [0x8048000:0x804f03f]>>>> project.loader.all_objects [>, <ExternObject Object cle##externs, maps [0x8100000:0x8100018]>, <ExternObject Object cle##externs, maps [0x8200000:0x8207fff]>, <ELFTLSObjectV2 Object cle##tls, maps [0x8300000:0x8314807]>, <KernelObject Object cle##kernel, maps [0x8400000:0x8407fff]>]

當然實際的屬性不止這些,而且在常規的使用中似乎也用不到這些信息,不過這里為了完整性就一起記錄一下吧。

Loader 模塊主要是負責記錄二進制程序的一些基本信息,包括段、符號、鏈接等。

>>> obj=project.loader.main_object>>> obj.plt {'strcmp': 134513616, 'printf': 134513632, '__stack_chk_fail': 134513648, 'puts': 134513664, 'exit': 134513680, '__libc_start_main': 134513696, '__isoc99_scanf': 134513 712, '__gmon_start__': 134513728}>>> obj.sections <Unnamed | offset 0x0, vaddr 0x0, size 0x0>, <.interp | offset 0x154, vaddr 0x8048154, size 0x13>, <.note.ABI-tag | offset 0x168, vaddr 0x8048168, size 0x20> , <.note.gnu.build-id | offset 0x188, vaddr 0x8048188, size 0x24>, <.gnu.hash | offset 0x1ac, vaddr 0x80481ac, size 0x20>, <.dynsym | offset 0x1cc, vaddr 0x80481cc, siz e 0xa0>, <.dynstr | offset 0x26c, vaddr 0x804826c, size 0x91>, <.gnu.version | offset 0x2fe, vaddr 0x80482fe, size 0x14>, <.gnu.version_r | offset 0x314, vaddr 0x804831 4, size 0x40>, <.rel.dyn | offset 0x354, vaddr 0x8048354, size 0x8>, <.rel.plt | offset 0x35c, vaddr 0x804835c, size 0x38>, <.init | offset 0x394, vaddr 0x8048394, size 0x23>, <.plt | offset 0x3c0, vaddr 0x80483c0, size 0x80>, <.plt.got | offset 0x440, vaddr 0x8048440, size 0x8>, <.text | offset 0x450, vaddr 0x8048450, size 0x4ea2>, < .fini | offset 0x52f4, vaddr 0x804d2f4, size 0x14>, <.rodata | offset 0x5308, vaddr 0x804d308, size 0x39>, <.eh_frame_hdr | offset 0x5344, vaddr 0x804d344, size 0x3c>, <.eh_frame | offset 0x5380, vaddr 0x804d380, size 0x110>, <.init_array | offset 0x5f08, vaddr 0x804ef08, size 0x4>, <.fini_array | offset 0x5f0c, vaddr 0x804ef0c, size 0x4>, <.jcr | offset 0x5f10, vaddr 0x804ef10, size 0x4>, <.dynamic | offset 0x5f14, vaddr 0x804ef14, size 0xe8>, <.got | offset 0x5ffc, vaddr 0x804effc, size 0x4>, <.go t.plt | offset 0x6000, vaddr 0x804f000, size 0x28>, <.data | offset 0x6028, vaddr 0x804f028, size 0x15>, <.bss | offset 0x603d, vaddr 0x804f03d, size 0x3>, <.comment | offset 0x603d, vaddr 0x0, size 0x34>, <.shstrtab | offset 0x67fa, vaddr 0x0, size 0x10a>, <.symtab | offset 0x6074, vaddr 0x0, size 0x4d0>, <.strtab | offset 0x6544, va ddr 0x0, size 0x2b6>]>

對外部庫的鏈接也同樣支持查找:

>>> project.loader.find_symbol('strcmp')     "strcmp" in cle##externs at 0x8100000>>>> project.loader.find_symbol('strcmp').rebased_addr 135266304 >>> project.loader.find_symbol('strcmp').linked_addr 0 >>> project.loader.find_symbol('strcmp').relative_addr 0

同時也支持一些加載選項:

  • auto_load_libs:是否自動加載程序的依賴
  • skip_libs:避免加載的庫
  • except_missing_libs:無法解析共享庫時是否拋出異常
  • force_load_libs:強制加載的庫
  • ld_path:共享庫的優先搜索搜尋路徑

我們知道,在一般情況下,加載程序都會將 auto_load_libs 置為 False ,這是因為如果將外部庫一并加載,那么 Angr 就也會跟著一起去分析那些庫了,這對性能的消耗是比較大的。

而對于一些比較常規的函數,比如說 malloc 、printf、strcpy 等,Angr 內置了一些替代函數去 hook 這些系統庫函數,因此即便不去加載 libc.so.6 ,也能保證分析的正確性。這部分內容接下來會另說。

factory 模塊

該模塊主要負責將 Project 實例化。

我們知道,加載一個二進制程序只是符號執行能夠開始的第一步,為了實現符號執行,我們還需要為這個二進制程序去構建符號、執行流等操作。這些操作會由 Angr 幫我們完成,而它也提供一些方法能夠讓我們獲取到它構造的一些細節。

Block 模塊

Angr 對程序進行抽象的一個關鍵步驟就是從二進制機器碼去重構 CFG ,而 Block 模塊提供了和它抽象出的基本塊間的交互接口:

>>> project.factory.block(project.entry)  for 0x8048450, 33 bytes> >>> project.factory.block(project.entry).pp()         _start: 8048450  xor     ebp, ebp 8048452  pop     esi 8048453  mov     ecx, esp 8048455  and     esp, 0xfffffff0 8048458  push    eax 8048459  push    esp 804845a  push    edx 804845b  push    __libc_csu_fini 8048460  push    __libc_csu_init 8048465  push    ecx 8048466  push    esi 8048467  push    main 804846c  call    __libc_start_main>>> project.factory.block(project.entry).instruction_addrs (134513744, 134513746, 134513747, 134513749, 134513752, 134513753, 134513754, 134513755, 134513760, 134513765, 134513766, 134513767, 134513772)

可以看出 Angr 用 call 指令作為一個基本塊的結尾。在 Angr 中,它所識別的基本塊和 IDA 里看見的 CFG 有些許不同,它會把所有的跳轉都盡可能的當作一個基本塊的結尾。

當然也有無法識別的情況,比如說使用寄存器進行跳轉,而寄存器的值是上下文有關的,它有可能是函數開始時傳入的一個回調函數,而參數有可能有很多種,因此并不是總能夠識別出結果的。
>>> block. block.BLOCK_MAX_SIZE           block.capstone                 block.instructions             block.reset_initial_regs()     block.size block.addr                     block.codenode                 block.parse(                   block.serialize()              block.thumb block.arch                     block.disassembly              block.parse_from_cmessage(     block.serialize_to_cmessage()  block.vex block.bytes                    block.instruction_addrs        block.pp(                      block.set_initial_regs()       block.vex_nostmt

State 模塊

>>> state=project.factory.entry_state()0x8048450>>>> state.regs.eip 0x8048450>>>> state.mem[project.entry].int.resolved 0x895eed31>>>> state.mem[0x1000].long = 4>>> state.mem[0x1000].long.resolved 0x4>

這個 state 包括了符號實行中所需要的所有符號。

通過 state.regs.eip 可以看出,所有的寄存器都會替換為一個符號。該符號可以由模塊自行推算,也可以人為的進行更改。也正因如此,Angr 能夠通過條件約束對符號的值進行解方程,從而去計算輸入,比如說:

>>> bv = state.solver.BVV(0x2333, 32)        0x2333>>>> state.solver.eval(bv) 9011

另外還存在一些值,它只有在運行時才能夠得知,對于這些值,Angr 會將它標記為 UNINITIALIZED :

>>> state.regs.edi WARNING  | 2023-04-12 17:28:41,490 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing register with an unspecified value. This could indicate unwanted behavior.WARNING  | 2023-04-12 17:28:41,491 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and con tinuing. You can resolve this by:WARNING  | 2023-04-12 17:28:41,491 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state WARNING  | 2023-04-12 17:28:41,492 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make un known regions hold nullWARNING  | 2023-04-12 17:28:41,492 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppr ess these messages.WARNING  | 2023-04-12 17:28:41,492 | angr.storage.memory_mixins.default_filler_mixin | Filling register edi with 4 unconstrained bytes referenced from 0x8048450 (_start +0x0 in 02_angr_find_condition (0x8048450))                                             

另外值得一提的是,除了 entry_state 外還有其他狀態可用于初始化:

blank_state:構造一個“空白板”空白狀態,其中大部分數據未初始化。當訪問未初始化的數據時,將返回一個不受約束的符號值。

entry_state:造一個準備在主二進制文件的入口點執行的狀態。

full_init_state:構造一個準備好通過任何需要在主二進制文件入口點之前運行的初始化程序執行的狀態,例如,共享庫構造函數或預初始化程序。完成這些后,它將跳轉到入口點。

call_state:構造一個準備好執行給定函數的狀態。

這些構造函數都能通過參數 addr 來指定初始時的 rip/eip 地址。而 call_state 可以用這種方式來構造傳參:call_state(addr, arg1, arg2, ...)

Simulation Managers 模塊

SM(Simulation Managers)是一個用來管理 State 的模塊,它需要為符號指出如何運行。

>>> simgr = project.factory.simulation_manager(state) 1 active> >>> simgr.active [0x8048450>]

通過 step 可以讓這組模擬執行一個基本塊:

>>> simgr.step() <SimulationManager with 1 active> >>> simgr.active []>>> simgr.active[0].regs.eip <BV32 0x8048420>

此時的 eip 對應了 __libc_start_main 的地址。

同樣也可以查看此時的模擬內存狀態,可以發現它儲存了函數的返回地址:

>>> simgr.active[0].mem[simgr.active[0].regs.esp].int.resolved    <BV32 0x8048471>

而我們比較熟悉的 simgr 其實就是 simulation_manager 簡寫:

>>> project.factory.simgr() <SimulationManager with 1 active> >>> project.factory.simulation_manager()                           <SimulationManager with 1 active>

SimProcedure

在前文中提到過 Angr 會 hook 一些常用的庫函數來提高效率。它支持一下這些外部庫:

>>> angr.procedures. angr.procedures.SIM_LIBRARIES   angr.procedures.glibc           angr.procedures.java_util       angr.procedures.ntdll           angr.procedures.uclibc angr.procedures.SIM_PROCEDURES  angr.procedures.gnulib          angr.procedures.libc            angr.procedures.posix           angr.procedures.win32 angr.procedures.SimProcedures   angr.procedures.java            angr.procedures.libstdcpp       angr.procedures.procedure_dict  angr.procedures.win_user32 angr.procedures.advapi32        angr.procedures.java_io         angr.procedures.linux_kernel    angr.procedures.stubs             angr.procedures.cgc             angr.procedures.java_jni        angr.procedures.linux_loader    angr.procedures.testing           angr.procedures.definitions     angr.procedures.java_lang       angr.procedures.msvcr           angr.procedures.tracer

以 libc 為例就可以看到,它支持了一部分 libc 中的函數:

>>> angr.procedures.libc. angr.procedures.libc.abort      angr.procedures.libc.fprintf    angr.procedures.libc.getuid     angr.procedures.libc.setvbuf    angr.procedures.libc.strstr angr.procedures.libc.access     angr.procedures.libc.fputc      angr.procedures.libc.malloc     angr.procedures.libc.snprintf   angr.procedures.libc.strtol......由于函數過多,這里就不展示了

因此如果程序中調用了這部分函數,默認情況下就會由 angr.procedures.libc 中實現的函數進行接管。但是請務必注意,官方文檔中也有提及,一部分函數的實現并不完善,比如說對 scanf 的格式化字符串支持并不是很好,因此有的時候需要自己編寫函數來 hook 它。

hook 模塊

緊接著上文提到的問題,Angr 接受由用戶自定義函數來進行 hook 的操作。

>>> func=angr.SIM_PROCEDURES['libc']['scanf']>>> project.hook(0x10000, func())>>> project.hooked_by(0x10000)     >>> project.unhook(0x10000)>>> project.hooked_by(0x10000) WARNING  | 2023-04-12 19:20:39,782 | angr.project   | Address 0x10000 is not hooked

第一種方案是直接對地址進行 hook,通過直接使用 project.hook(addr,function()) 的方法直接鉤取。

同時,Angr 對于有符號的二進制程序也運行直接對符號本身進行鉤取:project.hook_symbol(name,function) 。