背景
Qiling Framework是一個基于Python的二進制分析、模擬和虛擬化框架。它可以用于動態分析和仿真運行不同操作系統、處理器和體系結構下的二進制文件。除此之外,Qiling框架還提供了易于使用的API和插件系統,方便使用者進行二進制分析和漏洞挖掘等工作。其創始人是一名IoT Hacker,創建qiling的初衷便是解決在研究IoT時遇到的種種問題,這也是為什么上一小節說qiling框架比unicorn框架更加適合IoT研究初學者。
qiling使用基礎
qiling框架和AFLplusplus安裝
sudo apt-getupdate sudo apt-getinstall -ybuild-essential python3-dev automake cmake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools cargo libgtk-3-dev sudo apt-getinstall -ylld-14llvm-14llvm-14-dev clang-14 sudo apt-getinstall -ygcc-$(gcc --version|head -n1|sed 's/..*//'|sed 's/.* //')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/..*//'|sed 's/.* //')-dev pip3 install qiling git clone https://github.com/AFLplusplus/AFLplusplus make-C AFLplusplus cdAFLplusplus/unicorn_mode ./build_unicorn_support.sh
程序仿真
首先我們需要克隆qiling倉庫,倉庫中一些實例腳本可供我們學習。
git clone --recurse-submodules https://github.com/qilingframework/qiling.git
一個簡單的示例:
#include#include # gcc test.c -o test # 注意:編譯程序的主機libc需要與rootfs glibc版本(libc-2.7.so)相對應,其他架構同理 intmain(){ printf("hello world!"); return0; }
使用qiling編寫一個簡單的仿真腳本。
fromqiling import* fromqiling.const importQL_VERBOSE # 導入qiling模塊和qiling.const模塊中的QL_VERBOSE常量 if__name__ == "__main__": #創建Qiling對象,實例中三個參數分別為:path(仿真程序路徑)、rootfs(仿真程序文件系統目錄)和verbose(輸出信息參數),除此外還可以設置env和log_plain參數。 ql = Qiling(["./x8664_linux_symlink/test"], "./x8664_linux_symlink",verbose=QL_VERBOSE.DEBUG) #運行Qiling對象的run()方法,開始執行仿真程序 ql.run()
這里的verbose(輸出信息參數)有如下級別及其作用:
VFS劫持
x86_fetch_urandom程序的作用為打開/dev/urandom文件,生成隨機數。當qiling仿真x86_fetch_urandom程序時,環境需要用到仿真文件系統,我們就需要用到VFS劫持,這樣就可以模擬修改文件系統。下面的代碼中為仿真虛擬路徑 "/dev/urandom" 會被映射到宿主系統上的現有"/dev/urandom"文件。當模擬程序將訪問 /dev/random 時,將改為訪問映射文件。
fromqiling importQiling if__name__ == "__main__": ql = Qiling(["x86_linux/bin/x86_fetch_urandom"], "x86_linux") ql.add_fs_mapper(r'/dev/urandom', r'/dev/urandom') ql.verbose=0 ql.run()
如果我們想要控制虛擬文件'/dev/urandom'的交互結果,可以繼承QlFsMappedObject類,并可自定義read、write、fstat、ioctl、readline等方法。
fromqiling importQiling fromqiling.os.mapper importQlFsMappedObject classFakeUrandom(QlFsMappedObject): defread(self, size: int)-> bytes: returnb"x01"#可以修改讀取返回結果 deffstat(self)-> int: return-1 defclose(self)-> int: return0 if__name__ == "__main__": ql = Qiling(["x86_linux/bin/x86_fetch_urandom"], "x86_linux") ql.add_fs_mapper(r'/dev/urandom', FakeUrandom()) ql.run()
函數hook
下面示例中,我們給str1和str2倆個變量內存中分別復制"abcdef"和"ABCDEF"字符串。正常執行完畢后會打印出"str1 大于 str2"。我們可以使用qiling框架劫持strcmp實現為hook strcmp函數的效果,使其執行到不同分支的結果。
#include#include //cd ./x8664_linux/ //gcc demo.c -o test intmain() { charstr1[15]; charstr2[15]; intret; strcpy(str1, "abcdef"); strcpy(str2, "ABCDEF"); ret = strcmp(str1, str2); if(ret < 0) ??{ ?????printf("str1 小于 str2"); ??} ??else?if(ret > 0) { printf("str1 大于 str2"); } else { printf("str1 等于 str2"); } return(0); }
以下代碼為hook strcmp函數,并通過修改rax寄存器改變執行流程。
fromqiling import* fromqiling.const import* # 自定義strcmp hook函數。當程序執行strcmp函數退出時,會調用此函數,并且在比較完畢后,將 rax 寄存器的值修改為 0,表示相等。 defhook_strcmp(ql,*args): # qiling框架的寄存器取值為ql.arch.reg.xxx rax = ql.arch.regs.rax print("hook_addr_rax:",hex(rax)) ql.arch.regs.eax = 0# 0:等于; -1:小于 ;1:大于 # 使用 ql.os.set_api 函數為 strcmp 設置hook函數,第一個參數為要hook的函數名,第二個參數為自定義hook函數,第三個參數為hook類型,這里為退出時觸發hook函數。 defhook_func(ql): ql.os.set_api('strcmp',hook_strcmp,QL_INTERCEPT.EXIT) # 也可以使用ql.hook_address()函數進行hook,使用方法為ql.hook_address(hook_strcmp,0xXXXXXXXX) if__name__ == "__main__": ql = Qiling(["./x8664_linux/test"],"./x8664_linux",verbose=QL_VERBOSE.DEBUG) hook_func(ql) #ql.debugger = "gdb12345" ql.run()
定義hook函數時hook類型參數有以下三種:
qiling使用實例
使用qiling解密CTF賽題
當我們掌握了最基礎的三個用法后,我們可以測試一個簡單的例子來加深對qiling框架的理解。以上一小節中unicorn解密ctf題目為例,我們先簡單寫一個運行腳本。這里的ql.debugger="gdb12345"為開啟gdbserver服務,我們可以使用ida或者gdb進行調試。
簡單運行后發現程序和上一小節中unicorn的運行狀況類似。由于這里我設置了multithead為True,所以這里會比上一小節中unicorn的解密速度快不少。但是還是在有限時間內只輸出4個字符。
當我們將verbose設置為QL_VERBOSE.DISASM便可觀察模擬執行的匯編指令,根據匯編指令我們明顯看到程序在call 0x400670處進行了遞歸調用(或使用調試器調試查看),導致解密時間非常長。所以我們需要進行代碼優化,思路為使用棧空間來保存一個不同輸入參數以及對應計算結果的字典來避免重復計算。
這里qiling由于是由unicorn開發而來,所以很多用法和unicorn相似。
fromqiling import* fromqiling.const import* frompwn import* defhook_start(ql): arg0 = ql.arch.regs.rdi r_rsi = ql.arch.regs.rsi arg1 = u32(ql.mem.read(r_rsi,4)) if(arg0,arg1) indirect: (ret_rax,ret_ref) = direct[(arg0,arg1)] ql.arch.regs.rax = ret_rax ql.mem.write(r_rsi,p32(ret_ref)) ql.arch.regs.rip = 0x400582 else: ql.arch.stack_push(r_rsi) ql.arch.stack_push(arg1) ql.arch.stack_push(arg0) defhook_end(ql): arg0 = ql.arch.stack_pop() arg1 = ql.arch.stack_pop() r_rsi = ql.arch.stack_pop() ret_rax = ql.arch.regs.rax ret_ref = u32(ql.mem.read(r_rsi,4)) direct[(arg0,arg1)] = (ret_rax,ret_ref) defsolve(ql): start_address = 0x400670 end_address = 0x4006f1 end_address2 = 0x400709 ql.hook_address(hook_start,start_address) ql.hook_address(hook_end,end_address) ql.hook_address(hook_end,end_address2) if__name__ == '__main__': path = ["./x8664_linux_symlink/test"] rootfs = "./x8664_linux_symlink" direct = {} ql = Qiling(path, rootfs,verbose=QL_VERBOSE.DEFAULT) solve(ql) ql.run()
運行后便會打印出解密結果。
除了上一小節中的ctf題目掌握qiling的使用外,我們還可通過qilinglab來加深對qiling框架的使用。qilingLab是由11個小挑戰組成的二進制程序,用來幫助新手快速熟悉和掌握 Qiling 框架的基本用法。官方提供了aarch64程序的解題方法,我們根據這個作為參考解密一下x86_64架構的練習程序。
x86_64程序下載(https://www.shielder.com/attachments/qilinglab-x86_64)
首先運行程序,給我們提示,challenges會造成程序崩潰,只有當我們解出相應challenge后才會顯示信息。
我們可以通過ida逆向以及編寫qiling腳本進行動態調試來完成這些challenge。
最終的解密腳本如下:
fromqiling import* frompwn import* fromqiling.const import* fromqiling.os.mapper importQlFsMappedObject importos importstruct defhook_cpuid(ql, address, size): ifql.mem.read(address, size) == b'x0FxA2': regs = ql.arch.regs regs.ebx = 0x696C6951 regs.ecx = 0x614C676E regs.edx = 0x20202062 regs.rip += 2 defchallenge11(ql): begin, end = 0, 0 forinfo inql.mem.map_info: #print("=====") #print(info) #print("=====") ifinfo[2] == 5and'qilinglab-x86_64'ininfo[3]: begin, end = info[:2] #print("begin_addr",begin) #print("end_addr",end) ql.hook_code(hook_cpuid, begin=begin, end=end) classcmdline(QlFsMappedObject): defread(self, expected_len): returnb'qilinglab' defclose(self): return0 defchallenge10(ql): ql.add_fs_mapper('/proc/self/cmdline', cmdline()) defhook_tolower(ql): return0 defchallenge9(ql): ql.os.set_api('tolower', hook_tolower) deffind_and_patch(ql, *args, **kw): MAGIC = 0x3DFCD6EA00000539 magic_addrs = ql.mem.search(p64(MAGIC)) #print("magic_address:",hex(magic_addrs)) formagic_addr inmagic_addrs: malloc1_addr = magic_addr - 8 malloc1_data = ql.mem.read(malloc1_addr, 24) string_addr, _ , check_addr = struct.unpack("QQQ",malloc1_data) ifql.mem.string(string_addr) == "Random data": ql.mem.write(check_addr, b"x01") break defchallenge8(ql): base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) #print("base_addr",hex(base_addr)) ql.hook_address(find_and_patch, base_addr+0xFB5) defhook_sleep(ql): return0 defchallenge7(ql): ql.os.set_api('sleep',hook_sleep) defhook_rax(ql): ql.arch.regs.rax = 0 defchallenge6(ql): base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) #print("base_addr",hex(base_addr)) hook_addr = base_addr + 0xF16 ql.hook_address(hook_rax, hook_addr) defhook_rand(ql): ql.arch.regs.rax = 0 defchallenge5(ql): ql.os.set_api('rand',hook_rand) defenter_forbidden_loop_hook(ql): ql.arch.regs.eax = 1 defchallenge4(ql): base = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) hook_addr = base + 0xE43 print("qiling binary hookaddr:",hex(hook_addr)) ql.hook_address(enter_forbidden_loop_hook, hook_addr) classFakeUrandom(QlFsMappedObject): defread(self, size: int)-> bytes: ifsize == 1: returnb"x42" else: returnb"x41"* size defclose(self)-> int: return0 defhook_getrandom(ql, buf, buflen, flags): ifbuflen == 32: data = b'x41'* buflen # b'x41' = A ql.mem.write(buf, data) ql.os.set_syscall_return(buflen) else: ql.os.set_syscall_return(-1) defchallenge3(ql): ql.add_fs_mapper(r'/dev/urandom', FakeUrandom()) ql.os.set_syscall("getrandom", hook_getrandom) defmy_uname_on_exit_hook(ql, *args): rdi = ql.arch.regs.rdi print(f"utsname address: {hex(rdi)}") ql.mem.write(rdi, b'QilingOSx00') ql.mem.write(rdi + 65* 3, b'ChallengeStartx00') defchallenge2(ql): ql.os.set_api("uname", my_uname_on_exit_hook, QL_INTERCEPT.EXIT) defchallenge1(ql): ql.mem.map(0x1000, 0x1000, info='challenge1') ql.mem.write(0x1337, p16(1337)) if__name__ == '__main__': path = ["./x8664_linux/qilinglab-x86_64"] rootfs = "./x8664_linux" ql = Qiling(path, rootfs,verbose=QL_VERBOSE.OFF) challenge1(ql) challenge2(ql) challenge3(ql) challenge4(ql) challenge5(ql) challenge6(ql) challenge7(ql) challenge8(ql) challenge9(ql) challenge10(ql) challenge11(ql) #ql.debugger = "gdb12345" ql.run()
運行后,所有的challenge都會顯示SOLVED。
qiling設備仿真
qiling提供了路由器仿真案例,該腳本路徑為qiling/example路徑下
#!/usr/bin/env python3 # 1. Download AC15 Firmware from https://down.tenda.com.cn/uploadfile/AC15/US_AC15V1.0BR_V15.03.05.19_multi_TD01.zip # 2. unzip # 3. binwalk -e US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin # 4. locate squashfs-root # 5. rm -rf webroot && mv webroot_ro webroot # # notes: we are using rootfs in this example, so rootfs = squashfs-root # importos, socket, threading importsys sys.path.append("../../../") fromqiling importQiling # 從qiling.const中導入QL_VERBOSE,指定qiling的日志輸出級別 fromqiling.const importQL_VERBOSE # 定義patcher函數,用于跳過網卡信息檢測。在前面小節我們仿真tenda路由器時,路由器httpd程序在初始化網絡時會檢查網卡名稱是否為br0。這里腳本直接將代碼執行前內存中的br0字符串替換成了lo,從而跳過檢查。 defpatcher(ql: Qiling): br0_addr = ql.mem.search("br0".encode() + b'x00') foraddr inbr0_addr: ql.mem.write(addr, b'lox00') # 定義nvram_listener函數,使用該函數監聽Unix套接字,并在收到消息時返回數據。 defnvram_listener(): server_address = 'rootfs/var/cfm_socket' data = "" try: os.unlink(server_address) exceptOSError: ifos.path.exists(server_address): raise sock = socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) sock.bind(server_address) sock.listen(1) whileTrue: connection, _ = sock.accept() try: whileTrue: data += str(connection.recv(1024)) if"lan.webiplansslen"indata: connection.send('192.168.170.169'.encode()) else: break data = "" finally: connection.close() # 定義myvfork函數,仿真程序在執行系統調用vfork時被調用,返回值0。 defmyvfork(ql: Qiling): regreturn = 0 ql.log.info("vfork() = %d"% regreturn) returnregreturn # 仿真主函數,生成qiling實例和添加VFS映射。 defmy_sandbox(path, rootfs): print("path:",path) print("rootfs",rootfs) ql = Qiling(path, rootfs, verbose=QL_VERBOSE.DEBUG) print("ql:",ql) ql.add_fs_mapper("/dev/urandom","/dev/urandom") ql.hook_address(patcher, ql.loader.elf_entry) ql.debugger = False ifql.debugger == True: ql.os.set_syscall("vfork", myvfork) # vfork函數返回0時,debugger可正常調試。 ql.run() if__name__ == "__main__": # 創建后臺運行的線程并執行,以便收到Unix套接字的消息時進行響應。 nvram_listener_therad = threading.Thread(target=nvram_listener, daemon=True) nvram_listener_therad.start() # 運行仿真實例 my_sandbox(["rootfs/bin/httpd"], "rootfs")
當我們運行腳本后,會顯示路由器的ip和端口,當我們發現本地的8080正在監聽時,說明設備已經仿真成功。
仿真成功后可訪問http://localhost:8080查看效果:
在后面的小節中,我們會學習對仿真路由器設備進行fuzz。其中最為重要的一步便是編寫仿真腳本,后續在我們分析好固件程序中要fuzz地址范圍后,只有仿真設備可以順利觸發保存快照的功能,才可保證fuzz的正確性。
qiling fuzz
qiling框架可以使用AFLplusplus對arm架構程序進行fuzz測試,測試代碼如下:
#include#include #include // Program that will crash easily. #defineSIZE (10) intfun(inti) { char*buf = malloc(SIZE); charbuf2[SIZE]; while((*buf = getc(stdin)) == 'A') { buf[i++] = *buf; } strncpy(buf2, buf, i); puts(buf2); return0; } intmain(intargc, char**argv) { returnfun(argc); }
qiling提供的fuzz腳本如下:
#!/usr/bin/env python3 """ Simple example of how to use Qiling together with AFLplusplus. This is tested with the recent Qiling framework (the one you cloned), afl++ from https://github.com/AFLplusplus/AFLplusplus After building afl++, make sure you install `unicorn_mode/setup_unicorn.sh` Then, run this file using afl++ unicorn mode with afl-fuzz -i ./afl_inputs -o ./afl_outputs -m none -U -- python3 ./fuzz_x8664_linux.py @@ """ # No more need for importing unicornafl, try ql.afl_fuzz instead! importsys, os frombinascii importhexlify sys.path.append("../../..") fromqiling import* fromqiling.extensions importpipe fromqiling.extensions.afl importql_afl_fuzz defmain(input_file, enable_trace=False): ql = Qiling(["./arm_fuzz"], "../../rootfs/arm_qnx", console=enable_trace) # 設置ql的標準輸入為進程的標準輸入 ql.os.stdin = pipe.SimpleInStream(sys.stdin.fileno()) # 如果沒有啟用控制臺追蹤,則將標準輸出和標準錯誤流設置為Null ifnotenable_trace: ql.os.stdout = pipe.NullOutStream(sys.stdout.fileno()) ql.os.stderr = pipe.NullOutStream(sys.stderr.fileno()) defplace_input_callback(ql: Qiling, input: bytes, _: int): # 設置fuzz輸入點 ql.os.stdin.write(input) returnTrue defstart_afl(_ql: Qiling): # 設置fuzz實例 ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]) # 獲取libc的基地址 LIBC_BASE = int(ql.profile.get("OS32", "interp_address"), 16) # 設置hook函數,用于處理SignalKill信號 ql.hook_address(callback=lambdax: os.abort(), address=LIBC_BASE + 0x38170) # main函數地址 main_addr = 0x08048aa0 # 設置hook函數,在main函數運行時調用start_afl函數 ql.hook_address(callback=start_afl, address=main_addr) # 若啟用控制臺追蹤,則將設置相關信息輸出 ifenable_trace: # The following lines are only for `-t` debug output md = ql.arch.disassembler count = [0] defspaced_hex(data): returnb' '.join(hexlify(data)[i:i+2] fori inrange(0, len(hexlify(data)), 2)).decode('utf-8') defdisasm(count, ql, address, size): buf = ql.mem.read(address, size) try: fori inmd.disasm(buf, address): return"{:08X} {:08X}: {:24s} {:10s} {:16s}".format(count[0], i.address, spaced_hex(buf), i.mnemonic, i.op_str) except: importtraceback print(traceback.format_exc()) deftrace_cb(ql, address, size, count): rtn = '{:100s}'.format(disasm(count, ql, address, size)) print(rtn) count[0] += 1 ql.hook_code(trace_cb, count) # okay, ready to roll. # try: ql.run() # except Exception as ex: # # Probable unicorn memory error. Treat as crash. # print(ex) # os.abort() os._exit(0) # that's a looot faster than tidying up. if__name__ == "__main__": iflen(sys.argv) == 1: raiseValueError("No input file provided.") iflen(sys.argv) > 2andsys.argv[1] == "-t": main(sys.argv[2], enable_trace=True) else: main(sys.argv[1])
AFLplusplus執行腳本如下:
#!/usr/bin/sh AFL_AUTORESUME=1 AFL_PATH="$(realpath ../../../AFLplusplus)"PATH="$AFL_PATH:$PATH"afl-fuzz -i afl_inputs -o afl_outputs -U -- python3 ./fuzz_arm_qnx.py @@
運行后fuzz.sh后,便會出現afl++ 運行界面,等待幾秒后便出現crash。
crash的變異數據存放在afl_outputs目錄下,我們可以使用xxd id:000000,xxxxxx命令查看變異數據。
#xxdid:000000,sig:06,src:000000,time:4112,execs:1077,op:havoc,rep:8 00000000: 4141 4141 4141 4141 4141 4141 ff7f4241 AAAAAAAAAAAA..BA 00000010: 4141 4145 4141 be414dff0000 0041 4141 AAAEAA.AM....AAA 00000020: 41
責任編輯:彭菁
-
框架
+關注
關注
0文章
396瀏覽量
17269 -
虛擬化
+關注
關注
1文章
355瀏覽量
29671 -
IOT
+關注
關注
186文章
4097瀏覽量
195081
原文標題:物聯網安全之qiling框架初探
文章出處:【微信號:蛇矛實驗室,微信公眾號:蛇矛實驗室】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論