拆解某款来自国内小作坊的劣质远程
注:本文更偏向技术分析,如果需要less technical的文章,请参考这篇:《另一款劣质远程的分析》
远程的技术路线决定了隐蔽程度,工程水平决定了稳定性,黑客技术决定了抗回查能力。想要破坏考试软件的功能并不难,但若想完美绕过考试软件多管齐下的监管,可并不是一件容易的事:现代计算机每秒可执行成千上万条指令,而哪怕有一条监管指令没有被妥善处理,都可能埋下巨大隐患。所谓差之毫厘谬以千里大抵如此。
ELPIS的Tech Team组员均为在读或已毕业的北美TOP 5院校CS major,技术实力毋庸置疑。甚至一些同为CS专业的同学希望我们辅导Leetcode。而出于对同行的天然的好奇,我们当然也会对国内一些声称自己“有技术”的机构做一番详尽的田野调查。
ELPIS发现:在小红书等国内平台上,可以提供远程方案的机构上百上千,但是无论您选择哪一家,无论谁吹嘘自己有何种“独门技术”,您最后所使用的远程软件必定是其中3、4个之一。
究其原因,国内保分机构几乎没有门槛:不需要你是真正考过标准化考试的留学生、不需要你是技术过硬的计算机安全专家、不需要你是常年接触英文的双语者。因此,几乎没有机构有能力自研任何黑客级别的程序,甚至,机构本身都并不了解自己用的远程是何原理。所以,机构只能依赖技术提供商提供可用的远程软件。国内提供远程技术的提供商有3、4家。所以无论您最后与哪个机构合作,得到的无非是这3、4种远程之一。
那国内远程的技术到底如何呢?我们来对其中一款进行逆向工程和拆解分析。
首先在隔离的、不联网的沙盒(Sandbox)环境中运行exe文件,在阻止它与服务器通信的同时,查看它创造的进程和线程的活动记录。

运行之后无事发生。推测是由于我们禁止远程软件联网,所以远程软件无法从服务器接收指令。我们探究一下在未联网时远程软件做了哪些基础的操作。

当我们打开沙盒的文件系统时,我们可以看见远程软件在C盘Program Files中创建了一个名为Files的文件夹,并把自己安装进去。同时一起安装的还有一个自启动脚本,负责在电脑每次重启的时候将远程软件自动打开。这种自动方式让ELPIS梦回20年前。ETS只要稍微检查一下自启动软件名单,这款远程必然暴露无遗。
接下来我们给它开通网络权限,让它可以跟服务器通信。但我们会时刻监控网络流量,发现远程软件与服务器完成握手后就切断链接。

我们发现这款远程调用了Crypto库,也就是说软件可能以某种方式加密了自己和服务器之间的通信内容。但是,很快我们在这款远程软件的安装目录内找到了Setup.key这个文件。相当于远程软件的确是加密了,但是钥匙就放在旁边。当然了,我们并不关心软件和服务器之间到底说了什么,我们只关心ETS可能会如何识别这款远程软件,所以就不浪费时间解密了。

我们直接在Task Manager中找到这款软件软件的进程。如果我们可以找到,ETS也一样可以找到。ETS和我们不同的是,ETS会对可疑进程创建mini dump,相当于把进程内的内存信息“倒”出来,留做后续分析使用,而我们现在做的事情是实时的分析,本质上殊途同归。为了更贴近ETS的分析策略,我们也对其创建mini dump,留做后续分析使用。

现在开启debugger模式,重新打开exe文件,在进程刚刚被创建时就会被debugger挂起(suspend),相当于给进程施加了定身术。这样我们就可以近距离观察进程是如何一步步执行的,方便我们分析PE文件结构,以及查看它导入的DLL和API函数有哪些。
可以看出,它采用了UPX加壳技术,但是并没有什么卵用。通过PEiD和gdb分析后,我们很轻易的就找到了真实程序的起始点。虽然这部分对考试安全性没有什么影响,但是这种“加壳但又没完全加”的行为,相当于“化妆但是只化一半脸”,不太确定是技术不过关还是粗心大意。

有趣的是,我们发现在这款远程软件执行时会出现报错。如果您是这款远程的作者,如果您看到了这篇文章,麻烦改一下您的程序,谢谢。另外,我们看见了您写的“作者版权所有 请尊重并使用正版”,我们保证绝不会使用这款远程的!并推荐您也先别用了。
言归正传,找到程序入口点后,我们只需要去内存里,恢复IAT和程序原本的PE结构,就可以看到DLL导入路径和API函数。
结合程序调用DLL和API名单,通过GhiDra反编译工具,我们可以明显看出这是一个远控软件。因为它调用了大量跟图形界面相关的函数,其中有get/set函数负责获取屏幕信息,capture screen获取屏幕内容,最后通过Windows GDI提供的Desktop Dulication方法对屏幕进行捕获。以下是相关函数的反编译伪代码,直接取材自Ghidra反编译工具:


其实到了这一步,基本可以直接宣判死刑了。因为ETS会搜集考生计算机中进程的dump信息。计算机专业的同学应该清楚,dump信息里包含了线程信息、堆栈数据、全局变量、加载库等等丰富的信息。假设ETS对此稍微进行分析,该远程的真实意图将被一览无遗。

在这里,我们可以看到它对ETS的考试软件进行了code injection攻击:通过向ETS考试软件软件中强行注入原本不存在的代码,使得ETS考试软件的某些功能被强制关闭。诸如此类操作会像“电子推土机”一样在系统中留下大量痕迹,甚至被当场抓获。这是一种异常简单粗暴的解决方案,毫无优雅可言。如果做一个真实世界的类比,就好像有人强行把烧汽油的车改成烧煤块儿,并指望交警看不出来。
在本文的结尾,我们会附上根据反编译得来的这款远程软件关于code injection部分的伪代码(见附件)。有较强计算机基础的、对此感兴趣的同学可以自行查阅。但请注意,这并非是完整的code injection流程,ELPIS尽量在不违背该远程软件作者的“版权提醒”的前提下,给大家展示。
我们再接着探究一下,它为了隐藏自己做了什么样的努力。经仔细研究,ELPIS发现它居然根本就没试图去隐藏自己!除了把自己的进程名字改成和系统进程同名外,没做任何其他的事情!
这让我们感到非常意外!因为这代表这款远程的设计思路和我们的设计思路从根本上背道而驰。
ELPIS的思路是:让ETS找不到木马的踪迹,从而无法搜集任何信息,进而无法被复查
这款远程的思路是:ETS可以看见我的本体,但是我赌ETS不会对我进行分析
看到这里,我们恍然大悟为什么国内的远程总是集中暴雷。因为如果只有少数考生使用这款远程时,ETS即使可以搜集到关于该远程的信息,也很可能因为样本量的不足而将它“误判”成正常软件,从而不针对它进行分析——毕竟,ETS在大多数情况下并不会实时的分析考生电脑上所有进程的数据。但是一旦使用该远程的人数增加,它的运行特征就会被识别。想象一下,ETS看着100个考生的计算机中都有一个莫名其妙的进程,在考试中做着一些和“获取电脑屏幕显示内容”有关的事情。这,即便从常理想,也很难通过回查。而一旦ETS针对这款远程的特征回查,所有在考试中有过此类特征的考试电脑都将会被取消成绩。这就是集中暴雷的原因!
如果所谓的远程软件都是这种水准,暴雷就并不奇怪了。甚至,不爆雷反而很奇怪。因为其实这款远程的内部信息对ETS而言是完全可见的——它并不“抗回查”,它只“赌自己不被查”。
况且,ETS有类似于运动员飞行检查的“尿检”机制。虽然大多数情况下,考试软件并不会实时的对电脑上所有进程进行详细检查。但是一些情况下,电脑信息会被事无巨细的实时审查。ELPIS管这种机制叫“强检测”:不同于普通的,在考试当中进行的“弱检测”,“强检测”会检查电脑运行时(runtime)的所有细节,并将不合格的考生直接踢出考试。
所以最坏情况下,该远程会被系统直接识别,导致考试无法继续甚至禁考。
ELPIS对这款远程软件的评价是:emmmmm… 不合格。
附件:Code Injection伪代码
void FUN_004010d0(void)
{
wchar_t *_Dst; wchar_t wVar1;
code *pcVar2; HMODULE hModule;
DWORD DVar3; basic_streambuf<> *pbVar4; wchar_t *****pppppwVar5;
char *****pppppcVar6;
long lVar7; BOOL BVar8; int iVar9;
uint uVar10; wchar_t *****pppppwVar11;
uint uVar12; void *pvVar13;
void *pvVar14; uint uVar15;
int local_628 [4];
basic_ostream<> local_618 [8];
basic_streambuf<> local_610 [8];
basic_iostream<> local_608 [72];
basic_ios<> local_5c0 [68];
int iStack_57c;
int local_578 [4];
basic_streambuf<> local_568 [168];
undefined4 local_4c0;
undefined4 local_4bc;
wchar_t *local_4b8;
void *local_4b4;
void *local_4b0;
int local_4ac;
long local_4a8;
undefined4 local_4a4;
HMODULE local_4a0;
uint local_49c;
char ****local_498 [4];
int local_488;
uint local_484;
wchar_t ****local_480 [4];
uint local_470;
uint local_46c;
WCHAR local_468 [522];
wchar_t local_54 [24];
uint local_24;
undefined1 *puStack_20;
void *local_1c;
undefined1 *puStack_18;
undefined1 local_14;
undefined3 uStack_13;
puStack_20 = &stack0xfffffffc;
local_14 = 0xff;
uStack_13 = 0xffffff;
puStack_18 = &LAB_00405106;
local_1c = ExceptionList;
local_24 = DAT_00409004 ^ (uint)&stack0xfffffff0;
ExceptionList = &local_1c;
hModule = GetModuleHandleW((LPCWSTR)0x0);
if (hModule != (HMODULE)0x0) {
memset(local_468,0,0x410);
DVar3 = GetModuleFileNameW(hModule,local_468,0x208);
if ((DVar3 == 0) || (DVar3 = GetLastError(), DVar3 == 0x7a)) goto LAB_0040165e;
FUN_004037a0(local_480,local_468);
local_14 = 0;
uStack_13 = 0;
pppppwVar5 = local_480;
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
}
if (local_470 != 0) {
iVar9 = -1;
if (local_470 - 1 != -1) {
iVar9 = local_470 - 1;
}
pppppwVar11 = (wchar_t *****)((int)pppppwVar5 + iVar9 * 2);
wVar1 = *(wchar_t *)((int)pppppwVar5 + iVar9 * 2);
while (wVar1 != L'\\') {
if (pppppwVar11 == pppppwVar5) goto LAB_00401619;
pppppwVar11 = (wchar_t *****)((int)pppppwVar11 + -2);
wVar1 = *(wchar_t *)pppppwVar11;
}
uVar12 = (int)pppppwVar11 - (int)pppppwVar5 >> 1;
if (uVar12 != 0xffffffff) {
pppppwVar5 = local_480;
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
}
local_49c = (uint)(ushort)*(wchar_t *)((int)pppppwVar5 + (local_470 - 1) * 2);
if (local_470 < uVar12) {
FUN_004032f0();
pcVar2 = (code *)swi(3);
(*pcVar2)();
return;
}
uVar10 = local_49c;
if (local_470 - uVar12 < local_49c) {
uVar10 = local_470 - uVar12;
}
pppppwVar5 = local_480;
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
}
_Dst = (wchar_t *)((int)pppppwVar5 + uVar12 * 2);
uVar15 = local_470 - uVar10;
local_49c = uVar15;
memmove(_Dst,_Dst + uVar10,(uVar15 - uVar12) * 2 + 2);
if (local_46c - uVar15 < 9) {
local_470 = uVar15;
FUN_00403300(local_480,9,local_49c,local_46c,9);
}
else {
pppppwVar5 = local_480;
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
}
local_470 = uVar15 + 9;
memmove((wchar_t *)((int)pppppwVar5 + local_49c * 2),L"\\SM86.txt",0x12);
*(wchar_t *)((int)pppppwVar5 + (uVar15 + 9) * 2) = L'\0';
}
pppppwVar5 = local_480;
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
}
FUN_00402c80(local_578,(wchar_t *)pppppwVar5);
*(undefined ***)((int)local_578 + *(int *)(local_578[0] + 4)) =
std::basic_ifstream<>::vftable;
*(int *)((int)&iStack_57c + *(int *)(local_578[0] + 4)) = *(int *)(local_578[0] + 4) + -0x70
;
local_14 = 1;
if (*(int *)((int)local_578 + *(int *)(local_578[0] + 4) + 0xc) == 0) {
FUN_00401e80((basic_iostream<> *)local_628);
local_14 = 2;
std::basic_ostream<>::operator<<(local_618,local_568);
pbVar4 = FUN_00402c00(local_568);
if (pbVar4 == (basic_streambuf<> *)0x0) {
std::basic_ios<>::setstate
((basic_ios<> *)((int)local_578 + *(int *)(local_578[0] + 4)),2,false);
}
pppppwVar5 = local_480;
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
}
DeleteFileW((LPCWSTR)pppppwVar5);
FUN_00401d90(local_628,local_498);
if (local_488 != 0) {
pppppcVar6 = local_498;
if (0xf < local_484) {
pppppcVar6 = (char *****)local_498[0];
}
lVar7 = strtol((char *)pppppcVar6,(char **)0x0,10);
if (lVar7 != 0) {
local_4b4 = (void *)0x0;
local_4b0 = (void *)0x0;
local_4ac = 0;
local_4a4 = 0;
local_4a8 = lVar7;
local_4a0 = LoadLibraryW(L"user32.dll");
BVar8 = EnumWindows(FUN_00401010,(LPARAM)&local_4b4);
pvVar14 = local_4b0;
if ((BVar8 != 0) && (local_4b4 != local_4b0)) {
local_4b8 = local_54;
local_4c0 = 1;
builtin_wcsncpy(local_54,L"This sentence is false.",0x18);
local_4bc = 0x30;
pvVar13 = local_4b4;
do {
SendMessageW(*(HWND *)((int)pvVar13 + 4),0x4a,(WPARAM)*(HWND *)((int)pvVar13 + 4),
(LPARAM)&local_4c0);
pvVar13 = (void *)((int)pvVar13 + 8);
} while (pvVar13 != pvVar14);
}
if (local_4b4 != (void *)0x0) {
pvVar14 = local_4b4;
if ((0xfff < (local_4ac - (int)local_4b4 & 0xfffffff8U)) &&
(pvVar14 = *(void **)((int)local_4b4 + -4),
0x1f < (uint)((int)local_4b4 + (-4 - (int)pvVar14)))) goto LAB_00401645;
FUN_004043e1(pvVar14);
local_4b4 = (void *)0x0;
local_4b0 = (void *)0x0;
local_4ac = 0;
}
}
}
if (0xf < local_484) {
pppppcVar6 = (char *****)local_498[0];
if ((0xfff < local_484 + 1) &&
(pppppcVar6 = (char *****)local_498[0][-1],
(char *)0x1f < (char *)((int)local_498[0] + (-4 - (int)pppppcVar6))))
goto LAB_00401645;
FUN_004043e1(pppppcVar6);
}
local_488 = 0;
local_484 = 0xf;
local_498[0] = (char ****)((uint)local_498[0] & 0xffffff00);
*(undefined ***)((int)local_628 + *(int *)(local_628[0] + 4)) =
std::basic_stringstream<>::vftable;
*(int *)(local_610 + *(int *)(local_628[0] + 4) + -0x1c) =
*(int *)(local_628[0] + 4) + -0x68;
FUN_00401ce0(local_610);
std::basic_iostream<>::~basic_iostream<>(local_608);
std::basic_ios<>::~basic_ios<>(local_5c0);
FUN_00401690(local_578);
}
else {
pbVar4 = FUN_00402c00(local_568);
if (pbVar4 == (basic_streambuf<> *)0x0) {
std::basic_ios<>::setstate
((basic_ios<> *)((int)local_578 + *(int *)(local_578[0] + 4)),2,false);
}
pppppwVar5 = local_480;
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
}
DeleteFileW((LPCWSTR)pppppwVar5);
FUN_00401690(local_578);
}
}
}
LAB_00401619:
if (7 < local_46c) {
pppppwVar5 = (wchar_t *****)local_480[0];
if ((0xfff < local_46c * 2 + 2) &&
(pppppwVar5 = (wchar_t *****)((wchar_t *****)local_480[0])[-1],
0x1f < (uint)((int)local_480[0] + (-4 - (int)pppppwVar5)))) {
LAB_00401645:
/* WARNING: Subroutine does not return */
_invalid_parameter_noinfo_noreturn();
}
FUN_004043e1(pppppwVar5);
}
}
LAB_0040165e:
ExceptionList = local_1c;
FUN_00404380(local_24 ^ (uint)&stack0xfffffff0);
return;
}