qiling fuzz 基礎
qiling和AFL++環境的搭建在前面的小節中已經說過,這里就不再演示。我們進入到qiling的qiling/example/fuzzing目錄下,qiling框架官方庫提供了幾個fuzz的example供我們學習和測試。我們先對tenda ac15進行測試。
根據README文檔中的介紹,我們首先需要提取tenda ac 15文件系統并放置于腳本同級目錄中,操作步驟如下:
1. wget https://down.tenda.com.cn/uploadfile/AC15/US_AC15V1.0BR_V15.03.05.19_multi_TD01.zip 2. unzip US_AC15V1.0BR_V15.03.05.19_multi_TD01.zip 3. binwalk -e US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin 4. mv xxx/squashfs-root ./rootfs;cd rootfs 5. rm -rf webroot;mv webroot_ro webroot 6. mv etc_ro etc
隨后我們需要運行saver_tendaac15_httpd.py
使用netstat -pantl查看監聽端口,當發現python3程序正在監聽8080端口時,說明tenda ac仿真成功。
此時我們運行./addressNet_overflow.sh生成snapshot.bin文件。
運行./fuzz_tendaac15_httpd.sh進行fuzz,經過10分鐘左右出現了crash。
產生的crash文件內容如下
這樣我們就完成對實例中tenda ac15的fuzz復現。官方提供demo的saver和fuzz腳本如下,現在我們對其進行簡單分析并學習。
通過上一小節中對qiling基礎的學習,我們可以對兩個腳本中的函數功能進行拆分。
saver.py
保存快照
defsave_context(ql, *args, **kw): ql.save(cpu_context=False, snapshot="snapshot.bin")
替換網卡名稱
defpatcher(ql): br0_addr = ql.mem.search("br0".encode() + b'x00') foraddr inbr0_addr: ql.mem.write(addr, b'lox00')
檢查停止地址
defcheck_pc(ql): print("="* 50) print("Hit fuzz point, stop at PC = 0x%x"% ql.arch.regs.arch_pc) print("="* 50) ql.emu_stop()
網絡設置
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, client_address = 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()
仿真流程
defmy_sandbox(path, rootfs): ql = Qiling(path, rootfs, verbose=QL_VERBOSE.DEBUG) ql.add_fs_mapper("/dev/urandom","/dev/urandom") ql.hook_address(save_context, 0x10930) ql.hook_address(patcher, ql.loader.elf_entry) ql.hook_address(check_pc, 0x7a0cc) ql.run()
fuzz.py
替換網卡名稱
defpatcher(ql): br0_addr = ql.mem.search("br0".encode() + b'x00') foraddr inbr0_addr: ql.mem.write(addr, b'lox00')
fuzz流程
defmain(input_file, enable_trace=False): # 生成qiling實例 ql = Qiling(["rootfs/bin/httpd"], "rootfs", verbose=QL_VERBOSE.DEBUG, console = Trueifenable_trace elseFalse) # 恢復快照內容 ql.restore(snapshot="snapshot.bin") # 變異數據地址點定位 fuzz_mem=ql.mem.search(b"CCCCAAAA") target_address = fuzz_mem[0] # target_address為fuzz變異點,place_input_callback函數通過afl++對數據進行變異 defplace_input_callback(_ql: Qiling, input: bytes, _): _ql.mem.write(target_address, input) # fuzz函數定義 defstart_afl(_ql: Qiling): ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]) ql.hook_address(callback=start_afl, address=0x10930+8) # qiling實例運行 try: ql.run(begin = 0x10930+4, end = 0x7a0cc+4) os._exit(0) except: ifenable_trace: print(" Fuzzer Went Shit") os._exit(0)
通過功能的拆分以及我們的分析,可以知道fuzz大致流程是:
1.運行saver.py生成qiling實例仿真運行,此時運行addressNet_overflow.sh觸發相關執行流程,當pc寄存器運行到0x10930地址時,觸發保存快照功能。
2.fuzz.sh會調用afl++并執行fuzz.py腳本對其input輸入進行數據變異。
3.fuzz.py腳本中,首先會恢復快照狀態,并在內容中尋找數據變異點,并接受afl++的變異數據將其寫入數據變異點進行fuzz。
了解了大致fuzz流程,我們可能存在幾點疑慮:
1.saver.py腳本中如何知道在哪個地址觸發保存快照功能?在仿真的httpd程序觸發執行addressNet_overflow.sh執行流程后,程序將變異數據存儲至內存完成后,就可以觸發快照保存功能。
2.addressNet_overflow.sh腳本中為什么定義page為CCCCAAAA?CCCCAAAA為poc的溢出標識,以便后續我們進行查找定位。
3.fuzz.py腳本中為什么先要替換br0?tenda ac15路由器設備啟動時會檢測br0網卡狀態,我們本地沒有這個網卡所以替換成了lo。
4.fuzz.py腳本中如何知道ql.run的起始和結束地址?起始地址是保存快照后面的指令,需要保證執行流程的連貫性(保存寄存器狀態除外),結束地址便是漏洞函數可以觸發crash后的函數結束地址。
這里1、4還是不太清楚,帶著疑問我們接著往下分析:
根據addressNet_overflow.sh腳本中的poc,我們使用ida進行定義,發現漏洞函數如下,產生漏洞的原因便是沒有對用戶發送post包data數據中的entrys、mitInterface、page參數進行過濾,并使用sprintf危險函數進行了寫入。
那么我們要fuzz的函數就是formAddressNat函數了,首先第1點saver.py腳本中該如何定位保存地址點,這里其實當v1=sprintf(xxx)執行完畢后,已經將用戶參數數據保存到v6中時,就已經可以保存快照了(后續fuzz.py也要進行相應修改)。這里作者定位的是0x10930,那么后續fuzz的起始地址和結束地址分別就是0x10930執行流程的后面下一條指令和formAddressNet函數的結束地址。
在分析的過程中,我們可以打開QL_VERBOSE.DISASM來清楚的查看匯編指令的執行流程和對應指令的寄存器信息。
分析完tenda ac同理example中的dir815實例也是同樣的流程,只不過dir815的fuzz腳本并沒有使用保存快照功能,而是直接使用ql.mem.search進行查找變異數據點以及使用ql.mem.write對變異數據進行寫入。
分析完上面的流程后,我們對fuzz的流程有了大概理解。后面我們以dlink dir645路由器中的兩個棧溢出實例進行fuzz測試。
qiling fuzz 實例
以經典的dir645棧溢出為例,我們使用qiling框架對兩個棧溢出漏洞進行fuzz測試。
首先下載固件并使用binwalk -Me 固件名進行提取,簡單查看后發現本次分析的程序hedwig.cgi和authentication.cgi均為軟鏈接(鏈接到htdocs/cgibin),qiling對軟連接的處理不是很友好,建議將所有軟連接替換為源文件。
hedwig.cgi棧溢出
我們首先將cgibin拖入ida進行簡單分析,進入main函數后發現,main程序根據傳入參數與相關"*.cgi"進行比較,隨后進入相關的cgi_main函數中,那我們先分析一下hedwig.cgi觸發的棧溢出
fuzz的第一步就是摸清楚程序的執行流程,我們先簡單編寫仿真程序的腳本,隨后將其改為fuzz腳本。該仿真腳本定義了倆個hook函數,當程序執行執行地址處后會執行該hook函數,并打印出"Hit at xxx func"。
執行仿真腳本,發現我們定義的倆個hook都被觸發,說明我們簡單分析后的執行流程確實沒錯,并且調試信息后續還打印出了一下回應信息。
我們繼續在ida中查找定位該信息,發現是LABEL_25中的處理,根據交叉引用,我們追蹤該信息產生的原因是env中沒有REQUEST_METHOD
那么我們在env中設置該環境變量然后傳入給qiling實例就可以了,接著往下分析發現程序的溢出點位于sess_get_uid中,并通過QL_VERBOSE.DISASM信息,我們理清楚了大概的產生漏洞流程。定位了棧溢出地址后,那么我們根據漏洞產生流程設置相關的env參數數據并傳入qiling實例中,隨后我們使用mem.search()替換成為變異數據,程序仿真時就會取出環境變量中的值進行處理從而產生棧溢出。
注:由于程序是從env中讀取變量的值,所以也就不存在前面提到的拷貝到內存中然后觸發保存快照功能指定流程,這里可以直接觸發保存快照的功能。
最終編寫saver.py腳本如下:
importctypes, os, pickle, socket, sys, threading sys.path.append("..") fromqiling import* fromqiling.const importQL_VERBOSE MAIN = 0x402770 HEDWIGCGI_MAIN = 0x40bfc0 SESSION_UID = 0x4083f0 SAVE_ADDRESS = 0x40c070 deftest_print1(ql: Qiling)->None: print("Hit at main func") deftest_print2(ql: Qiling)->None: print("Hit at hedwig func") deftest_print3(ql: Qiling)->None: print("Hit at session uid func") defsaver(ql: Qiling): print('[!] Hit Saver 0x%X'%(ql.arch.regs.arch_pc)) ql.save(cpu_context=False, snapshot='./context.bin') defmy_sandbox(path, rootfs): env_vars = { "REQUEST_METHOD": "POST", "REQUEST_URI": "/hedwig.cgi", "CONTENT_TYPE": "application/x-www-form-urlencoded", "REMOTE_ADDR": "127.0.0.1", "HTTP_COOKIE": "uid=AAAABBBB" } ql = Qiling(path, rootfs,env=env_vars,verbose=QL_VERBOSE.DEBUG) ql.hook_address(test_print1, MAIN) ql.hook_address(test_print2, HEDWIGCGI_MAIN) ql.hook_address(test_print3, SESSION_UID) ql.hook_address(saver, SAVE_ADDRESS) ql.run() if__name__ == "__main__": my_sandbox(["rootfs/htdocs/web/hedwig.cgi"], "rootfs")
執行后,保存的快照為context.bin,我們可以使用strings定位棧溢出標識字符串。
接下來我們編寫fuzz.py,前面我們觸發快照的地址為getenv("REQUEST_METHOD") 執行后的一條指令,那么我們在編寫fuzz.py中ql.run的起始地址時就應該為下一條指令,這里為了方便我直接讓其跳過if判斷直接從cgibin_parse_request處開始執行(0x40c0a4)。結束地址呢,這里直接指定hedwigcgi_main函數的結尾就可以(0x40c598),因為有溢出數據時程序執行到函數最后一定會觸發crash。
最終hedwig.cgi棧溢出fuzz.py的腳本如下:
importos, pickle, socket, sys, threading sys.path.append("../../../") fromqiling import* fromqiling.const importQL_VERBOSE fromqiling.extensions.afl importql_afl_fuzz defmain(input_file, enable_trace=False): ql = Qiling(["rootfs/htdocs/web/hedwig.cgi"], "rootfs", verbose=QL_VERBOSE.DEBUG) ql.restore(snapshot="context.bin") fuzz_mem=ql.mem.search(b"AAAABBBB") target_address = fuzz_mem[0] defplace_input_callback(_ql: Qiling, input: bytes, _): _ql.mem.write(target_address, input) defstart_afl(_ql: Qiling): ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]) ql.hook_address(callback=start_afl, address=0x40c0a4) try: ql.run(begin = 0x40c0a4, end = 0x40c598) os._exit(0) except: ifenable_trace: print(" Fuzzer Went Shit") os._exit(0) 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])
不到1分鐘就fuzz到了crash,還是比較快的。
authentication.cgi棧溢出
和上面的分析同理,我們首先跟一下程序的執行流程,authentication.cgi的處理函數為authenticationcgi_main函數。
進入到authenticationcgi_main函數后,我們發現和上面的hedwig類似,也是同樣獲取env中的變量進行處理。
那么我們將前面的腳本進行修改,這里直接將HEDWIGCGI_MAIN改為0x40afcc。
運行后發現執行觸發了倆個hook函數,說明確實執行到了authenticationcgi_main函數中。
authentication.cgi觸發棧溢出的執行流程為REQUEST METHOD方法為POST,并且需要設置"CONTENT_TYPE"和"CONTENT_LENGTH"環境變量。
那么我們在env變量中設置如下參數并傳入qiling實例。
注:CONTENT_LENGTH中的999在程序執行時還沒有溢出,v73定義的1024字節。
運行后發現已經執行我們想要其執行的流程了,并且需要我們輸入一些信息才可執行后面的流程。
input中含有的內容如下,需要包含"id=xxx&password=xxx"
再次執行,輸入"id=1&password=123"后,程序正常執行。那么我們的fuzz思路如下:
傳入env環境變量,使其按照漏洞觸發流程進行執行,隨后在程序賦值content_length時,進行hook,將需要用到的寄存器修改成afl++變異數據的大小(其實這里應該取出所有header的字節,不過這里不是很影響),隨后進行調用start_afl進行fuzz。寫入棧溢出標識地址的content格式為:b"id=1&password="+input
根據上面的信息,我們編寫的fuzz.py如下:
importctypes, os, pickle, socket, sys, threading sys.path.append("../../../") fromqiling import* fromqiling.const importQL_VERBOSE fromqiling.extensions importpipe fromqiling.extensions.afl importql_afl_fuzz MAIN = 0x402770 AUTHENTICATION_MAIN = 0x40afcc CONTENT_LENGTH = 0x40b48c CONTENT_SIZE = 0x40b4b4 size = 0 deftest_print1(ql: Qiling)->None: print("Hit at main func") deftest_print2(ql: Qiling)->None: print("Hit at authentication func") deftest_print3(ql: Qiling): print("address:",hex(ql.arch.regs.s0)) deftest_print4(ql: Qiling): print("Hit at exit func") deftest_size(ql: Qiling): globalsize ql.arch.regs.s0 = size ql.arch.regs.a2 = size print("Hit at test_size func") defmain(input_file, enable_trace=False): env_vars = { "REQUEST_METHOD": "POST", "REQUEST_URI": "/authentication.cgi", "CONTENT_TYPE": "application/x-www-form-urlencoded", "REMOTE_ADDR": "127.0.0.1", "CONTENT_LENGTH": "100" } ql = Qiling(["rootfs/htdocs/web/authentication.cgi"], "rootfs",env=env_vars,verbose=QL_VERBOSE.DEBUG) ql.os.stdin = pipe.SimpleInStream(0) 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): globalsize content = b"id=1&password="+input size = len(content) ql.os.stdin.write(content) ql.hook_address(test_size,CONTENT_SIZE) defstart_afl(_ql: Qiling): ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]) ql.hook_address(test_print1, MAIN) ql.hook_address(test_print2, AUTHENTICATION_MAIN) ql.hook_address(test_print3, CONTENT_LENGTH) ql.hook_address(test_print4,address=0x40bc90) ql.hook_address(callback=start_afl, address=AUTHENTICATION_MAIN) try: ql.run() os._exit(0) except: ifenable_trace: print(" Fuzzer Went Shit") os._exit(0) 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])
運行后發現,afl++給到的變異數據確實傳入了進去。
但是fuzz了一會兒發現沒crash,原來是read(fd=0x0,buf=0x7ff3c940,length=0x64),這里讀取的length還是100,也就是說afl++不管變異多少數據都只讀取100字節,這說明我們的hook有問題。
打開QL_VERBOSE.DUMP模式,查看read時寄存器變量的值,確實hook沒生效。那么我們直接在其調用read函數時hook,使其寄存器變成我們變異數據的長度就可以了。
重新改正腳本
importctypes, os, pickle, socket, sys, threading sys.path.append("../../../") fromqiling import* fromqiling.const importQL_VERBOSE fromqiling.extensions importpipe fromqiling.extensions.afl importql_afl_fuzz MAIN = 0x402770 AUTHENTICATION_MAIN = 0x40afcc CONTENT_LENGTH = 0x40b48c CONTENT_SIZE = 0x40b4a8 size = 1000 deftest_print1(ql: Qiling)->None: print("Hit at main func") deftest_print2(ql: Qiling)->None: print("Hit at authentication func") deftest_print3(ql: Qiling): print("address:",hex(ql.arch.regs.s0)) deftest_print4(ql: Qiling): print("Hit at exit func") deftest_size(ql: Qiling): globalsize ql.arch.regs.s0 = size ql.arch.regs.a1 = size print("Hit at test_size func") defmain(input_file, enable_trace=False): globalsize env_vars = { "REQUEST_METHOD": "POST", "REQUEST_URI": "/authentication.cgi", "CONTENT_TYPE": "application/x-www-form-urlencoded", "REMOTE_ADDR": "127.0.0.1", "CONTENT_LENGTH": "100" } ql = Qiling(["rootfs/htdocs/web/authentication.cgi"], "rootfs",env=env_vars,verbose=QL_VERBOSE.DEBUG) ql.os.stdin = pipe.SimpleInStream(0) 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): globalsize content = b"id=1&password="+input size = len(content) ql.os.stdin.write(content) defstart_afl(_ql: Qiling): ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]) ql.hook_address(test_print1, MAIN) ql.hook_address(test_print2, AUTHENTICATION_MAIN) ql.hook_address(test_print3, CONTENT_LENGTH) ql.hook_address(test_print4,address=0x40bc90) ql.hook_address(callback=start_afl, address=AUTHENTICATION_MAIN) ql.hook_address(test_size,CONTENT_SIZE) try: ql.run() os._exit(0) except: ifenable_trace: print(" Fuzzer Went Shit") os._exit(0) 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])
運行后,3分鐘左右fuzz出crash。
綜上,我們以實例的方式分析并fuzz了dir645路由器的倆個棧溢出漏洞,可能很多人覺得在fuzz時已經知道了漏洞點和執行流程,使用qiling進行fuzz有點多余。但是對于我們沒有實體設備或者qemu不能完全仿真時,即使我們知道了漏洞點,我們也沒法去簡單測試或驗證,那么這時qiling框架就是一個比較好的選擇。這個小節中qiling 的fuzz思路像是在驗證漏洞而且比較基礎,而在真正fuzz漏洞時,也許我們可以hook危險函數并進行fuzz,又或者其他思路,這些就靠大家的思維拓展了。
總結
在這小節中,我們使用qiling框架分析并測試了tenda ac15、dir 815以及dir 645實例設備的棧溢出漏洞,掌握了在分析fuzz時的基礎思路。熟練掌握qiling框架的使用,對于后續我們的漏洞測試方面還是有很多的幫助。
審核編輯:劉清
-
寄存器
+關注
關注
31文章
5322瀏覽量
120019 -
仿真器
+關注
關注
14文章
1016瀏覽量
83644 -
路由器
+關注
關注
22文章
3708瀏覽量
113546 -
觸發器
+關注
關注
14文章
1996瀏覽量
61053 -
python
+關注
關注
56文章
4782瀏覽量
84460
原文標題:Qiling Fuzz實例分析
文章出處:【微信號:蛇矛實驗室,微信公眾號:蛇矛實驗室】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論