背景
Qiling Framework是一個基于Python的二進制分析、模擬和虛擬化框架。它可以用于動態(tài)分析和仿真運行不同操作系統(tǒng)、處理器和體系結(jié)構(gòu)下的二進制文件。除此之外,Qiling框架還提供了易于使用的API和插件系統(tǒng),方便使用者進行二進制分析和漏洞挖掘等工作。其創(chuàng)始人是一名IoT Hacker,創(chuàng)建qiling的初衷便是解決在研究IoT時遇到的種種問題,這也是為什么上一小節(jié)說qiling框架比unicorn框架更加適合IoT研究初學者。
qiling使用基礎(chǔ)
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)相對應(yīng),其他架構(gòu)同理 intmain(){ printf("hello world!"); return0; }
使用qiling編寫一個簡單的仿真腳本。
fromqiling import* fromqiling.const importQL_VERBOSE # 導入qiling模塊和qiling.const模塊中的QL_VERBOSE常量 if__name__ == "__main__": #創(chuàng)建Qiling對象,實例中三個參數(shù)分別為:path(仿真程序路徑)、rootfs(仿真程序文件系統(tǒng)目錄)和verbose(輸出信息參數(shù)),除此外還可以設(shè)置env和log_plain參數(shù)。 ql = Qiling(["./x8664_linux_symlink/test"], "./x8664_linux_symlink",verbose=QL_VERBOSE.DEBUG) #運行Qiling對象的run()方法,開始執(zhí)行仿真程序 ql.run()
這里的verbose(輸出信息參數(shù))有如下級別及其作用:
VFS劫持
x86_fetch_urandom程序的作用為打開/dev/urandom文件,生成隨機數(shù)。當qiling仿真x86_fetch_urandom程序時,環(huán)境需要用到仿真文件系統(tǒng),我們就需要用到VFS劫持,這樣就可以模擬修改文件系統(tǒng)。下面的代碼中為仿真虛擬路徑 "/dev/urandom" 會被映射到宿主系統(tǒng)上的現(xiàn)有"/dev/urandom"文件。當模擬程序?qū)⒃L問 /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'的交互結(jié)果,可以繼承QlFsMappedObject類,并可自定義read、write、fstat、ioctl、readline等方法。
fromqiling importQiling fromqiling.os.mapper importQlFsMappedObject classFakeUrandom(QlFsMappedObject): defread(self, size: int)-> bytes: returnb"x01"#可以修改讀取返回結(jié)果 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()
函數(shù)hook
下面示例中,我們給str1和str2倆個變量內(nèi)存中分別復制"abcdef"和"ABCDEF"字符串。正常執(zhí)行完畢后會打印出"str1 大于 str2"。我們可以使用qiling框架劫持strcmp實現(xiàn)為hook strcmp函數(shù)的效果,使其執(zhí)行到不同分支的結(jié)果。
#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函數(shù),并通過修改rax寄存器改變執(zhí)行流程。
fromqiling import* fromqiling.const import* # 自定義strcmp hook函數(shù)。當程序執(zhí)行strcmp函數(shù)退出時,會調(diào)用此函數(shù),并且在比較完畢后,將 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 函數(shù)為 strcmp 設(shè)置hook函數(shù),第一個參數(shù)為要hook的函數(shù)名,第二個參數(shù)為自定義hook函數(shù),第三個參數(shù)為hook類型,這里為退出時觸發(fā)hook函數(shù)。 defhook_func(ql): ql.os.set_api('strcmp',hook_strcmp,QL_INTERCEPT.EXIT) # 也可以使用ql.hook_address()函數(shù)進行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函數(shù)時hook類型參數(shù)有以下三種:
qiling使用實例
使用qiling解密CTF賽題
當我們掌握了最基礎(chǔ)的三個用法后,我們可以測試一個簡單的例子來加深對qiling框架的理解。以上一小節(jié)中unicorn解密ctf題目為例,我們先簡單寫一個運行腳本。這里的ql.debugger="gdb12345"為開啟gdbserver服務(wù),我們可以使用ida或者gdb進行調(diào)試。
簡單運行后發(fā)現(xiàn)程序和上一小節(jié)中unicorn的運行狀況類似。由于這里我設(shè)置了multithead為True,所以這里會比上一小節(jié)中unicorn的解密速度快不少。但是還是在有限時間內(nèi)只輸出4個字符。
當我們將verbose設(shè)置為QL_VERBOSE.DISASM便可觀察模擬執(zhí)行的匯編指令,根據(jù)匯編指令我們明顯看到程序在call 0x400670處進行了遞歸調(diào)用(或使用調(diào)試器調(diào)試查看),導致解密時間非常長。所以我們需要進行代碼優(yōu)化,思路為使用棧空間來保存一個不同輸入?yún)?shù)以及對應(yīng)計算結(jié)果的字典來避免重復計算。
這里qiling由于是由unicorn開發(fā)而來,所以很多用法和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()
運行后便會打印出解密結(jié)果。
除了上一小節(jié)中的ctf題目掌握qiling的使用外,我們還可通過qilinglab來加深對qiling框架的使用。qilingLab是由11個小挑戰(zhàn)組成的二進制程序,用來幫助新手快速熟悉和掌握 Qiling 框架的基本用法。官方提供了aarch64程序的解題方法,我們根據(jù)這個作為參考解密一下x86_64架構(gòu)的練習程序。
x86_64程序下載(https://www.shielder.com/attachments/qilinglab-x86_64)
首先運行程序,給我們提示,challenges會造成程序崩潰,只有當我們解出相應(yīng)challenge后才會顯示信息。
我們可以通過ida逆向以及編寫qiling腳本進行動態(tài)調(diào)試來完成這些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設(shè)備仿真
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函數(shù),用于跳過網(wǎng)卡信息檢測。在前面小節(jié)我們仿真tenda路由器時,路由器httpd程序在初始化網(wǎng)絡(luò)時會檢查網(wǎng)卡名稱是否為br0。這里腳本直接將代碼執(zhí)行前內(nèi)存中的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函數(shù),使用該函數(shù)監(jiān)聽Unix套接字,并在收到消息時返回數(shù)據(jù)。 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函數(shù),仿真程序在執(zhí)行系統(tǒng)調(diào)用vfork時被調(diào)用,返回值0。 defmyvfork(ql: Qiling): regreturn = 0 ql.log.info("vfork() = %d"% regreturn) returnregreturn # 仿真主函數(shù),生成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函數(shù)返回0時,debugger可正常調(diào)試。 ql.run() if__name__ == "__main__": # 創(chuàng)建后臺運行的線程并執(zhí)行,以便收到Unix套接字的消息時進行響應(yīng)。 nvram_listener_therad = threading.Thread(target=nvram_listener, daemon=True) nvram_listener_therad.start() # 運行仿真實例 my_sandbox(["rootfs/bin/httpd"], "rootfs")
當我們運行腳本后,會顯示路由器的ip和端口,當我們發(fā)現(xiàn)本地的8080正在監(jiān)聽時,說明設(shè)備已經(jīng)仿真成功。
仿真成功后可訪問http://localhost:8080查看效果:
在后面的小節(jié)中,我們會學習對仿真路由器設(shè)備進行fuzz。其中最為重要的一步便是編寫仿真腳本,后續(xù)在我們分析好固件程序中要fuzz地址范圍后,只有仿真設(shè)備可以順利觸發(fā)保存快照的功能,才可保證fuzz的正確性。
qiling fuzz
qiling框架可以使用AFLplusplus對arm架構(gòu)程序進行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) # 設(shè)置ql的標準輸入為進程的標準輸入 ql.os.stdin = pipe.SimpleInStream(sys.stdin.fileno()) # 如果沒有啟用控制臺追蹤,則將標準輸出和標準錯誤流設(shè)置為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): # 設(shè)置fuzz輸入點 ql.os.stdin.write(input) returnTrue defstart_afl(_ql: Qiling): # 設(shè)置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) # 設(shè)置hook函數(shù),用于處理SignalKill信號 ql.hook_address(callback=lambdax: os.abort(), address=LIBC_BASE + 0x38170) # main函數(shù)地址 main_addr = 0x08048aa0 # 設(shè)置hook函數(shù),在main函數(shù)運行時調(diào)用start_afl函數(shù) ql.hook_address(callback=start_afl, address=main_addr) # 若啟用控制臺追蹤,則將設(shè)置相關(guān)信息輸出 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執(zhí)行腳本如下:
#!/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后,便會出現(xiàn)afl++ 運行界面,等待幾秒后便出現(xiàn)crash。
crash的變異數(shù)據(jù)存放在afl_outputs目錄下,我們可以使用xxd id:000000,xxxxxx命令查看變異數(shù)據(jù)。
#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
責任編輯:彭菁
-
框架
+關(guān)注
關(guān)注
0文章
403瀏覽量
17487 -
虛擬化
+關(guān)注
關(guān)注
1文章
373瀏覽量
29796 -
IOT
+關(guān)注
關(guān)注
187文章
4210瀏覽量
196795
原文標題:物聯(lián)網(wǎng)安全之qiling框架初探
文章出處:【微信號:蛇矛實驗室,微信公眾號:蛇矛實驗室】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論