sunnycandy 发表于 2024-9-28 21:54

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

本帖最后由 sunnycandy 于 2024-9-29 19:19 编辑

下篇已更新: https://www.52pojie.cn/thread-1968679-1-1.html
参考:

(https://github.com/smallzhong/gslab-2022-competition?tab=readme-ov-file)

(https://www.52pojie.cn/thread-1904205-1-1.html)

(https://www.52pojie.cn/thread-1952072-1-1.html)


前言:

8月在爱破网的精华帖里看到了对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技术的文章有:

(https://zhuanlan.zhihu.com/p/459912527)

[万字长文!inlinehook看这一篇足矣! - 东北码农 - 博客园 (cnblogs.com)](https://www.cnblogs.com/northeast-coder/p/15782665.html)

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[] = { //目标将开头修改成HookCode
    0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//mov rax,xxx
    0xFF,0xE0                                           //jmp rax
};
BYTE OriginCode; //存储修改前的开头
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;
}

void HookShellcode() { // 第一次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,
                      DWORDul_reason_for_call,
                      LPVOID lpReserved
                     )
{//dll文件的main函数
    switch (ul_reason_for_call)
    {
      case DLL_PROCESS_ATTACH://dll文件被注入时调用
            AllocConsole();//启动一个控制台
            freopen("CONOUT$", "w", stdout);//设置输出
            HookShellcode();//进行第一次hook
      case 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理解为坐标,绘制蓝色图案


发现是赛题中给出图案的上下翻转。由此确定前两个参数确实为坐标,而黄色图案的前两个参数中存在负数,可能这就是无法显示的原因,具体分析请见下期教程(三天内更新)

Hmily 发表于 2024-10-21 10:39

oxygen1a1 发表于 2024-10-21 10:30
想问下H大,非针对楼主。腾讯安全竞赛每一年都有大量的文章,说白了就是被逆烂了,分析烂了,有大量的相 ...

哈,这个问题我在管理内部培训帖子中说过,转出来大家看看:
什么样的帖子够优秀精华帖要求呢,如果是一个新手发帖,那么他的第一篇技术贴很需要我们斑竹对其鼓励,只要帖子能达到基本的技术要点,不一定很高深比如一个UPX脱壳或者一个软件的爆破,只要他能说详细清楚,我们也可以给其优秀或者精华,并回帖评价对其鼓励,期待他更多要求更高的作品,如果这位同学已经有了精华帖了,那么他以后发的技术贴评定就需要更高了,不单是简单的一篇脱壳或者爆破就可以轻松拿到的,大概意思就是这样,新手一定要鼓励来引导,这样才有持续不断的产出(ps想当年在upk我发了一个脚本脱壳加去校验的帖子给fly加了精华,很受感动,这也是让我能持续在逆向界坚持走下来的重要原因,我也很感激他,希望你们也能成为新手之路上一位重要的人物,他会一辈子感激你这一次鼓励!现在回头看看我当年发的贴确实惨不忍睹,但大家应该都是这么走过来的)

Hmily 发表于 2024-9-29 18:09

上下篇一起给与精华鼓励,期待以后有更多分享。

L__ 发表于 2024-9-28 23:12

得多看几遍这样的好文章

alonestree 发表于 2024-9-29 00:04

太太太太强了吧

Goven 发表于 2024-9-29 10:43

学习学习

princekin 发表于 2024-9-30 10:16

涨知识了

wobuaipojie7 发表于 2024-10-3 09:37

小白友好,非常棒

fenger882 发表于 2024-10-3 20:40

太牛了,

princekin 发表于 2024-10-3 23:40

信息量很大,值得深入学习。

MinuxCyber 发表于 2024-10-4 07:07

非常详细的技术分享!
页: [1] 2 3 4 5 6 7
查看完整版本: 小白向—2022腾讯游戏安全初赛分析(上)