freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

恶意脚本0检出?shc工具的破解之法
2024-01-23 23:40:23

挖矿病毒当前仍然是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)=%s\n", 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 查询结果
VT

extractSHC工具执行结果
execute

重新使用shc默认参数打包挖矿脚本再上传VT,仅3家检出,hash:77718361D43A399344F34F94D97CE2F3
VT2

默认参数重新打包挖矿脚本和添加额外安全选项(-U -H)再打包的,extractSHC工具也都能正常提取出原始脚本
execute1
execute2

而添加额外安全选项(-U -H)打包的脚本上传VT,无一家检出,hash: 2FD77C19DDF1DC05827D26A26445762C
VT3

其他打包后挖矿脚本提取效果,hash: B1109F556662EB51F3B90E4B1A506114
execute3

hash: D1B419608F1D37676AE50E3ED8B1F810
execute4

总结

从上面的分析和测试可以看到,VT有报毒的shc打包脚本,只需要重新打包一下检出厂商数量就大量减少了,而添加额外的安全选项后同样一份脚本就无厂商检出了,可以看出shc对脚本免杀是比较有效的。同时我们也测试了我们根据shc解密执行流程所编写的一个简单工具,可以有效提取出各种shc打包的样本的原始脚本,即使添加额外安全参数也丝毫不影响提取效果。项目地址 https://github.com/sh1ve/extractSHC,欢迎star,有任何问题欢迎反馈。

# 系统安全 # linux挖矿 # 静态免杀
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录