作者:崎山松形@RainSec
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

前言

最近在搞Iot的時候接觸到Qiling框架,用了一段時間后感覺確實模擬功能挺強大的,還支持Fuzz,于是開始學習對Iot webserver這樣的程序進行Fuzz。

官方給出了類似的例子如Tenda AC15 的httpd的fuzz腳本,但是也就光禿禿一個腳本還是需要自己來一遍才能學到一些東西;因為面向的是Iot webserver的Fuzz因此需要對嵌入式設備中常用web開源框架有一些了解,這里是對于Boa框架的fuzz案例。

環境準備

  • qiling-dev branch:這里并沒有選擇直接pip安裝,方便修改源碼

  • AFL++:在python中可以import unicornafl就行

    git clone https://github.com/AFLplusplus/AFLplusplus.git
    make -C AFLplusplus
    cd AFLplusplus/unicorn_mode ; ./build_unicorn_support.sh
  • 一個坑是最好獲取版本高于3.15的cmake要不然編譯的時候有些cmake參數識別有問題,我遇到的就是:cmake -S unicorn/ -B unicorn/build -D BUILD_SHARED_LIBS=no問題

  • 需要對Qiling,AFL有些了解

Fuzz思路

Iot設備就連環境模擬都比較棘手就就更別說Fuzz了,但是Qiling提供的進程快照(snapshot)功能給了我們一個不錯的思路,這也是Qiling官方Fuzz案例的一個思路:即對某函數部分Fuzz(Partial Fuzz)

Tenda-AC15

Qiling使用4個腳本來實現對該款路由器上httpd程序的Fuzz

image-20221213114209793

首先是saver_tendaac15_httpd.py用于保存fuzz的起始狀態快照,主要代碼如下:

def save_context(ql, *args, **kw):
    ql.save(cpu_context=False, snapshot="snapshot.bin")

def check_pc(ql):
    print("=" * 50)
    print("Hit fuzz point, stop at PC = 0x%x" % ql.arch.regs.arch_pc)
    print("=" * 50)
    ql.emu_stop()


def my_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()

ql.hook_address(save_context, 0x10930):表示當程序跑到0x10930地址時調用save_context函數將保存此刻模擬狀態

但需要輸入來觸發程序按照預想的跑到0x10930位置,帶上面腳本跑起來后使用addressNat_overflow.sh觸發

#!/bin/sh

curl -v -H "X-Requested-With: XMLHttpRequest" -b "password=1234" -e http://localhost:8080/samba.html -H "Content-Type:application/x-www-form-urlencoded" --data "entrys=sync" --data "page=CCCCAAAA" http://localhost:8080/goform/addressNat

那么我們就獲得了模擬進程快照snapshot.bin之后fuzz就重復利用該文件啟動就行,對應fuzz_tendaac15_httpd.py

def main(input_file, enable_trace=False):
    ql = Qiling(["rootfs/bin/httpd"], "rootfs", verbose=QL_VERBOSE.DEBUG, console = True if enable_trace else False)

    # save current emulated status
    ql.restore(snapshot="snapshot.bin")

    # return should be 0x7ff3ca64
    fuzz_mem=ql.mem.search(b"CCCCAAAA")
    target_address = fuzz_mem[0]

    def place_input_callback(_ql: Qiling, input: bytes, _):
        _ql.mem.write(target_address, input)

    def start_afl(_ql: Qiling):
        """
        Callback from inside
        """
        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)

    try:
        ql.run(begin = 0x10930+4, end = 0x7a0cc+4)
        os._exit(0)
    except:
        if enable_trace:
            print("\nFuzzer Went Shit")
        os._exit(0)        

if __name__ == "__main__":
    if len(sys.argv) == 1:
        raise ValueError("No input file provided.")

    if len(sys.argv) > 2 and sys.argv[1] == "-t":
        main(sys.argv[2], enable_trace=True)
    else:
        main(sys.argv[1])
  • 恢復快照:ql.restore(snapshot="snapshot.bin")

  • 變異數據緩存定位:fuzz_mem=ql.mem.search(b"CCCCAAAA")

  • 以hook方式從起始地址附近的開始fuzz:ql.hook_address(callback=start_afl, address=0x10930+8)

最后開始Fuzz

#!/usr/bin/sh

AFL_DEBUG_CHILD_OUTPUT=1 AFL_AUTORESUME=1 AFL_PATH="$(realpath ./AFLplusplus)" PATH="$AFL_PATH:$PATH" ./AFLplusplus/afl-fuzz -i afl_inputs -o afl_outputs -U -- python3 ./fuzz_tendaac15_httpd.py @@

說實話這樣連最關鍵的fuzz范圍0x109300x7a0cc怎么來的都不知道當時逆向定位這兩個地址也是一頭霧水毫無特征,還是得自己實操

因此選定了Boa框架(之前了解過源碼)從零開始對其進行Fuzz

Boa Fuzz

選擇一個網上有許多漏洞分析的設備:vivetok 攝像頭,固件鏈接;而且webservre為Boa框架

Poc:

echo -en "POST /cgi-bin/admin/upgrade.cgi HTTP/1.0\nContent-Length:AAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIXXXX\n\r\n\r\n"  | ncat -v 192.168.57.20 80

Boa框架

主要處理邏輯在process_requests函數中:

           /*獲取就緒隊列并處理*/
    current = request_ready;

    while (current) {
        time(&current_time);
        if (current->buffer_end && /* there is data in the buffer */
            current->status != DEAD && current->status != DONE) {
            retval = req_flush(current);
            /*
             * retval can be -2=error, -1=blocked, or bytes left
             */
            if (retval == -2) { /* error */
                current->status = DEAD;
                retval = 0;
            } else if (retval >= 0) {
                /* notice the >= which is different from below?
                   Here, we may just be flushing headers.
                   We don't want to return 0 because we are not DONE
                   or DEAD */

                retval = 1;
            }
        } else {/*主要處理請求部分在這里*/
            switch (current->status) {
            case READ_HEADER:
            case ONE_CR:
            case ONE_LF:
            case TWO_CR:
                retval = read_header(current);    //解析request頭部,該函數類似與FILE_IO
                break;                            //函數request內部有8192+1字節的buffer,data的頭尾指針等,最終調用
            case BODY_READ:                       //bytes = read(req->fd, buffer + req->client_stream_pos, buf_bytes_left);讀取
                retval = read_body(current);
                break;
            case BODY_WRITE:
                retval = write_body(current);
                break;
            case WRITE:
                retval = process_get(current);
                break;
            case PIPE_READ:
                retval = read_from_pipe(current);
                break;
            case PIPE_WRITE:
                retval = write_from_pipe(current);
                break;
            case DONE:
                /* a non-status that will terminate the request */
                retval = req_flush(current);
                /*
                 * retval can be -2=error, -1=blocked, or bytes left
                 */
                if (retval == -2) { /* error */
                    current->status = DEAD;
                    retval = 0;
                } else if (retval > 0) {
                    retval = 1;
                }
                break;
            case DEAD:
                retval = 0;
                current->buffer_end = 0;
                SQUASH_KA(current);
                break;
            default:
                retval = 0;
                fprintf(stderr, "Unknown status (%d), "
                        "closing!\n", current->status);
                current->status = DEAD;
                break;
            }
        }

主要看中間的Switch case:

  • read_header:解析request頭部,該函數類似FILE_IO函數
  • request內部有8192+1字節的buffer,data的頭尾指針等,最終調用bytes = read(req->fd, buffer + req->client_stream_pos, buf_bytes_left);讀取client發送的請求
  • 會提取并解析頭部信息
  • 對于GET傳參,主要使用read_header, read_from_pipe, write_from_pipe完成cgi的調用
  • 對于POST傳參,主要調用read_header, read_body, write_body完成cgi調用

就拿read_header函數來說,廠商應該會在里面增加一些url過慮以及響應處理,在這個攝像頭中漏洞也確實出在這個函數:

image-20221213133117933

沒有對Content-Length成員做限制;根據源碼中提示字符串Unknown status (%d), closing可以輕松定位到這幾個函數:

image-20221213133545416

那么接下來就嘗試利用Qiling 啟動這個程序并且Partial Fuzz函數"read_header"

模擬啟動

模擬啟動的宗旨(我的)是遇到啥錯誤修最后一個報錯點

啟動模板:

import os, sys
sys.path.append('/home/iot/workspace/Emulator/qiling-dev')
from qiling import Qiling
from qiling.const import QL_INTERCEPT, QL_VERBOSE


def boa_run(path: list, rootfs: str, profile: str = 'default'):
    ql = Qiling(path, rootfs, profile=profile, verbose=QL_VERBOSE.OFF, multithread=False)
    """setup files"""
    ql.add_fs_mapper('/dev/null', '/dev/null')

    """hooks"""

    ql.run()


if __name__ == '__main__':
    os.chdir('/home/iot/workspace/Emulator/qiling-dev/vivetok')
    path = ['./rootfs/usr/sbin/httpd', "-c", "/etc/conf.d/boa", "-d"]
    rootfs = './rootfs'
    profile = './boa_arm.ql'
    boa_run(path=path, rootfs=rootfs, profile=profile)

嘗試啟動

首先遇到的是:gethostbyname:: Success

在IDA中定位到:

image-20221213134138571

函數原型:

struct hostent *gethostbyname(const char *hostname);
struct hostent{
    char *h_name;  //official name
    char **h_aliases;  //alias list
    int  h_addrtype;  //host address type
    int  h_length;  //address lenght
    char **h_addr_list;  //address list
}

獲取返回的結構體還挺復雜的,問題的原因是 在調用gethostname將獲得ql_vm作為主機名所以當以此調用gethostbyname無法獲得主機信息,所以hook這個函數,并提前開辟空間存放偽造信息:

"""
struct hostent{
    char *h_name;  //official name
    char **h_aliases;  //alias list
    int  h_addrtype;  //host address type
    int  h_length;  //address lenght
    char **h_addr_list;  //address list
}
"""
def hook_memSpace(ql: Qiling):
    ql.mem.map(0x1000, 0x1000, info='my_hook')
    data = struct.pack('<IIIII', 0x1100, 0x1100, AF_INET, 4, 0x1100)
    ql.mem.write(0x1000, data)
    ql.mem.write(0x1100, b'qiling')

def lib_gethostbyname(ql: Qiling):
    args = ql.os.resolve_fcall_params({'name':STRING})
    print('[gethostbyname]: ' + args['name'])
    ql.arch.regs.write('r0', 0x1000)

還有一個嚴重問題就是模擬過程中程序自動采用ipv6協議,這就很煩因為qiling的ipv6協議支持的不是很好

ipv6 socket

AttributeError: 'sockaddr_in' object has no attribute 'sin6_addr'

問題處在對ipv6的系統調用bind:

elif sa_family == AF_INET6 and ql.os.ipv6:
    sockaddr_in6 = make_sockaddr_in(abits, endian)
    sockaddr_obj = sockaddr_in6.from_buffer(data)

    port = ntohs(ql, sockaddr_obj.sin_port)
    host = inet6_ntoa(sockaddr_obj.sin6_addr.s6_addr)

    if ql.os.bindtolocalhost:
        host = '::1'

    if not ql.os.root and port <= 1024:
        port = port + 8000

def make_sockaddr_in(archbits: int, endian: QL_ENDIAN):
    Struct = struct.get_aligned_struct(archbits, endian)

    class in_addr(Struct):
        _fields_ = (
            ('s_addr', ctypes.c_uint32),
        )

    class sockaddr_in(Struct):
        _fields_ = (
            ('sin_family', ctypes.c_int16),
            ('sin_port',   ctypes.c_uint16),
            ('sin_addr',   in_addr),
            ('sin_zero',   ctypes.c_byte * 8)
        )

    return sockaddr_in

def make_sockaddr_in6(archbits: int, endian: QL_ENDIAN):
    Struct = struct.get_aligned_struct(archbits, endian)

    class in6_addr(Struct):
        _fields_ = (
            ('s6_addr', ctypes.c_uint8 * 16),
        )

    class sockaddr_in6(Struct):
        _fields_ = (
            ('sin6_family',   ctypes.c_int16),
            ('sin6_port',     ctypes.c_uint16),
            ('sin6_flowinfo', ctypes.c_uint32),
            ('sin6_addr',     in6_addr),
            ('sin6_scope_id', ctypes.c_uint32)
        )

    return sockaddr_in6

make_sockaddr_in, make_sockaddr_in6基于ctypes構造嚴格的sockaddr結構體,因為是ipv6所以得用make_sockaddr_in6

還有就是函數(function) inet6_ntoa: (addr: bytes) -> str需要bytes對象而sockaddr_obj.sin6_addr.s6_addr是cbytes類型所以得bytes轉

sockaddr_in6 = make_sockaddr_in6(abits, endian)
sockaddr_obj = sockaddr_in6.from_buffer(data)
port = ntohs(ql, sockaddr_obj.sin6_port)
host = inet6_ntoa(bytes(sockaddr_obj.sin6_addr.s6_addr))

OSError: [Errno 98] Address already in use

還是在調用bind時候,因為qiling會對低于1024的端口bind進行修改:

if not ql.os.root and port <= 1024:
        port = port + 8000

而后面還對8080端口進行一次bind,所以這里得改,然后其實就能進入核心處理邏輯了 :

image-20221213134113202

當然還得看看鏈接有沒有問題:嘗試訪問又出現問題

$ echo -en "GET /index.html HTTP/1.0\n\rContent-Length:20\n\r\n\r"  | nc -v ::1 9080
Connection to ::1 9080 port [tcp/*] succeeded!

File "/home/iot/workspace/Emulator/qiling-dev-stb/qiling/os/posix/syscall/socket.py", line 669, in ql_syscall_accept
    host, port = address
ValueError: too many values to unpack (expected 2)

ValueError: too many values to unpack (expected 2)

經調試原來在python中accept ipv6的連接后會返回一個長度為4的元組的address:

image-20221213134207632

同樣的問題還發生在ql_syscall_getsockname:sockname = sock.getsockname()

TypeError: expected c_ubyte_Array_16 instance, got int

[x]     Syscall ERROR: ql_syscall_accept DEBUG: expected c_ubyte_Array_16 instance, got int
Traceback (most recent call last):
  File "/home/iot/workspace/Emulator/qiling-dev-stb/qiling/os/posix/posix.py", line 280, in load_syscall
    retval = syscall_hook(self.ql, *params)
  File "/home/iot/workspace/Emulator/qiling-dev-stb/qiling/os/posix/syscall/socket.py", line 674, in ql_syscall_accept
    obj.sin6_addr.s6_addr = inet6_aton(str(host))
TypeError: expected c_ubyte_Array_16 instance, got int

解決:bytes轉cbyts類

obj.sin6_addr.s6_addr = (ctypes.c_ubyte * 16).from_buffer_copy(inet6_aton(str(host)).to_bytes(16, 'big'))

主要問題就這些(修了挺久的),然后就可以對一些函數進行fuzz了

Fuzz Partial

確定Fuzz范圍,這個范圍主要是給到ql_afl_fuzz函數,這里是打算Fuzz read_header函數(sub_17F80),那么從數據入口下手:

image-20221213135606979

讀取POST或者GET方法的http包那么肯定要解析處理的,處理完成返回一個狀態(源碼中retval)來指示下一步處理,找到退出點:

image-20221213135843221 因此要從0x180F8附近開始Fuzz,然后0x18398表示函數正常退出將執行下一輪fuzz

腳本模板:

import os, sys
sys.path.append('/home/iot/workspace/Emulator/qiling-dev')
from qiling.const import QL_INTERCEPT, QL_VERBOSE
from qiling import Qiling

from qiling.extensions.afl import ql_afl_fuzz


def main(input_file: str, trace: bool = False):
    ql = Qiling(['./rootfs/usr/sbin/httpd', "-c", "/etc/conf.d/boa", "-d"], rootfs='./rootfs', profile='./boa_arm.ql', verbose=QL_VERBOSE.OFF, console = True if trace else False)
    ql.restore(snapshot='./context.bin')

    def place_input_callback(_ql: Qiling, input: bytes, _):
        # print(b"**************** " + input)
        _ql.mem.write(target_addr, input)

    def start_afl(_ql: Qiling):
        """
        Callback from inside
        """
        ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[0x018398])

    ql.hook_address(callback=start_afl, address=0x180F8)

    try:
        # ql.debugger = True
        ql.run(begin=0x180F8)
        os._exit(0)
    except:
        if trace:
            print("\nFuzzer Went Shit")
        os._exit(0)  

if __name__ == "__main__":
    if len(sys.argv) == 1:
        raise ValueError("No input file provided.")

    os.chdir('/home/iot/workspace/Emulator/qiling-dev/vivetok')
    if len(sys.argv) > 2 and sys.argv[1] == "-t":
        main(sys.argv[2], trace=True)
    else:
        main(sys.argv[1])
  • ql.hook_address(callback=start_afl, address=0x180F8):在執行到0x180F8這個位置時調用start_afl函數
  • ql.run(begin=0x180F8):從0x180F8開始執行
  • ql_afl_fuzz:就是unicornafl提供的fuzz接口uc_afl_fuzz_custom的一個wrapper
  • place_input_callback:ql_afl_fuzz會調用的回調函數,負責寫入fuzz數據

Fuzz buf

根據網上的漏洞分析比對源碼框架,利用:

cho -en "POST /cgi-bin/admin/upgrade.cgi HTTP/1.0nContent-Length:AAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIXXXXnrnrn"  | nc -v ::1 9080

可以觸發漏洞,具體位于框架中http頭部解析函數:read_header,位于httpd中17F80位置

那么該如何fuzz呢,根據網上unicorn-afl官方用例和qiling官方用例:buf-fuzz,即定位代碼中讀取數據位置,然后讀取完后劫持搜索特定字符串定位fuzz的buff_addr,當然需要狀態保存(當然這個方法肯定不是很嚴謹,因此后面還會介紹劫持read函數方法)

快照

import os, sys, struct
from socket import AF_INET
sys.path.append('/home/iot/workspace/Emulator/qiling-dev')
from qiling import Qiling
from qiling.const import QL_INTERCEPT, QL_VERBOSE
from qiling.os.const import STRING
from unicorn.unicorn import UcError
"""
struct hostent{
    char *h_name;  //official name
    char **h_aliases;  //alias list
    int  h_addrtype;  //host address type
    int  h_length;  //address lenght
    char **h_addr_list;  //address list
}
"""
def hook_memSpace(ql: Qiling):
    ql.mem.map(0x1000, 0x1000, info='my_hook')
    data = struct.pack('<IIIII', 0x1100, 0x1100, AF_INET, 4, 0x1100)
    ql.mem.write(0x1000, data)
    ql.mem.write(0x1100, b'qiling')

def lib_gethostbyname(ql: Qiling):
    args = ql.os.resolve_fcall_params({'name':STRING})
    print('[gethostbyname]: ' + args['name'])
    ql.arch.regs.write('r0', 0x1000)


def saver(ql: Qiling):
    print('[!] Hit Saver 0x%X'%(ql.arch.regs.arch_pc))
    ql.save(cpu_context=False, snapshot='./context.bin')
    print(ql.mem.search(b'fuck'))


#[read(5,  0x4edca,  0x2000)] locate buf
def read_syscall(ql: Qiling, fd: int, buf: int, size: int, *args) -> None:
    print(f'[read({fd}, {buf: #x}, {size: #x})]')

def boa_run(path: list, rootfs: str, profile: str = 'default'):
    ql = Qiling(path, rootfs, profile=profile, verbose=QL_VERBOSE.OFF, multithread=False)
    """setup files"""
    ql.add_fs_mapper('/dev/null', '/dev/null')

    """set ram"""
    hook_memSpace(ql)

    """hooks"""
    ql.os.set_api('gethostbyname', lib_gethostbyname, QL_INTERCEPT.CALL)
    ql.os.set_syscall('read', read_syscall, QL_INTERCEPT.ENTER)

    """setup saver"""
    ql.hook_address(saver, 0x0180FC)        #read finish

    ql.run()

if __name__ == '__main__':
    os.chdir('/home/iot/workspace/Emulator/qiling-dev/vivetok')
    path = ['./rootfs/usr/sbin/httpd', "-c", "/etc/conf.d/boa", "-d"]
    rootfs = './rootfs'
    profile = './boa_arm.ql'
    boa_run(path=path, rootfs=rootfs, profile=profile)

然后使用poc觸發就行

fuzz

import os, sys, struct
import capstone as Cs
sys.path.append('/home/iot/workspace/Emulator/qiling-dev')
from qiling.const import QL_INTERCEPT, QL_VERBOSE
from qiling import Qiling
from qiling.extensions.afl import ql_afl_fuzz


def simple_diassembler(ql: Qiling, address: int, size: int, md: Cs) -> None:
    buf = ql.mem.read(address, size)

    for insn in md.disasm(buf, address):
        ql.log.debug(f':: {insn.address:#x} : {insn.mnemonic:24s} {insn.op_str}')

def main(input_file: str, trace: bool = False):
    ql = Qiling(['./rootfs/usr/sbin/httpd', "-c", "/etc/conf.d/boa", "-d"], rootfs='./rootfs', profile='./boa_arm.ql', verbose=QL_VERBOSE.OFF, console = True if trace else False)
    ql.restore(snapshot='./context.bin')

    fuzz_mem = ql.mem.search(b'fuck')

    target_addr = fuzz_mem[0]

    def place_input_callback(_ql: Qiling, input: bytes, _):
        # print(b"**************** " + input)
        _ql.mem.write(target_addr, input)


    def start_afl(_ql: Qiling):
        """
        Callback from inside
        """
        ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[0x018398])

    ql.hook_address(callback=start_afl, address=0x0180FC+4)
    # ql.hook_code(simple_diassembler, begin=0x0180FC, end=0x018600, user_data=ql.arch.disassembler)

    try:
        # ql.debugger = True
        ql.run(begin=0x0180FC+4, end=0x018600)    #注意arm函數返回地址比較奇怪,不一定在函數末尾
        os._exit(0)
    except:
        if trace:
            print("\nFuzzer Went Shit")
        os._exit(0)  

if __name__ == "__main__":
    if len(sys.argv) == 1:
        raise ValueError("No input file provided.")

    os.chdir('/home/iot/workspace/Emulator/qiling-dev/vivetok')
    if len(sys.argv) > 2 and sys.argv[1] == "-t":
        main(sys.argv[2], trace=True)
    else:
        main(sys.argv[1])

這里很坑的一點是,在漏洞中因為Content-Length成員不以\n結尾時就會讓v31等于0會讓strncpy報錯但是不一定是pc指針錯誤,而是某些指令地址操作數問題

v30 = strstr(haystack, "Content-Length");
v31 = strchr(v30, '\n');
v32 = strchr(v30, ':');
strncpy(dest, v32 + 1, v31 - (v32 + 1));

在源碼中AFL模塊調用以下函數完成fuzz執行:

def _dummy_fuzz_callback(_ql: "Qiling"):
            if isinstance(_ql.arch, QlArchARM):
                pc = _ql.arch.effective_pc
            else:
                pc = _ql.arch.regs.arch_pc
            try:
                _ql.uc.emu_start(pc, 0, 0, 0)
            except UcError as e:
                os.abort()              #添加部分
                return e.errno

因此添加os.abort通知AFL程序崩潰

效果

image-20221213140214049

Fuzz sys_read

上面直接對buf寫入Fuzz數據肯定不是一個很理想的辦法(比如Fuzz數據超出讀取長度),當然人家給的例子就是這么Fuzz的也不失一種方法;之后

就嘗試利用Qiling的系統調用劫持功能讓Fuzz效果更好。

從read函數調用處開始執行,在這之前劫持read函數調用讓程序直接讀取文件輸入:

def read_syscall(ql: Qiling, fd: int, buf: int, size: int, *args) -> int:
    # print(fd, buf, size)
    data = ql.os.stdin.read(size)
    # print(data)
    ql.mem.write(buf, data)
    return len(data)

def place_input_callback(_ql: Qiling, input: bytes, _):
    # print(b"**************** " + input)
    ql.os.stdin.write(input)

    return True


def start_afl(_ql: Qiling):
    """
    Callback from inside
    """
    ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[0x018398])

效果

同樣寫個腳本把服務并且設置debugger等待gdb連接:

image-20221213143927097

然后將crash中的數據發送:

image-20221213144007558

也確實觸發到了漏洞:

0x900a5d74 in strncpy () from target:/lib/libc.so.0
gef?  backtrace 
#0  0x900a5d74 in strncpy () from target:/lib/libc.so.0
#1  0x0001853c in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
gef?  

技巧

fuzz過程中不好調試連寫的harness有沒有效果都不知道,可以使用capstone同步解析執行匯編情況:

def simple_diassembler(ql: Qiling, address: int, size: int, md: Cs) -> None:
    buf = ql.mem.read(address, size)

    for insn in md.disasm(buf, address):
        ql.log.debug(f':: {insn.address:#x} : {insn.mnemonic:24s} {insn.op_str}')

參考


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2043/