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

目录:

☆ 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
https://www.virustotal.com/gui/file/cad80071b9af3fa742ec7fbbeae0e2ffe2566742b20bfdf436b8138da3fd20e9
————————————————————————–

有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 )
————————————————————————–

☆ 用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’\0′ )]
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案例。

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

Spread the word. Share this post!

Meet The Author