Angr符号执行练习XorDDoS某样本字符串解密

admin 2025年4月28日23:59:09评论5 views字数 11035阅读36分47秒阅读模式

 

创建: 2025-04-24 22:26
更新: 2025-04-25 19:04
链接: https://scz.617.cn/unix/202504242226.txt

目录:

    ☆ XorDDoS某样本
    ☆ 用r2pipe模块静态分析
        1) 获取函数入口/出口地址
        2) 获取到指定函数的交叉引用
        3) 析取dec_conf()的参数
        4) static_analyses()
    ☆ 用angr模拟调用dec_conf()
        1) proj.factory.call_state
        2) proj.factory.callable
    ☆ r2pipe+angr
    ☆ 用angr模拟调用encrypt_code()
    ☆ 后记

☆ XorDDoS某样本

参看

XorDDoS僵尸网络家族的某样本
https://www.virustotal.com/gui/file/0e9e859d22b009e869322a509c11e342

有VirusTotal企业账号的,可下载该ELF样本,也可尝试从微步在线下载。

$ file -b 0e9e859d22b009e869322a509c11e342
ELF 32-bit LSB executable, Intel 80386, ..., statically linked, ..., not stripped

用IDA32反汇编,样本没有strip,留有调试符号。

0804CFA3 C7 44 24 08 0B 00 00 00     mov     dword ptr [esp+8], 0Bh
0804CFAB C7 44 24 04 B1 2F 0B 08     mov     dword ptr [esp+4], offset aM7a4nqNa_0 ; "m7A4nQ_/nA"
0804CFB3 8D 85 B3 EA FF FF           lea     eax, [ebp+var_154D]
0804CFB9 89 04 24                    mov     [esp], eax
0804CFBC E8 67 B2 FF FF              call    dec_conf
0804CFC1 C7 44 24 08 07 00 00 00     mov     dword ptr [esp+8], 7
0804CFC9 C7 44 24 04 BC 2F 0B 08     mov     dword ptr [esp+4], offset aMN3_0 ; "m [(n3"
0804CFD1 8D 85 B3 E9 FF FF           lea     eax, [ebp+var_164D]
0804CFD7 89 04 24                    mov     [esp], eax
0804CFDA E8 49 B2 FF FF              call    dec_conf

dec_conf(v23, "m7A4nQ_/nA", 11);
dec_conf(v22, "m [(n3", 7);
dec_conf(v21, "m6_6n3", 7);
dec_conf(v19, aM4s4nacNZv, 18);
dec_conf(v18, aMN4C, 17);
dec_conf(v17, "m.[$n3", 7);
dec_conf(v16, a6f6, 512);
dec_conf(v20, "m4S4nAC/nA", 11);

样本含有一些加密字符串,dec_conf()用于解密字符串。

08048228                         dec_conf proc
08048228 55                          push    ebp
08048229 89 E5                       mov     ebp, esp
0804822B 83 EC 18                    sub     esp, 18h
0804822E 8B 45 10                    mov     eax, [ebp+arg_8]
08048231 89 44 24 08                 mov     [esp+8], eax
08048235 8B 45 0C                    mov     eax, [ebp+arg_4]
08048238 89 44 24 04                 mov     [esp+4], eax
0804823C 8B 45 08                    mov     eax, [ebp+arg_0]
0804823F 89 04 24                    mov     [esp], eax
08048242 E8 09 E6 01 00              call    memmove
08048247 8B 45 10                    mov     eax, [ebp+arg_8]
0804824A 89 44 24 04                 mov     [esp+4], eax
0804824E 8B 45 08                    mov     eax, [ebp+arg_0]
08048251 89 04 24                    mov     [esp], eax
08048254 E8 9B 11 00 00              call    encrypt_code
08048259 B8 00 00 00 00              mov     eax, 0
0804825E C9                          leave
0804825F C3                          retn
0804825F                         dec_conf endp

int dec_conf(char *dst, char *src, int size )
{
    memmove( dst, src, size );
    /*
     * 就地修改dst,而非返回什么
     */
    encrypt_code( dst, size );
    return 0;
}

dst用于保存解密结果,src是固化在.rodata中的加密数据,size对应src的长度。dec_conf()实际调用encrypt_code()完成解密。

/*
 * 就地修改buf
 */
char *__cdecl encrypt_code(char *buf, int size)
{
    char *p;
    int i;

    p = buf;
    for ( i = 0; i < size; ++i )
        *p++ ^= xorkeys[i % 16];
    return buf;
}

080CF3E8 42 42 32 46 41 33 36 41…xorkeys db 'BB2FA36AAA9541F0'

encrypt_code()并不复杂,就是简单异或,xorkeys内置在ELF中,固定。但我们假设encrypt_code()很复杂,比如被控制流平坦化过,不想静态分析其逻辑,准备用angr模拟调用dec_conf()或encrypt_code(),黑盒调用,只关心in/out。

样本不只调用dec_conf()解密字符串,也会直接调用encrypt_code()解密字符串。下面是几处直接调用encrypt_code()解密字符串的地方:

08048C08 C7 44 24 08 0A 00 00 00     mov     dword ptr [esp+8], 0Ah
08048C10 C7 44 24 04 07 2D 0B 08     mov     dword ptr [esp+4], offset aM7a4nqNa ; "m7A4nQ_/nA"
08048C18 8D 85 F1 FA FF FF           lea     eax, [ebp+var_50F]
08048C1E 89 04 24                    mov     [esp], eax
08048C21 E8 2A DC 01 00              call    memmove
08048C26 C7 44 24 04 0A 00 00 00     mov     dword ptr [esp+4], 0Ah      ; int
08048C2E 8D 85 F1 FA FF FF           lea     eax, [ebp+var_50F]
08048C34 89 04 24                    mov     [esp], eax                  ; char *
08048C37 E8 B8 07 00 00              call    encrypt_code

0804F12F C7 44 24 08 00 02 00 00     mov     dword ptr [esp+8], 200h
0804F137 C7 44 24 04 4C 32 0B 08     mov     dword ptr [esp+4], offset unk_80B324C
0804F13F C7 04 24 C0 1C 0D 08        mov     dword ptr [esp], offset remotestr
0804F146 E8 05 77 01 00              call    memmove
0804F14B C7 44 24 04 00 02 00 00     mov     dword ptr [esp+4], 200h     ; int
0804F153 C7 04 24 C0 1C 0D 08        mov     dword ptr [esp], offset remotestr ; char *
0804F15A E8 95 A2 FF FF              call    encrypt_code

memmove(remotestr, &unk_80B324C, 512);
encrypt_code(remotestr, 512);

0804D093 C7 45 CC 00 00 00 00        mov     [ebp+var_34], 0
0804D09A EB 26                       jmp     short loc_804D0C2
0804D09C
0804D09C                         loc_804D09C:
0804D09C 8B 55 CC                    mov     edx, [ebp+var_34]
0804D09F 89 D0                       mov     eax, edx
0804D0A1 C1 E0 02                    shl     eax, 2
0804D0A4 01 D0                       add     eax, edx
0804D0A6 C1 E0 02                    shl     eax, 2
/*
 * daemonname位于.data,而非.rodata
 */
0804D0A9 05 20 F1 0C 08              add     eax, offset daemonname      ; "!#Ff3VE.-7"
0804D0AE C7 44 24 04 14 00 00 00     mov     dword ptr [esp+4], 14h      ; int
0804D0B6 89 04 24                    mov     [esp], eax                  ; char *
0804D0B9 E8 36 C3 FF FF              call    encrypt_code
0804D0BE 83 45 CC 01                 add     [ebp+var_34], 1
0804D0C2
0804D0C2                         loc_804D0C2:
0804D0C2 83 7D CC 16                 cmp     [ebp+var_34], 16h
0804D0C6 76 D4                       jbe     short loc_804D09C

for ( i = 0; i <= 22; ++i )
    encrypt_code(&daemonname[20 * i], 20);

还有其他调用encrypt_code()解密字符串的地方,但那些地方都是动态提供输入,不是固定串,此处略过。

☆ 用r2pipe模块静态分析

关于r2pipe模块,参看

《Angr符号执行练习--SecuInside 2016 mbrainfuzz》
https://scz.617.cn/unix/202503311347.txt

1) 获取函数入口/出口地址

将来angr模拟调用dec_conf(),至少有两种方案。一种需要知道函数入口/出口地址,另一种只需知道函数入口地址。

def get_func_info ( r2, func ) :
    cmd         = f"afij sym.{func}"
    info        = r2.cmd( cmd )
    info        = json.loads( info )
    info        = info[0]
    func_entry  = info['offset']
    func_exit   = info['offset'] + info['size'] - 1
    return ( func_entry, func_exit )

假设已打开r2句柄,此处简化处理,假设ret是最后一条指令。

2) 获取到指定函数的交叉引用

样本调用dec_conf()的模式是固定的,只要找到"call dec_conf"指令所在地址,可从附近的汇编指令析取dec_conf()的参数,比如加密字符串的地址、长度。通过交叉引用找出所有"call dec_conf"指令所在地址。

def find_xrefs_to_func ( r2, func ) :
    xrefs   = []
    cmd     = f"axtj sym.{func}"
    #
    # 返回str
    #
    info    = r2.cmd( cmd )
    #
    # 返回list
    #
    info    = json.loads( info )
    for item in info :
        xrefs.append( item['from'] )
    return xrefs

3) 析取dec_conf()的参数

def get_call_params ( r2, calladdr ) :
    cmd         = f"pdj -4 @ {calladdr}"
    info        = r2.cmd( cmd )
    info        = json.loads( info )
    return ( info[1]['val'], info[0]['val'] )

此实现只针对调用dec_conf()的情形,意思是,从"call dec_conf"向低址方向移动四条指令,反汇编这四条指令,分别析取第二条、第一条指令的立即数。

0804CFA3 C7 44 24 08 0B 00 00 00     mov     dword ptr [esp+8], 0Bh
0804CFAB C7 44 24 04 B1 2F 0B 08     mov     dword ptr [esp+4], offset aM7a4nqNa_0 ; "m7A4nQ_/nA"
0804CFB3 8D 85 B3 EA FF FF           lea     eax, [ebp+var_154D]
0804CFB9 89 04 24                    mov     [esp], eax
0804CFBC E8 67 B2 FF FF              call    dec_conf

假设处理上述代码片段,get_call_params()将返回(0x80b2fb1,0xb),此即一条加密字符串,分别是地址、长度。

4) static_analyses()

将前面的小模块整合到一起,完成r2pipe静态分析

def static_analyses ( binary ) :
    r2      = r2pipe.open( binary, flags=['-e','bin.relocs.apply=true','-e','log.quiet=true'] )
    r2.cmd( 'aaaa' )
    func    = 'dec_conf'
    info    = get_func_info( r2, func )
    xrefs   = find_xrefs_to_func( r2, func )
    parray  = []
    for addr in xrefs :
        params  = get_call_params( r2, addr )
        parray.append( params )
    r2.quit()
    #
    # 后面是直接调用encrypt_code()时的参数,同样可用来调用dec_conf()
    #
    # IDA手工分析后添加至此
    #
    parray.append( ( 0x80b2d07, 0xa ) )
    parray.append( ( 0x80b324c, 0x200 ) )
    for i in range( 23 ) :
        parray.append( ( 0x80cf120 + 20 * i, 20 ) )
    #
    # 入口、出口、参数
    #
    return ( info[0], info[1], parray )

用r2分析样本,比用angr的CFGFast分析样本快得多。

☆ 用angr模拟调用dec_conf()

参看

Source code for angr.callable
https://docs.angr.io/en/stable/_modules/angr/callable.html

https://docs.angr.io/en/stable/api.html#module-angr.callable

VPNFilter Stage 1 - [2018-05-28]
https://sh3ll.me/posts/vpnfilter-stage-1/

How Can I execute a function in angr using concrete value - [2023-07-24]
https://stackoverflow.com/questions/76757631/how-can-i-execute-a-function-in-angr-using-concrete-value

angr callable - Mahmoud Elfawair [2024-02-11]
https://mahmoudelfawair.medium.com/angr-callable-d51f568c78dc

angr至少有两种模拟调用dec_conf()的办法,分别是call_state、callable。前者控制粒度更细,比如执行到函数中部某个位置便停止模拟;后者使用起来更简洁。

1) proj.factory.call_state

def angr_dec_conf ( proj, dec_conf_entry, dec_conf_exit, src, size ) :
    dst         = proj.loader.extern_object.allocate( size )
    prototype   = angr.types.parse_type( 'int ( char *, char *, int )' )
    init_state  = proj.factory.call_state(
        dec_conf_entry,
        dst,
        src,
        size,
        prototype   = prototype,
        add_options = {
            angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
            angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
            angr.options.BYPASS_UNSUPPORTED_SYSCALL,
        }
    )
    sm          = proj.factory.simulation_manager( init_state )
    sm.explore( find=dec_conf_exit )
    if sm.found :
        state   = sm.found[0]
        src     = state.memory.load( src, size )
        src     = src.concrete_value.to_bytes( size, byteorder='big', signed=False )
        dst     = state.memory.load( dst, size )
        dst     = dst.concrete_value.to_bytes( size, byteorder='big', signed=False )
        return ( src, dst )

sm.explore()的find参数可位于函数中部某个位置,不一定是ret指令所在地址。

2) proj.factory.callable

def angr_dec_conf_a ( proj, dec_conf_entry, src, size ) :
    dst         = proj.loader.extern_object.allocate( size )
    prototype   = angr.types.parse_type( 'int ( char *, char *, int )' )
    #
    # 本例无需指定base_state
    #
    dec_conf    = proj.factory.callable(
        dec_conf_entry,
        prototype   = prototype,
        add_options = {
            angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
            angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
            angr.options.BYPASS_UNSUPPORTED_SYSCALL,
        }
    )
    dec_conf( dst, src, size )
    state       = dec_conf.result_state
    src         = state.memory.load( src, size )
    src         = state.solver.eval( src, cast_to=bytes )
    dst         = state.memory.load( dst, size )
    dst         = state.solver.eval( dst, cast_to=bytes )
    return ( src, dst )

用callable时,无需知道函数出口地址。

☆ r2pipe+angr

将前面的小模块整合到一起

def main ( argv ) :
    info    = static_analyses( argv[1] )
    proj    = angr.Project( argv[1], auto_load_libs=False )
    for params in info[2] :
        # tmp = angr_dec_conf( proj, info[0], info[1], params[0], params[1] )
        tmp = angr_dec_conf_a( proj, info[0], params[0], params[1] )
        dst = tmp[1]
        dst = dst[:dst.index( b'' )]
        print( f"{params[0]:#x} {dst}" )

正常的话,应该输出

0x80b2f31 b'/var/run/gcc.pid'
0x80b2f43 b'/lib/libudev.so'
0x80b2f54 b'/lib/'
0x80b2fb1 b'/usr/bin/'
0x80b2fbc b'/bin/'
0x80b2fc3 b'/tmp/'
0x80b2fca b'/var/run/gcc.pid'
0x80b2fdc b'/lib/libudev.so'
0x80b2fed b'/lib/'
0x80b2ff4 b'http://pcdown.gddos.com:8080/cfg.rar'
0x80b31f4 b'/var/run/'
0x80b344c b'/var/run/gcc.pid'
0x80b2d07 b'/usr/bin/'
0x80b324c b'soft8.gddos.com:25|103.233.83.245:25|baidu.gddos.com:25'
0x80cf120 b'cat resolv.conf'
0x80cf134 b'sh'
0x80cf148 b'bash'
0x80cf15c b'su'
0x80cf170 b'ps -ef'
0x80cf184 b'ls'
0x80cf198 b'ls -la'
0x80cf1ac b'top'
0x80cf1c0 b'netstat -an'
0x80cf1d4 b'netstat -antop'
0x80cf1e8 b'grep "A"'
0x80cf1fc b'sleep 1'
0x80cf210 b'cd /etc'
0x80cf224 b'echo "find"'
0x80cf238 b'ifconfig eth0'
0x80cf24c b'ifconfig'
0x80cf260 b'route -n'
0x80cf274 b'gnome-terminal'
0x80cf288 b'id'
0x80cf29c b'who'
0x80cf2b0 b'whoami'
0x80cf2c4 b'pwd'
0x80cf2d8 b'uptime'

☆ 用angr模拟调用encrypt_code()

def angr_encrypt_code ( proj, encrypt_code_entry, dst, size ) :
    #
    # 函数原型有变
    #
    prototype   = angr.types.parse_type( 'char * ( char *, int )' )
    encrypt_code    = proj.factory.callable(
        encrypt_code_entry,
        prototype   = prototype,
        add_options = {
            angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
            angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
            angr.options.BYPASS_UNSUPPORTED_SYSCALL,
        }
    )
    #
    # 测试表明,angr模拟调用时,dst在.data还是.rodata无影响,即使代码逻辑
    # 是就地修改dst,并不会触发写异常。非模拟调用时,dst位于.rodata肯定不
    # 行。这算是模拟调用的优势之一。
    #
    encrypt_code( dst, size )
    state       = encrypt_code.result_state
    dst         = state.memory.load( dst, size )
    dst         = state.solver.eval( dst, cast_to=bytes )
    return dst

☆ 后记

据小宋说,XorDDoS家族现仍在活跃,但流行变种已将原始版本的rootkit部分移除。

本文目的并非分析XorDDoS样本,仅视之为Angr符号执行的练习目标,毕竟是现实世界逆向工程真实案例,而非CTF案例。

本文学习目的是黑盒式模拟调用关键函数,尝试获取函数结果。

原文始发于微信公众号(青衣十三楼飞花堂):Angr符号执行练习XorDDoS某样本字符串解密

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月28日23:59:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Angr符号执行练习XorDDoS某样本字符串解密https://cn-sec.com/archives/4009446.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息