小白向—2022腾讯游戏安全初赛分析(上)
小白向—2022腾讯游戏安全初赛分析(上)参考:
smallzhong/gslab-2022-competition: 2022腾讯游戏安全PC初赛答案 (github.com)
2022腾讯游戏安全大赛复盘 - 吾爱破解 - 52pojie.cn
2022腾讯游戏安全PC端初赛复现 - 吾爱破解 - 52pojie.cn
媒介:
在爱破网的精华帖里看到了对2022腾讯游戏安全初赛的分析(“参考”中的第二条链接),感觉挺有意思的,但因为当时看的时候楼主是纯小白(以致没用过ida pro),完全看不懂,就想着去学一学,试一试。学到了很多东西,以为是一次不错的入门履历,因此记录下来,向其他小白详细地介绍整个分析以及操纵的流程。
小白将学到
[*] ida pro的基本使用。
[*] hook的概念以及操纵方式
[*] 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丢失了,需要把它找返来。
题目:
https://i-blog.csdnimg.cn/direct/5579ccc6e6fd4da792d259e328a6afb3.png
找回flag样例:
https://i-blog.csdnimg.cn/direct/6d26c5ef5b104fa0a9348ab8dcef9b62.png
操纵与分析
接下来正式开始。
使用IDA Pro打开赛题的exe程序
首先使用ida pro打开赛题的exe程序
https://i-blog.csdnimg.cn/direct/b0ad43d2179b471995e13e70f92b6100.png
在弹出的文件选择窗口中选择赛题的exe程序
https://i-blog.csdnimg.cn/direct/f746e510d5b74f7297d977a6e8f3005e.png
之后弹出的第一个窗口直接选ok,第二个选择no。
https://i-blog.csdnimg.cn/direct/c5d19c08718c45c7811fed96ea4e4bf7.png
https://i-blog.csdnimg.cn/direct/70bee4a4ad014176a5836aef25f83048.png
文件就加载完成了
https://i-blog.csdnimg.cn/direct/5ea0defdbdcb4d22b1700acaf7d59c93.png
静态分析
我们的主界面初始位于IDA View-A窗口,且看到的是一个被称为graphs的界面,按下空格键可在graphs界面与hex代码界面(这只是我的称呼,官方名称我也不太清楚)切换,此中展示的是汇编代码。
IDA的左边具有一个Functions窗口,罗列了体系检索到的函数,此中具有一个名为WinMain函数,雷同于C中的main函数,是整个程序的起始函数。双击WinMain,IDA会跳转到WinMain汇编代码的部分。
https://i-blog.csdnimg.cn/direct/cac3cded0eaf467385501940ecf1a3cd.png
汇编代码很难分析,我们想要看c语言代码,按下Tab键,将跳转到WinMain函数的c代码界面,即Pseudocode-A界面。
https://i-blog.csdnimg.cn/direct/fb4b9f99b019411ea097c8bc35179026.png
现在我们可以开始分析代码的逻辑了。查看WinMain函数中的代码,可以发现在60行以前都是赋值,在第61行开始举行逻辑功能处理
https://i-blog.csdnimg.cn/direct/d1cadd8dd8f5406b825d7bd1d1dac0cb.png
粉紫色的函数为windows提供的官方api,且经扣问ai得知与图像绘制无关
https://i-blog.csdnimg.cn/direct/8f65368971464a1ca8ba49ad3b9b8318.png
因此推测图像处理逻辑存在于else中的sub_140001090函数,双击此函数,进入其函数实现界面,观察代码
https://i-blog.csdnimg.cn/direct/5ab14a9c1be54e6888afda0823005595.png
从第18行开始进入逻辑处理,观察代码,每碰到一个深蓝色变量都双击观察其是否具有初始值,如byte_140008314, 双击后显示db 为 “?”, 即没有初始值,不深究
https://i-blog.csdnimg.cn/direct/cd575b50481743baae76a90630cd81b9.png
而xmmword_140003490双击后为
https://i-blog.csdnimg.cn/direct/206c4c1782644cae8e5423b08dd739cd.png
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
https://i-blog.csdnimg.cn/direct/a9ef1c4d8388475ea61be72f2e65d180.png
但是双击unk_140005040查看其初始值,并不能直接获取到什么有效信息
https://i-blog.csdnimg.cn/direct/2241c2e3229c4e0692a72f7ff2a54911.png
我们之前推测v9大概用来存储函数,假如真的是如许,那么unk_140005040应该就是那个函数。我们按下c键,IDA将把这些数据转化为汇编码
https://i-blog.csdnimg.cn/direct/6a7aeed35f3246738eb5af4ba45cf77f.png
14000504A及之后的代码挺像一回事的,但是前部分作为函数的话缺少几个push,我们就先回到Pseudocode-A,看看之后有没有对函数举行其他处理。
分析代码逻辑,会发现第40行将v9的值赋值给了v6,而第51-53,65-67行,有使用v6来对函数所在内存的开头部分以及其他一些点重新赋值(使用地点定位来举行的小范围赋值一样平常称为patch)
https://i-blog.csdnimg.cn/direct/652f4bfd16964b678c6513f8b37fbd50.png
我们想要查看重新赋值后的函数,就要进入动态分析
动态分析
点击菜单栏的Debugger,选中Select debugger
https://i-blog.csdnimg.cn/direct/c7d81a708bdc4c47b777cbc97a1c157c.png
在弹出的窗口中选择Local Windows debugger,点击OK
https://i-blog.csdnimg.cn/direct/7c85121b9f8549dc86eac84feb3a0d89.png
如许我们就设置好了调试器,接下来就是打断点
由于我们盼望看到patch后的函数,因此直接在patch后的下一条指令打下断点即可
https://i-blog.csdnimg.cn/direct/950216d8fb8146968c1b49fb7c3ac27a.png
再次点击菜单栏的Debugger, 会发现展开内容变化了,选择Start process,即开始调试。弹出的窗口全点yes或ok。
https://i-blog.csdnimg.cn/direct/c41a57a608bd4d89ab51e8c0363f93e9.png
代码会停止在下断点处
这时我们就可以查看patch后的代码,查看方式为
鼠标悬浮在v6上,查看v6的值
https://i-blog.csdnimg.cn/direct/437df4f645b143d19aa36161f40ecd1b.png
我们就得到了v6的值为0x17DD94E0000(不固定,每一次调试的详细值都大概不同),也就是说0x17DD94E0000指向代码的开始
按下g键,弹出地点跳转窗口,输入v6的值,点击ok。
https://i-blog.csdnimg.cn/direct/1e511b8f455f403e97a320afd40fa2c5.png
跳转到新界面后按下c键,将数据转化为汇编代码
https://i-blog.csdnimg.cn/direct/9906b5561df74b8f8a7700e41e14f31f.png
可以看到之前的nop都酿成详细的代码了。想要看这些汇编代码对于的c语言代码,则右键函数的起始位置,即17DD94E0000 ,在菜单中选择create function
https://i-blog.csdnimg.cn/direct/cdb3d11a50e0404e91e389e2c8e81017.png
点击后可以注意到代码再次产生了变化
https://i-blog.csdnimg.cn/direct/32a9405b44824bfc96d2c76a75e3b227.png
这时选中函数名,按下tab键,就将跳转到c语言代码界面。
https://i-blog.csdnimg.cn/direct/b8526c503dd74681bc7f82e80870aef7.png
这段代码有一些赋值以及看不明确的函数调用,搞不清楚,于是先回到winMain里的那个名为sub_140001090的函数。按下窗口左上角的左向箭头,返回上一个界面
https://i-blog.csdnimg.cn/direct/bec6d39c71dd4521b812a8fa4072456c.png
回到sub_140001090,重新梳理程序逻辑,发现第一次调用v6指向的函数为第64行(__fastcall*即意为将之后的内存作为函数调用),会发现这里并不是调用v6的起始位置,而是还有一个1616(十进制,有0x前缀才为16进制)的偏移
https://i-blog.csdnimg.cn/direct/fa1a017d42ff48a38f3eabf09fde0f00.png
这意味着v6指向的一大段内存中大概不止一个函数,因此我们再次按下g键,看一看v6偏移1616的函数。1616转为十六进制为0x650,因此函数的地点为:v6+650,以我这次运行的v6=0x17DD94E0000,加上0x650的偏移,就是 0x17DD94E0650。
按下g键,输入地点,按下c键,转化为汇编码,右键函数起始,create function,选中函数名,按下tab,以操纵0x17DD94E0000的步骤,操纵0x17DD94E0650,查看其c语言代码
https://i-blog.csdnimg.cn/direct/f8927424e0924d3c9eec92976ea440e0.png
效果还是莫名其妙的赋值以及函数调用
但在第111行的字符串中,看到了position,有看到了color,由此推测图像绘制的焦点代码大概就存在于v6指向的那一大块代码里。
https://i-blog.csdnimg.cn/direct/9fa57b2606764a00a7f4caee64e8fe99.png
由于sub_140001090中只有一处有调用v6内存中的函数——v6+650,因此对绘制图像的处理大概率存在于v6+650或是从v6+650中跳转。详细逻辑分析起来太过于繁杂,我们先看深蓝色的变量
前几个是对变量举行操纵,由于操纵的变量意义不明,因此这几步也看不出来什么
https://i-blog.csdnimg.cn/direct/a0f470afbd324ddebc15189f661a2053.png
但在第249行可以注意到有调用一个函数,且地点为v6+0x420(函数默认名称去除前缀就是函数地点),这意味着v6+0x420处也存在一个函数,并且会在v6+0x650中举行调用。
https://i-blog.csdnimg.cn/direct/63a788eeb43f48359aa742bfc76eb4e5.png
那我们就再看一看v6+0x420处的代码:按下g键,输入地点,按下c键,转化成汇编码,右键首行,create function;选中函数名,按下tab键查看c代码
https://i-blog.csdnimg.cn/direct/6234ad357e3545349ba540f88bae02be.png
终于是一段能看明确结构的while + switch代码,这种结构被其他博主称作假造机结构。
看一下各个case,0,1,2,3,4都是做了些意义不明的运算,5,6调用了同一个函数,函数地点是v6+0,且只有第五个参数不同。为了之后碰到的时候更好辨认这个函数,我们给v6+0起一个名字。
右键单击函数,在菜单中选择Rename global item
https://i-blog.csdnimg.cn/direct/1d4a43710a73448fab25fe7811c5f76d.png
我将其命名为shellCode0,点击ok。其他的可以同理命名为shellCode420,shellCode650。
https://i-blog.csdnimg.cn/direct/abae6d122412433fa7f1f0d004a80fb4.png
重命名后,函数就好辨认多了
https://i-blog.csdnimg.cn/direct/c051be0bff6f46caae861e08cb1198d6.png
shellCode0里只有第五个参数不同,因此想研究一下第五个参数的意义
鼠标悬浮在第五个参数时,会显示invsign,通过扣问ai,得知invsign是倒数的意思,因此先把他从倒数转化会平凡值(我这里莫名奇妙突然显示起了函数以及参数的类型,我也不太清楚是按到了哪个键,不过不影响之后的过程)
https://i-blog.csdnimg.cn/direct/87ca6ee5ad0d4ade92db23055e9ae161.png
右键-256,在菜单中选择Hexadecimal,将其为平凡值,同理转化case6的13771801
https://i-blog.csdnimg.cn/direct/0d88b543190a4e49bbcceabb12115c0d.png
分别得到0xFFFFFF00,以及0xFF0DDBE7
https://i-blog.csdnimg.cn/direct/7d5c3c6cea3c4740a2feeca0d92617bb.png
很像是16进制的颜色代码,因此在取色表看一下,发现真的是题目绘制需要的两种颜色,前置的两个ff应该是占位。既然shellcode0需要用到颜色,因此推测shellcode0即为绘制代码。
https://i-blog.csdnimg.cn/direct/de51a7739a3648438e19e2fa0e336942.png
但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新项目
https://i-blog.csdnimg.cn/direct/e671a1643fac43c189374578aab4e52e.png
将新项目的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出现的那个界面
https://i-blog.csdnimg.cn/direct/e06339bfe7c84d4dbaa3aeed78274678.png
我们发现,在给v6赋值的时候,还将v9的值赋值给了一个全局变量qword_7FF60D008308,这意味着qword_7FF60D008308的值即为v6,即v9,也即shellcode0的地点,而qword_7FF60D008308的地点为0x7FF60D008308,根据右边第二个窗口中可以看到,此程序的基地点为0x7FF6D000000(不一定一样,以致每次调试都大概变化),也就是qword_7FF60D008308的地点为基地点+0x8308,因此在dll文件中,通过基地点+8308可以获取到qword_7FF60D008308的值,然后这个值就是shellcode的地点。
https://i-blog.csdnimg.cn/direct/c5f7d4eb04c64e988496cad8251cf84f.png
接下来我们就生成dll文件。
生成之前,还需要加在pch.cpp中增长一句宏定义代码,取消visual studio的默认安全模式
#define _CRT_SECURE_NO_WARNINGS
#include "pch.h"
https://i-blog.csdnimg.cn/direct/86d7c8514c61419d81b501eceb15e7d4.png
鼠标右击项目名,菜单中点击“生成”
https://i-blog.csdnimg.cn/direct/a7ea8dae3b9842e296b1ef0b4ee0e210.png
dll文件就生成了,生成路径就在窗口底部的输出中。
https://i-blog.csdnimg.cn/direct/b510290e21964140989eabd43a1f7814.png
接下来准备运行注入器,vs新建一个控制台项目,并将主文件的代码修改为之前给出的注入器代码(要修改末尾的dll文件路径)。
运行注入器代码(请确定此时ida pro还在调试),弹出命令行窗口显示找到句柄后,再点击 ida pro这个运行按钮,跳过打下的断点。
https://i-blog.csdnimg.cn/direct/530bf6aa8f934d20baa8b5d5b95841ab.png
在弹出的窗口中可以看到100次调用shellcode0时使用的参数,并且可以看到箭头指向的两个地方参数是开始重复的。也就是说画图不只调用一次。
https://i-blog.csdnimg.cn/direct/83147a0612e74fe58662b7fab4dc7127.png
但是注意到赛题程序界面是一片白
https://i-blog.csdnimg.cn/direct/ef4b52520fe34cb2a6ef1408b063ceef.png
不知道是不是hook出问题了,于是不调试,直接在文件夹中打开赛题程序,然后发现程序是显示绘制的图像大概4秒,就会清空,然后显示一片白,由此看出不是hook的问题,大概只是4秒过了。
通过hook的效果可以看到,参数不重复的调用一共有42次,而赛题目的的图案中恰好有42个点,且第五个参数为-256的有11个点,为-13771801的有31个点,与赛题目的黄蓝点的数量也相同,由此更加确认shellcode就是绘制图像的函数。
然后观察参数,我们已知第五个是颜色,后五个参数每次调用都相同,那应该就是前4个参数控制位置。前两个参数的格式像是x,y坐标,于是尝试将其视为坐标举行绘制,由于蓝色图案的显示是正常的,因此尝试将x,y理解为坐标,绘制蓝色图案
https://i-blog.csdnimg.cn/direct/5fb74ce882c648fdb30de0b2e6ca2c08.png
发现是赛题中给出图案的上下翻转。由此确定前两个参数确实为坐标,而黄色图案的前两个参数中存在负数,大概这就是无法显示的缘故原由,详细分析请见下期教程
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]