【软件逆向-Angr符号执行练习--XorDDoS某样本字符串解密】此文章归类为:软件逆向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 创建: 2025-04-24 22:26更新: 2025-04-25 19:04目录: ☆ 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某样本
参看
1 2 | XorDDoS僵尸网络家族的某样本https://8d2K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4k6A6M7Y4g2K6N6r3!0@1j5h3I4Q4x3X3g2U0L8$3@1`./gui/file/0e9e859d22b009e869322a509c11e342 |
有VirusTotal企业账号的,可下载该ELF样本,也可尝试从微步在线下载。
1 2 | $ file -b 0e9e859d22b009e869322a509c11e342ELF 32-bit LSB executable, Intel 80386, ..., statically linked, ..., not stripped |
用IDA32反汇编,样本没有strip,留有调试符号。
1 2 3 4 5 6 7 8 9 10 | 0804CFA3 C7 44 24 08 0B 00 00 00 mov dword ptr [esp+8], 0Bh0804CFAB 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], eax0804CFBC E8 67 B2 FF FF call dec_conf0804CFC1 C7 44 24 08 07 00 00 00 mov dword ptr [esp+8], 70804CFC9 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], eax0804CFDA E8 49 B2 FF FF call dec_conf |
1 2 3 4 5 6 7 8 | 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()用于解密字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 08048228 dec_conf proc08048228 55 push ebp08048229 89 E5 mov ebp, esp0804822B 83 EC 18 sub esp, 18h0804822E 8B 45 10 mov eax, [ebp+arg_8]08048231 89 44 24 08 mov [esp+8], eax08048235 8B 45 0C mov eax, [ebp+arg_4]08048238 89 44 24 04 mov [esp+4], eax0804823C 8B 45 08 mov eax, [ebp+arg_0]0804823F 89 04 24 mov [esp], eax08048242 E8 09 E6 01 00 call memmove08048247 8B 45 10 mov eax, [ebp+arg_8]0804824A 89 44 24 04 mov [esp+4], eax0804824E 8B 45 08 mov eax, [ebp+arg_0]08048251 89 04 24 mov [esp], eax08048254 E8 9B 11 00 00 call encrypt_code08048259 B8 00 00 00 00 mov eax, 00804825E C9 leave0804825F C3 retn0804825F dec_conf endp |
1 2 3 4 5 6 7 8 9 | 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()完成解密。
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* * 就地修改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;} |
1 | 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()解密字符串的地方:
1 2 3 4 5 6 7 8 9 | 08048C08 C7 44 24 08 0A 00 00 00 mov dword ptr [esp+8], 0Ah08048C10 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], eax08048C21 E8 2A DC 01 00 call memmove08048C26 C7 44 24 04 0A 00 00 00 mov dword ptr [esp+4], 0Ah ; int08048C2E 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 |
1 2 3 4 5 6 7 | 0804F12F C7 44 24 08 00 02 00 00 mov dword ptr [esp+8], 200h0804F137 C7 44 24 04 4C 32 0B 08 mov dword ptr [esp+4], offset unk_80B324C0804F13F C7 04 24 C0 1C 0D 08 mov dword ptr [esp], offset remotestr0804F146 E8 05 77 01 00 call memmove0804F14B C7 44 24 04 00 02 00 00 mov dword ptr [esp+4], 200h ; int0804F153 C7 04 24 C0 1C 0D 08 mov dword ptr [esp], offset remotestr ; char *0804F15A E8 95 A2 FF FF call encrypt_code |
1 2 | memmove(remotestr, &unk_80B324C, 512);encrypt_code(remotestr, 512); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 0804D093 C7 45 CC 00 00 00 00 mov [ebp+var_34], 00804D09A EB 26 jmp short loc_804D0C20804D09C0804D09C loc_804D09C:0804D09C 8B 55 CC mov edx, [ebp+var_34]0804D09F 89 D0 mov eax, edx0804D0A1 C1 E0 02 shl eax, 20804D0A4 01 D0 add eax, edx0804D0A6 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 ; int0804D0B6 89 04 24 mov [esp], eax ; char *0804D0B9 E8 36 C3 FF FF call encrypt_code0804D0BE 83 45 CC 01 add [ebp+var_34], 10804D0C20804D0C2 loc_804D0C2:0804D0C2 83 7D CC 16 cmp [ebp+var_34], 16h0804D0C6 76 D4 jbe short loc_804D09C |
1 2 | for ( i = 0; i <= 22; ++i ) encrypt_code(&daemonname[20 * i], 20); |
还有其他调用encrypt_code()解密字符串的地方,但那些地方都是动态提供输入,不是固定串,此处略过。
☆ 用r2pipe模块静态分析
关于r2pipe模块,参看
1 2 | 《Angr符号执行练习--SecuInside 2016 mbrainfuzz》https://scz.617.cn/unix/202503311347.txt |
1) 获取函数入口/出口地址
将来angr模拟调用dec_conf(),至少有两种方案。一种需要知道函数入口/出口地址,另一种只需知道函数入口地址。
1 2 3 4 5 6 7 8 | 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"指令所在地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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()的参数
1 2 3 4 5 | 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"向低址方向移动四条指令,反汇编这四条指令,分别析取第二条、第一条指令的立即数。
1 2 3 4 5 | 0804CFA3 C7 44 24 08 0B 00 00 00 mov dword ptr [esp+8], 0Bh0804CFAB 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], eax0804CFBC E8 67 B2 FF FF call dec_conf |
假设处理上述代码片段,get_call_params()将返回(0x80b2fb1,0xb),此即一条加密字符串,分别是地址、长度。
4) static_analyses()
将前面的小模块整合到一起,完成r2pipe静态分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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()
参看
1 2 3 4 5 6 7 8 9 10 11 12 13 | Source code for angr.callablehttps://docs.angr.io/en/stable/_modules/angr/callable.htmlhttps://docs.angr.io/en/stable/api.html#module-angr.callableVPNFilter 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-valueangr callable - Mahmoud Elfawair [2024-02-11]https://mahmoudelfawair.medium.com/angr-callable-d51f568c78dc |
angr至少有两种模拟调用dec_conf()的办法,分别是call_state、callable。前者控制粒度更细,比如执行到函数中部某个位置便停止模拟;后者使用起来更简洁。
1) proj.factory.call_state
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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
将前面的小模块整合到一起
1 2 3 4 5 6 7 8 9 | 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'\0' )] print( f"{params[0]:#x} {dst}" ) |
正常的话,应该输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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'f80K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4m8U0k6r3!0%4L8W2)9J5k6h3N6V1k6r3!0K6i4K6u0W2j5$3!0E0i4K6y4m8z5o6l9^5x3q4)9J5c8X3y4X3k6#2)9J5k6i4u0S2M7W2)9J5y4H3`.`.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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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案例。
本文学习目的是黑盒式模拟调用关键函数,尝试获取函数结果。
方便测试,附件是样本,infected
更多【软件逆向-Angr符号执行练习--XorDDoS某样本字符串解密】相关视频教程:www.yxfzedu.com