萌新逆向学习笔记——API钩取
# 前言著名的迪奥布兰多先生曾说过“人类是有极限的。”
确实如此,但也正因为我们不能飞,所以才造出了飞机。正因为我们跑的不快,所以才造出了汽车。人类确实存在极限,但人的学习能力却是无限的。而作为一个正在入门逆向萌新的来说,正确认识自身的极限和意识到学习的无穷是很重要的。
为什么这么说呢。回想几个星期前,笔者还在苦苦做论坛上总结的120个CM。每当破解并分析了一个CM感到满足的同时,也为自己耗费长时间去破解分析CM的行为是否值得而感到疑惑,似乎就在原地踏步,而没有学到新的知识。
后来我才意识到,这正是目前自身的极限。缺乏C++编程知识,缺乏系统的API知识,缺乏常规破解手段的知识等等。所以笔者才决定突破自身的极限,学习《逆向工程核心思想》一逆向书籍,才有了今天这篇文章。
可这谈何容易呢?要想突破极限就必须付出相应的努力,去提高自己极限的程度。笔者必须学习C++,必须跟着书籍编写win32程序。当我们知识储备扩展后,极限的程度也就高了,而只有不断提高自身的极限,或许才能成为真正的“大师”吧。
# 准备工作
本篇文章从原理和实现过程两个角度总结书中API钩取一章节,笔者也是萌新。因此难免会有令人困惑和误解甚至错误的地方。倘若存有疑问请务必留言指教。
为了读者能有更好的阅读体验,**最好但不一定必要具备**以下知识和工具:
1. C++编程基础
2. [到微软官方查看文档的能力](https://docs.microsoft.com/en-us/)
3. 用于编程的集成环境工具visual studio
4. 用于查看进程PID的监控软件Process Explorer
5. 文章附带的附件
再次提醒一下,就算读者没有C++编程基础,也可浏览此文到原理部分,实践部分若无编程基础可忽略不看。当熟知原理以及记住几个关键的函数过后,全文核心内容基本可算基本掌握。
# API钩取的作用
我们为什么要做API钩取,它有什么作用。在描述原理之前,这是我们必须要弄清楚的问题。如同我们去逆向分析软件,都会问自己为什么会这么做。可能是兴趣使然,可能是好玩,也可能是为了钻研等等。为什么很重要,因为它是我们唯一的学习动力。
API钩取它让我们能够直接修改程序中的流程或代码。说的通俗易懂一点,它可以让我们在别人的程序里为所欲为。
例如我们可以直接修改弹窗的标题或内容;又如可以改变我们输入。只要我们知道要修改的地址,我们就可以让他断下来,然后进行各种操作,来达到我们的目的。如果用过IDA或者olldbg的读者一定会发现,这不正是这些调试器的功能吗?
# 原理
那到底要怎么做呢?为什么别人写的程序本来好好的却能够突然停下来让你操作,这难道是什么魔法吗?
然而并不是,众所周知Window的编程是函数编程,说的明白一点,程序员在写诸如EXE的Window程序时,都会去使用微软早已做好的函数。而API钩取,也正是使用微软提供的函数去拦截程序使用微软的函数(有点拗口)。有点以毒攻毒的味道呢。
当一个程序被注册了调试程序,他们关系就会发生3600°的变化。本来互不干扰的俩,被注册之后只要产生了调试事件,它就会过问调试自己的程序,把自己的控制权交给调试者。而我们正是利用这个原理,注册成为别人的程序的调试者,再促使被调试程序产生断点调试事件,来获得其生死大权。
如何促使被调试程序产生断点调试事件呢?这里有两个关键点:
1. 当程序被注册调试器成功后,会发送一个调试事件来告诉调试者你成功了。
2. 当被调试的程序遇到16进制为CC的指令就会产生中断的调试事件。
结合这两点,我们可以在注册成功的时候,改写相应的地址为CC,使其下次遇到CC时触发中断调试事件。
这样我们接收CC的中断调试事件后便可更改被调试程序的相关数据。
# 实践
实践步骤需要有C++的基础知识,否则可能会难以下咽,没有C++的基础可以跳过。
整个过程如下:
附加要修改的程序使目标程序成为被调试者——>收取附加成功消息——>找到要修改的地址并下断点——>触发断点——>收到异常,程序断下——>实现自己的操作(修改数据)——>让程序继续运行
## 步骤一:使目标程序成为被调试者
为什么要先附加程序,使其成为被调试者?从流程上讲因为他是必须的,就像有了加法才有乘法,有了减法才有除法一样。
**而从原理上讲,附加程序就是向目标程序注册成为调试器,只有拥有调试与被调试的关系后,每当目标程序产生调试事件(如断点产生的异常)后才会报告给调试者。因此我们附加程序,是为了获得目标程序产生异常而造成的空隙,这样我们才有修改数据等操作的机会。**
从代码中看,使用```DebugActiveProcess ```函数,同时传递目标程序PID作为参数,来使特定的程序成为被调试者,而自己的程序成为调试者。以下为[函数文档](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-debugactiveprocess)
```
BOOL DebugActiveProcess(
DWORD dwProcessId
);
```
## 步骤二:收取附加成功消息
为了给特定地址下断点,我们需要合适的机会,而这个机会便是程序被附加成功的时候。因此我们只要在接收到附加成功的事件后进行断点操作即可。
[同样,贴心的微软提供了一个接收调试事件的方法给我们:](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-waitfordebugevent)
```
BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent,//接收调试事件存储的地方
DWORD dwMilliseconds//等待事件的时间,可设置永久
);
```
``` WaitForDebugEvent ```函数会接收调试事件,例如附加成功,遇到断点产生异常等。那我们如何才知道哪个事件是附加成功呢?
[当事件接收成功后,会将一个叫DEBUG_EVENT 的结构体存储在函数的第一个参数lpDebugEvent中,这个结构体的结构如下](https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-debug_event):
```
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;//调试事件各类代码
DWORD dwProcessId;//进程ID
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFOCreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;//附加成功后产生
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFODebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;
```
通过dwDebugEventCode的事件代码我们能判断当前事件的类型,而附加成功的代码是CREATE_PROCESS_DEBUG_EVENT,当产生这个事件,(https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-create_process_debug_info)。
是不是看的头晕眼花,简单来说,我们只要使用```WaitForDebugEvent```来接收调试事件,用其结果中的dwDebugEventCode来判断是否为附加成功。最后再用其结果产生的CreateProcessInfo来初始化一些设置:
```
DEBUG_EVENT event;
while(WaitForDebugEvent(&event, INFINITE)){// 循环获得调试事件
DWORD code = event.dwDebugEventCode;
switch (code)
{
case CREATE_PROCESS_DEBUG_EVENT:
//附加成功后设置断点
onCreateProEvent(&event);
break;
case EXCEPTION_DEBUG_EVENT:
//断点触发后修改数据
onExceptionEvent(&event);
break;
case EXIT_PROCESS_DEBUG_EVENT:
//退出
break;
}
}
```
## 步骤三:找到要修改的地址下断点
从上面的代码注释可以看到,在判断为附加成功的事件后,调用了一个onCreateProEvent的函数。这是微软提供的吗?当然不是,这是笔者自己写的:
```
LPVOID address;
void onCreateProEvent(LPDEBUG_EVENT event) {
//设置断点,获取断点地址,因为是系统DLL的函数因此与目标进程地址一致
address = GetProcAddress(hmod, "MessageBoxA");
BYTE bp = 0xCC;
memcpy_s(&ori, sizeof(event->u.CreateProcessInfo), &event->u.CreateProcessInfo, sizeof(event->u.CreateProcessInfo));
if (!ReadProcessMemory(event->u.CreateProcessInfo.hProcess, address, &oribyte, sizeof(BYTE), NULL)) {
//读取原地址开头
cout << "读取目标进程内存失败";
return;
}
if (!WriteProcessMemory(event->u.CreateProcessInfo.hProcess, address, &bp, sizeof(BYTE), NULL))
{
//写入CC
cout << "写入目标进程内存失败";
return;
}
}
```
首先我们要获取地址。比如笔者想修改弹窗的标题,那我们就要知道目标程序调用弹窗的代码地址,而当前已知目标程序调用弹窗的函数名叫```MessageBoxA```。利用**系统函数地址不变**的原理,使用``` GetProcAddress```函数获取其弹窗函数地址:
```address = GetProcAddress(hmod, "MessageBoxA"); ```
然后我们只要在这地址下断点就行了。这样当目标程序调用弹窗函数时,便会产生调试事件并断下。
可我们要如何在这地址下断呢?其实很简单,我们只要把这地址的首部替换成0xCC即可。**这是因为0xCC在汇编中代表的是一个int3中断指令,当程序遇到它时会报告给调试者**
例如我们通过```GetProcAddress```获得```MessageBoxA```的地址为76EA13D0,那我们下断点后就断成了:CCEA13D0了。
为了修改数据后能让程序正常运行,同时还要保存原地址开头的76,用来复原。
## 步骤四:触发断点
触发断点很简单,如果像笔者一样是断在弹框处,那只要让目标程序弹出弹窗即可。
## 步骤五:接收断点调试事件
同样的,我们接收断点调试事件也是使用步骤二的方法```WaitForDebugEvent ```函数,只不过这次的调试事件类型为EXCEPTION_DEBUG_EVENT
```
DEBUG_EVENT event;
while(WaitForDebugEvent(&event, INFINITE)){// 循环获得调试事件
DWORD code = event.dwDebugEventCode;
switch (code)
{
case CREATE_PROCESS_DEBUG_EVENT:
//附加成功后设置断点
onCreateProEvent(&event);
break;
case EXCEPTION_DEBUG_EVENT:
//断点触发后修改数据
onExceptionEvent(&event);
break;
case EXIT_PROCESS_DEBUG_EVENT:
//退出
break;
}
}
```
在接收到类型为EXCEPTION_DEBUG_EVENT的调试事件后,结果里会产生一个叫(https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-exception_debug_info)的结构体:
```
typedef struct _EXCEPTION_DEBUG_INFO {
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
```
然后里面又会有一个叫(https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-exception_record)的结构体(套娃)
```
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation;
} EXCEPTION_RECORD;
```
我们需要用到ExceptionCode来判断是不是我们的断点事件,然后再用ExceptionAddress来判断是不是我们断下的窗口地址:
```
if(ExceptionRecord == EXCEPTION_BREAKPOINT) {
if(ExceptionAddress == address) {
...修改窗口标题
}
}
```
## 步骤六:实现自己的操作
从上面代码可以看到,程序断点调试事件收到后会调用onExceptionEvent函数,这个也是自己书写的函数,他的内容便是修改弹窗标题。
那要如何获得弹窗的标题呢?我们可以使用```GetThreadContext```来进行获取```CONTEXT```结构体,再从其中来获取我们的标题。
[对照MessageBoxA文档:](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa)
```
int MessageBoxA(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
);
```
观察文档可以看到,我们要的标题在第三个参数(lpCaption)。在提取标题前我们必须知道一个汇编的知识:**ESP代表调用栈的栈顶,当调用一个函数时,根据调用约束的不同,对函数的参数进行不同方式的传递。而C/C++的默认约定便是将参数压入栈。**
听不懂不要紧,我们只要知道ESP+4是第一个参数,ESP+8是第二个参数,ESP+C就是我们要的标题。
而所谓的ESP我们可以从```CONTEXT```结构体获得:
```
CONTEXT context;
context.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(ori.hThread, &context);
DWORD ptitle;
ReadProcessMemory(ori.hProcess, (LPCVOID)(context.Esp + 0xC), &ptitle, sizeof(DWORD), NULL);
```
然后通过```WriteProcessMemory```更改标题即可:
```
CStringA newTitle = "z";
WriteProcessMemory(ori.hProcess, (LPVOID)ptitle, newTitle.GetBuffer(), titleSize, NULL)
```
最后我们要把更改成CC了的地址恢复为原样,并把EIP复原。因为EIP的值指示着当前代码的地址,地址改了当然EIP也要改:
```
WriteProcessMemory(ori.hProcess, address, &oribyte, sizeof(byte), NULL);
context.Eip = (DWORD)address;// EIP复原
SetThreadContext(ori.hThread, &context);
```
## 最终步骤:让程序继续运行
当调试事件发生后,程序会断下来。而我们为了在修改程序后还能继续运行,必须通知程序没事了你继续吧。[因此我们可以调用ContinueDebugEven函数来通知:](https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-continuedebugevent)
```
BOOL ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);
```
# 结果
例子是将弹框标题t改成z:
# 函数列表
以下为一些较为关键的函数(仅名称):
DebugActiveProcess——附加程序,成为目标程序的调试者
WaitForDebugEvent——接收调试事件
GetProcAddress——获取要下断的目标地址
GetThreadContext——获取上下文,以此来获取对应数据,如弹窗标题内容等。
SetThreadContext——上下文设置,用来恢复原来的地址
ContinueDebugEvent——让程序继续运行
ReadProcessMemory——读取目标相关数据
WriteProcessMemory——更改目标数据,如写入CC断点,修改弹窗标题,内容等。
VirtualProtectEx——更改虚拟内存权限,若无法写入数据可使用
# 总结
随着学习的不断深入,知识的难度越来越难是理所当然的。在写这篇文章的时,如何去用简洁的语言去描述过程,并让那些没有编程基础的读者也能看的明白就成为了一个问题。因为知识难度越高,解释中用到的其他的知识铺垫也会越来越多,这就会陷入一个“不断解释”的问题。
例如给别人解释2x3的意义,我们可以用加法来告诉他2x3就是2个3相加或者3个2相加。但这前提是他必须懂得加法。
那如果说我解释的东西用了n个知识铺垫呢?那我是不是得解释这n个知识。所以说,倘若读者不太看得懂这篇文章那只有两种可能:
1. 笔者文笔不好
2. 这n个的知识铺垫的缺失
所以说只有不断提高自己,才能向更高的层次进发。
# 问题:
当我们改写标题的时候,如果新标题的长度超过了4个字节,会使新标题溢出到弹窗内容区:
如上图所示,原标题为t,原内容为z。写入新标题I am title title title title后内容也变成了title tile tile tile。
这是为什么呢?因为在虚拟内存当中,弹窗的标题和内容是连在一起的:
因此标题过长会覆盖到隔壁的内容区域。
可如果我们想用一段任意长的内容替代它而不造成溢出,该怎么做呢?笔者想过是否可以替换掉指向标题的地址:
可一直没成功,如有解决方法希望读者可以留言指点!
# 附件
工程源码及可直接运行的示例(https://share.weiyun.com/IHC4yI7k) jixun66 发表于 2020-8-27 06:34
1. 申请足够大的内存空间并写入数据(注意需要在目标进程申请)。
2. 将堆栈上对应的指针指向新的内存地址 ...
像这样:
```
CStringA newTitle = "I am title title title title";//标题
LPCVOID titlebuffer =newTitle.GetBuffer();
int titleSize = (newTitle.GetLength()+1) * sizeof(char); //标题大小
LPVOID titleAddress = VirtualAllocEx(ori.hProcess, NULL, titleSize, MEM_COMMIT, PAGE_READWRITE);//向目标进程申请虚拟内存空间
WriteProcessMemory(ori.hProcess, titleAddress, newTitle.GetBuffer(), titleSize, NULL); //向虚拟内存写入标题
DWORD old;
VirtualProtectEx(ori.hProcess, (LPVOID)(context.Esp + 0xC), titleSize, PAGE_READWRITE, &old)//提权
WriteProcessMemory(ori.hProcess, (LPVOID)(context.Esp + 0xC), &titlebuffer, titleSize, NULL)//写入记载标题的虚拟内存地址
```
不知道代码有没有错误,我C++不是很在行。
运行后没有任何反应,弹窗也不弹了。 小小泽丶 发表于 2020-8-27 11:02
楼主,我的评论或许是千万评论中微不足道的一条,不过希望你能认真看一下,是这样的最近在学,想请教一下 ...
个人感觉刚开始学不建议就马上学诸如c++和汇编或者看比较深入的逆向分析书籍,因为这样只会让自己变得烦躁,渐渐失去兴趣。刚入门应该按着自己的兴趣来,循序渐进按需要来学。像我一开始就是对破解感兴趣,然后就去尝试破解软件(论坛有很多各种破解的教程,可以跟着试一试),这过程就慢慢熟悉了一些破解工具,遇到不会的汇编通过百度解决。
再后来随着逐渐深入才觉得有必要去体系的学习汇编和c++,以及一些逆向的书籍。
不错已经学习 不错已经学习 感谢分享 谢楼主分享
收藏学习,请继续{:1_921:} 学习了感谢分享已收藏 感谢大佬的技术贴,同为新手学习中 原來實際上是這樣咦鞯? 辛苦了,期待能看到目标地址受保护时提权的操作
{:301_1003:}