IDA flare-emu示例

2021-02-16 青衣十三樓飛花堂

標題: IDA flare-emu示例


☆ 簡介

IDA靜態分析時想調用目標binary中一段代碼,或者說模擬/仿真執行目標binary中一段代碼,這個需求比較常見。最簡單的一種場景是,想檢查目標binary中某種算法函數的in/out,想自己提供in,讓其過一遍算法函數,檢查out。稍微複雜點的場景,目標binary中有私有字符串反混淆函數,想調用之,自動反混淆目標binary中的一段數據區。

過去為了應對這種需求,可以將彙編代碼片段扒出來重編譯,可以用Python模擬實現,是否可行視該段代碼複雜度而不同。還有更野蠻的方案,2006年hume和我逆向Skype時需要調用若干Skype.exe中的代碼片段,hume把Skype.exe簡單改成Skype.dll的效果。後來搞一些ELF時我也用過類似的技術思路,好處是不用扒代碼出來,懶人超愛。

不久前bluerust向我推薦使用flare-emu應對這種需求,原話大致如下:

》這小半年斷斷續續接觸到之前主觀上不太願意碰的一些東西,包括llvm、docker、unicorn等等。除了開眼界外,對業務水平、生產力提高也是幫助巨大。像模擬執行,以前我就恃著自己寫過幾年彙編,經常硬生生把彙編代碼從IDA摳出,當庫函數用。現在不幹了,幾句python讓unicorn跑去,就地解決。還是腦袋似木瓜,很多年輕的娃本科畢業就把這些玩意玩得爛熟了。

》flare-emu是個IDA插件,封裝了unicorn。我用了幾輪,感覺十分友好,封裝的文件就三個,十分方便二次開發。逆向時,有時需要調用原來的一段代碼,改寫當然可以,扒出來重編譯也是條路,但模擬執行就地解決在大多數情況下可能是最優解。

bluerust是我前同事,後來曾入職過FireEye幾年,現下在北美逍遙自在。若scz曾經是前浪的話,bluerust就是將前者拍死在沙灘上的後浪。現在這位後浪亦將步入中年,遲早會被後後浪拍死在沙灘上。他將來怎麼死的我不知道,反正現如今我對他的各種技術推薦甚為重視,畢竟老年程式設計師眼界縮窄,再不虛心好學的話,只會加快自身被遺棄於技術垃圾堆的進程。不是所有的前浪都像hume那樣,浪奔浪流,萬裡濤濤江水永不休。

本文不從上帝視角展開,沒有直接給精簡演示方案,會介紹完整學習過程,稍顯冗長,諸君可依據自身技術背景進行跳躍式閱讀。

主體技術方案由bluerust提供。

☆ 尋找練手對象

決定用x64/RedHat上的md5sum測試flare-emu的效果。md5sum必然包含MD5算法相關函數,較容易定位它們,然後用flare-emu模擬執行,檢驗MD5算法的in/out。

1) 獲取md5sum源碼

這與flare-emu無關,獲取md5sum源碼只是便於在本文演示中減少一些解釋性文字。


放狗搜之,比如:

http://ftp.scientificlinux.org/linux/scientific/7.2/SRPMS/vendor/coreutils-8.22-23.el7.src.rpm

md5sum源碼位置:


2) DIGEST_STREAM()/md5_stream()

看過md5sum.c才知道,從md5sum到sha384sum用一套模具,調的都是DIGEST_STREAM(),區別只是:

#if HASH_ALGO_MD5
# define DIGEST_STREAM md5_stream
#elif HASH_ALGO_SHA384
# define DIGEST_STREAM sha384_stream
#endif

int md5_stream ( FILE *stream, void *resblock )
{
    struct md5_ctx  ctx;
    size_t          sum;
    char           *buffer  = malloc( BLOCKSIZE + 72 );

    if ( !buffer )
        return 1;
    md5_init_ctx( &ctx );
    while ( 1 )
    {
        size_t  n;

        sum = 0;
        while ( 1 )
        {
            n       = fread( buffer + sum, 1, BLOCKSIZE - sum, stream );
            sum    += n;
            if ( sum == BLOCKSIZE )
                break;
            if ( n == 0 )
            {
                /*
                 * Check for the error flag IFF N == 0, so that we don't
                 * exit the loop after a partial read due to e.g., EAGAIN
                 * or EWOULDBLOCK.
                 */
                if ( ferror( stream ) )
                {
                    free( buffer );
                    return 1;
                }
                goto process_partial_block;
            }
            /*
             * We've read at least one byte, so ignore errors. But always
             * check for EOF, since feof may be true even though N > 0.
             * Otherwise, we could end up calling fread after EOF.
             */
            if ( feof( stream ) )
                goto process_partial_block;
        }
        md5_process_block( buffer, BLOCKSIZE, &ctx );
    }

process_partial_block:

    if ( sum > 0 )
        md5_process_bytes( buffer, sum, &ctx );
    md5_finish_ctx( &ctx, resblock );
    free( buffer );
    return 0;
}

☆ IDA反彙編時識別MD5算法

假設不知道目標binary包含哪些知名算法,IDA 7.1及之前版本用findcrypt.plw,之後的IDA用"IDA Signsrch"或findcrypt-yara。

1) IDA Signsrch/signsrch.exe

https://github.com/nihilus/IDA_Signsrch
http://aluigi.altervista.org/mytoolz/signsrch.zip
http://aluigi.altervista.org/mytoolz/signsrch.sig.zip (特徵資料庫)

"IDA Signsrch"有BUG,掃md5sum未能找到MD5算法特徵常量,bluerust也曾跟我吐槽說它不靈。但我在下文用它找到過ZIP算法特徵常量:

《喚醒沉睡的木馬》
http://scz.617.cn:8/windows/202011231525.txt

"IDA Signsrch"是可執行版本的IDA插件移植版,原版無BUG:

$ signsrch.exe -e md5sum

  offset   num  description [bits.endian.size]
  ----
  00402c32 1018 MD5 digest [32.le.272&]
  00402c47 2053 RIPEMD-128 InitState [32.le.16&]
  00406b40 1038 padding used in hashing algorithms (0x80 0 ... 0) [..64]

signsrch.exe找到MD5算法特徵常量。指定"-e"時,把目標binary當成PE/ELF,給出的offset是RVA而不是文件偏移,0x402c32對應md5_init_ctx()。

2) findcrypt-yara

https://github.com/polymorf/findcrypt-yara

這也是一個IDA插件。若你的IDA、Python都是安裝版,跳過本小節內容。假設你是"Portable IDA+IDAPython"愛好者,參看:

《Portable Python》
http://scz.617.cn:8/python/202011191444.txt

findcrypt-yara依賴yara-python模塊,在有Visual Studio 2019 社區版的環境中執行:

$ python.exe -m pip install yara-python

不要求手動設置編譯環境,主要是得到

<Python39>\Lib\site-packages\yara.cp39-win_amd64.pyd

複製yara.cp39-win_amd64.pyd到

<IDA>\Lib\site-packages\yara.cp39-win_amd64.pyd

複製findcrypt3.py、findcrypt3.rules到

<IDA>\plugins\

Edit
 Plugins
   Findcrypt (Ctrl+Alt+F)

findcrypt-yara沒有明顯BUG,在"Findcrypt results"窗口顯示找到的MD5算法特徵值,雙擊跳過去。

☆ flare-emu示例

關於flare-emu的安裝,參[1]

1) Hex-Rays下的md5_stream()

int __fastcall md5_stream(FILE *stream, void *resblock)
{
  int *buffer; // r12
  int ret; // eax
  size_t sum; // rbx
  size_t n; // rax
  char ctx[168]; // [rsp+0h] [rbp-E8h]

  buffer = (int *)malloc(0x8048uLL);
  ret = 1;
  if ( buffer )
  {
    sum = 0LL;
    md5_init_ctx(ctx);
    while ( 1 )
    {
      while ( 1 )
      {
        n = fread_unlocked((char *)buffer + sum, 1uLL, 0x8000 - sum, stream);
        sum += n;
        if ( sum != 0x8000 )
          break;
        md5_process_block(buffer, 0x8000LL, ctx);
        sum = 0LL;
      }
      if ( !n )
        break;
      /*
       * feof()
       */
      if ( stream->_flags & 0x10 )
        goto process_partial_block;
    }
    /*
     * ferror()
     */
    if ( stream->_flags & 0x20 )
    {
      free(buffer);
      return 1;
    }
process_partial_block:
    if ( sum )
      md5_process_bytes(buffer, sum, ctx);
    md5_finish_ctx(ctx, (__int64)resblock);
    free(buffer);
    ret = 0;
  }
  return ret;
}

為了聚焦演示flare-emu,上面的F5結果已重命名過,真實世界沒有這麼理想的F5結果。注意到ferror()、feof()在彙編代碼中已inline展開。

最初想得挺簡單,假設md5sum的實現是讀文件到buf,然後對buf求MD5,此時求MD5的代碼將只涉及算法,不涉及文件I/O或其他什麼系統調用、庫函數調用。若真是如此實現,非常適合演示flare-emu,事實上在逆向工程中很多驗證in/out的需求就是這類情形。對於前述理想情形驗證in/out,還可以在調試器中直接修改PC寄存器指向算法函數入口,臨時組織函數形參,當然這已超出靜態分析範疇。

起初我沒有去找md5_stream()的源碼,只在F5中看到上述代碼,發現有I/O,就問bluerust,是不是沒法用flare-emu模擬執行md5_stream();他說可以,然後給我秀了一番。

2) emu_md5_stream.py

#!/usr/bin/env python3
# -*- encoding: cp936 -*-

#
# Author: bluerust, scz
#

#
# IDA 7.5.1+Python 3.9
#
# 對"c:\windows\system.ini"求MD5,在IDA中看到類似輸出
#
# Opening 0x7fff1e33fa90
# ret = 0
# 00000000: 28 6A 9E DB 37 9D C3 42  3A 52 8B 08 64 A0 F1 11  (j..7..B:R..d...
# 00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  .
# Closing 0x7fff1e33fa90
#
# 對比如下命令的求值結果
#
# $ wsl md5sum /mnt/c/windows/system.ini
# 286a9edb379dc3423a528b0864a0f111  /mnt/c/windows/system.ini
#

import sys, ctypes, inspect, traceback, functools
import hexdump
import flare_emu

#
# dir(ctypes)
# dir(ctypes.cdll)
#
cso                 = ctypes.cdll.msvcrt

#
# https://docs.python.org/3/library/ctypes.html
#
# 參看"Fundamental data types",這樣可以避免NoneType。
#
# >>> ctypes.sizeof( VOIDP )
# 8
#
class VOIDP ( ctypes.c_void_p ) :

    #
    # 有這個才可以對VOIDP類型求int()
    #
    def __int__ ( self ) :
        return self.value

#
# end of class VOIDP
#

#
# 為被調函數指定函數原型,即指定restype(返回值類型)、argtypes(參數類形),
# 否則極易觸發"段錯誤"。
#
# FILE *fopen(const char *pathname, const char *mode);
# size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
# int fclose(FILE *stream);
# void *calloc(size_t nmemb, size_t size);
# void free(void *ptr);
# void *memmove(void *dest, const void *src, size_t n);
#
# >>> ctypes.sizeof( ctypes.c_size_t )
# 8
#
cso.fopen.argtypes      = ( ctypes.c_char_p, ctypes.c_char_p )
cso.fopen.restype       = VOIDP

cso.fread.argtypes      = ( VOIDP, ctypes.c_ulong, ctypes.c_ulong, VOIDP )
cso.fread.restype       = ctypes.c_size_t

#
# 該行只能用[],不能用()
#
cso.fclose.argtypes     = [ VOIDP ]
cso.fclose.restype      = ctypes.c_int

cso.calloc.argtypes     = ( ctypes.c_size_t, ctypes.c_size_t )
cso.calloc.restype      = VOIDP

cso.free.argtypes       = [ VOIDP ]
cso.free.restype        = None

#
# 其實有ctypes.memmove()、ctypes.memset(),不需要自己準備這兩個函數
#
cso.memmove.argtypes    = ( VOIDP, VOIDP, ctypes.c_size_t )
cso.memmove.restype     = VOIDP

#
# Be called before each instruction is emulated.
#
def PrivateInstructionHook ( unicornObject, address, instructionSize, userData ) :
    #
    # 如果不需要跟蹤到指令級別,注釋掉這條語句
    #
    print( "=> %#x [%s]" % ( address, ida_lines.tag_remove( ida_lines.generate_disasm_line( address ) ) ) )
    pass
#
# end of PrivateInstructionHook
#

#
# How can I find the number of arguments of a Python function
# https://stackoverflow.com/questions/847936/how-can-i-find-the-number-of-arguments-of-a-python-function
#
def GetFuncParamNum ( func ) :
    sig = inspect.signature( func )
    return len( sig.parameters )
#
# end of GetFuncParamNum
#

#
# 關於裝飾器參看
#
# https://www.programiz.com/python-programming/decorator (入門推薦)
# https://www.python-course.eu/python3_decorators.php
# https://www.runoob.com/w3cnote/python-func-decorators.html
#
# 這是一個通用裝飾器函數
#
def PrivateHook ( func ) :

    #
    # args will be the tuple of positional arguments and kwargs will be
    # the dictionary of keyword arguments.
    #
    # 下面這句隱式包含
    #
    # func_wrapper.__name__   = func.__name__
    # func_wrapper.__doc__    = func.__doc__
    # func_wrapper.__module__ = func.__module__
    #
    @functools.wraps(func)
    def func_wrapper ( *args, **kwargs ) :
        #
        # print( func.__name__ )
        # print( args )
        # print( kwargs )
        #
        # 參看flare_emu_hooks.py,本例所涉及的函數原型是
        #
        # (eh, address, argv, funcName, userData)
        #
        eh          = args[0]
        address     = args[1]
        #
        # flare-emu無法獲知形參類型,它只是按一般ABI固定提取8個實參,遇上
        # 浮點傳參或是Delphi那種調用約定,得自己提取實參
        #
        argv        = args[2]
        funcName    = args[3]
        userData    = args[4]
        try :
            #
            # 減1是減去eh所佔形參
            #
            n   = GetFuncParamNum( func ) - 1
            #
            # 傳遞不定長形參
            #
            ret = func( eh, *argv[:n] )
            #
            # hook_free()會返回None
            #
            if ret is not None :
                eh.uc.reg_write( eh.regs["ret"], ret )
            return ret
        except :
            traceback.print_exc()
            sys.exit()
    #
    # end of func_wrapper
    #

    return func_wrapper
#
# end of PrivateHook
#

#
# 原始意圖是讓FILE結構保持同步,但實際上有巨坑等著我們。考慮在Windows上模
# 擬執行ELF的情形,兩種OS的FILE結構定義並不相同。
#
def update_stream ( eh, stream ) :
    f   = eh.ftable[stream]
    buf = ctypes.create_string_buffer( 256 )
    assert( buf )
    cso.memmove( buf, f, 256 )
    #
    # 若想檢查FILE結構,讓下述語句生效
    #
    # hexdump.hexdump( buf )
    #
    # 寫入Guest進程空間
    #
    eh.writeEmuMem( stream, buf.raw[:256] )
#
# end of update_stream
#

#
# 這是bluerust的版本
#
@PrivateHook
def hook_fread ( eh, ptr, size, nmemb, stream ) :
    f   = eh.ftable[stream]
    p   = ctypes.create_string_buffer( size * nmemb )
    n   = cso.fread( p, size, nmemb, f )
    update_stream( eh, stream )
    #
    # p.raw是bytes類型,ptr指向Guest進程空間
    #
    eh.writeEmuMem( ptr, p.raw[:size * n] )
    return n
#
# end of hook_fread
#

#
# 這是scz的版本,二者都可以
#
# @PrivateHook
# def hook_fread ( eh, ptr, size, nmemb, stream ) :
#     f   = eh.ftable[stream]
#     p   = cso.calloc( nmemb, size )
#     n   = cso.fread( p, size, nmemb, f )
#     update_stream( eh, stream )
#     eh.writeEmuMem( ptr, ctypes.string_at( p, size * n ) )
#     cso.free( p )
#     return n
# #
# # end of hook_fread
# #

@PrivateHook
def hook_fclose( eh, stream ):
    f   = eh.ftable[stream]
    ret = cso.fclose( f )
    update_stream( eh, stream )
    return ret
#
# end of hook_fclose
#

@PrivateHook
def hook_free ( eh, ptr ) :
    #
    # 這種返回None
    #
    return

def main () :

    #
    # 如果前面沒有"@functools.wraps(func)",此處將輸出"func_wrapper",反之
    # 輸出"hook_fread"
    #
    # print( hook_fread.__name__ )
    #

    eh              = flare_emu.EmuHelper()

    #
    # 參看flare_emu.py,已經hook malloc(),不必自己幹這事
    #
    # 目標binary中feof()、ferror()已inline展開,無法用eh.addApiHook()
    #

    #
    # 此處的hook_fread()函數原型是
    #
    # (eh, address, argv, funcName, userData)
    #
    # 此處寫"_fread_unlocked"、"_free"也可以
    #
    eh.addApiHook( "fread_unlocked", hook_fread )
    eh.addApiHook( "free", hook_free )

    f               = cso.fopen( b"c:\\windows\\system.ini", b"rb" )
    if not f :
        print( "Unable to open file" )
        return
    print( "Opening", hex( int( f ) ) )

    #
    # 獲取被模擬函數的起始地址
    #
    startAddr       = eh.analysisHelper.getNameAddr( "md5_stream" )
    assert( startAddr and startAddr != 0 )
    #
    # 準備被模擬函數的形參,在Guest進程空間分配內存
    #
    FILE            = eh.allocEmuMem( 256 )
    resblock        = eh.allocEmuMem( 0x20 )

    #
    # 這是自己臨時增加的屬性,用於存放文件句柄(FILE*)
    #
    eh.ftable       = dict()
    eh.ftable[FILE] = f

    update_stream( eh, FILE )

    #
    # 參看flare_emu.py
    #
    # 在x64/Win10上用寄存器傳遞兩個實參,模擬/仿真執行md5_stream()
    #
    # eh.emulateRange( startAddr, skipCalls=False, instructionHook=PrivateInstructionHook, registers={'arg1':FILE, 'arg2':resblock} )
    #
    # 如果不需要跟蹤到指令級別,注釋掉上面這條語句,換用下面這條語句
    #
    eh.emulateRange( startAddr, skipCalls=False, registers={'arg1':FILE, 'arg2':resblock} )
    #
    # 獲取模擬/仿真執行md5_stream()返回值
    #
    ret             = eh.getRegVal( "rax" )
    print( "ret =", ret )
    #
    # 獲取MD5結果
    #
    md5             = eh.getEmuBytes( resblock, 0x20 )
    hexdump.hexdump( md5 )
    #
    # 本例只有一個句柄需要關閉
    #
    for k,v in eh.ftable.items() :
        if v :
            cso.fclose( v )
            print( "Closing", hex( int( v ) ) )
#
# end of main
#

if __name__ == '__main__' :
    main()

該腳本事實上有重大BUG,但陰差陽錯間BUG並未影響最終模擬執行結果,這事後面再細說。

Alt-F7加載emu_md5_stream.py

若在update_stream()中輸出FILE結構的內容,將看到三次

3) 用windbg調試IDA對emu_md5_stream.py的加載執行

tasklist | findstr ida64
"X:\Green\Windows Kits\10\x64\Debuggers\x64\cdb.exe" -noinh -snul -hd -o -p <ida64 pid>

.prompt_allow +reg +ea +dis;rm 0xa
.load jsprovider.dll;.scriptload dbghelper_20201205.js
bp msvcrt!fopen "dx @$scriptContents.BreakEndWithAEx(@rcx,\"system.ini\",true,false);.if(@$t19==0x9c85130d){gc}"

當ida64進程試圖打開system.ini時斷下來。不要照搬調試命令,按原始意圖換成自己環境中的等價命令。

為什麼攔截msvcrt!fopen()呢?首先emu_md5_stream.py中通過ctypes調過該函數,其次Process Monitor調用棧回溯中看到它。為什麼用windbg?因為Process Monitor看不到更多細節,比如fopen()返回值、FILE結構內容等等。

> da @rcx
0000025e`4883bd10  "c:\windows\system.ini"

> kpn
 # Child-SP          RetAddr           Call Site
00 000000b9`6f3f9578 00007fff`1b204461 msvcrt!fopen
01 000000b9`6f3f9580 00007fff`1b20418d libffi_7!ffi_prep_go_closure+0x71
02 000000b9`6f3f95b0 00007fff`1b204042 libffi_7!ffi_call_go+0x13d
03 000000b9`6f3f9600 00007fff`16472bd2 libffi_7!ffi_call+0x12
04 000000b9`6f3f9640 00007fff`164728c8 _ctypes+0x2bd2
05 000000b9`6f3f97a0 00007fff`164725ab _ctypes+0x28c8
06 000000b9`6f3f98d0 00007fff`1015410c _ctypes+0x25ab
07 000000b9`6f3f9980 00007fff`101d3047 python39!PyObject_MakeTpCall+0x14c
...
20 000000b9`6f3face0 00000000`762255c9 python39!PyObject_CallFunctionObjArgs+0x2d
21 000000b9`6f3fad10 00000000`762215ba idapython3_64+0x55c9
22 000000b9`6f3fb1c0 00007ff6`2af9c086 idapython3_64+0x15ba
23 000000b9`6f3fb240 00007ff6`2af9ef4a ida64_exe+0x18c086
...
29 000000b9`6f3fb5d0 00000000`767c81b2 Qt5Core!QT::QMetaObject::activate+0x591
...
41 000000b9`6f3ffe40 00007fff`1eb47034 ida64_exe+0x229002
42 000000b9`6f3ffe80 00007fff`1fa9d0d1 KERNEL32!BaseThreadInitThunk+0x14
43 000000b9`6f3ffeb0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

讓fopen()完成,查看其返回值及FILE結構:

> g poi(@rsp)
> r rax
rax=00007fff1e33fa90

> db @rax l 0n256
00007fff`1e33fa90  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  .
00007fff`1e33faa0  00 00 00 00 00 00 00 00-01 00 00 00 03 00 00 00  .
...
00007fff`1e33fb80  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  .

攔截fclose(),查看關閉前的FILE結構:

> bp msvcrt!fclose ".if(@rcx!=0x7fff1e33fa90){gc}.else{db @rcx l 0x100}"
> g
00007fff`1e33fa90  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  .
00007fff`1e33faa0  00 00 00 00 00 00 00 00-11 00 00 00 03 00 00 00  .
...
00007fff`1e33fb80  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  .

fopen()返回值、FILE結構與IDA中輸出相符。

☆ FILE結構

1) FILEStructureTest.c

#if 0

x64/RedHat gcc 4.8.5

gcc -Wall -pipe -O3 -s -o FILEStructureTest_linux FILEStructureTest.c
gcc -Wall -pipe -O0 -g -o FILEStructureTest_linux FILEStructureTest.c

$ ./FILEStructureTest_linux FILEStructureTest.c
sizeof( FILE ) = 216
OFFSETOF( FILE*, _flags ) = 0

Visual Studio 2019 社區版

cl.exe FILEStructureTest.c /FeFILEStructureTest_windows.exe /Zi /FdFILEStructureTest_windows.pdb /nologo /Os /Gs65536 /W4 /WX /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /link /machine:x64 /pdbaltpath:FILEStructureTest_windows.pdb /RELEASE /opt:ref
editbin.exe /dynamicbase:no FILEStructureTest_windows.exe

"/pdbaltpath"是link.exe的參數,使得將來.exe中的.pdb只有指定名字,而不是缺
省的絕對路徑,減少信息洩露。

editbin.exe禁止對FILEStructureTest_windows.exe啟用ASLR,便於調試。

$ FILEStructureTest_windows.exe FILEStructureTest.c
sizeof( FILE ) = 8
OFFSETOF( FILE*, _Placeholder ) = 0

#endif

/*
 * 抑制VS 2019關於fopen()的安全警告
 */
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>

#define OFFSETOF(TYPE, MEMBER)  ((size_t)&((TYPE)0)->MEMBER)
#define BLOCKSIZE               32768

static void hexdump
(
    FILE           *out,
    unsigned char  *in,
    size_t          insize,
    size_t          count,
    size_t          offset
)
{
    size_t          k, j, i;

    if ( insize <= 0 || count <= 0 || NULL == in || NULL == out )
    {
        return;
    }
    i       = 0;
    for ( k = insize / count; k > 0; k--, offset += count )
    {
        fprintf( out, "%016zx:", offset );
        for ( j = 0; j < count; j++, i++ )
        {
            fprintf( out, " %02x", in[i] );
        }
        fprintf( out, "  " );
        i  -= count;
        for ( j = 0; j < count; j++, i++ )
        {
            if ( ( in[i] >= ' ' ) && ( in[i] < 0x7f ) )
            {
                fprintf( out, "%c", in[i] );
            }
            else
            {
                fprintf( out, "." );
            }
        }
        fprintf( out, "\n" );
    }  /* end of for */
    k       = insize - i;
    if ( k <= 0 )
    {
        return;
    }
    fprintf( out, "%016zx:", offset );
    for ( j = 0 ; j < k; j++, i++ )
    {
        fprintf( out, " %02x", in[i] );
    }
    i      -= k;
    for ( j = count - k; j > 0; j-- )
    {
        fprintf( out, "   " );
    }
    fprintf( out, "  " );
    for ( j = 0; j < k; j++, i++ )
    {
        if ( ( in[i] >= ' ' ) && ( in[i] < 0x7f ) )
        {
            fprintf( out, "%c", in[i] );
        }
        else
        {
            fprintf( out, "." );
        }
    }
    fprintf( out, "\n" );
    return;
}  /* end of hexdump */

#ifdef WIN32
#pragma warning( push )
#pragma warning( disable : 4100 )
#endif

/*
 * VS 2019如下告警被抑制
 *
 * FILEStructureTest.c(num): warning C4100: 'argc': unreferenced formal parameter
 *
 * 聚焦測試,未做各種安全檢查
 */
int main ( int argc, char * argv[] )
{
    char           *filename    = argv[1];
    FILE           *stream      = fopen( filename, "rb" );
    size_t          sum;
    unsigned char  *buffer      = malloc( BLOCKSIZE + 72 );
    size_t          offset      = 0;

    while ( 1 )
    {
        size_t  n;

        sum     = 0;
        while ( 1 )
        {
            n       = fread( buffer + sum, 1, BLOCKSIZE - sum, stream );
            sum    += n;
            if ( sum == BLOCKSIZE )
                break;
            if ( n == 0 )
            {
                if ( ferror( stream ) )
                {
                    free( buffer );
                    return 1;
                }
                goto process_partial_block;
            }
            if ( feof( stream ) )
                goto process_partial_block;
        }
        hexdump( stdout, buffer, BLOCKSIZE, 16, offset );
        offset += BLOCKSIZE;
    }

process_partial_block:

    if ( sum > 0 )
        hexdump( stdout, buffer, sum, 16, offset );
    free( buffer );
    fclose( stream );

#ifdef WIN32
    /*
     * C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\corecrt_wstdio.h
     * C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\mbstring.h
     */
    printf( "sizeof( FILE ) = %zu\n", sizeof( FILE ) );
    printf( "OFFSETOF( FILE*, _Placeholder ) = %zu\n", OFFSETOF( FILE*, _Placeholder ) );
#else
    /*
     * /usr/include/libio.h
     */
    printf( "sizeof( FILE ) = %zu\n", sizeof( FILE ) );
    printf( "OFFSETOF( FILE*, _flags ) = %zu\n", OFFSETOF( FILE*, _flags ) );
#endif

    return 0;
}

#ifdef WIN32
#pragma warning( pop )
#endif

2) Linux的FILE結構

vi /usr/include/libio.h

struct _IO_FILE {
  int _flags;           /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
...
};

ferror()、feof()這些運行時庫函數屏蔽了FILE結構細節,正經編程時不需要了解FILE結構細節,更不應該直接操作FILE結構。但是,對於emu_md5_stream.py這種場景,逆向工程時不得不直面FILE結構細節。

反彙編md5sum時注意到ferror()、feof()已inline展開:

/*
 * ferror()
 */
0000000000403840 F6 45 00 20                 test    byte ptr [rbp+0], 20h

/*
 * feof()
 */
00000000004037D0 F6 45 00 10                 test    byte ptr [rbp+0], 10h

此時無法通過eh.addApiHook()安裝Hook模擬ferror()、feof()。bluerust直接複製Host進程空間的FILE結構到Guest進程空間,就是應對前述inline展開。就ferror()、feof()而言,它們只操作偏移0處的_flags成員的最低字節,可以只向Guest空間複製1位元組,而不是複製整個FILE結構。

反彙編FILEStructureTest_linux,注意到ferror()、feof()以動態連結的運行時庫函數方式出現,並未inline展開,估計靜態編譯時有可能inline展開。

3) Windows的FILE結構

查看Visual Studio 2019 社區版中這兩個頭文件:

C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\corecrt_wstdio.h
C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt\mbstring.h

#ifndef _FILE_DEFINED
    #define _FILE_DEFINED
    typedef struct _iobuf
    {
        void* _Placeholder;
    } FILE;
#endif

而在VC 6時代,是這樣定義的:

#ifndef _FILE_DEFINED
struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
        };
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif

VS 2019的定義非常具有迷惑性,起初我以為_Placeholder指向一個更不透明的內部數據結構。但反彙編FILEStructureTest_windows.exe後發現我想多了,事實上FILE結構仍同VC 6時代。_Placeholder真地就如其名所言,只是個佔位成員。微軟認為FILE結構是未文檔化的,不應該直接操作它,包括sizeof(FILE)這種都是不應有的操作,乾脆在頭文件中徹底屏蔽了FILE結構細節,只能通過反彙編觀察其內部細節。

x64的_flag成員位於偏移0x14,佔4位元組。ferror()、feof()會訪問_flag成員:

/*
 * ferror()
 */
0000000140003241 8B 41 14        mov     eax, [rcx+14h]
0000000140003244 C1 E8 04        shr     eax, 4
0000000140003247 83 E0 01        and     eax, 1

/*
 * feof()
 */
0000000140003215 8B 41 14        mov     eax, [rcx+14h]
0000000140003218 C1 E8 03        shr     eax, 3
000000014000321B 83 E0 01        and     eax, 1

Windows的_flag成員偏移不同於Linux的_flags成員偏移,並且ferror()、feof()具體訪問的二進位位也不一樣,前者整體左移了一位。

4) emu_md5_stream.py中update_stream()

考慮這樣一種場景,md5_stream()本來是在x64/Linux中運行的,現在在x64/Win10中用flare-emu模擬執行它。

Linux、Windows的FILE結構完全不一樣,像emu_md5_stream.py中update_stream()那樣簡單複製FILE結構,沒有意義,更有可能導致不可預期的混亂。

單就md5_stream()這一場景而言,應該在update_stream()中完成Windows _flag到Linux _flags的映射,這才是完備有效的模擬。此次並未涉及FILE結構其他成員,很容易重新實現update_stream(),留給讀者自己完成。

5) 為什麼emu_md5_stream.py有BUG仍然得到正確結果

flare-emu可以在彙編指令級別跟蹤,下列代碼會顯示每一條被模擬執行的彙編指令:

def PrivateInstructionHook ( unicornObject, address, instructionSize, userData ) :
    print( "=> %#x [%s]" % ( address, ida_lines.tag_remove( ida_lines.generate_disasm_line( address ) ) ) )

eh.emulateRange( startAddr, skipCalls=False, instructionHook=PrivateInstructionHook, registers={'arg1':FILE, 'arg2':resblock} )

輸出類似這樣:

=> 0x4037ba [call    _fread_unlocked]
=> 0x4037bf [add     rbx, rax]
=> 0x4037c2 [cmp     rbx, 8000h]
=> 0x4037c9 [jz      short loc_403818]
=> 0x4037cb [test    rax, rax]
=> 0x4037ce [jz      short loc_403840]
=> 0x403840 [test    byte ptr [rbp+0], 20h]
=> 0x403844 [jz      short loc_4037D6]

通過這招,可以看到發生了什麼:

int __fastcall md5_stream(FILE *stream, void *resblock)
{
...
  if ( buffer )
  {
...
    while ( 1 )
    {
      while ( 1 )
      {
        n = fread_unlocked((char *)buffer + sum, 1uLL, 0x8000 - sum, stream);
        sum += n;
        if ( sum != 0x8000 )
          break;
        md5_process_block(buffer, 0x8000LL, ctx);
        sum = 0LL;
      }
      /*
       * 第二次到達此處時,n為0,break,跳去檢查ferror()。
       */
      if ( !n )
        break;
      /*
       * 緩衝區夠大,一次就讀完了整個文件。但模擬執行時feof()判斷不為真,
       * 因為偏移0處的字節值為0。繼續while循環。
       */
      if ( stream->_flags & 0x10 )
        goto process_partial_block;
    }
    /*
     * 模擬執行時ferror()判斷不為真,因為偏移0處的字節值為0。幸運地離開。
     */
    if ( stream->_flags & 0x20 )
    {
      free(buffer);
      return 1;
    }
process_partial_block:
    if ( sum )
      md5_process_bytes(buffer, sum, ctx);
    md5_finish_ctx(ctx, (__int64)resblock);
    free(buffer);
    ret = 0;
  }
  return ret;
}

模擬執行時feof()、ferror()依次訪問了錯誤的_flags成員,由於其固定為0,對於md5_stream()具體實現來說,未影響最終MD5結果。

事實上bluerust最早實現的emu_md5_stream.py連update_stream()都沒有,也求得正確MD5值。然後他在靜態審計中意識到應該有update_stream(),就是前面演示的版本。即使這樣,仍然在特定場景中存在BUG。emu_md5_stream.py誤打誤撞逃過一劫。

後來bluerust解釋,FILE結構的平臺差異我是有想到的,我對FILE結構非常熟悉,但我當時以為那個.i64對應一個PE文件,第二覺得要映射結構好煩啊,作為DEMO,就別折騰了。

☆ 後記

被模擬代碼如果有I/O、作業系統相關的動作或其他更複雜的什麼,是模擬執行還是動態調試,需要具體情況具體分析,選用較優解。函數調用可以Hook,inline展開則涉及內部數據結構,如果非要模擬執行,務必深刻理解上下文後謹慎處理。

flare-emu小巧精悍,相比之下另一些模擬框架顯得重型,是否適用於你的目標場景需要另行評估。不要相信它們宣稱的NB,在逆向工程的世界裡,永遠有一些意想不到的坑等著你。

flare-emu的iterate()功能也很實用。舉個例子,目標binary中有個私有解碼函數對binary中混淆存放的字符串進行解碼,iterate()通過分析交叉引用信息自動定位該解碼函數的主調位置,根據ABI自動分析、抽取形參,自動調用我們提供的回調函數;回調函數中可以調用解碼函數獲取反混淆後的明文字符串,在IDA中自動增加注釋。這種功能都有重大假設,ABI就是其中之一,當目標binary不滿足這些先驗假設時,就需要其他Hacking。本文未就iterate()進行示例,相比之下emu_md5_stream.py已把最困難的部分示例清楚了。

相關測試用例:

http://scz.617.cn:8/python/202012021733.7z

如果以上帝視角展開,本文將精簡許多,但學習過程中林林總總的坑比精簡的結論更有價值。

☆ 參考資源

相關焦點

  • Cloudflare
    https://my.oschina.net/u/4396881/blog/3375249 Cloudflare
  • Flan Scan:Cloudflare開源的輕量級網絡漏洞掃描程序
    日前,Cloudflare宣布開源其內部的輕型網絡漏洞掃描工具Flan Scan。Flan Scan是一款基於Nmap打包的Python漏洞掃描程序。基於Nmap的開源強大,靈活性,Cloudflare利用他取代了之前使用昂貴的安全廠商專業產品,大大的降低了運營成本,並提供了無與倫比的部署靈活性,功能修改,漏洞庫快速更新(基於CVS開源資料庫)等優勢。
  • Cloudflare 宣布支持 gRPC
    於是 Cloudflare 決定著手解決此問題,引入對 gRPC 的支持,以保護和改善在網絡上運行 gRPC API 的體驗。根據 Cloudflare 的計劃,在添加 gRPC 支持後,WAF 用於檢查傳入的 gRPC 請求,用戶可使用託管規則或制定自己的規則;可配置多個 gRPC 後端以處理負載,讓 Cloudflare 在它們之間分配負載;
  • Cloudflare 放棄谷歌 reCAPTCHA,遷移到 hCaptcha
    Cloudflare 客戶 。而鑑於 reCAPTCHA 有效、可以擴展且免費提供的因素,Cloudflare 自成立以來就一直使用谷歌的 reCAPTCHA 服務。現如今,針對棄用 reCAPTCHA 而改用 hCaptcha 一事,Cloudflare 則表示,「多年來,隱私和屏蔽問題足以使我們考慮從 reCAPTCHA 切換」。從早期開始,一些 Cloudflare 客戶就對使用其 Google 服務來提供驗證碼表示擔憂,隱私問題也引起了人們越來越多的關注。
  • SAST Weekly|靜態反編譯軟體IDA使用簡介
    首先要看這個遊戲的主體代碼在哪裡,解包後發現是il2cpp編譯的(簡單介紹一下,il2cpp是把c#編譯後的il字節碼翻譯成cpp然後編譯,在跨平臺上提高性能用),因此可以確定代碼在libil2cpp.so中,因此我們用ida打開這個文件。    隨即ida將會極為迅速的(大約兩小時)進行反編譯操作,反編譯完成後,看到函數表,可能就懵了。
  • Cloudflare 推出域名註冊服務,表示只收取成本價
    繼於2016年針對大型企業推出主打安全的 Cloudflare Registrar 域名註冊服務之後,Cloudflare 上周更新了該服務,表示將以成本價為旗下用戶提供域名註冊服務,聲明不賺取利潤,且承諾不會逐年加價。
  • 眼科實用英語242-Aqueous flare
    Visual impairment depends on the intensity of the flare. It is a sign of intraocular inflammation.當裂隙燈光束直接斜向照射虹膜平面、前房,可見散射的光線。房水閃輝是由於房水中蛋白含量增加造成的,通常含有炎症細胞。視力下降的程度取決於閃輝的程度,它是眼內炎症的表現。
  • flair 和 flare,發音一樣,意思不一樣,怎麼區分呢
    跟 bare 和 bear 的區別一樣,flair 和 flare 的發音也一樣,意思完全不一樣, 那如何區分它們呢?二、flare 作名詞,意為「旺火,火光,閃光裝置,照明彈,喇叭形「等,例如:There was a sudden flare when she threw the petrol onto the fire.當她把汽油潑到火上時,突然發出一聲閃光。
  • 創始人親述:衰退中誕生的Cloudflare如何從0到IPO
    阿爾法公社說:2009年在經濟衰退中誕生,2019年在紐交所上市,Cloudflare是一個在困難環境中崛起的絕佳樣本。Cloudflare的聯合創始人、CEO Matthew Prince,近日在訪談中親述了Cloudflare的創業歷史和崛起的關鍵因素,值得現在面臨同樣環境的創始人們學習參考。
  • Cloudflare 要推新公共 DNS 服務,地址是 1.1.1.1
    提供內容傳遞網絡和阻斷攻擊保護服務的 Cloudflare 正準備與亞太網絡資訊中心(Asia-Pacific Network
  • Cloudflare發布OdoH,改善DNS安全和隱私
    日前,Cloudflare發布了一項用於改善DNS安全和隱私的新的DNS標準ODoH。標準由Cloudflare,Apple和Fastly的工程師共同撰寫,標準通過將IP位址與查詢分隔開,防止相關信息的洩露。Cloudflare也在Github開源了協議實現和客戶端的原始碼,可以讓大家自己試運行和驗證OdoH服務。
  • 國外伺服器|免費CDN加速CloudFlare加速和防護(免費站必需)
    cloudflare是一款免費的CDN工具,CloudFlare可以幫助受保護站點抵禦包括拒絕服務攻擊(DenialofService)在內的大多數網絡攻擊,確保該網站長期在線,同時提升網站的性能、訪問速度以改善訪客體驗。
  • Cloudflare如何分析每秒上百萬的DNS查詢
    本文是Cloudflare在處理DNS查詢分析的時候,作出的技術選型和踩過的坑。上周五,我們宣布了所有Cloudflare DNS分析工具。 由於我們的規模很大(當你讀完這篇文章的時候,Cloudflare DNS將處理數以百萬計的DNS查詢) 我們必須非常有創意的解決該問題。 在本文中,我們將介紹DNS分析工具的組件,這些組件幫助我們每月處理數以萬億計的日誌。
  • 免費網站加速哪家強,CloudFlare CDN加速詳細操作拿走不謝
    截止目前Cloudflare已擁有77個位於全球各地的數據中心,其中包括亞太地區的韓國、日本、香港、臺北、新加坡等。國內的話CloudFlare據說是與百度合作,也就是百度雲加速。CDN加速僅僅是CloudFlare的一項業務,DDoS保護才是Cloudflare最大的吸引力,如果你的網站正在遭受DDOS攻擊接入到CloudFlare即可享受免費的防護。
  • Cloudflare 推家庭 1.1.1.1 公共 DNS,阻止惡意軟體和成人內容
    2018 年的 4 月 1 日愚人節,Cloudflare 宣布推出 1.1.1.1 公共 DNS 服務。
  • 逆向大法好——實戰中的IDA和OD一把梭
    Ok,上ida,按g鍵,輸入地址:0051158B往上翻,查看函數頭(地址為5114A4):分析sub_5114A4函數。
  • [原]排錯實戰——拯救加載調試符號失敗的IDA
    IDA應該也支持加載PDB,通過查看IDA安裝目錄下的idahelp.chm(打開後搜索PDB即可找到相關說明)發現還真支持。但是當我加載符號的時候,卻失敗了。本文記錄了整個調查過程。效果對比 先放兩張對比圖,大家直觀感受下區別。沒有調試符號的幫助的情況下,我們看到的效果:有了調試符號的幫助的情況下,我們看到的效果:
  • OKR示例
    我們創建了OKR示例庫,以便你可以輕鬆入門並編寫自己的OKR。對於CFR(對話,反饋和認可),我們提供了指南,模板和最佳實踐,這將使OKR和CFR的定位,培訓和制度化變得容易。「O」—目標(在整個公司範圍內透明且可見):我們要去哪裡?「KR」—關鍵結果:我們如何知道我們是否到達那裡?任務(要做的事情,進行中並完成):我要怎麼做才能到達那裡?
  • 【Access聯合查詢示例】三張表合一的示例
    UNION ALL       SELECT 車號,日期,0 AS 維修費,燃油費,0 as 橋路費 FROM 燃油費       UNION ALL       SELECT 車號,日期,0 AS 維修費,0 as 燃油費,橋路費 FROM 橋路費)  AS t GROUP BY t.車號, t.日期;示例下載