Debian 8, GDB, GDB-peda,,IDA-pro
阿里云安骑士是阿里云盾在用户VPS上的客户端,类似于OSSEC的客户端,它可以持续地运行在客户服务器中,每隔一定时间就去检查服务器下敏感目录下的文件,以及相关系统进程,判断是否存在漏洞,webshell,以及挂马程序,其中,我们最感兴趣的一部分是阿里安骑士是如何对webshell进行识别的。阿里安骑士的主进程名是AliYunDun,其文件目录在/usr/local/aegis/aegis_client/下,可能会存在不同版本,在这里我们选择 aegis_10_25作为我们的分析对象。
在安骑士的文件夹下,除了主程序AliYunDun外,我们还能看到他自带的一些动态链接库,从动态链接库的名称我们可以大致明白这些动态链接库的功能,比如说libModuleWebShell.so肯定就是和我们感兴趣的webshell检测相关的。
之后我们又分别在conf目录下和rule目录下发现和webshell检测相关的信息。在conf目录下,有着webshell检测的配置文件:
update_webshell_rule=1max_dir_watch=1000watch_svr=1timer_webshell=1time_scan_begin=4time_scan_end=8webshell_check=1custom_rule=0
在data目录存储的是安骑士的日志数据,data后面的后缀是由当日的星期决定的,例如今天是周五,那么在data目录下生成的日志文件名就是data.5。通过这些日志我们可以判断出安骑士的各个模块的状态。
在rule目录下存储的是和匹配规则相关一些数据,大部分数据都是都是加密,而此次报告的主要目的便是介绍如何破解这些加密的数据。
确定webshell规则存储的文件位置:
我们可以逐个删除rule目录下的文件,并重启AliYunDun的进程,然后去看data目录下的日志是否有webshell模块启动失败的报错,即可判断和webshell相关的配置文件是哪些,最终我们可以测试出plusdata.data是webshell模块正常工作必须存在的模块。
逆向libModuleWebShell.so动态链接库:
把该函数库扔到IDA-pro中,然后尝试去search text,text的内容就是我们刚才找到的webshell规则存储的加密文件plusdata.data,找到这个字符串后,然后通过xref转到使用这个字符串的函数中。
这个函数是CWebShellChecker类中的LoadDb函数,然后我们简单看一下这个函数,可以发现这个函数调用了CWebShellChecker::DecodeRuleFile这个解密函数,而且秘钥看上去就是aaaaabbbb。
继续跟踪下去,我们可以发现,最后真正实现解密的函数是aqs::CAlgorithmUtil::xxAesDecrypt:
猜测可能是常见的AES算法对文件加密,但也不排除阿里自己封装了一个AES函数,如果继续追踪下去成本太高,我们不如直接写一个cpp程序去调用这个函数。但是我们对需要传递给这给函数的参数并不是很确定,因此,不如直接去调用不接受任何参数的LoadDb函数,然后通过gdb跟踪,在cpp销毁CWebShellChecker类之前,把解密的文件从内存中导入出来。
完整的解密函数调用过程如下所示:
CWebShellChecker::LoadDb => CWebShellChecker::DecodeRuleFile aqs::CAlgorithmUtil::xxAesDecrypt
最早尝试的方法是直接调试AliYunDun这个主程序,但是这个主程序存在 未知的反调试机制,所以未能成功。
我们先写如下的CPP程序去调用webshell模块中的LoadDb函数。
1.cpp
using namespace std;
class CWebShellChecker
{
public: int LoadDb(void);
};
int main(){
CWebShellChecker a;
a.LoadDb();
return 0;
}
编译的时候需要注意的是,因为我们调用了libModuleWebShell.so这个函数库,所以需要在编译中指定链接的函数库,此外,这些函数库之间都有复杂的相互依赖关系,我们干脆把所有的函数库全部链接进来。这个编译命令的选项可以通过python脚本产生:
so.py
#!/usr/bin/env python
sos = open('so.txt').readlines()
res = ""
for so in sos:
so = so[3:so.find(".so")]
res += "-l" + so + " "
print res
so.txt的内容可用使用awk命令产生。
最终的编译命令是:
g++ 1.cpp -L. -laegisConfig -laegisControl -laegisError -laegisIpc -laegisModuleManager -laegisMonitor -laegisNetWork -laegisProc -laegisVulFix -laqsHttp -laqsIpc -laqsNetWork -laqsSqlite -laqsUtil -le2p -lFileQuara -lglib-2.0 -lgthread-2.0 -lModuleCheck -lModuleCommon -lModuleCrack -lModuleExec -lModuleHook -lModuleProc -lModuleRtap -lModuleSoftware -lModuleSysWatch -lModuleUpdate -lModuleVul -lModuleWebShell -lmodWebShellPlus -ldl -lcurl -levent -lpthread -levent_pthreads -lhack -o 1
需要注意的是后面需要额外添加一些链接库以满足编译的依赖关系:-ldl -lcurl -levent -lpthread -levent_pthreads -lhack。其中libhack.so是因为找不到get_uuid属于哪一个函数库,于是我们自己构造了一个函数库以满足编译依赖。libhack.so的源码如下:
int get_uuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
return 1;
}
然后将源码编译成共享链接库:
gcc -c -fPIC hack.c -o hack
gcc -shared hack -o libhack.so
最后,我们可以使用gdb对生成的二进制文件1进行调试。
4. 调试二进制程序
在调试之前我们需要确定几个关键函数的名称,以方便在gdb调试过程中设置断点:
loaddb运行时函数名: _ZN16CWebShellChecker6LoadDbEv
decoderulefile运行时函数名:_ZN16CWebShellChecker14DecodeRuleFileERKSsRSsS1_
xxAesDecrypt运行时函数名:_ZN3aqs14CAlgorithmUtil12xxAesDecryptERKSsS2_RSs
GetInstance运行时函数名:_ZN10CSingletonI20CWebShellPlusScannerE11GetInstanceEv
CMutexLockUtil运行时函数名:_ZN3aqs14CMutexLockUtilC1EPNS_10CMutexUtilE (一个和锁有关的函数,需要绕过)
以上的函数名都是通过attach到原AliYunDun进程上,通过searchmem找到的。
$rax=1即可。
在执行到CMutexLockUtil,直接通过set $rip=下一指令地址 跳过该函数即可。
接着追到decoderulefile,我们此时可以很清楚地看见各个参数传参的内容:
在这个函数结束后,解密后的内容已经出现在了文件中,我们可以通过一些关键字定位文件在内存中的起始地址,最终使用dumpmem 666666.data 0x7ffff7f3f028 0x7ffff7f85803导出文件。
检查一下666666.data的内容,发现正是我们想要提取的webshell检测的规则。
通过本次的逆向过程,简单回顾了gdb以及gdb-peda的使用,学习了如何调试和加载动态链接库。此外,调试过程中对多进程程序的处理还是需要注意的:
set detach-on-fork on (在attach到新的进程时先detach原先调试的进程)
set follow-fork-mode child (在出现子进程的时候,attach到子进程上)