挖矿病毒当前仍然是Linux服务器的流行威胁之一,挖矿病毒在攻防对抗中不断演化,为了使挖矿活动更隐蔽,产生了各种规避安全软件检测与查杀的手段,如劫持动态链接库、篡改系统命令、使用rootkit模块、挂载/proc目录、卸载杀毒软件等各种方法来隐藏自身并持久化。挖矿病毒经常需要使用特定脚本来启动挖矿进程和释放文件,而对于挖矿脚本,当前流行的挖矿家族有不少使用了SHC(Shell Script Compiler)工具对挖矿脚本打包来规避检测以及提高逆向难度,SHC是一个用于将Shell脚本编译成C语言源代码并进一步生成可执行的二进制文件(ELF格式)的工具,通过这种方式,挖矿脚本可以被打包成一个看似正常的二进制程序,而真正的脚本代码则被加密存储,只有在运行起来后才会进行解密执行,这使得安全软件更难以通过传统的基于脚本文件内容的检测方法来识别恶意行为。同时,由于生成的是编译后的二进制文件,并不是直接可读的脚本文件,这也增加部分逆向分析的难度。
shc介绍
shc是一个开源在GitHub的项目,地址 https://github.com/neurobin/shc
安装
在基于Debian的系统,可以使用以下命令直接安装:
sudo apt-get install shc
也可以直接下载源码进行编译安装或者直接下载二进制文件来使用:
wget https://github.com/neurobin/shc/archive/refs/tags/4.0.3.tar.gz tar xzvf 4.0.3.tar.gz cd shc-4.0.3 ./configure make sudo make install
使用
SHC的基本用法是:
shc -f script.sh
这将生成一个名为script.sh.x的二进制文件和一个script.sh.x.c的C语言源码。script.sh.x就是脚本的打包后的ELF文件。
除此之外,shc还有一些其他的选项
shc -f script.sh -o binary # -o 指定生成的二进制文件名 shc -U -f script.sh -o binary # -U 无法追踪选项 (阻止strace, ptrace 等命令追踪) shc -H -f script.sh -o binary # -H 无需root权限即可开启的无法追踪选项 (仅支持无参数bourne shell脚本(sh脚本))
后面的两个选项提供了额外的安全机制,阻止追踪,进一步提升逆向分析的难度,其中开启-U选项执行脚本生成的二进制文件需要root权限,而-H选项可以无需root权限达到阻止追踪的效果,但仅为实验性的功能,可能并不支持所有系统,兼容性相对较差。
shc源码分析
由于shc是先将脚本编译打包为C语言代码然后再编译成二进制文件,因此我们可以通过shc编译出的C源码来对其的解密脚本并执行的动作一探究竟。
在上文的介绍中,提到了将script.sh编译为二进制文件时会附带产生一个script.sh.x.c的源码,这个源码即是产生的二进制文件的源码。通过分析这个源码,即可知道编译成二进制文件的脚本的解密执行过程。
由于篇幅有限,因此本文仅对解密和执行中的关键步骤进行介绍,其他步骤暂不作过多分析,可自行参考项目源码。
解密流程
shc生成的C源码中,加密的脚本通常以一系列的字符数组的形式存在。这些字符数组使用rc4算法加密并在程序运行时被解密,还原成原始的shell脚本。
如果未开启HARDENING
标志,解密执行流程主要在xsh
函数中实现的,下面是主要的解密逻辑,text
变量即为加密的脚本文件,在下面通过arc4
函数解密后即可还原原始脚本。
if (ret) { arc4(rlax, rlax_z); if (!rlax[0] && key_with_file(shll)) return shll; arc4(opts, opts_z); #if HARDENING arc4_hardrun(text, text_z); exit(0); /* Seccomp Sandboxing - Start */ seccomp_hardening(); #endif arc4(text, text_z); // text为原始脚本 arc4(tst2, tst2_z); key(tst2, tst2_z); arc4(chk2, chk2_z); if ((chk2_z != tst2_z) || memcmp(tst2, chk2, tst2_z)) return tst2; /* Prepend hide_z spaces to script text to hide it. */ scrpt = malloc(hide_z + text_z); if (!scrpt) return 0; memset(scrpt, (int) ' ', hide_z); memcpy(&scrpt[hide_z], text, text_z); } else { /* Reexecute */ if (*xecc) { scrpt = malloc(512); if (!scrpt) return 0; sprintf(scrpt, xecc, me); } else { scrpt = me; } }
但需要注意的是正常执行中并不是第一次执行时就能执行到这一步解密操作,这里有一个if
判断,只有当ret
变量大于0时才会执行上述所说的操作,对脚本文件进行解密。
回到上面ret
变量赋值的代码,ret
变量的值为chkenv
函数的返回值,chkenv
主要检测环境变量中是否存在指定变量名的变量,如果获取为空则返回0,将会重新执行自身并设置环境变量,这样在下次的chkenv
就能正常获取设置的环境变量了,这些操作应该也是用来避免追踪的,第一次启动的进程并不执行脚本。
char * xsh(int argc, char ** argv) { ...... ret = chkenv(argc); ...... } int chkenv(int argc) { char buff[512]; unsigned long mask, m; int l, a, c; char * string; extern char ** environ; mask = (unsigned long)getpid(); stte_0(); key(&chkenv, (void*)&chkenv_end - (void*)&chkenv); key(&data, sizeof(data)); key(&mask, sizeof(mask)); arc4(&mask, sizeof(mask)); sprintf(buff, "x%lx", mask); string = getenv(buff); #if DEBUGEXEC fprintf(stderr, "getenv(%s)=%sn", buff, string ? string : "<null>"); #endif l = strlen(buff); if (!string) { /* 1st */ sprintf(&buff[l], "=%lu %d", mask, argc); putenv(strdup(buff)); return 0; // 环境变量获取为空则返回0 } c = sscanf(string, "%lu %d%c", &m, &a, buff); if (c == 2 && m == mask) { /* 3rd */ rmarg(environ, &string[-l - 1]); return 1 + (argc - a); } return -1; }
通过动态调试可以很容易获取到解密的脚本,首先需要在判断ret
的语句下断点,然后手动设置IP寄存器或者直接修改ret
变量,运行arc4
函数解密text
后即可在内存中获取到解密的shell脚本,实际的样本中基本是没有符号的,但代码结构比较类似调试起来也没有区别。
HARDENING标志下的执行流程
-H 选项可以开启HARDENING标志。在script.sh.x.c搜索HARDENING
这个字符串,可以发现有多个相关的宏定义,开启这个标志后就多了一些额外的安全措施阻止程序被追踪,同时开启后脚本解密执行的流程也发生了改变,因为开启HARDENING
标志后这个宏定义就会展开,解密执行流程直接在其中的arc4_hardrun
函数中进行了。
#if HARDENING arc4_hardrun(text, text_z); exit(0); /* Seccomp Sandboxing - Start */ seccomp_hardening(); #endif arc4(text, text_z); // text为原始脚本
arc4_hardrun
函数中可以看到,解密并执行脚本的操作是在fork出的子进程中执行的,并且运行完成后就退出了,开启HARDENING
标志后同样可以通过调试在内存中读取到脚本,但是需要注意和跳过的点更多了。
void arc4_hardrun(void * str, int len) { //Decode locally char tmp2[len]; char tmp3[len+1024]; memcpy(tmp2, str, len); unsigned char tmp, * ptr = (unsigned char *)tmp2; int lentmp = len; int pid, status; pid = fork(); shc_x_file(); if (make()) {exit(1);} setenv("LD_PRELOAD","/tmp/shc_x.so",1); if(pid==0) { //Start tracing to protect from dump & trace if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) { kill(getpid(), SIGKILL); _exit(1); } //Decode Bash while (len > 0) { indx++; tmp = stte[indx]; jndx += tmp; stte[indx] = stte[jndx]; stte[jndx] = tmp; tmp += stte[indx]; *ptr ^= stte[tmp]; ptr++; len--; } //Do the magic sprintf(tmp3, "%s %s", "'********' 21<<<", tmp2); //Exec bash script //fork execl with 'sh -c' system(tmp2); //Empty script variable memcpy(tmp2, str, lentmp); //Clean temp remove("/tmp/shc_x.so"); //Sinal to detach ptrace ptrace(PTRACE_DETACH, 0, 0, 0); exit(0); } else {wait(&status);} ....... }
关闭TRACEABLE标志下的执行流程
-U 选项可以关闭TRACEABLE标志,关闭TRACEABLE标志后只是在初始化过程会fork一个子进程进行一些操作来使进程变得不可追踪。通过排他方式打开父进程的内存映射文件阻止其他进程(如调试器)再次打开,通过ptrace附加到父进程,阻止其他进程来附加。但这些并不改变脚本解密执行流程。
破解之法
上文中分析了解密执行的流程,其实可以发现是比较简单的,只有少数几个关键函数。虽然通过动态调试可以轻易手动提取到解密后的脚本,但为了方便此后对这类样本的分析,我编写了一个简单的工具可以一键提取出shc打包的脚本,开源在GitHub,项目地址 https://github.com/sh1ve/extractSHC。此前GitHub上也有类似项目,但对于最新版shc打包的脚本已经不能正常提取出脚本原始代码了。
原理与动态调试差不多,动态调试是手动运行到解密后的位置然后从内存提取,因此自然可以通过修改二进制程序部分关键处代码实现运行后自动输出解密后的脚本。
chkenv返回值修改
chkenv
函数返回值决定了是否执行解密脚本流程还是重新执行自身,因此需要寻找chkenv
函数中可以用来定位的特征,修改该处的代码使其返回大于0的值,注意到未开启HARDENING
标志时,仅有chkenv
函数调用了getpid
函数,因此可以通过替换call getpid
处的代码为mov eax, 1; leave; ret
来实现chkenv
函数返回1。
在开启HARDENING
标志后,会有3个执行getpid
的地方,默认的代码结构中,chkenv
函数中的call getpid
为中间的那个,因此可以通过替换中间的call getpid
来使得chkenv
函数返回1。
脚本输出
修改解密后有引用text变量的代码即可实现将脚本内容输出。未开启HARDENING
标志时,可以通过修改解密后的call memcpy
来输出脚本内容,因为这个memcpy有引用text变量及text_z,可以简单修改后直接进行输出。
在开启HARDENING
标志后,解密的操作主要在arc4_hardrun
函数中进行,arc4_hardrun
函数中的memcpy同样有引用脚本和脚本长度,但在system函数执行之后,因此需要先修改call system
来避免执行。
模式区分
只有开启HARDENING
标志才会影响脚本解密的流程,观察到只有开启HARDENING
标志时才会有system
函数调用,因此可以用是否有system函数作为区分。
关键代码
根据以上的分析,可以使用python对二进制文件进行修改,对上述函数的修改的关键代码如下
def patch64(): # patch exec and system avoid unexpected execution patch_func('exec', 0) hard = patch_func('system', 0) # if system exist, the HARDENING flag is on if hard: # mov eax, 1 | leave | ret # if HARDENING, `getpid` in `chkenv` at rindex 1 patch_func('getpid', 1, 'B8 01 00 00 00 C9 C3') patch_func('memcpy', 0, '48 89 FE 48 31 FF FF C7 48 89 F8 0F 05 B8 3C 00 00 00 0F 05') else: patch_func('getpid', 0, 'B8 01 00 00 00 C9 C3') # mov eax,1 | mov edi, eax | syscall | xor eax, eax | exit patch_func('memcpy', 0, 'B8 01 00 00 00 89 C7 0F 05 31 C0 B8 3C 00 00 00 0F 05')
实际样本提取效果测试
yayaya挖矿会使用shc打包挖矿安装脚本,下面测试一下yayaya挖矿打包后的脚本的提取效果。
样本hash: 2AEE6DC8E5F8A6AEEF78BD93CDBCD9B4
VT 查询结果
extractSHC工具执行结果
重新使用shc默认参数打包挖矿脚本再上传VT,仅3家检出,hash:77718361D43A399344F34F94D97CE2F3
默认参数重新打包挖矿脚本和添加额外安全选项(-U -H)再打包的,extractSHC工具也都能正常提取出原始脚本
而添加额外安全选项(-U -H)打包的脚本上传VT,无一家检出,hash: 2FD77C19DDF1DC05827D26A26445762C
其他打包后挖矿脚本提取效果,hash: B1109F556662EB51F3B90E4B1A506114
hash: D1B419608F1D37676AE50E3ED8B1F810
总结
从上面的分析和测试可以看到,VT有报毒的shc打包脚本,只需要重新打包一下检出厂商数量就大量减少了,而添加额外的安全选项后同样一份脚本就无厂商检出了,可以看出shc对脚本免杀是比较有效的。同时我们也测试了我们根据shc解密执行流程所编写的一个简单工具,可以有效提取出各种shc打包的样本的原始脚本,即使添加额外安全参数也丝毫不影响提取效果。项目地址 https://github.com/sh1ve/extractSHC,欢迎star,有任何问题欢迎反馈。
本文作者:, 转载请注明来自FreeBuf.COM