2021南极动物厂游戏高赛竞赛决赛分析02
2021-05-09 18:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:133 收藏

本文为看雪论坛优秀文章
看雪论坛作者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
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

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


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458384608&idx=2&sn=d7cf94ce8a5f949bff46df2246accdf8&chksm=b180c86a86f7417c0380aea09b5e8a209b5fc67efbdfe85e4e634ca4f5da08e5a6e9c1da63ca#rd
如有侵权请联系:admin#unsafe.sh