2022腾讯游戏安全技术大赛-PC方向初赛
2022腾讯游戏安全技术大赛-PC方向初赛

赛题分析
这年的赛题挺有意思,要求是还原图案,但没有要求怎么还原。我第一反应是Hook Present直接自己画图,但还得考虑精度、颜色的问题,先开IDA逆一下看看怎么回事。
容易定位到绘图逻辑:

调用ZwAllocateVirtualMemory创建了一片内存,v6是这片内存的指针,
if ( v6 == (char *)0xFFFFFFFFFFFFECFFLL )
{
*errno() = 22;
invalid_parameter_noinfo();
}
else
{
memcpy(v6 + 0x1301, &unk_7FF6DE906350, 0x18F0uLL);// Opcode
}
*(_QWORD *)(v6 + 0x2BF1) = D3DCompile;
qword_7FF6DE908318 = (__int64 (__fastcall *)(_QWORD))(v6 + 0x650);
*((_DWORD *)v6 + 411) = 0x2581;
*(_DWORD *)(v6 + 1097) = 0xEB4;
*((_DWORD *)v6 + 384) = 0x1301;
byte_7FF6DE908314 = 1;
qword_7FF6DE908318是一个函数指针,看下后面的逻辑:
if ( qword_7FF6DE908318 )
{
qword_7FF6DE908318(a1[6]);
if ( GetTickCount() - dword_7FF6DE908310 > 4000 )
{
v9 = (void *)qword_7FF6DE908308;
*(__m128i *)ProcName = _mm_load_si128((const __m128i *)"ZwFreeVirtualMem");
v10 = 11257LL;
strcpy(ModuleName, "ntdll.dll");
strcpy(v14, "ory");
v7 = GetModuleHandleA(ModuleName);
v8 = GetProcAddress(v7, ProcName);
if ( v8 )
((void (__fastcall *)(__int64, void **, __int64 *, __int64))v8)(-1LL, &v9, &v10, 0x4000LL);
memset(&unk_7FF6DE905040, 0, 0x1301uLL);
memset(&unk_7FF6DE906350, 0, 0x18F0uLL);
qword_7FF6DE908318 = 0LL;
}
}
(*(void (__fastcall **)(_QWORD, _QWORD, _QWORD))(*(_QWORD *)a1[6] + 64LL))(a1[6], 0LL, 0LL);
}
}
qword_7FF6DE908318可能是用来执行shellcode的,绘制四秒后自动释放内存,绘制的图形也就消失了,和目前的现象还是对的上的。先看看qword_7FF6DE908318这里面的东西
.text:00007FF6DE9011FE mov cs:qword_7FF6DE908318, rax
IDA动调后跳去rax看看,函数有点长这里就不放了,总之这个函数是用来初始化DirectX着色器的。值得注意的是,这里初始化了两套着色器:
第一套:
cbuffer ConstantBuffer : register(b0) {
matrix World;
matrix View;
matrix Projection;
};
struct VS_OUTPUT {
float4 Pos : SV_POSITION;
float4 Color : COLOR0;
};
VS_OUTPUT VS(float4 Pos : POSITION, float4 Color : COLOR) {
VS_OUTPUT output = (VS_OUTPUT)0;
output.Pos = mul(Pos, World);
output.Pos = mul(output.Pos, View);
output.Pos = mul(output.Pos, Projection);
output.Color = Color;
return output;
}
float4 PS(VS_OUTPUT input) : SV_Target {
return input.Color;
}
第二套:
struct VSOut {
float4 Col : COLOR;
float4 Pos : SV_POSITION;
};
VSOut VS(float4 Col : COLOR, float4 Pos : POSITION) {
VSOut Output;
Output.Pos = Pos;
Output.Col = Col;
return Output;
}
float4 PS(float4 Col : COLOR) : SV_TARGET {
return Col;
}
怀疑可能对应了示例图的两种图案。
上面这个函数只是shellcode的一部分,还没分析完,根据这一句代码:
memcpy(v9, sub_7FF710385040, 0x1301ui64);
shellcode应该有0x1301个字节,另外原本v9中的shellcode是不完整的,函数序言被抹去了,这里就是用来修复的:
*(_DWORD *)v6 = 0x53C48B48;
*((_DWORD *)v6 + 1) = 0x41575655;
*((_WORD *)v6 + 4) = 0x4156;
对应下面的汇编指令:
48 8B C4 mov rax, rsp
53 push rbx
55 push rbp
56 push rsi
57 push rdi
41 56 push r14
shellcode
来捋一下shellcode的内存是怎么分布的:
v6是shellcode头部,第一个函数:sub_128891E0000占用的内存为:v6 ~ v6+0x411。函数放在下面了,这是一个正方形的绘制函数,v28~v31分别是RGBA四个颜色分量,很显然这个函数是用来画图形中的蓝/黄正方形的。
__int64 __fastcall sub_128891E0000(
int a1,
int a2,
int a3,
int a4,
int n1132396544,
__int64 r8,
__int64 a7,
__int64 a8,
__int64 a9,
__int64 a10)
{
__int64 v14; // r14
unsigned int v15; // ebx
int v16; // ecx
float v17; // xmm9_4
float v18; // xmm11_4
float v19; // xmm7_4
float v20; // xmm10_4
float *v21; // rcx
int n1132396544_1; // xmm0_4
float v23; // xmm8_4
float v24; // xmm6_4
float v25; // xmm3_4
float v26; // xmm1_4
float v27; // xmm8_4
float v28; // xmm7_4
float v29; // xmm5_4
float v30; // xmm4_4
float v31; // xmm2_4
__int64 v32; // rax
float *v34; // [rsp+30h] [rbp-C8h] BYREF
char v35[8]; // [rsp+40h] [rbp-B8h] BYREF
float v36; // [rsp+48h] [rbp-B0h]
float v37; // [rsp+4Ch] [rbp-ACh]
int v38; // [rsp+108h] [rbp+10h] BYREF
int v39; // [rsp+110h] [rbp+18h] BYREF
int n28; // [rsp+118h] [rbp+20h] BYREF
v14 = r8;
v38 = 1;
v15 = n1132396544 + (a3 ^ (a1 + a2)) % 256 - a4 % 256;
(*(void (__fastcall **)(__int64, int *, char *))(*(_QWORD *)r8 + 760i64))(r8, &v38, v35);
v16 = (a3 ^ (a2 * a1)) % 256 - (a4 >> 8) % 256;
v17 = (float)(v37 - (float)(2 * (v16 + a2) - 1)) / v37;
v18 = (float)(v37 - (float)(2 * a2 + 99)) / v37;
v19 = (float)((float)(2 * (v16 + a1) - 1) - v36) / v36;
v20 = (float)((float)(2 * a1 + 99) - v36) / v36;
(*(void (__fastcall **)(__int64, __int64, _QWORD, __int64, _DWORD, float **))(*(_QWORD *)v14 + 112i64))(
v14,
a7,
0i64,
4i64,
0,
&v34);
v21 = v34;
n1132396544 = 0x437F0000;
v34[2] = (float)0;
n1132396544_1 = n1132396544;
v23 = (float)((a3 ^ (a2 + a1 * (a2 + 1))) % 256 - (a4 >> 16) % 256);
v24 = v23 + v19;
v25 = v23 + v17;
*v21 = v23 + v19;
v26 = v23 + v20;
v21[1] = v23 + v17;
v27 = v23 + v18;
v28 = (float)BYTE2(v15) / *(float *)&n1132396544_1;
v29 = (float)(unsigned __int8)(v15 >> 12) / *(float *)&n1132396544_1;
v30 = (float)(unsigned __int8)v15 / *(float *)&n1132396544_1;
v21[3] = v28;
v21[4] = v29;
v31 = (float)HIBYTE(v15) / *(float *)&n1132396544_1;
v21[6] = v31;
v21[9] = (float)0;
v21[13] = v31;
v21[16] = (float)0;
v21[20] = v31;
v21[27] = v31;
v21[5] = v30;
v21[7] = v26;
v21[8] = v25;
v21[10] = v28;
v21[11] = v29;
v21[12] = v30;
v21[14] = v24;
v21[15] = v27;
v21[17] = v28;
v21[18] = v29;
v21[19] = v30;
v21[21] = v26;
v21[22] = v27;
v21[24] = v28;
v21[25] = v29;
v21[26] = v30;
v21[23] = (float)0;
(*(void (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v14 + 120i64))(v14, a7, 0i64);
v32 = *(_QWORD *)v14;
n28 = 28;
v39 = 0;
(*(void (__fastcall **)(__int64, _QWORD, __int64, __int64 *, int *, int *))(v32 + 144))(
v14,
0i64,
1i64,
&a7,
&n28,
&v39);
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v14 + 0xC0i64))(v14, 5i64);
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v14 + 0x88i64))(v14, a8);
(*(void (__fastcall **)(__int64, __int64, _QWORD, _QWORD))(*(_QWORD *)v14 + 0x58i64))(v14, a9, 0i64, 0i64);
(*(void (__fastcall **)(__int64, __int64, _QWORD, _QWORD))(*(_QWORD *)v14 + 0x48i64))(v14, a10, 0i64, 0i64);
(*(void (__fastcall **)(__int64, _QWORD, _QWORD, _QWORD))(*(_QWORD *)v14 + 0xB8i64))(v14, 0i64, 0i64, 0i64);
return (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)v14 + 0x68i64))(v14, 4i64);
}
v6+0x412 ~ v6+0x41F处未使用
v6+0x420 ~ v6+0x624处为第二个函数:sub_128891E0420
__int64 __fastcall sub_128891E0420(
__int64 rcx0,
__int64 a2,
__int64 r8_0,
__int64 a4,
__int64 a8,
__int64 a9,
__int64 a10)
{
__int64 v7; // rsi
__int64 result; // rax
__int64 v11; // rcx
int v12; // edx
int v13; // r9d
int v14; // eax
__int64 v15; // [rsp+58h] [rbp-28h]
int a1[4]; // [rsp+60h] [rbp-20h]
int a3[2]; // [rsp+70h] [rbp-10h]
int n50; // [rsp+78h] [rbp-8h]
int n50_1; // [rsp+7Ch] [rbp-4h]
v7 = 0i64;
v15 = 0i64;
*(_OWORD *)a1 = 0i64;
*(_QWORD *)a3 = 0i64;
n50 = 50;
n50_1 = 50;
while ( 2 )
{
result = (__int64)dword_128891E1301;
switch ( dword_128891E1301[v7] )
{
case 0:
result = HIDWORD(v15);
LODWORD(v15) = HIDWORD(v15) + v15;
goto LABEL_10;
case 1:
result = HIDWORD(v15);
LODWORD(v15) = v15 - HIDWORD(v15);
goto LABEL_10;
case 2:
v11 = dword_128891E1301[v7 + 1];
v7 += 2i64;
result = (unsigned int)a1[v11 - 2];
a1[dword_128891E1301[v7] - 2] = result;
goto LABEL_10;
case 3:
v12 = dword_128891E1301[v7 + 1];
v7 += 2i64;
result = (__int64)dword_128891E1301;
a1[dword_128891E1301[v7] - 2] = v12;
goto LABEL_10;
case 4:
++v7;
v13 = v15;
v14 = v15 * (HIDWORD(v15) + 1);
LODWORD(v15) = dword_128891E1301[v7] ^ 0x414345;
result = (unsigned int)((int)(v15 ^ (HIDWORD(v15) + v13)) % 256
+ (((int)(v15 ^ (v13 * HIDWORD(v15))) % 256
+ (((int)(v15 ^ (HIDWORD(v15) + v14)) % 256) << 8)) << 8));
HIDWORD(v15) = result;
goto LABEL_10;
case 5:
result = sub_128891E0000(a1[2], a1[3], a3[0], a3[1], 0xFFFFFF00, r8_0, a4, a8, a9, a10);
goto LABEL_10;
case 6:
result = sub_128891E0000(a1[2], a1[3], a3[0], a3[1], 0xFF2DDBE7, r8_0, a4, a8, a9, a10);
goto LABEL_10;
case 7:
return result;
default:
LABEL_10:
if ( (unsigned __int64)++v7 < 0x1301 )
continue;
return result;
}
}
}
这是一个虚拟机的处理函数,命名为VMOperator,值得注意的是最后调用正方形绘制函数的参数:


恰好是两个正方形的颜色,那么程序的绘制逻辑应该是:传入Opcode,通过虚拟机决策来绘制出图案。暂时按下不处理,继续分析完整个shellcode结构,函数与函数之间有一些不使用的内存我直接跳过了
v6+0x650 ~v6+0x1300处为函数 sub_128891E0650,即着色器初始化函数。至此,shellcode的函数部分结束。
0x1300后的内存部分,通过这样一条代码赋值:

其中unk_7FF710386350长这样:

其实就是在VMOperator函数中的Opcode。至此shellcode分析基本完毕了,开始解决问题。
首先是为什么只有蓝色方块被绘制出来,根据VMOperator函数,理论上来说两种方块的绘制函数都有被调用的机会的,究竟是黄色方块的绘制函数没有被调用,还是传参时有问题?这里先动调VMOperator函数看看怎么回事,虚拟机牵一发动全身,最好不要贸然动Opcode。
动调看了一下,黄色方块的绘制函数确实是被调用了的。

没画出来可能是参数原因?写个脚本,每次调用时就打印函数的参数:
import idaapi
import idc
TARGET_ADDR = 0x128891E0000
class ParamMonitorHook(idaapi.DBG_Hooks):
def dbg_bpt(self, tid, ea):
if ea == TARGET_ADDR:
a1 = idc.get_reg_value("rcx")
a2 = idc.get_reg_value("rdx")
a3 = idc.get_reg_value("r8")
a4 = idc.get_reg_value("r9")
rsp = idc.get_reg_value("rsp")
color = idc.get_wide_dword(rsp + 0x28)
print(f"a1={a1}, a2={a2}, a3={a3}, a4={a4}, color=0x{color:08X}")
return 0
idc.add_bpt(TARGET_ADDR)
hook = ParamMonitorHook()
hook.hook()
print("监控已设置。当函数被调用时将打印参数。")
这里只选择打印前几个参数是因为,后面的参数调试过了都是一样的,应该是DirtectX上下文指针、顶点缓冲区指针、输入布局指针之类的东西,与题目无关,而前两个参数是X,Y坐标,比较重要。
下面是打印结果:
a1=4294966346, a2=50, a3=4280263596, a4=1248208, color=0xFFFFFF00
a1=50, a2=4294966906, a3=1822057, a4=4293442757, color=0xFFFFFF00
a1=4294966346, a2=170, a3=4287541859, a4=14227863, color=0xFFFFFF00
a1=50, a2=230, a3=1897472, a4=15215384, color=0xFFFFFF00
a1=4294966346, a2=4294967086, a3=3743658, a4=7267794, color=0xFFFFFF00
a1=50, a2=350, a3=966258, a4=14122466, color=0xFFFFFF00
a1=4294966406, a2=4294967026, a3=463184, a4=7666472, color=0xFFFFFF00
a1=170, a2=4294967026, a3=4292492823, a4=12856971, color=0xFFFFFF00
a1=4294966526, a2=230, a3=3460907, a4=4281474767, color=0xFFFFFF00
a1=110, a2=4294966906, a3=4288977945, a4=14280177, color=0xFFFFFF00
a1=4294966466, a2=170, a3=4291452647, a4=12856715, color=0xFFFFFF00
a1=650, a2=50, a3=15384343, a4=11002795, color=0xFFFFFF00
a1=590, a2=110, a3=12856715, a4=13307703, color=0xFF2DDBE7
a1=530, a2=170, a3=14096303, a4=2054931, color=0xFF2DDBE7
a1=470, a2=230, a3=12869865, a4=15314261, color=0xFF2DDBE7
a1=410, a2=290, a3=13085065, a4=12188981, color=0xFF2DDBE7
a1=470, a2=290, a3=11235584, a4=6581496, color=0xFF2DDBE7
a1=530, a2=290, a3=12847529, a4=3263901, color=0xFF2DDBE7
a1=710, a2=50, a3=14122635, a4=3090291, color=0xFF2DDBE7
a1=770, a2=50, a3=14265601, a4=10052917, color=0xFF2DDBE7
a1=830, a2=50, a3=3793238, a4=14305830, color=0xFF2DDBE7
a1=710, a2=110, a3=1253500, a4=3434568, color=0xFF2DDBE7
a1=770, a2=170, a3=13177867, a4=745383, color=0xFF2DDBE7
a1=830, a2=230, a3=5441311, a4=13085499, color=0xFF2DDBE7
a1=890, a2=290, a3=14094598, a4=14037658, color=0xFF2DDBE7
a1=950, a2=290, a3=15425697, a4=10849657, color=0xFF2DDBE7
a1=1010, a2=290, a3=15427533, a4=16116185, color=0xFF2DDBE7
a1=1070, a2=290, a3=14094689, a4=884017, color=0xFF2DDBE7
a1=950, a2=50, a3=6780433, a4=6659577, color=0xFF2DDBE7
a1=1010, a2=50, a3=4200253, a4=5601561, color=0xFF2DDBE7
a1=1070, a2=50, a3=14120545, a4=4037889, color=0xFF2DDBE7
a1=1130, a2=50, a3=15728538, a4=13250054, color=0xFF2DDBE7
a1=1010, a2=110, a3=12846315, a4=11999115, color=0xFF2DDBE7
a1=1070, a2=170, a3=14141167, a4=9134903, color=0xFF2DDBE7
a1=1130, a2=170, a3=14118943, a4=6781707, color=0xFF2DDBE7
a1=1190, a2=170, a3=14094705, a4=16600353, color=0xFF2DDBE7
a1=1250, a2=170, a3=3737259, a4=769831, color=0xFF2DDBE7
a1=1130, a2=230, a3=616035, a4=15687475, color=0xFF2DDBE7
a1=1190, a2=290, a3=7545175, a4=8608671, color=0xFF2DDBE7
a1=1250, a2=290, a3=3487536, a4=3683380, color=0xFF2DDBE7
a1=1310, a2=290, a3=6444969, a4=9786857, color=0xFF2DDBE7
a1=1370, a2=290, a3=14120879, a4=14638035, color=0xFF2DDBE7
注意到当打印黄色方块的时,前四个参数的值特别奇怪。我这里是按32位无符号整数打印的,所以打印出来是一个正值,那么如果是32位有符号整数,这很显然就是一个负值。也就是说黄色正方形没画出来的原因之一是坐标为负值,被画在画面之外了。
另外还有一种错误类型,长这样:
a1=50, a2=350, a3=966258, a4=14122466, color=0xFFFFFF00
看起来X,Y坐标都在屏幕之内,但是依然没画出来,原因暂时不明,猜测是参数a3和a4导致的。
这个题目虚拟机之外的东西都逆的差不多了,最后尝试模拟一下虚拟机,trace一下寄存器看看负数的形成原因
unsigned char opcode_table = {..};
//步长1
int reg[] = {0,0,0,0,0,0,0,0,50,50};
int idx = 0;
int drawCount = 0;
/*
reg[0]:LODWORD(v15)
reg[1]:HIDWORD(v15)
*/
int key1[11]{};
int key2[11]{};
int main() {
int i = 0,j=0;
while (1) {
int opcode = (int)opcode_table[idx];
int nextOpcode = (int)opcode_table[(idx + 1)];
int nextNextOpcode = (int)opcode_table[(idx + 2)];
if (opcode == 0) {
printf("[+]reg[0]:%d+reg[1]:%d\n", reg[0], reg[1]);
reg[0] += reg[1];
printf("reg[0]:%d\n", reg[0]);
}
else if (opcode == 1) {
printf("[+]reg[0]:%d-reg[1]:%d\n", reg[0], reg[1]);
reg[0] -= reg[1];
printf("reg[0]:%d\n", reg[0]);
}
else if (opcode == 2) {
printf("[+]reg[%d]:%d = reg[%d]:%d\n", nextNextOpcode, reg[nextNextOpcode], nextOpcode, reg[nextOpcode]);
reg[nextNextOpcode] = reg[nextOpcode];
printf("reg[%d]:%d\n", nextNextOpcode, reg[nextNextOpcode]);
idx += 2;
}
else if (opcode == 3) {
printf("[+]reg[%d]:%d = %d\n", nextNextOpcode, reg[nextNextOpcode], nextOpcode);
reg[nextNextOpcode] = nextOpcode;
printf("reg[%d]:%d\n", nextNextOpcode, reg[nextNextOpcode]);
idx += 2;
}
else if (opcode == 4) {
idx++;
int v13 = reg[0];
int v14 = reg[0] * (reg[1] + 1);
int nextVal = opcode_table[(idx)];
reg[0] = nextVal ^ 0x414345;
unsigned int color = ((int)(reg[0] ^ (reg[1] + v13)) % 256) +
(((int)(reg[0] ^ (v13 * reg[1])) % 256 +
(((int)(reg[0] ^ (reg[1] + v14)) % 256) << 8)) << 8);
reg[1] = color;
printf("[decrypt]reg[0]:%d reg[1]:%d\n", reg[0], reg[1]);
}
else if (opcode == 5) {
drawCount++;
printf("[%d]Render(X : %d,Y : %d , %d, %d , Yellow)\n", drawCount, reg[4], reg[5], reg[6], reg[7]);
if (drawCount <= 11) {
key1[drawCount - 1] = reg[6];
key2[drawCount - 1] = reg[7];
}
}
else if (opcode == 6) {
drawCount++;
printf("[%d]Render(X : %d,Y : %d , %d, %d , Blue)\n", drawCount, reg[4], reg[5], reg[6], reg[7]);
}
else if (opcode == 7) {
printf("End!\n");
break;
}
idx++;
if (idx > 1595)break;
}
printf("[+]key1:");
for (int i = 0;i < 11;i++) {
printf("%d,",key1[i]);
}
printf("\n");
printf("[+]key2:");
for (int i = 0;i < 11;i++) {
printf("%d,", key2[i]);
}
printf("\n");
}
这里给出第一次Render Yellow的trace结果:
[+]reg[0]:0 = reg[8]:50
reg[0]:50
[+]reg[4]:0 = reg[0]:50
reg[4]:50
[+]reg[0]:50 = reg[4]:50
reg[0]:50
[+]reg[1]:0 = 1000
reg[1]:1000
[+]reg[0]:50-reg[1]:1000 //负数来自这里
reg[0]:-950
[+]reg[4]:50 = reg[0]:-950
reg[4]:-950
[+]reg[0]:-950 = reg[9]:50
reg[0]:50
[+]reg[5]:0 = reg[0]:50
reg[5]:50
[+]reg[0]:50 = reg[4]:-950
reg[0]:-950
[+]reg[1]:1000 = reg[5]:50
reg[1]:50
[decrypt]reg[0]:1248208 reg[1]:-14703700
[+]reg[3]:0 = reg[0]:1248208
reg[3]:1248208
[+]reg[0]:1248208 = reg[1]:-14703700
reg[0]:-14703700
[+]reg[1]:-14703700 = reg[3]:1248208
reg[1]:1248208
[+]reg[6]:0 = reg[0]:-14703700
reg[6]:-14703700
[+]reg[7]:0 = reg[1]:1248208
reg[7]:1248208
[1]Render(X : -950,Y : 50 , -14703700, 1248208 , Yellow)
观察可得,X的负数是从case 2的减法来的。那么只需要加回去就能得到正确的坐标。
[1]Render(X : 50,Y : 50 , -14703700, 1248208 , Yellow)
但是这样手动加回去X只修改了第一个参数,而虚拟机的执行是与四个参数都相关的,所以不能仅仅通过修改X,Y坐标还原绘制,需要将虚拟机的减法逻辑去掉:
else if (opcode == 1) {
//printf("[+]reg[0]:%d-reg[1]:%d\n", reg[0], reg[1]);
//reg[0] -= reg[1];
//printf("reg[0]:%d\n", reg[0]);
}
将新虚拟机执行后的结果打印:
[1]Render(X : 50,Y : 50 , 16258228, 1248208 , Yellow)
[2]Render(X : 50,Y : 110 , 1822057, 7673289 , Yellow)
[3]Render(X : 50,Y : 170 , 8889163, 14227863 , Yellow)
[4]Render(X : 50,Y : 230 , 1897472, 15215384 , Yellow)
[5]Render(X : 50,Y : 290 , 3743658, 5377790 , Yellow)
[6]Render(X : 50,Y : 350 , 966258, 14122466 , Yellow)
[7]Render(X : 110,Y : 230 , 463184, 7898116 , Yellow)
[8]Render(X : 170,Y : 230 , 13055771, 12856971 , Yellow)
[9]Render(X : 230,Y : 230 , 3460907, 6000615 , Yellow)
[10]Render(X : 110,Y : 110 , 13743405, 14280177 , Yellow)
[11]Render(X : 170,Y : 170 , 11759583, 12856715 , Yellow)
看起来所有参数都还原了,但实际上以上面的数据去Hook绘制函数,发现只有4个黄色方块被画了出来,显然还存在其他问题。这里我们挑选一个未被画出的方块的Render函数执行流,如第四个,看看怎么回事:
[+]reg[0]:8889163 = reg[8]:50
reg[0]:50
[+]reg[4]:50 = reg[0]:50
reg[4]:50
[+]reg[0]:50 = reg[9]:50
reg[0]:50
[+]reg[1]:14227863 = 180
reg[1]:180
[+]reg[0]:50+reg[1]:180
reg[0]:230
[+]reg[5]:170 = reg[0]:230
reg[5]:230
[+]reg[0]:230 = reg[4]:50
reg[0]:50
[+]reg[1]:180 = reg[5]:230
reg[1]:230
[decrypt]reg[0]:15215384 reg[1]:1897472
[+]reg[3]:14227863 = reg[0]:15215384
reg[3]:15215384
[+]reg[0]:15215384 = reg[1]:1897472
reg[0]:1897472
[+]reg[1]:1897472 = reg[3]:15215384
reg[1]:15215384
[+]reg[6]:8889163 = reg[0]:1897472
reg[6]:1897472
[+]reg[7]:14227863 = reg[1]:15215384
reg[7]:15215384
[4]Render(X : 50,Y : 230 , 1897472, 15215384 , Yellow)
注意到在Render前执行了这样的操作:
[+]reg[0]:15215384 = reg[1]:1897472
reg[0]:1897472
[+]reg[1]:1897472 = reg[3]:15215384
reg[1]:15215384
这样就是交换了reg[0]和reg[1]的值,这个在正常的逻辑中是没有的。(看不出来的话可以打印正常逻辑的case跟这个对比下)。尝试在Hook实现中交换参数三和四。
Hook实现
综上,我们需要进行两个操作:
- 传入正确的X,Y坐标。
- 调换失败绘制的第三和第四个参数。
在赛题分析部分,我们已知存在一个全局变量指针,指向了shellcode,可以以此定位shellcode。
qword_7FF6CEA58308 = (__int64)v9;
这个指针的偏移是:0x8380,根据赛题分析,可知sub_128891E0000函数就是shellcode[0],即绘制函数被定位了。下面开始Hook,我用的Hook方案比较易懂:放行蓝色方块的绘制,根据预计算得到的正确的黄色方块坐标即三四参数修改黄色方块的绘制。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include <stdio.h>
#include "../MinHook/MinHook.h"
#include <math.h>
ULONG64 renderAddr = (ULONG64)GetModuleHandleA("2022游戏安全技术竞赛初赛.exe") + 0x8308;
void DebugLog(const char* format, ...)
{
char buffer[1024];
va_list args;
va_start(args, format);
vsprintf_s(buffer, format, args);
va_end(args);
OutputDebugStringA(buffer);
}
typedef __int64(__fastcall* fpRenderFunc)(int a1, int a2, int a3, int a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10);
fpRenderFunc oriRenderFunc = NULL;
int drawCount = 0;
int key1[11] = { 16258228,1822057,8889163,1897472,3743658,966258,463184,13055771,3460907,13743405,11759583 };
int key2[11] = { 1248208,7673289,14227863,15215384,5377790,14122466,7898116,12856971,6000615,14280177,12856715 };
__int64 __fastcall myRender(int a1, int a2, int a3, int a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10) {
if (a5 == 0xFF2DDBE7) { //蓝色方块直接执行
return oriRenderFunc(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
}
else if (a5 == 0xFFFFFF00) {
drawCount++;
if (a1 == -950 && a2 == 50) {
a1 = 50;
a2 = 50;
a3 = key2[0];
a4 = key1[0];
}
else if (a1 == 50 && a2 == -390) {
a1 = 50;
a2 = 110;
a3 = key1[1];
a4 = key2[1];
}
else if (a1 == -950 && a2 == 170) {
a1 = 50;
a2 = 170;
a3 = key2[2];
a4 = key1[2];
}
else if (a1 == 50 && a2 == 230) {
a3 = key2[3];
a4 = key1[3];
}
else if (a1 == -950 && a2 == -210) {
a1 = 50;
a2 = 290;
a3 = key1[4];
a4 = key2[4];
}
else if (a1 == 50 && a2 == 350) {
a3 = key2[5];
a4 = key1[5];
}
else if (a1 == -890 && a2 == -270) {
a1 = 110;
a2 = 230;
a3 = key1[6];
a4 = key2[6];
}
else if (a1 == 170 && a2 == -270) {
a1 = 170;
a2 = 230;
a3 = key2[7];
a4 = key1[7];
}
else if (a1 == -770 && a2 == 230) {
a1 = 230;
a2 = 230;
a3 = key1[8];
a4 = key2[8];
}
else if (a1 == 110 && a2 == -390) {
a1 = 110;
a2 = 110;
a3 = key2[9];
a4 = key1[9];
}
else if (a1 == -830 && a2 == 170) {
a1 = 170;
a2 = 170;
a3 = key2[10];
a4 = key1[10];
}
DebugLog("[%d]a1:%d,a2:%d,a3:%d,a4:%d", drawCount, a1, a2, a3, a4);
return oriRenderFunc(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
}
}
void InstallHook() {
MH_Initialize();
ULONG64 oriRenderFuncPTR = *(ULONG64*)renderAddr;
DebugLog("[+]renderAddr:%p", renderAddr);
DebugLog("[+]oriRenderFuncPTR:%p", oriRenderFuncPTR);
MH_CreateHook((LPVOID)oriRenderFuncPTR, (LPVOID)myRender, (LPVOID*)&oriRenderFunc);
MH_EnableHook(MH_ALL_HOOKS);
return;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
InstallHook();
}
}
注入效果:

Solved.
后记(题目点评以及碎碎念)
用一周多的时间复现了24、23、22三年的PC方向初赛,下周五就是25年的初赛了,初次参赛还是有点紧张哈哈哈。
斗胆从我个人的角度来评价一下这三年的赛题,先是24年的吧。这也是近三年来唯一涉及0环驱动分析、编程的题,同时也是反黑工具最强大的题,涉及的面是很广的,但毕竟是初赛难度不是很高,我觉得是做的比较酣畅淋漓的一道,能看出出题人下了很大工夫和心思,看完出题人Qfrost的博客后不禁感慨逆向真是学无止境。
再是23年的,这一年的赛题对选手inlineHook的考察很深,几乎考到了inlineHook的每一个方面,而且程序整体VM程度远大于24年的,基本是完全动调来做。我还看了一眼这一年的决赛赛题,风格也是很独特,涉及程序优化以及无源码更改程序功能,个人认为与“游戏安全”不太贴,但是非常贴合“PC逆向”,毕竟无源码改程序也是每个逆向手绕不开的坎。
最后是22年的,这一年赛题真的很有意思。个人认为是和“外挂”最贴的一集。初赛是通过创建窗口来绘制,决赛则是通过挂钩DWM Present函数来绘制,这两种都是外挂常用的绘制思路,而决赛给出的赛题就是对抗这种截图思路,很有实战性质。初赛的分析也比较贴合常见的CTF吧,加了个虚拟机分析,还挖了两三个坑,不过我觉得这个坑挖的是很有引导性质的,初次Hook给你画出4个方块,然后引导选手去想为什么剩下的方块没画出来。
当然解法也不只有这一种,本文开头提到的Hook Present函数来手动绘制也是一种方法,这种解法的逆向成本其实也不低,固然可以绕开虚拟机,不过又得自己分析绘制函数去找坐标的转换算法,还有就是怎么调用自己的绘制call,对汇编基本功要求也比较高。日后有时间我再补上这一种方法的WP吧(给自己挖个坑)。
非常感谢你读到这里,正如我所说的,逆向学无止境,诸位共勉!

浙公网安备 33010602011771号