好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 sunnycandy 于 2024-9-29 19:19 编辑
下篇已更新: https://www.52pojie.cn/thread-1968679-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](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)](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文件。
[C++] 纯文本查看 复制代码 //注入器代码
#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文件中的代码修改为
[C++] 纯文本查看 复制代码 // 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[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;
}
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,
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();//进行第一次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的默认安全模式
[C++] 纯文本查看 复制代码 #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理解为坐标,绘制蓝色图案
发现是赛题中给出图案的上下翻转。由此确定前两个参数确实为坐标,而黄色图案的前两个参数中存在负数,可能这就是无法显示的原因,具体分析请见下期教程(三天内更新) |
免费评分
-
查看全部评分
|