0


小白向—2022腾讯游戏安全初赛分析(上)

小白向—2022腾讯游戏安全初赛分析(上)

参考:

smallzhong/gslab-2022-competition: 2022腾讯游戏安全PC初赛答案 (github.com)

2022腾讯游戏安全大赛复盘 - 吾爱破解 - 52pojie.cn

2022腾讯游戏安全PC端初赛复现 - 吾爱破解 - 52pojie.cn

前言:

在爱破网的精华帖里看到了对2022腾讯游戏安全初赛的分析(“参考”中的第二条链接),感觉挺有意思的,但因为当时看的时候楼主是纯小白(甚至没用过ida pro),完全看不懂,就想着去学一学,试一试。学到了很多东西,觉得是一次不错的入门经历,因此记录下来,向其他小白详细地介绍整个分析以及操作的流程。

小白将学到

  1. ida pro的基本使用。
  2. hook的概念以及操作方式
  3. dump的概念以及操作方式

所需前置知识

c语言基础

所需工具

IDA Pro

Visual Studio

赛题说明

赛题下载链接:https://gslab.qq.com/html/competition/2022/doc/PC%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%AE%89%E5%85%A8-%E5%88%9D%E8%B5%9B%E8%B5%9B%E9%A2%98.zip

这里有一个画了flag的小程序,可好像出了点问题,flag丢失了,需要把它找回来。
题目:

请添加图片描述
找回flag样例:

请添加图片描述

操作与分析

接下来正式开始。

使用IDA Pro打开赛题的exe程序

首先使用ida pro打开赛题的exe程序
请添加图片描述

在弹出的文件选择窗口中选择赛题的exe程序

请添加图片描述
之后弹出的第一个窗口直接选ok,第二个选择no。

请添加图片描述
请添加图片描述

文件就加载完成了

请添加图片描述

静态分析

我们的主界面初始位于IDA View-A窗口,且看到的是一个被称为graphs的界面,按下空格键可在graphs界面与hex代码界面(这只是我的称呼,官方名称我也不太清楚)切换,其中展示的是汇编代码。

IDA的左边具有一个Functions窗口,罗列了系统检索到的函数,其中具有一个名为WinMain函数,类似于C中的main函数,是整个程序的起始函数。双击WinMain,IDA会跳转到WinMain汇编代码的部分。
请添加图片描述

汇编代码很难分析,我们想要看c语言代码,按下Tab键,将跳转到WinMain函数的c代码界面,即Pseudocode-A界面。
请添加图片描述

现在我们可以开始分析代码的逻辑了。查看WinMain函数中的代码,可以发现在60行以前都是赋值,在第61行开始进行逻辑功能处理
请添加图片描述

粉紫色的函数为windows提供的官方api,且经询问ai得知与图像绘制无关
请添加图片描述

因此推测图像处理逻辑存在于else中的sub_140001090函数,双击此函数,进入其函数实现界面,观察代码
请添加图片描述

从第18行开始进入逻辑处理,观察代码,每遇到一个深蓝色变量都双击观察其是否具有初始值,如byte_140008314, 双击后显示db 为 “?”, 即没有初始值,不深究
请添加图片描述

而xmmword_140003490双击后为
请添加图片描述

617574726956657461636F6C6C41775Ah,尝试转换成字符串(末尾h表示16进制,且以两个数字为单位组合,如61表示a, 具体对应关系请查看ascall码表),得到autriVetacollAwZ

由于在内存中,两个数字为一个单位,且右边的单位为高位(比如1234,我们读是一千二百三十四,但是在内存中的顺序来读,则代表三千四百一十二),因此需要将字符串倒过来为ZwAllocateVirtua

结合第29行的lMemory,构成函数ZwAllocateVirtualMemory,用于申请内存。至于为什么能结合,在第14与第15行声明时,v14与ProcName时相邻声明的,因此在内存中其位置也是相邻的,无需再手动进行拼接。

而后通过第30、31行,使procAddress指代函数ZwAllocateVirtualMemory,并于33行调用,将内存分配到v9,内存的大小为v10,v10在第27行赋值为11257i64,很大,因此推测v9并不是用来存储普通变量,而是可能存储数组或者函数,但是这个程序并没有哪一处需要用到这么大的数组(绘制点的存储也不需要这么大),因此推测v9可能用来存储函数。

接下来要盯着v9,分析分配出的内存会被用来做什么。

第44行中,将unk_140005040的数据分配到v9
请添加图片描述

但是双击unk_140005040查看其初始值,并不能直接获取到什么有效信息
请添加图片描述
我们之前推测v9可能用来存储函数,如果真的是这样,那么unk_140005040应该就是那个函数。我们按下c键,IDA将把这些数据转化为汇编码
请添加图片描述

14000504A及之后的代码挺像一回事的,但是前部分作为函数的话缺少几个push,我们就先回到Pseudocode-A,看看之后有没有对函数进行其他处理。

分析代码逻辑,会发现第40行将v9的值赋值给了v6,而第51-53,65-67行,有使用v6来对函数所在内存的开头部分以及其他一些点重新赋值(使用地址定位来进行的小范围赋值一般称为patch)
请添加图片描述

我们想要查看重新赋值后的函数,就要进入动态分析

动态分析

点击菜单栏的Debugger,选中Select debugger
请添加图片描述

在弹出的窗口中选择Local Windows debugger,点击OK
请添加图片描述

这样我们就设置好了调试器,接下来就是打断点

由于我们希望看到patch后的函数,因此直接在patch后的下一条指令打下断点即可
请添加图片描述

再次点击菜单栏的Debugger, 会发现展开内容变化了,选择Start process,即开始调试。弹出的窗口全点yes或ok。
请添加图片描述

代码会停止在下断点处

这时我们就可以查看patch后的代码,查看方式为

鼠标悬浮在v6上,查看v6的值
请添加图片描述

我们就得到了v6的值为0x17DD94E0000(不固定,每一次调试的具体值都可能不同),也就是说0x17DD94E0000指向代码的开始

按下g键,弹出地址跳转窗口,输入v6的值,点击ok。
请添加图片描述

跳转到新界面后按下c键,将数据转化为汇编代码
请添加图片描述

可以看到之前的nop都变成具体的代码了。想要看这些汇编代码对于的c语言代码,则右键函数的起始位置,即17DD94E0000 ,在菜单中选择create function
请添加图片描述

点击后可以注意到代码再次产生了变化
请添加图片描述

这时选中函数名,按下tab键,就将跳转到c语言代码界面。
请添加图片描述

这段代码有一些赋值以及看不明白的函数调用,搞不清楚,于是先回到winMain里的那个名为sub_140001090的函数。按下窗口左上角的左向箭头,返回上一个界面
请添加图片描述

回到sub_140001090,重新梳理程序逻辑,发现第一次调用v6指向的函数为第64行(__fastcall*即意为将之后的内存作为函数调用),会发现这里并不是调用v6的起始位置,而是还有一个1616(十进制,有0x前缀才为16进制)的偏移
请添加图片描述

这意味着v6指向的一大段内存中可能不止一个函数,因此我们再次按下g键,看一看v6偏移1616的函数。1616转为十六进制为0x650,因此函数的地址为:v6+650,以我这次运行的v6=0x17DD94E0000,加上0x650的偏移,就是 0x17DD94E0650。

按下g键,输入地址,按下c键,转化为汇编码,右键函数起始,create function,选中函数名,按下tab,以操作0x17DD94E0000的步骤,操作0x17DD94E0650,查看其c语言代码
请添加图片描述

结果还是莫名其妙的赋值以及函数调用

但在第111行的字符串中,看到了position,有看到了color,由此推测图像绘制的核心代码大概就存在于v6指向的那一大块代码里。
请添加图片描述

由于sub_140001090中只有一处有调用v6内存中的函数——v6+650,因此对绘制图像的处理大概率存在于v6+650或是从v6+650中跳转。具体逻辑分析起来太过于繁杂,我们先看深蓝色的变量

前几个是对变量进行操作,由于操作的变量意义不明,因此这几步也看不出来什么
请添加图片描述

但在第249行可以注意到有调用一个函数,且地址为v6+0x420(函数默认名称去除前缀就是函数地址),这意味着v6+0x420处也存在一个函数,并且会在v6+0x650中进行调用。
请添加图片描述

那我们就再看一看v6+0x420处的代码:按下g键,输入地址,按下c键,转化成汇编码,右键首行,create function;选中函数名,按下tab键查看c代码
请添加图片描述

终于是一段能看明白结构的while + switch代码,这种结构被其他博主称作虚拟机结构。

看一下各个case,0,1,2,3,4都是做了些意义不明的运算,5,6调用了同一个函数,函数地址是v6+0,且只有第五个参数不同。为了之后遇到的时候更好辨认这个函数,我们给v6+0起一个名字。

右键单击函数,在菜单中选择Rename global item
请添加图片描述

我将其命名为shellCode0,点击ok。其他的可以同理命名为shellCode420,shellCode650。
请添加图片描述

重命名后,函数就好辨认多了
请添加图片描述
shellCode0里只有第五个参数不同,因此想研究一下第五个参数的意义

鼠标悬浮在第五个参数时,会显示invsign,通过询问ai,得知invsign是倒数的意思,因此先把他从倒数转化会普通值(我这里莫名奇妙突然显示起了函数以及参数的类型,我也不太清楚是按到了哪个键,不过不影响之后的过程)
请添加图片描述

右键-256,在菜单中选择Hexadecimal,将其为普通值,同理转化case6的13771801
请添加图片描述

分别得到0xFFFFFF00,以及0xFF0DDBE7
请添加图片描述

很像是16进制的颜色代码,因此在取色表看一下,发现真的是题目绘制需要的两种颜色,前置的两个ff应该是占位。既然shellcode0需要用到颜色,因此推测shellcode0即为绘制代码。
请添加图片描述

但shecode0我们只知道第五个参数的含义(颜色),其他参数连具体值都不知道,因此我们尝试获取每一次调取shellcode0时的各个参数。

hook

我们将使用hook技术获取到每一次调取shellcode0时的十个参数。

hook技术分为几种,这里使用inline hook,我学习inline hook技术的文章有:

InlineHook & 原理与实现 - 知乎 (zhihu.com)

万字长文!inlinehook看这一篇足矣! - 东北码农 - 博客园 (cnblogs.com)

hook的细节请查看这两篇文章学习,这里只是结合赛题简单讲一讲hook

简单来说hook就是将函数的开头代码修改为一段跳转代码,跳转到一个自定义的函数。比如我们现在想要获取到每一次调取shellcode0时的十个参数,就将函数的开头修改为跳转到一个自定义的print函数,将参数全部打印输出。

hook一般需要两个东西,一个是自己编写的dll文件,用于实现修改函数开头,以及实现自定义函数;还有一个被称为注入器,用于将dll文件注入到进程中。

首先是注入器的代码,思路就是查看是否有名为”2022游戏安全技术竞赛初赛.exe”的进程,如果有,则使用windows提供的api注入最后一句代码路径中的dll文件。

//注入器代码
#include<windows.h>
#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<TlHelp32.h>
#define EXEFILEW L"2022游戏安全技术竞赛初赛.exe"
#define EXEFILE "2022游戏安全技术竞赛初赛.exe"
DWORD old;
SIZE_T written;
DWORD FindProcess() {
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32;
    pe32 = { sizeof(pe32) };
    BOOL ret = Process32First(hSnap, &pe32);
    while (ret)
    {
        if (!wcsncmp(pe32.szExeFile, EXEFILEW, lstrlen(EXEFILEW))) {
            printf("找到程序 %s ,PID=%d\n", EXEFILE, pe32.th32ProcessID);
            return pe32.th32ProcessID;
        }
        ret = Process32Next(hSnap, &pe32);
    }
    return 0;
}
void InjectModule(DWORD ProcessId, const char* szPath)
{
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
    printf("进程句柄:%p\n", hProcess);
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    SIZE_T dwWriteLength = 0;
    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
    WaitForSingleObject(hThread, -1);
    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    CloseHandle(hThread);
}
int main() {
    DWORD ProcessId = FindProcess();
    while (!ProcessId) {
        printf("未找到%s程序,等待两秒中再试\n", EXEFILE);
        Sleep(2000);
        ProcessId = FindProcess();
    }
    InjectModule(ProcessId, "C:\\ZencyData\\CODE\\C_plus_plus\\injectionDll\\x64\\Debug\\injectionDll.dll");
}

然后是 dll文件,首先打开visual studio,创建一个dll新项目

请添加图片描述

将新项目的dllmain.cpp文件中的代码修改为

// dllmain.cpp : 定义 DLL 应用程序的入口点。#include"pch.h"#include<Windows.h>#include<stdio.h>#include<math.h>typedef__int64(*Func)(int a1,int a2,int a3,int a4,int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10);
__int64 GetBaseAddr(){
    HMODULE hMode =GetModuleHandle(nullptr);return(__int64)hMode;}void* shellcode =0;
BYTE HookCode[]={//目标将开头修改成HookCode0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//mov rax,xxx0xFF,0xE0//jmp rax };
BYTE OriginCode[0x50];//存储修改前的开头
size_t HookLen =12;// 修改内存大小为100
__int64 times =100;//只输出100次hook结果
__int64 HackShellcode(int a1,int a2,int a3,int a4,int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10){memcpy(shellcode, OriginCode,HookLen);//将开头恢复成原来的样子//int x = a1, y = a2;
    __int64 ret=(*(Func)shellcode)(x, y, a3, a4, a5, a6, a7, a8, a9, a10);//
    times--;if(times>0){printf("call shellcode(%d,%d,%d,%d,%d,%p,%p,%p,%p,%p)\n",x, y, a3, a4, a5, a6, a7, a8, a9, a10);}memcpy(shellcode, HookCode, HookLen);//将开头修改为跳转到自定义函数return ret;}voidHookShellcode(){// 第一次hook代码
    __int64 base =GetBaseAddr();//程序基地址
    __int64 Ptr = base +0x8308;//指针的地址为程序地址 + 0x8308

    shellcode =(void*)(*(__int64*)Ptr);//获取shellcode0代码起始地址while(!shellcode){//上一步获取失败,间隔0.2秒后再次尝试
        shellcode =(void*)(*(__int64*)Ptr);printf("Find shellcode Fail\n");Sleep(200);}printf("shellcode addr=%p\n", shellcode);//输出shellcode0代码起始地址memcpy(OriginCode, shellcode,HookLen);//存储原本起始地址
    Func FuncPtr = HackShellcode;//获取自定义函数地址*(__int64*)(HookCode +2)=(__int64)FuncPtr;//将HookCode跳转到的地址改为自定义函数地址memcpy(shellcode, HookCode, HookLen);//将原本函数开头修改为跳转指令}

BOOL APIENTRY DllMain( HMODULE hModule,
                      DWORD  ul_reason_for_call,
                      LPVOID lpReserved
                     ){//dll文件的main函数switch(ul_reason_for_call){case DLL_PROCESS_ATTACH://dll文件被注入时调用AllocConsole();//启动一个控制台freopen("CONOUT$","w",stdout);//设置输出HookShellcode();//进行第一次hookcase DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;}

整个dll文件的逻辑也比较清晰,就是将shellcode0的开头修改成一个跳转指令,跳转到HackShellcode函数;然后在HackShellcode中print输出参数,并且将开头恢复为修改前的状态,重新调用一次shellcode0,完成shellcode0原本需要完成的功能;调用完成后,再将开头修改为跳转,以输出shellcode0的下一次调用。

这里主要解释HookShellcode函数第二行的0x8308是怎么来的。

hook时,我们需要知道函数的地址。在我们重命名前,shellcode0我们称为v6+0,因为它的地址是v6偏移量为0的地址。所以我们需要去看v6的值。

按下窗口左上角 左向箭头旁边的扩展力,点击倒数第三个选项(具体名称大概不一样,但是地址以1090结尾),我们就能跳转回v6出现的那个界面
请添加图片描述

我们发现,在给v6赋值的时候,还将v9的值赋值给了一个全局变量qword_7FF60D008308,这意味着qword_7FF60D008308的值即为v6,即v9,也即shellcode0的地址,而qword_7FF60D008308的地址为0x7FF60D008308,根据右边第二个窗口中可以看到,此程序的基地址为0x7FF6D000000(不一定一样,甚至每次调试都可能变化),也就是qword_7FF60D008308的地址为基地址+0x8308,因此在dll文件中,通过基地址+8308可以获取到qword_7FF60D008308的值,然后这个值就是shellcode的地址。
请添加图片描述

接下来我们就生成dll文件。

生成之前,还需要加在pch.cpp中增加一句宏定义代码,取消visual studio的默认安全模式

#define_CRT_SECURE_NO_WARNINGS#include"pch.h"

请添加图片描述

鼠标右击项目名,菜单中点击“生成”
请添加图片描述

dll文件就生成了,生成路径就在窗口底部的输出中。
请添加图片描述

接下来准备运行注入器,vs新建一个控制台项目,并将主文件的代码修改为之前给出的注入器代码(要修改末尾的dll文件路径)。

运行注入器代码(请确定此时ida pro还在调试),弹出命令行窗口显示找到句柄后,再点击 ida pro这个运行按钮,跳过打下的断点。
请添加图片描述
在弹出的窗口中可以看到100次调用shellcode0时使用的参数,并且可以看到箭头指向的两个地方参数是开始重复的。也就是说绘图不只调用一次。
请添加图片描述

但是注意到赛题程序界面是一片白
请添加图片描述

不知道是不是hook出问题了,于是不调试,直接在文件夹中打开赛题程序,然后发现程序是显示绘制的图像大概4秒,就会清空,然后显示一片白,由此看出不是hook的问题,大概只是4秒过了。

通过hook的结果可以看到,参数不重复的调用一共有42次,而赛题目标的图案中正好有42个点,且第五个参数为-256的有11个点,为-13771801的有31个点,与赛题目标黄蓝点的数量也相同,由此更加确认shellcode就是绘制图像的函数。

然后观察参数,我们已知第五个是颜色,后五个参数每次调用都相同,那应该就是前4个参数控制位置。前两个参数的格式像是x,y坐标,于是尝试将其视为坐标进行绘制,由于蓝色图案的显示是正常的,因此尝试将x,y理解为坐标,绘制蓝色图案
请添加图片描述
发现是赛题中给出图案的上下翻转。由此确定前两个参数确实为坐标,而黄色图案的前两个参数中存在负数,可能这就是无法显示的原因,具体分析请见下期教程

标签: 游戏 安全

本文转载自: https://blog.csdn.net/m0_72582234/article/details/142751209
版权归原作者 sunny candy 所有, 如有侵权,请联系我们删除。

“小白向—2022腾讯游戏安全初赛分析(上)”的评论:

还没有评论