题目说明:
- shootgame是一个游戏,hack.exe是游戏shootergame的一个外挂程序。
- 运行shootgame游戏,运行hack.exe,成功执行外挂功能并分析外挂实现过程。
- 实现一个与hack.exe的功能相同的,但是游戏逻辑原理不同的外挂程序。(游戏逻辑原理不同:指外挂程序对读写游戏数据结构或代码的攻击内容不同,并不是读写内存方式、注入内存方式、外挂核心核心代码载体差异的不同)
评分标准:满分10分
- flag(2分) 成功执行hack功能,给出外挂执行成功的flag。
- 代码(3分) 与hack.exe外挂功能相同,但实现原理与hack.exe不同的程序源码,仅需要提供可编译的工程完整代码,不需要提供已经编译出来的bin文件。
- 文档(4分) 详细描述解题过程,如涉及编写程序,必须提供源代码。
- 时间(1分) 正确提交flag、代码、文档的顺序,第1名计1分,后每1名-0.05分。
很明显,今年是写挂了。本fw通了两个宵只能很勉强的做出几个简单的功能。羡慕带哥直接dump SDK两小时生成带几十个功能的盖。(师傅们tql
hack.exe
加载程序进IDA,main函数开头解密字符串,试图打开hack.dat文件,因为dat文件不存在,所以hack.exe退出
我随便创了一个文件然后往里面写了一些东西,然后hack.exe就可以往下运行读取其中的内容然后进decode解密,decode函数里面是SSE优化后的解密算法,其中有两个分支,若长度大于0x40会走SSE优化的算法,若小于则走下面的分支,但本质的解密算法是一样的。具体算法后面给出分析
这一部分是计算shellcode的长度,shellcode会用于dll的加载
再下面v9就是decode函数对hack.dat解密出来的内容,通过取值取出ProcName,结合上下文可以知道这个ProcName就是游戏的进程名,在下面的 CreateToolhelp32Snapshot + Process32First 进程枚举中搜索游戏进程
再往下走下面两个循环分别是解密出flag和check flag
如果check成功则解密出 flag:%s 字符串并调用printf函数输出flag
下面会解密出一个dll并申请空间用于后续的注入
起动调程序,通过setIP手动的让程序运行decode函数解密DLL,然后通过idc脚本对其dump确定是一个dll文件,附dump.idc
#include <idc.idc>
#define PT_LOAD 1
#define PT_DYNAMIC 2
static main(void)
{
auto ImageBase,StartImg,EndImg;
auto i,dumpfile;
StartImg=0x19B11F51440;
EndImg=0x19B11F51440 + 0xFA00;
if(dumpfile = fopen("D:\\DumpFile","wb"))
{
dump(dumpfile,StartImg,EndImg);
fclose(dumpfile);
}
}
static dump(dumpfile,startimg,endimg)
{
auto i;
auto size;
size = endimg-startimg;
for ( i=0; i < size; i=i+1 )
{
fputc(Byte(startimg+i),dumpfile);
}
}
利用注入器将DLL注入到游戏中,弹出提示,测试发现带有右键自瞄的外挂功能
回过头来继续看hack.exe。后面一大段都是解密出各种函数(这里就不一一截图了)
最后将dll,shellcode写入到游戏进程空间并起远线程调用shellcode加载dll
内容分析完了,用x64dbg对decode函数动态调试,获得解密逻辑,通过flag反推出正确hack.dat文件 附exp.py
string = "2RSRhrofoWtLeLrJCSlTireznrtx.oeLxuehyyAwbpCOZq0tsS7MZyVdOUoE8\x00\x00\x00"
# code = [
# 0x32,0x52,0x53,0x52,0x68,0x72,0x6F,0x66,0x6F,0x57,0x74,0x4C,0x65,0x4C,0x72,0x4A
# ,0x43 ,0x53 ,0x6C ,0x54 ,0x69 ,0x72 ,0x65 ,0x7A ,0x6E ,0x72 ,0x74 ,0x78 ,0x2E ,0x6F ,0x65 ,0x4C
# ,0x78 ,0x75 ,0x65 ,0x68 ,0x79 ,0x79 ,0x41 ,0x77 ,0x62 ,0x70 ,0x43 ,0x4F ,0x5A ,0x71 ,0x30 ,0x74
# ,0x73 ,0x53 ,0x37 ,0x4D ,0x5A ,0x79 ,0x56 ,0x64 ,0x4F ,0x55 ,0x6F ,0x45 ,0x38 ,0x0, 0x0, 0x0]
code = [ord(x) for x in string]
i = 0;
j = 0;
k = 0;
for i in range(4):
for j in range(16):
code[k] += 0x13;
code[k] = code[k] & 0xFF
code[k] ^= 0x3F;
k += 1
for i in range(len(code)):
print(hex(code[i]), end=" ")
with open("hack.dat", 'wb') as fp:
fp.write(bytes(code))
可以看到外挂成功启动并输出flag
Flag: 2RSRhrofoWtLeLrJCSlTireznrtx.oeLxuehyyAwbpCOZq0tsS7MZyVdOUoE8
DumpFile.dll
直接看这个dll发现很乱,来回看了一下发现是封装了一个hook引擎进去。外挂注入后可以实现右键自瞄的功能,同时只有敌人离自己在一定范围内才会触发自瞄。因为是按键自瞄,直接想到GetAsyncKeyState函数。IDA看dll的导入表果然看到交叉引用,只有sub_180005050一个函数调用过该函数,确定此处为作弊功能
发现浮点数写操作,0x398很像是一个偏移,附加上去跟了一下
动调跟了发现这两个值分别是角色的上下摇摆角和左右偏移角。向上回溯寻找v16的来源定位到这个地方
获得计算公式,发现此处的基址是出于作弊模块空间的,回到初始化函数找到原基址,输入CE
以此为入手点,从头看这个作弊函数。这个大循环就是自瞄了
GetAimTarget函数会尝试匹配符合条件的自瞄目标,匹配成功则返回目标对象,否则返回-1,其也是通过枚举角色结构列表来实现的
然后会获取每个角色的名字进行匹配
会在CampName函数中对名字进行字符串匹配
下面是距离计算逻辑
通过屏幕中心点与敌人的屏幕坐标 距离 来匹配距离准星 最近的敌人,若能匹配到,敌人对象就会通过v3返回。至此GetAimTarget函数分析完毕。
通过GetAimTarget取到的敌人对象,则通过内存对其取坐标,易分析得0x164,0x168,0x16C分别为对象的X,Y,Z坐标
最后会对坐标做角度变换和归一化处理,最终计算得到上下摇摆角和左右偏移角填入游戏数据
至此外挂功能函数分析完毕
外挂攻击的数据(实现自瞄的方式)
[[[[[ShooterClient.exe + 2F71060 + 160] + 38] + 0] + 30] + 398] // 上下摇摆角
[[[[[ShooterClient.exe + 2F71060 + 160] + 38] + 0] + 30] + 39C] // 左右偏移角
外挂实现
写了两个外挂,编译环境均为 visual studio 2019 x64 release
Cheat1
纯外部跨进程的通过修改镜头角度的实现右键自瞄
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
#include "Offset.hpp"
#include "Myd3d.hpp"
#include "GameControler.hpp"
// #include "GameControlerByDriver.hpp"
using namespace std;
CGameControler GameControler("ShooterClient.exe");
D3DXVECTOR3 local_pos, target_pos;
string GetName(DWORD64 Entity)
{
DWORD64 Index = GameControler.read<DWORD>((Entity + 0x18));
DWORD64 Base = GameControler.read<DWORD64>((DWORD64)GameControler.GetGameBase() + offset_Name);
DWORD64 tmp1 = GameControler.read<DWORD64>(Base + 8 * ((Index) / 0x4000));
DWORD64 tmp2 = GameControler.read<DWORD64>(tmp1 + 8 * ((Index) % 0x4000));
return GameControler.ReadString((LPCVOID)(tmp2 + 0xC), 64);
}
int Cheat()
{
DWORD64 Uworld = GameControler.read<DWORD64>((DWORD64)GameControler.GetGameBase() + offsets_UWorld);
// printf("Uworld:%llx\n", Uworld);
DWORD64 ULevel = GameControler.read<DWORD64>(DWORD64(Uworld + offsets_Ulevel));
DWORD64 AActor = GameControler.read<DWORD64>(DWORD64(ULevel + offsets_Actor));
// printf("AActor:%p\n", AActor);
DWORD num = GameControler.read<DWORD>(DWORD64(ULevel + offsets_Actor) + 8);
DWORD64 GameInstance = GameControler.read<DWORD64>(DWORD64(Uworld + offsets_GameInstance));
DWORD64 LocalPlayer = GameControler.read<DWORD64>( GameControler.read<DWORD64>(DWORD64(GameInstance + offsets_LocalPlayer)) );
DWORD64 PlayerController = GameControler.read<DWORD64>(DWORD64(LocalPlayer + offsets_PlayerController));
DWORD64 LocalPawn = GameControler.read<DWORD64>(DWORD64(PlayerController + offsets_LocalPawn));
// printf("LocalPawn:%p\n", LocalPawn);
// printf("My HP:%f\n\n", GameControler.read<float>((DWORD64)(LocalPawn + offsets_Health)));
DWORD64 playobject = GameControler.read<DWORD64>(DWORD64(LocalPawn + offsets_PlayObject));
float MyX = GameControler.read<float>(DWORD64(playobject + offsets_TargetX));
float MyY = GameControler.read<float>(DWORD64(playobject + offsets_TargetY));
float MyZ = GameControler.read<float>(DWORD64(playobject + offsets_TargetZ));
// printf(" Local : X: %f \t Y : %f \t Z : %f\n", MyX, MyY, MyZ);
local_pos = GameControler.read<D3DXVECTOR3>(playobject + offsets_TargetX);
DWORD64* EntityList = (DWORD64*)malloc((num + 1) * sizeof(DWORD64*));
GameControler.Read((LPCVOID)AActor, EntityList, num * sizeof(DWORD64*));
for (int i = 0; i < num; ++i) {
DWORD64 Entity = EntityList[i];
if (Entity == LocalPawn) continue;
if (!Entity) continue;
float HP = GameControler.read<float>((DWORD64)(Entity + offsets_Health));
if (HP <= 0.f) continue;
string sName = GetName(Entity);
if (strcmp(sName.c_str(), "BotPawn_C")) continue;
DWORD64 playobject = GameControler.read<DWORD64>(DWORD64(Entity + offsets_PlayObject));
float targetX = GameControler.read<float>(DWORD64(playobject + offsets_TargetX));
float targetY = GameControler.read<float>(DWORD64(playobject + offsets_TargetY));
float targetZ = GameControler.read<float>(DWORD64(playobject + offsets_TargetZ));
// printf(" target[%d] : %s : X: %f \t Y : %f \t Z : %f\n", i, sName.c_str(), targetX, targetY, targetZ);
target_pos = GameControler.read<D3DXVECTOR3>(playobject + offsets_TargetX);
}
free(EntityList);
// printf("\n");
if (GetAsyncKeyState(2) != 0) {
D3DXVECTOR3 angle = { 0, 0, 0 };
float diff_x = target_pos.x - local_pos.x; // X差
float diff_y = target_pos.y - local_pos.y; // Y差
angle.x = atan2f(target_pos.z - local_pos.z, sqrtf((float)(diff_x * diff_x) + (float)(diff_y * diff_y)) ) * 57.295784;
angle.y = (float)((float)(atan2f(diff_y, diff_x) * 360.0) * 0.25) / 1.5707963;
if (angle.x < 0.0)
angle.x = angle.x + 360.0;
if (angle.y < 0.0)
angle.y = angle.y + 360.0;
GameControler.write<D3DXVECTOR3>(PlayerController + offsets_Pitch, angle);
// printf("Write X : %f \t Y : %f\n", angle.x, angle.y);
}
}
int main() {
printf("Strat Cheating......\n");
while (1) {
Cheat();
// Sleep(500);
}
}
Cheat2
思路:因为要实现自瞄,而镜头又不知道还有什么别的攻击方式,因此换了一个思路,把子弹出发的坐标改到敌人的坐标上,这样可以开枪直接就可以打死敌人 并且可以无视任何建筑物。我从子弹数量入手,找到游戏开枪的地方,开枪的地方一定会有一个子弹开始坐标,我们先用CE搜索子弹数量,并对其下访问断点
定位到了子弹数量减少处。因为要实现修改子弹发射点到敌人坐标处,我们需要找到子弹发射的地方,因此这里向上回溯
回溯了一层发现大量的call,我们在头部下断,粗粗看了一下这些call,发现在子弹减少call的上一个call里调用了rand函数。
因为弹道是具有随机性的, rand函数引起了我的注意,然后想起第一天在百度上搜到的关于这款游戏的开发文档的源码
上下看了一下发现其他部分也很像,认为这个call就是武器开火函数
[/img][img=300,0]
从call的头部下断 然后在游戏中开枪 再往下可以看到调用了函数取了一个坐标,我不知道这个坐标具体是个什么坐标 但是肯定是跟弹道有关的坐标
我通过在此处下断点,面对墙壁开枪,并修改了这些值,发现墙壁上没有出现弹孔,认为这些值就是控制子弹的起始坐标,而下面这个call就是碰撞call。如果能修改这个参数为敌人的坐标,应该就可以实现子弹全图自瞄。 我采用了外部注入shellcode hook此处,跳转到写入的自瞄shellcode上,完成了参数的修改。经测试,确实可以达到开枪后子弹能直接打中敌人并且无视建筑物的效果。 同时,在我添加bot后发现bot开枪也是经过的这个函数 所以我们hook这个函数 不仅可以让bot的子弹打不中我们 我们还可以打死这些bot (同时实现了无敌跟子弹穿墙追踪的效果)
下面贴一下shellcode
// AimBotShell
push rcx | 保护寄存器 压栈
push rdx | 保护寄存器 压栈
mov rcx,6666666666666666 | target_pos_addr
mov rdx,qword ptr ds:[rcx] | 读取xy 两个8字节 所以直接用rdx
mov qword ptr ss:[rsp+70],rdx | 更改0x60这个位置 因为前面保护寄存器压栈了0x10字节,所以这里是0x70
mov edx,dword ptr ds:[rcx+8] | 读取z坐标 4字节 所以是edx
mov dword ptr ss:[rsp+78],edx |
pop rdx | 恢复寄存器
pop rcx | 恢复寄存器 先进后出
mulss xmm8,xmm0 | 执行被破坏的原代码
mulss xmm7,xmm0 |
addss xmm8,dword ptr ss:[rsp+60] |
push 66666666 |跳回原处
mov dword ptr ss:[rsp+4],6666 |
ret |
// JmpShell 为了不影响寄存器 我们用栈做跳板 这样还能支持跨4gb跳转
push 66666666h
mov dword ptr [rsp+4], 6666h
retn
待外挂启动后,会先将shellcode注入游戏空间,并跨进程的不断枚举游戏对象列表,发现存活的敌人便将其坐标写入自瞄坐标内存上,shellcode会在每次开枪的时候将自瞄坐标内存上的值替换到碰撞call的参数上,以此就可以实现站着不动开枪杀死全图敌人且无敌的效果。
// Cheat2.cpp
include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
#include "Offset.hpp"
#include "Myd3d.hpp"
#include "GameControler.hpp"
// #include "GameControlerByDriver.hpp"
using namespace std;
CGameControler GameControler("ShooterClient.exe");
D3DXVECTOR3 target_pos;
DWORD64 target_pos_addr, payload_addr;
BYTE AimBotShell[] = "\x51\x52\x48\xB9\x66\x66\x66\x66\x66\x66\x66\x66\x48\x8B\x11\x48\x89\x54\x24\x70\x8B\x51\x08\x89\x54\x24\x78\x5A\x59\xF3\x44\x0F\x59\xC0\xF3\x0F\x59\xF8\xF3\x44\x0F\x58\x44\x24\x60\x68\x66\x66\x66\x66\xC7\x44\x24\x04\x66\x66\x00\x00\xC3";
/* AimBotShell
push rcx | 保护寄存器 压栈
push rdx | 保护寄存器 压栈
mov rcx, 6666666666666666 | target_pos_addr
mov rdx, qword ptr ds : [rcx] | 读取xy 两个8字节 所以直接用rdx
mov qword ptr ss : [rsp + 70] , rdx | 更改0x60这个位置 因为前面保护寄存器压栈了0x10字节,所以这里是0x70
mov edx, dword ptr ds : [rcx + 8] | 读取z坐标 4字节 所以是edx
mov dword ptr ss : [rsp + 78] , edx |
pop rdx | 恢复寄存器
pop rcx | 恢复寄存器 先进后出
mulss xmm8, xmm0 | 执行被破坏的原代码
mulss xmm7, xmm0 |
addss xmm8, dword ptr ss : [rsp + 60] |
push 66666666 | 跳回原处
mov dword ptr ss : [rsp + 4] , 6666 |
ret |
*/
BYTE JmpShell[] = "\x68\x66\x66\x66\x66\xC7\x44\x24\x04\x66\x66\x00\x00\xC3";
/* JmpShell
push 66666666h
mov dword ptr [rsp+4], 6666h
retn
*/
string GetName(DWORD64 Entity)
{
DWORD64 Index = GameControler.read<DWORD>((Entity + 0x18));
DWORD64 Base = GameControler.read<DWORD64>((DWORD64)GameControler.GetGameBase() + offset_Name);
DWORD64 tmp1 = GameControler.read<DWORD64>(Base + 8 * ((Index) / 0x4000));
DWORD64 tmp2 = GameControler.read<DWORD64>(tmp1 + 8 * ((Index) % 0x4000));
return GameControler.ReadString((LPCVOID)(tmp2 + 0xC), 64);
}
int Cheat()
{
DWORD64 Uworld = GameControler.read<DWORD64>((DWORD64)GameControler.GetGameBase() + offsets_UWorld);
// printf("Uworld:%llx\n", Uworld);
DWORD64 ULevel = GameControler.read<DWORD64>(DWORD64(Uworld + offsets_Ulevel));
DWORD64 AActor = GameControler.read<DWORD64>(DWORD64(ULevel + offsets_Actor));
DWORD num = GameControler.read<DWORD>(DWORD64(ULevel + offsets_Actor) + 8); // 取对象数量
DWORD64 GameInstance = GameControler.read<DWORD64>(DWORD64(Uworld + offsets_GameInstance));
DWORD64 LocalPlayer = GameControler.read<DWORD64>( GameControler.read<DWORD64>(DWORD64(GameInstance + offsets_LocalPlayer)) );
DWORD64 PlayerController = GameControler.read<DWORD64>(DWORD64(LocalPlayer + offsets_PlayerController));
DWORD64 LocalPawn = GameControler.read<DWORD64>(DWORD64(PlayerController + offsets_LocalPawn));
// printf("My HP:%f\n\n", GameControler.read<float>((DWORD64)(LocalPawn + offsets_Health)));
DWORD64* EntityList = (DWORD64*)malloc((num + 1) * sizeof(DWORD64*));
GameControler.Read((LPCVOID)AActor, EntityList, num * sizeof(DWORD64*));
for (int i = 0; i < num; ++i) {
DWORD64 Entity = EntityList[i];
if (Entity == LocalPawn) continue;
if (!Entity) continue;
float HP = GameControler.read<float>((DWORD64)(Entity + offsets_Health));
if (HP <= 0.f) continue;
string sName = GetName(Entity);
if (strcmp(sName.c_str(), "BotPawn_C")) continue;
DWORD64 playobject = GameControler.read<DWORD64>(DWORD64(Entity + offsets_PlayObject));
float targetX = GameControler.read<float>(DWORD64(playobject + offsets_TargetX));
float targetY = GameControler.read<float>(DWORD64(playobject + offsets_TargetY));
float targetZ = GameControler.read<float>(DWORD64(playobject + offsets_TargetZ));
// printf(" target[%d] : %s : X: %f \t Y : %f \t Z : %f\n", i, sName.c_str(), targetX, targetY, targetZ);
target_pos = GameControler.read<D3DXVECTOR3>(playobject + offsets_TargetX);
GameControler.Write((LPVOID)target_pos_addr, target_pos, 12); // 写入敌人坐标至自瞄坐标
}
free(EntityList);
}
int main() {
printf("Strat Cheating......\n");
target_pos_addr = (DWORD64)VirtualAllocEx(GameControler.GetHandle(), NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (target_pos_addr == NULL) {
printf("VirtualAllocEx target_pos_addr Error! Error Code:%d\n", GetLastError());
exit(0);
}
payload_addr = (DWORD64)VirtualAllocEx(GameControler.GetHandle(), NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (payload_addr == NULL) {
printf("VirtualAllocEx payload_addr Error! Error Code:%d\n", GetLastError());
exit(0);
}
DWORD64 HookAddress = (DWORD64)GameControler.GetGameBase() + 0x51C48C;
DWORD64 ReturnAddress = (DWORD64)GameControler.GetGameBase() + 0x51C49C;
*(DWORD64*)(AimBotShell + 0x4) = target_pos_addr;
*(DWORD*)(AimBotShell + 0x2E) = *(DWORD32*)(&ReturnAddress);
*(DWORD*)(AimBotShell + 0x36) = *(DWORD32*)((DWORD64)(&ReturnAddress) + 4);
*(DWORD*)(JmpShell + 0x1) = *(DWORD32*)(&payload_addr);
*(DWORD*)(JmpShell + 0x9) = *(DWORD32*)((DWORD64)(&payload_addr) + 4);
GameControler.Write((LPVOID)payload_addr, AimBotShell, sizeof(AimBotShell) - 1);
DWORD OldProtect{ 0 };
BOOL nStatus = TRUE;
nStatus = VirtualProtectEx(GameControler.GetHandle(), (LPVOID)HookAddress, 0x1000, PAGE_EXECUTE_READWRITE, &OldProtect);
GameControler.Write((LPVOID)HookAddress, JmpShell, sizeof(JmpShell) - 1);
nStatus = VirtualProtectEx(GameControler.GetHandle(), (LPVOID)HookAddress, 0x1000, OldProtect, &OldProtect);
printf("target_pos_addr:%llx \t payload_addr:%llx\n", target_pos_addr, payload_addr);
if (nStatus == TRUE)
printf("Cheat Init Success!\n");
else {
printf("Cheat Init Fail!\t Error Code:%d\n", GetLastError());
exit(-1);
}
while (1) {
Cheat();
// Sleep(500);
}
}
至此,外挂实现完毕