查看原文
其他

2021南极动物厂游戏高赛竞赛决赛分析02

淡然他徒弟 看雪学院 2021-05-14
本文为看雪论坛优秀文章
看雪论坛作者ID:淡然他徒弟



前言




上一篇文章,对整个赛题进行了大概的分析。接下来就是如何实现个跟自瞄不同的功能了。那怎么样才能够更暴力,那当然是...当当当~ 子弹追踪!接下来就是讲解如何找子弹追踪,并且怎么实现子弹穿墙追踪。进入我们的Part 2部分吧。


Part  2


0x01 过程




功能分析


使用工具:IDA Pro 7.2、CheatEngine 7.2、x64dbg、PYArk

1. 好咧,首先子弹是从枪上打出去的,也就是子弹出发的坐标是以枪口坐标为准的。那我们第一个思路是更改子弹出发的坐标,直接把坐标改到敌人的坐标,岂不是子弹一出发就可以直接把敌人打死,那我们的关键就是如何找到开枪的函数了。

2. 开枪会跟什么有关系呢->那肯定是子弹数量啦,我们先把Bot的数量设置为0,防止老被Bot打死,然后我们CE搜索出子弹数量的地址,右键查找是什么改写了此地址。

子弹数量一般都存储在武器指针下面,rbx应该就是武器指针哦。



3. 看见一个地址,我们在这F5下个断返回到上一层,武器开火的函数就能在附近找到哦 (为什么武器开火的函数就在附近呢?因为开火->子弹减少)一般正常流程都是这样~ 那我们先把子弹减少这个call nop看看。




我们可以看见,墙上依然会有弹孔(假设开火函数在子弹数量减少的这个call里面的话,那么nop掉之后相当于子弹不会发射,那墙上也就不会有弹孔),而且我们已知rcx是武器指针,那我们看看附近有没有用到了武器指针的函数。
 
而且通常都会是虚表函数,为什么呢?因为游戏引擎基本都是基于对象去管理事件的。

例如武器.开火()这样子。

 
我们nop这个函数试试。

 
发现弹孔消失了,只剩下枪口的特效了,那我们就要在这个call内,认真分析了,先把这个虚表函数恢复了,然后在这里下断。

 
4. 武器子弹弹道函数的分析


我们可以看到函数内有大量的浮点操作,此时我们慢慢单步,观察这个函数内每一个函数的参数返回值,看看有没有取出来什么坐标之类的,因为这个虚表函数已经被确认为开火函数,并且这个虚表函数除了this之外是没有其他参数的,所以子弹出发的坐标一定是在这个函数里面,通过其他函数取出来的。所以我们要格外认真的去查看这些函数的返回值参数(因为可能是通过参数返回的坐标)。




没单步一会就看到这个函数返回了一个坐标(存储在rax 或参数[rsp+0x60]里),此时我们先运行起来,然后我们对着墙开枪,中断以后把返回的坐标清0,看看效果。

 
发现弹孔消失了,所以这个坐标一定跟弹道有关。但是并不能确认这个坐标就是子弹出发的坐标,所以我们手写一个shellcode来试试。
 
5. 选取一个合适的位置来hook,首先我们要保证不破坏原来的上下文(就是寄存器保护好) ,并且不破坏原来的执行代码。
 
所以...当当当~ 我使用了这种支持跨4gb的跳转,并且不会影响寄存器的跳转。

push xxxxmov [rsp+4],xxxxret





我们对着天开枪,这个Bot就死了。
 
这样就实现了穿墙加子弹追踪的效果。
 
然后在后续中,发现多个Bot存在时,Bot会出现打不中我的情况,在这个函数下断分析,发现机器人开火的时候也会走这里,那我们这时候可以改改我们的shellcode。

判断下武器指针是否属于我们自己,不是我们自己的话就不修改,是我们自己的话就给Bot坐标,这样就不会影响Bot的开枪了(懒得写了)。

// dllmain.cpp : 定义 DLL 应用程序的入口点。#define _CRT_SECURE_NO_WARNINGS#include <windows.h>#include <iostream> void DllEntry();char* GetName(uintptr_t Object); BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: AllocConsole(); freopen("CON", "w", stdout); CreateThread(0, 0, (LPTHREAD_START_ROUTINE)DllEntry, 0, 0, 0); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;} uintptr_t GameBase{ 0 };uintptr_t BotPosPtr{ 0 };const char BotName[10] = "BotPawn_C"; struct Array{ uintptr_t* ArrayEntry; int Count;}; struct Vec3{ float x; float y; float z;}; Vec3* GetPos(uintptr_t Object);void Hook(); void DllEntry(){ GameBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(L"ShooterClient.exe")); Hook(); while (true) { auto UOjbectArrayPtr = *(uintptr_t*)(GameBase + 0x2F71060); if (!UOjbectArrayPtr) continue; UOjbectArrayPtr = *(uintptr_t*)(UOjbectArrayPtr + 0x30); if (!UOjbectArrayPtr) continue; auto UOjbectArray = *(Array*)(UOjbectArrayPtr + 0x98);//自己定义一个结构 for (int index = 0; index < UOjbectArray.Count; index++) { auto Object = UOjbectArray.ArrayEntry[index]; if (Object) { auto NamePtr = GetName(Object); if (NamePtr) { if (!strcmp(NamePtr, "BotPawn_C")) { auto pPos = GetPos(Object); memcpy((void*)BotPosPtr, pPos, sizeof(Vec3)); } //printf("Ptr:%llx Name:%s\n", Object, NamePtr); } } } }} void Hook(){ uint8_t BulletShellCode[] = "\x81\xC1\x6B\x63\x19\x36\x8B\xC1\x25\xFF\xFF\x7F\x00\x0D\x00\x00\x80\x3F\x89\x85\x00\x01\x00\x00\x50\x51\x48\xB8\x66\x66\x66\x66\x66\x66\x36\x12\x48\x8B\x08\x48\x89\x4C\x24\x70\x8B\x48\x08\x89\x4C\x24\x78\x59\x58\x68\x78\x56\x34\x12\xC7\x44\x24\x04\x34\x12\x00\x00\xC3"; uint8_t JmpShellCode[] = "\x68\x78\x56\x34\x12\xC7\x44\x24\x04\x34\x12\x00\x00\xC3"; BotPosPtr = (uintptr_t)malloc(sizeof(Vec3)); auto HookMemory = (uintptr_t)VirtualAlloc(0, 0x1000, 0x1000, PAGE_EXECUTE_READWRITE); if (BotPosPtr && HookMemory) { auto HookAddress = GameBase + 0x51C162; auto ReturnAddress = GameBase + 0x51C17A; *(uintptr_t*)(BulletShellCode + 0x1C) = BotPosPtr; *(uint32_t*)(BulletShellCode + 0x36) = *(uint32_t*)(&ReturnAddress); *(uint32_t*)(BulletShellCode + 0x3E) = *(uint32_t*)((uint64_t)(&ReturnAddress) + 4); memcpy((void*)HookMemory, BulletShellCode, sizeof(BulletShellCode) - 1); *(uint32_t*)(JmpShellCode + 0x1) = *(uint32_t*)(&HookMemory); *(uint32_t*)(JmpShellCode + 0x9) = *(uint32_t*)((uint64_t)(&HookMemory) + 4); DWORD old{ 0 }; VirtualProtect((void*)HookAddress, 0x100, 0x40, &old); memcpy((void*)HookAddress, JmpShellCode, sizeof(JmpShellCode) - 1); VirtualProtect((void*)HookAddress, 0x100, old, &old); } printf("HookMemory:%llx BotPosPtr:%llx\n", HookMemory, BotPosPtr);} char* GetName(uintptr_t Object){ if (IsBadReadPtr((void*)Object, 8)) { return 0; } auto NameIndex = *(int*)(Object + 0x18); if (!NameIndex) return NULL; auto NameBase = *(uintptr_t*)(GameBase + 0x2E6E0C0); if (!NameBase) return NULL; auto NameIndexPtr = *(uintptr_t*)(NameBase + 8 * (static_cast<uint64_t>(NameIndex) / 0x4000)); if (!NameIndexPtr) return NULL; NameIndexPtr = *(uintptr_t*)(NameIndexPtr + 8 * (static_cast<uint64_t>(NameIndex) % 0x4000)); if (!NameIndexPtr) return NULL; return (char*)(NameIndexPtr + 0xC); //v4 = (*(*(qword_1800091A0 + 8i64 * (*v3 / 0x4000)) + 8i64 * (*v3 % 0x4000)) + 0x10i64); 这里dump出来的dll最后是0x10哦 //这里 0xC 为什么跟 dump出来的 那个0x10不一样呢 因为0x10取出来的名字是不完整的 不知道为什么出题人要这样写} Vec3* GetPos(uintptr_t Object){ if (IsBadReadPtr((void*)Object, 8)) { return 0; } auto PosPtr = *(uintptr_t*)(Object + 0x158); return (Vec3*)(PosPtr + 0x164); }

FLAG


FLAG毫无技术难度,重新去看的时候,没多久就搞定了...

重新回来看FLAG,发现了这里,不清楚是个啥,于是x64dbg,设置RIP,可以看见解密出来以后的字符串 FileName = "flag:%s\n\r"

进到140001010可以看见明显的特征(分析多了,这里其实可以看出来,这是一个printf函数)。

所以在v23为假的情况下会执行这个流程打印v9,v9是从hack.dat解密出来的。



往上重新分析下v23是如何被更改的。








回到加密函数,第一个参数是要被解密的BufferPtr,第二个是长度,并且经过这个解密函数后解密前后的Buffer长度是一样的,并且在大于等于0x40的时候会走上面这个分支,小于0x40会走下面这个分支。我们已经知道了FLAG的长度为0x3E,所以我们只需要看下面这个分支就好了 (实际上,这两个分支的算法都是一样的,只不过是被编译器用SSE加速优化了)。





#include <iostream>#include <Windows.h> int main(){ uint8_t flag[] = "2RSRhrofoWtLeLrJCSlTireznrtx.oeLxuehyyAwbpCOZq0tsS7MZyVdOUoE8"; for (int i = 0; i < sizeof(flag); i++) { flag[i] += 0x13; flag[i] ^= 0x3F; } HANDLE hFile = CreateFileA("hack.dat", GENERIC_ALL, NULL, NULL, CREATE_ALWAYS, NULL, NULL); DWORD lpNumberOfBytesWritten{ 0 }; WriteFile(hFile, &flag, sizeof(flag), &lpNumberOfBytesWritten, NULL); std::cout << "Hello World!\n";}



0x02 总结




发的两篇帖子,都是大晚上随手写的,写的不好的地方,欢迎指正啦~
 
而且这两篇帖子在写的时候,都是从一个不太了解UE4引擎的普通的参赛选手的角度出发去写的(为了让没有做过游戏逆向的朋友们也能看懂,并且如何得出做题的思路~~)。
 
所以我觉得这次的赛题无论懂UE4引擎的数据结构好,不懂UE4引擎的数据结构好,其实只要思路正确都是可以快速做出答案的哦~ 并且还可以剩余很多时间去精进自己的WriteUp(意思就是基于做完题目的前提下,疯狂内卷,写个无限血量、无限子弹,分析下hack.dll的hook点、绘制的实现,自己写个透视:漏脚打脚、漏头打头之类的提高分数)。
 
当然,如果是懂UE4引擎的数据结构或者有相关FPS外挂经验的选手可能在做题的过程中更容易猜出hack.dll的意图,从而更好的去解题。其实题目还有很多有意思的地方,例如hack.dll的实现或者是比子弹穿墙更加变态的实现(模拟弹道)、UE4 SDK的生成等等,涉及的面太多太多,我没有在帖子中一一讲解,因为真的讲不完。
 
相关代码已经贴出来了,如果有讲的不好的地方或者不懂的地方,欢迎跟帖~
 
赛题链接:
https://gslab.qq.com/html/competition/2021/race-pre.htm

本文附件可点击左下方阅读原文自行下载!


- End -




看雪ID:淡然他徒弟

https://bbs.pediy.com/user-home-620278.htm

  *本文由看雪论坛 淡然他徒弟 原创,转载请注明来自看雪社区。



《安卓高级研修班》2021年6月班火热招生中!



# 往期推荐






公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存