背景
- 内存挂断崖式下降,新型外挂往硬件层面靠拢(DMA、Kmbox、VT、uefi)
正文
提取作弊uefi程序
FE外挂为uefi劫持hyperV类型外挂,此类型外挂原理基本相同,详细原理可以参考开源的voyager项目。(但是本外挂的原理细节和voyager开源有极大区别,甚至可以认为FE外挂是voyager的超集)。
因此第一件事需要先提取出来作弊的uefi,可以通过map uefi的ESP分区到X盘拿到,命令行如下(可以参考voyager)
mountvol X: /S
即可挂载ESP到X盘,X盘内容直接全部copy就行
拿到后,diff原版文件,发现外挂修改了bootmgfw.efi
和bootmgr.efi
(可以通过签名有效性判断出来)
后者不用管,因为那是进PE的时候的bootloader,只看前者。
逻辑分析
拖入ida,发现他对原来的bootmgfw.efi加了壳,这一点和voyager就不一样,voyager是保存了原始文件的一个备份,fake-bootloader启动后,删除自身,然后加载back文件。
加壳之后,外挂efi修改了原始的入口点,到cheater_entry
cheat_entry
-
check_init
这是作弊程序的入口点,他首先通过uefi运行时服务GetVariable
和SetVariable
来获取几个变量
首先进行了一次验证,验证函数由于不重要没有逆向,如果验证失败,则reset 机器。
同时,这里通过BootService分配了一块EfiRuntimeServiceCode,这里要清楚,uefi runtime service内存是可以在os启动(ExitBootService)后仍然驻留。记住这个g_big_pool_buf,很重要哦!因为这是外挂在操作系统启动后还要驻留的payload。
-
pattern_find ImgFwStartBootApplication并hook
ImgFwStartBootApplication
是原始bootmgrw的重要初始化函数,可以发现后续也有构造0xff 0x25来进行hook的操作。
-
hook BgpGxParseBitmap
这个作用是修改boot时候的展示,可以理解为机器启动boot的时候,向用户展示的GUI
外挂hook这个,达到了启动的时候是一个巨大的狼头(外挂就叫狼头)。不得不说,还是很酷的。
-
hook EfiGetVarible,伪造安全启动
这个是干啥的呢,继续跟进这个函数
这个其实是uefi的runtime service
,也是会长期滞留在内存中,哪怕os已经启动了。
通过这个,可以获取uefi的一些固件信息,比如是否安全启动等。毫无疑问,这个外挂肯定是不能安全启动的,除非微软给他签名,这个证书是非常严格的,因此需要关闭安全启动。
因此他需要欺骗反作弊或者安全软件,就通过挂钩这个来达到目的。巧的是,测试电脑恰好装着vgk的boot驱动,测试发现,这个外挂完美兼容vgk.sys。
函数内具体逻辑比较简单,判断是否是在查询securebootenable
,如果是,则返回true。
如果这样看起来比较复杂的话,我让chatgpt帮我优化了下代码,不得不说,o1模型逻辑推理能力简直强到爆炸。
#include <string.h>
#include <ctype.h>
unsigned __int64 filter_func(
unsigned __int16 *name, // 名称字符串
int *guid, // GUID,数组形式
unsigned __int64 *a3, // 标志指针
unsigned char *a4 // 结果指针
)
{
unsigned __int64 ERROR_CODE_DEFAULT = 0x800000000000000E;
unsigned __int64 ERROR_CODE_SPECIFIC = 0x8000000000000005;
// 检查全局变量
if (!byte_5C5E265)
return ERROR_CODE_DEFAULT;
// 将 GUID 组合成完整的 GUID 类型(伪代码,实际需根据具体实现)
GUID variable_guid = make_guid(guid);
// 定义要检查的变量名和对应的 GUID
typedef struct {
GUID guid;
const char *variable_name;
} VariableInfo;
VariableInfo variables_to_check[] = {
{ EFI_GLOBAL_VARIABLE_GUID, "SecureBoot" },
{ EFI_GLOBAL_VARIABLE_GUID, "SetupMode" },
{ CUSTOM_GUID, "CustomMode" },
{ SB_CONFIG_STATE_GUID, "SbConfigState" },
};
// 遍历检查
for (int i = 0; i < sizeof(variables_to_check) / sizeof(VariableInfo); i++)
{
if (compare_guid(variable_guid, variables_to_check[i].guid))
{
// 比较变量名(不区分大小写)
if (_wcsicmp((wchar_t *)name, (wchar_t *)variables_to_check[i].variable_name) == 0)
{
if (*a3)
{
// 根据变量名设置 *a4 的值
if (strcmp(variables_to_check[i].variable_name, "CustomMode") == 0 ||
strcmp(variables_to_check[i].variable_name, "SetupMode") == 0)
{
*a4 = 0;
}
else
{
*a4 = 1;
}
return 0; // 成功
}
else
{
*a3 = 1;
return ERROR_CODE_SPECIFIC;
}
}
}
}
// 未匹配,返回默认错误码
return ERROR_CODE_DEFAULT;
}
可以看到,他修改了上面几个变量的返回逻辑,就绕过了os的安全启动检查。
-
总结
这个cheat_entry
做的事情比较少,主要就是预检查、绕过secure boot检测、hook重要函数ImgArchStartBootApplication
ImgArchStartBootApplication
按照顺序执行,接下来会执行这个函数,而这个函数已经被外挂hook了,所以看detour逻辑。
-
查找winload必要导出函数
这里需要注意的是,image_base是winload.efi!这些函数后面都会用到,而且是winload.efi导出的,因此查找后保存。
-
随机BIOS特征
这一段代码比较有意思,发现他通过特征定位到了BlUtlCopySmBiosTable
函数。尝试搜索这个函数的资料,发现其介绍如下:
NTSTATUS
BlUtlCopySmBiosTable (
__deref_out PVOID *SmBiosTableCopy
);
/*++
Routine Description:
This routine will attempt to find the SMBIOS table. When successful,
a buffer is allocated and the table is copied to the newly allocated
buffer.
Arguments:
SmBiosTableCopy - On successful output, contains the address of the
allocated buffer containing the SMBIOS table.
Return Value:
STATUS_SUCCESS when successful.
STATUS_NOT_FOUND if the requested table could not be found.
STATUS_NO_MEMORY if a memory allocation fails.
STATUS_INVALID_PARAMETER if the table was corrupt.
--*/
SMBIOS table,搜了下:
发现外挂钩了这个函数,跟进detour函数看
在random_modify_smbios
这个里面可以看到,他生成了一些随机的序列
这个214013和2531011特征太明显了,是C的random函数,这个函数的大体逻辑就是随机了主板bios的一些序列号、制造商等,这个貌似是外挂在进行随机硬件序列的逻辑。
-
hook winload 的EfiGetVariable
bootmgr已经hook了,hook这个来继续伪装SecureBoot等。
-
hook winload.BlLdrLoadImage
在函数最后,hook了winload.BlLdrLoadImage然后调用原始的g_ImgArchStartBootApplication
BlLdrLoadImage
函数是作弊程序的核心,里面有几千行伪c。
BlLdrLoadImage
这是一个位于winload.efi的函数,winload作用是加载所有的windows内核基础组件,比如ntoskrnl、hal.dll、hvloader.dll、hv.exe…同时结束uefi阶段,正式进入os。外挂hook这个地方作用不言而喻,此处也正是外挂最核心的逻辑。
-
调用原始函数
这里和因为需要调用原始函数,真正地获取一个新加载的镜像位置,所以先调用原始函数,在执行过滤、hook逻辑。
-
ntoskrnl
对ntoskrnl.exe的hook和操作,看着玩就行。说实话,看她写了一大堆ntoskrnl.exe的操作,感觉没什么关键信息,是一些辅助性的操作。所以随便看看
对字符串解密,发现是ntoskrnl.exe,发现ntoskrnl被加载,外挂执行如下逻辑
有一些全局变量比如g_hv_entrypoint_rva,第一次逆向可能没办法正确判断,其实这就是一个代码先后执行顺序的问题,hv.exe执行时机是早于ntoskrnl的。
这个do_some_hooks,发现它hook了几个比较奇怪的地方
首先它hook的ntoskrnl.exe的启动
save_something里面似乎也没啥好看的
就是保存一些东西,这个地方的逻辑不用太关心,不是核心内容。
-
hvloader.dll
发现是加载hvloader.dll,开始执行下面的逻辑
查找hvloader.dll的text
节,pattern find如下特征0x48 0x8b 0x51 0x10
可以发现是位于hvloader.dll.HvlLaunchHypervisor
因为hvloader.dll都是没有符号表的,所以目前只能判断他hook了这个函数,而这个函数很简单:
就是换了个cr3,没有太多逻辑,因为没有符号,a1 a2这两个参数完全不知道是干嘛的!只能通过逆向外挂来旁敲侧击a1 a2代表着什么。
detour函数如下:
关键逻辑在reloc_hvpayload里面,a1、a2被传了进去,跟进去,可以发现一些端倪。
首先,他进行了非常多的页表操作,
这个地方比较复杂,我们逐行分析,首先是
可以看到,他获取了g_big_pool_buf的pteaddress,然后对其解引用,获得了pte.flags(pte表项) 对其 |=3,参考intel手册
3开启了R/W和U/S位,接着去看rw_physical_mem
逻辑
这几行代码,竟然非常类似[原创]AXx-BASE里好玩的代码这个的实现!简而言之就是安全地读写物理内存。顺带着我发现了这个作者写的BUG,即他刷新了两次TLB,而第一次刷新的竟然是PteAddress
,这显然不是必须的,可能外挂作者有些逻辑混乱了。
逆向完了rw_pa
这个函数,可以看他的参数传递:
这样看就很明显了,a1是个物理地址,而pte是外挂刚才set U/S R/W之后的属性,而1代表写入这块物理内存,同时写入的偏移位于页面内的0x7f8,写入的还是一个pte.
0x7f8
,其实这个偏移非常特殊,如果它位于页表中,那么它位于页表4kb255个,也就是刚好位于内核态和用户态中间的Pte,这个非常特殊,同时,难道是否也表明了a1是一个物理地址,并且a1是一个页目录基址(cr3)?,如果是cr3,那么也只可能是hv.exe的cr3?这里先存下疑问,继续往下逆向。因为根据已有的信息,无法推理出确定性结论。
继续往下看,发现它循环了128次,是从g_big_pool_buf
开始,写入了一个很明显是Pxe Entry的东西。
现在我们知道了,g_big_pool_buf
前0x1000字节就是一个PML4E
,那么很明显,现在正在填写这个PML4E指向的PDPTE
而这个offset是从g_big_pool_buf
+0x1000字节开始的,也就是跳过了第一个PML4E,它设置该PMLE4(PDPT)指向的每一个PDPTE
都是rwx,并且不是1Gb的页面,然后offset+=16
,ok,现在拨开云雾了。我们得到了重要的结论
g_big_pool_buf
– g_big_pool_buf+0xfff
是一个PML4E指向的PDPT
g_big_pool_buf+0x1000
– g_big_pool_buf+0x1fff
是每个PDPTE表项,其中PDPT指向他们。
其实到了这一步,有基础的同学应该可以猜出来,这一步和EPT 的身份映射非常像,即构造的第一个PML4E,构造2Mb大页,虚拟地址0就对应物理地址0,以此类推
而下面填充Pdpt操作,则彻底印证了我们的想法。正如同我们猜测,他正在做身份映射。只不过这个不算在EPT 页目录基址,而是在hv.exe的cr3中。
接着,外挂似乎做了一个迷惑性操作
0x7FA000000000
不让他去映射物理地址,请记住这个虚拟地址,非常重要!非常重要!非常重要!这里先按下不表,留个悬念。
总之,现在这块内存映射了g_big_pool_buf+0x83000
开始的,大小为0xFFE000的地址
接着外挂通过cpuid=0判断了AMD还是intel,从而去重定位不同的payload(此时payload已经解密,似乎hv.exe先于hvloader.dll加载,不然解释不通)
我们看到了什么,是的,0x7FA000000000
,果然是伏笔,这个地址就是payload
重定位的虚拟地址!(reloc函数逆向了下,就是非常基础的重定位pe,老生常谈的iat,重定位表等修复操作).剩下的就是重定位确定比如g_ept_violation
等真正的地址。这几个全局变量,在外挂拦截hv.exe
的时候会体现的,这下似乎基本上确定了,hv.exe先于hvloader.dll加载,然后后面hv.exe的地址会变?也就是一开始加载的hv.exe似乎是不对的,要不然这里他也不会进行重定位hv.exe了。
外挂的这个函数重定位了payload
,并插入了hv.exe的cr3的一块特殊地址里面,这里大胆推测下,hv.exe是否就是host的cr3?
说实话我真不太确定hv.exe在开启vtx是否其加载地址会变,但是想想理论上应该会变。这里我看了Voyager的实现,他的实现似乎聪明得多:
它在hv.exe的后面新加了个section,这样就算hv.exe变化,也依然是可以找到新加的代码,也不需要去往hv.exe的cr3去插入payload了。如果以后要自己实现这个的话,我想我应该也会去这样实现。
而这里它进行了一堆映射物理地址,其实大概率就是用来读写内存的,只不过采用的物理地址读写
下面是解密payload的逻辑
其实外挂有N个payload,至少我逆向的时候,有3个以上,而且每个各司其职,非常复杂。
解密函数如下:
void __fastcall decrypt_payload(__int64 org_p, unsigned int len, __int64 dest_p, unsigned int offset)
{
__int64 v4; // rax
__int64 v5; // r10
__int64 v6; // rcx
__int64 v7; // r8
char v8; // dl
if (offset < len)
{
v4 = len;
v5 = offset;
if ((((_BYTE)len - (_BYTE)offset) & 1) != 0)
{
*(_BYTE*)(dest_p + offset) = *(_BYTE*)(org_p + offset) ^ (105 * offset - 85);
v5 = offset + 1i64;
}
if (len - 1i64 != offset)
{
v6 = org_p + 1;
v7 = dest_p + 1;
v8 = 105 * v5 - 85;
do
{
*(_BYTE*)(v7 + v5 - 1) = v8 ^ *(_BYTE*)(v6 + v5 - 1);
*(_BYTE*)(v7 + v5) = *(_BYTE*)(v6 + v5) ^ (v8 + 105);
v4 -= 2i64;
v6 += 2i64;
v7 += 2i64;
v8 -= 46;
} while (v5 != v4);
}
}
}
一个很简单的异或加密,解密这个payload,拖入ida,发现果然是一个pe格式的文件,而且还导出了几个函数:
-
hv.exe
接下来到重头戏,外挂对于hv.exe的操作
其实这个我们甚至可以不用看了,因为我们知道了hvloader.dll哪里其实就干了一件事情,而且很傻。
他拦截了hvloader去初始化host cr3,将自己的payload插入,同时去重定位了hv.exe,以及hv.exe的关键函数ept_violation_xxx
。
不过保持严谨,我们可以继续逆向,说不定还能发现这个外挂的骚操作。
首先定位到hv.exe的text
节,进行pattern find,特征码是0xe8 ?? ?? ?? ?? 0x48 0x8d ?? ?? 0xb9 0x6 0x0 0x8
这个特征码,找到hv.exe尝试扫一扫
本机上的hvix(intel平台,win11 23h2)的找到了,换个1909版本的
1909的找到了,总之这个特征码普适性很强,点进去看下:
什么都看不出来,因为hv.exe没有符号,所以看下外挂的逻辑吧,
外挂使用rip+offset获取了那个函数地址,老生常谈的问题了,原理不再赘述。接着往下看
外挂紧接着执行了一次_cpuid(0),获取了当前cpu的型号
因为我机器是intel的,所以amd暂且跳过,直接去看intel的逻辑。这说明刚才的那个特征码不仅全系统hv.exe通用,并且似乎amd的hv.exe和intel的hv.exe也是一样的。
外挂接着往下走,开始pattern find新的特征码
这个特征码是0xe8 ?? ?? ?? ?? 0x48 0x89 0x4 0x24 0xe9
,发现怎么都找不到,结果往上一看这个特征码还是amd的,发现逆向的有点错误,总之忽略这些细节
AMD都不管,继续往下找intel的
又是无聊的特征码,因为此时正式进入intel
的相关操作,所以特征码一定都是和intel的hv.exe紧紧关联的,这里把第一个特征码称作intel_pattern_code1
,特征码内容是0xe8 ?? ?? ?? ?? 0xe9 ?? ?? ?? ?? 0x41 0x83 0xBD
这个特征码在最新版的win11是找到了的,但是1909版本没有找到,所以应该是比较新的hv.exe才有的,anyway,我们还是点进去看看上下文:
发现神奇的东西,这很明显是一个switch case的结构,所以它switch的什么呢?或者是hv里面有哪些会用到switch case并且被外挂关心呢?其实写过vt的同学心里应该有答案,那就是vm-exit的exit reason,总之我们随便找个vt框架对下吧,看看能不能对的上。
Ept Violation
,0x30和0x31,难道真是这样,我们再确认下,随便找个 vm-exit-reason验证下,看下处理:
毫无疑问,外挂找到了hv.exe的vm-exit,并且是ept_violation
的相关处理。继续往下看
发现外挂尝试查找第二个特征码(如果第一个找不到),
0xe8 ?? ?? ?? ?? 0xe9 ?? ?? ?? ?? ?? 8B ?? 83 ?? 0X1F
测试发现,这个最新版的windows还是找不到,总之1909我们找到了,这个估计是其他版本的。剩下的就不上ida逆向图了,简而言之,它搜索了四个特征码
intel hv.exe:
pattern_find_1:0XE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? 0X41 0X83 0XBD #最新版windows hv.exe
pattern_find_2:0xE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? ?? 0X8B ?? 0X83 ?? 0X1F
pattern_find_3:0xE8 ?? ?? ?? ?? 0x83 0x3e 0x1b 0x74 0x08
pattern_find_4:0xE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? ?? 0X8B 0X4D ?? 0X48 0X33 0XCC #1909
总之,最后全找到了对应的特征,不过这里很疑惑的是,我自己通过vmresume
在1909的hv.exe找到的vm-exit和这个好像不太一样?
上图是我自己找到的,这个很明显是在分发Vm Exit Reason,很奇怪,看起来位置是不一样,因此我尝试找到这两个函数的调用关系
果然是有调用关系的,因此这里猜测,早期的hv.exe对于ept violation的处理是内联的,而不是一个call的形式,所以只能往更高层hook。
接下来,外挂解密自己的intel payload
相关解密函数前面已经展示了,是一个非常简单的异或加密。前面一共用了4个不同的特征码,目前还不能确定到底定位到的是不是ept_violation_call
,总之继续往下看,上下文全了肯定会有答案。
解密了payload,又开始进行特征码搜索。
_____________解密payload_______________________________________
pattern_find_5:e8 ?? ?? ?? ?? 90 e9 ?? ?? ?? ?? 48 8d 4d 8f #最新版win11
pattern_find_6:e8 ?? ?? ?? ?? eb 24 83
仍然是特征码定位,不过这次定位的刚好是ox30,看来刚才的那个并不是定位到了VMX_EXIT_REASON_EPT_VIOLATION
。这个才是?如果这个没找到,则更换特征码为:
0xE8 ?? ?? ?? ?? E9 ?? ?? ?? ?? ?? 83 ?? 31
上面那个特征最新版win11还能搜到,而这个我两个hv.exe都搜不到,继续往下逆:
总之,解密后,又是四个特征
pattern_find_1575:0x8B ?? 0XE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? 0X83 ?? 0X31
pattern_find_1645:0xe8 ?? ?? ?? ?? 0x90 0xe9 ?? ?? ?? ?? 0x48 0x8d 0x4d 0x8f #win11
pattern_find_1715:0xe8 ?? ?? ?? ?? 0xe9 ?? ?? ?? ?? 0x83 ?? 0x31
pattern_find_1779:0xe8 ?? ?? ?? ?? 0xeb 0x24 0x83
奇怪的是,win10 1909上面四个我哪个也没找到。而这里他也进行了一次区分,分为找到和没找到,如果找到了,计算ept_violation_call,然后去执行hook逻辑
在这里,我们看到了熟悉的0x7FA00008A000i64
,这个就是hvloader.dll插入的物理地址。
所以,到这里我们下了结论,这个地址就是用来装payload的。
外挂首先去定位了payload的导致函数,是按照序号导出的,第二个,接着去hv.exe里面搜索特征:
0xc3 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc
一眼看出来这是在寻找函数空隙,一般这样是为了间接跳转,不会破坏函数字节。
果然,它的这块free space就是跳板,而这块free space位于call ept_violation的上下2GB之内。最终hook到我们自己的ept_violation_handler,即payload的第二个导出函数。
到这里,其实整个劫持hyperV的流程基本上结束了,还有一点,就是如果上面四个pattern_find都没有找到怎么办?
其实外挂也有处理:
这个特征码,我对比了下,发现和voyager的一模一样
pattern_find_1896:0xe8 ?? ?? ?? ?? 0xe9 ?? ?? 0xff 0xff 0x74 和voyager一样
pattern_find_1957:0x80 0xe1 0x0f 0x80 0xf9 0x3 //1909可以找到(从voyager后开始)
可以发现,对于这种情况找到的特征码,hook到的函数是第三个导出函数,
还记得我们前面说的吗,hv.exe 在最新版的windows下的EPT_VIOLATION
是一个单独的函数处理的,因此可以直接hook这个单独的函数,但是低版本的windows,是内联到vm -exit中的,因此需要用payload的第三个导出函数来hook,这属于不同情况。
至此,我们基本上逆向完了整个bootmgfw.efi
的流程,进一步,我们需要知道外挂具体是如何hook vm-exit-ept-violation的,因此开始逆向Payload的导出函数