事情起因,是因为实验室有一台服务器的占用率从开机启动就是100%,很怀疑就是中了某种矿机木马,拿去挖矿了,然后经过师兄的不懈努力,终于找到了木马文件,给他命名为virus_sample
然后我就拿着样本去逆了
木马分析
搜索一下main函数看下代码结构,看到一个诡异函数sub_400F7E
我们跟进一下,然后在这个函数里面看到诡异的fwrite,应该是在写一个错误信息
直接google搜索一下这串字符串
进去代码发现代码结构与IDA看到的基本上一致,那就可以大致确定了这就是shc加密了
找到了相关的博客
Obfuscated, Encrypted, Converted to C source:https://gist.github.com/NullArray/f39b026b9e0d19f1e17390a244d679ec
于是确定了这是一个shc加密shell脚本生成的二进制文件
shc加密shell脚本:文章链接
从资料看出来shc加密以后会生成三个文件
- .sh源文件
- .sh.x 加密后的二进制文件
- .sh.x.c 脚本对应的c源码
我们自己尝试写一个来加密一下看一下。
此时生成的二进制文件只能通过 ./xxxx
命令来执行
demo.sh.x.c分析
chkenv()
然后我们跟进到chkenv函数中
我们发现它首先会获取我们的pid,然后会让pid经过RC4以及一系列加密,最后保存到buff中。
这也就能解释我们在服务器中看到的隐藏进程为什么是一串十六进制数字(d4c46f88.service)
然后会让string等于buff的环境变量的值,这里肯定是没有的(因为经过加密后的进程名是一串十六进制数字,系统里面肯定没有这个环境变量),所以也就到了下面的if语句
通过sprintf函数,将"=mask argc"字符串写入了buff[l],又因为l是buff的长度,所以这里相当于直接在数组后面续写了进程的pid与参数个数
int putenv(char *string); 用于设置环境变量的值
string
:一个以 “变量名=值” 格式表示的字符串,用于设置环境变量的名称和值。char *strdup(const char *str); 用于创建一个字符串的副本。
str
:要复制的字符串。
然后用了putenv函数给新产生进程名的字符串副本创建了对应的环境变量,这也就是为什么要用sprintf函数给这个字符串赋值为=xxx
此时当脚本再次执行的时候,因为string我们前面已经设置过变量了,所以此时string也就有了值,此时sscanf就会从string里面按照格式读取数据,然后如果获取的个数为2,且获取到的m和我们刚才获取的pid一致,就会调用rmarg函数来从环境变量数组中删除第一个数组。
下图为/proc/2030/environ 环境变量数组的内容
gets_process_name()
接下来又看到了它是如何获取我们的进程脚本的,直接通过proc/pid/cmdline就可以找到进程对应的脚本
用fread函数一个字符一个字符读取,读取的大小为procfile数组的大小,然后判断如果字符串最后一个字符是换行符的话就把其替换为字符串终止符。相当于将字符串截断到了换行符这里
下图是pid为2030的一个进程的命令行参数字符串
/usr/bin/dbus-daemon
:这是可执行文件的路径。
--config-file=/usr/share/defaults/at-spi2/accessibility.conf
:这是一个命令行选项,指定了配置文件的路径。参数值为 /usr/share/defaults/at-spi2/accessibility.conf
。
--nofork
:这是一个命令行选项,表示进程在后台运行而不创建子进程。
--print-address
:这是一个命令行选项,表示进程在启动时打印出其地址信息。
在
/proc/pid/cmdline
文件中,命令行参数字符串是以 null 字符(‘\0’)分隔的,而不是以换行符分隔的。然而,有一种特殊情况可能导致在命令行参数字符串的末尾找到换行符。这种情况是当命令行参数中的某个参数值本身包含了换行符时。
#!/bin/bash
echo "Hello world!"
所以如果命令行的参数为上面的代码话,就会触发截断。
untraceable()
这个函数是用来使子进程来难以被跟踪和调试的
难以追踪和调试体现在
-
当前如果是子进程的话,就要获取其父进程的pid,然后将该进程的内存的路径写入proc变量
-
然后打开了/proc/pid/mem 打开
/proc
文件并进行读写操作可以干扰调试器的正常功能 -
然后立即关闭标准输入(fd=0的时候),也就是close(0),关闭标准输入会导致调试器无法通过标准输入与子进程进行交互,从而减少了调试的可能性。
-
mine为子进程此时是否为不能跟踪状态,mine为真即表示不可跟踪的状态,然后调用kill函数(SIGCONT信号)来恢复子进程,如果mine为假,即表示子进程无法进入不可跟踪的状态,它会发送
SIGKILL
信号给父进程,从而强制终止父进程的执行。这样,调试器将无法继续跟踪父进程的执行。
xsh()
它首先会让me变量获取到自身的文件名,如果没有获得到就用getenv函数来获取环境变量。总之代码会首先获得到脚本的路径名。
而在Unix系统中,环境变量
_
存储了当前程序的可执行文件的路径。通过调用getenv("_")
,可以获取该路径的字符串。
直接跳到隐藏这里,前面都是一些加密的代码
malloc会给scrpt分配一块4096+40字节大小的空间
代码会将scrpt指向的内存块的前4096个字节(hide_z的值)都设置为空格的ASCII值
memcpy将text中40字节的内容,复制到scrpt的第4097个字节,相当于此时scrpt的前4096个字节都是空格,只有后面的40个字节才是真正的数据
如果此时ret为假,则会将xecc格式化字符串以及me变量(自己脚本的路径)传入scrpt中
然后经过一系列的对varg参数赋值,最后调用了execvp函数执行了shll,并且varg作为参数。
远程debugger
现在在自己的机器上执行一下这个文件,用远程debugger一下。
就光执行了一下,就已经看到了删除了我很多的日志了。
先找到main函数,找到标志性字符串,分别给没有识别出来的函数重新命名一下
sub_401412函数是main函数
在ret处打断点,修改值
然后在这里打断点来获取shellcode
可以看到已经获取到了shellcode
用个脚本来获取一下
base = 0x000000000602B83
end = 0x00000000006074F0
ans=[]
for i in range(base,end):
tmp = idc.get_wide_byte(i)
ans.append(tmp)
if(tmp == 0):
print(bytes(ans))
ans=[]
存到文件里面,文件名 shell.sh.tmp
shlll = b'xxx'
with open("shell.sh.tmp", "w") as f:
print(shlll.decode(),file=f)
大致看了一下包含了删除日志、下载矿机、设置iptables等功能