目录
硬编码
内存对齐和文件对齐
节表
实战
滴水逆向03-17
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
// 定义窗口类
WNDCLASS wc = { 0 };
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"MyWindowClass";
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
// 注册窗口类
if (!RegisterClass(&wc))
{
MessageBox(NULL, L"窗口注册失败!", L"错误", MB_ICONERROR);
return 0;
}
// 创建窗口
HWND hwnd = CreateWindow(
L"MyWindowClass", // 窗口类名
L"Hello, Windows!", // 窗口标题
WS_OVERLAPPEDWINDOW, // 窗口样式
CW_USEDEFAULT, CW_USEDEFAULT, // 窗口位置
400, 200, // 窗口大小
NULL, // 父窗口
NULL, // 菜单句柄
hInstance, // 实例句柄
NULL // 附加参数
);
if (!hwnd)
{
MessageBox(NULL, L"窗口创建失败!", L"错误", MB_ICONERROR);
return 0;
}
// 显示窗口
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
// 进入消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 10, 10, L"Hello, Windows!", 15);
EndPaint(hwnd, &ps);
}
return 0;
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
链接:https://pan.baidu.com/s/1CfdGhw4S-iHOlYu-jb8bvg?pwd=l0ug
提取码:l0ug
这是我编写好的,尽量和我的一致吧
随便写一个简单的GUI程序。,功能就是简单地弹出一个窗口
我们的目的是在代码空白区段添加一个shellcode,添加一个MessageBox函数
开始:
硬编码
E8:Call
E9: Jmp
举个例子
call 0x77E5425F
E8 13 88 E1 76
jmp 0x2345678
E9 2B 2B 00 00
但是我们分析可以发现,无论是E8还是E9,后面的地址貌似都不是直接小端序转化过来的地址
真正要跳转的地址=E8这条指令的下一条指令的地址 + X X=真正要跳转的地址 - E8要要跳转的地址的下一条指令的地址
这里用具体例子分析
#include <iostream>
using namespace std;
void func()
{
cout << "666" << endl;
}
int main()
{
func();
return 0;
}
int main()
{
00DA25C0 55 push ebp
00DA25C1 8B EC mov ebp,esp
00DA25C3 81 EC C0 00 00 00 sub esp,0C0h
00DA25C9 53 push ebx
00DA25CA 56 push esi
00DA25CB 57 push edi
00DA25CC 8B FD mov edi,ebp
00DA25CE 33 C9 xor ecx,ecx
00DA25D0 B8 CC CC CC CC mov eax,0CCCCCCCCh
00DA25D5 F3 AB rep stos dword ptr es:[edi]
00DA25D7 B9 29 F0 DA 00 mov ecx,offset _8810881B_stack@cpp (0DAF029h)
00DA25DC E8 A3 ED FF FF call @__CheckForDebuggerJustMyCode@4 (0DA1384h)
func();
00DA25E1 E8 62 ED FF FF call func (0DA1348h)
return 0;
00DA25E6 33 C0 xor eax,eax
}
00DA25E8 5F pop edi
00DA25E9 5E pop esi
00DA25EA 5B pop ebx
00DA25EB 81 C4 C0 00 00 00 add esp,0C0h
00DA25F1 3B EC cmp ebp,esp
00DA25F3 E8 97 EC FF FF call __RTC_CheckEsp (0DA128Fh)
对照着汇编可以看到call func 的机器码对应的是E8 62 ED FF FF
之前说的
真正要跳转的地址=E8这条指令的下一条指令的地址 + X
X=真正要跳转的地址 - E8要要跳转的地址的下一条指令的地址
我们来实际计算一下
真正要跳转的地址是0XDA1348
Call结束的下一个地址是0xDA25E6
因此我们用计算器算一下
刚好就是对应着FF FF ED 62,小端序转换就是62 ED FF FF ,和我们看到的对应的机器码是一样的
内存对齐和文件对齐
由于教程使用的飞鸽.exe的 SectionAlignment和FileAlignment 大小是一样的,导致很多人没有理解到添加ShellCode的精髓,所以推荐大家用文件和内存对齐不一致的程序来练手,就比如我上传的文件
我上传的这俩对齐大小就不一样,非常适合拿来练手
由于这俩对齐不一样,因此我们要多算一些东西,比如在在编写Shellcode的时候,使用Call,后面跟着计算出来的地址应该是以在内存中的地址算出的,而不是文件中的
以及后面E9 Jmp后面跟着的地址也是一样,都是按照拉伸后的ImageBuffer来算的
还有就是OEP,也就是 AddressOfEntryPoint ,这个入口点的计算是在内存的入口点地址直接减去ImageBase算出来的,也就是说这个偏移是内存的偏移而不是文件中的偏移!
比如我们在图片看到的OEP是0x1474,并不意味着程序入口点在文件中的位置是0x1474,它的真正意思是程序的入口点在 内存 中的地址是 ImageBase+ OEP,所以程序在内存的入口点是0x401474. (这一点很重要,我被带沟里了,后续也会用到)
节表
typedef struct _IMAGE_SECTION_HEADER {
0x00 BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
0x08 DWORD PhysicalAddress;
0x08 DWORD VirtualSize;
} Misc;
0x0c DWORD VirtualAddress;
0x10 DWORD SizeOfRawData;
0x14 DWORD PointerToRawData;
0x18 DWORD PointerToRelocations;
0x1c DWORD PointerToLinenumbers;
0x20 WORD NumberOfRelocations;
0x22 WORD NumberOfLinenumbers;
0x24 DWORD Characteristics;
};
/*
- Name:段名,是一个8字节的`ASCII`字符串,不足8字节用0补齐。
- VirtualSize:虚拟大小,标识节在**内存**中占用的大小,请勿与`PhysicalSize`(物理大小)混淆。(对其前得大小)
这Misc联合体 双字 是该节在没有对其前的真实尺寸,该值可以不准确,(在内存中拉伸后的实际大小)
- VirtualAddress:虚拟地址,标识**节**在**内存中**对应段头的地址,与实际加载的位置有关。(**节**在内存的偏移地址,加上ImageBase才是在内存的真正地址)
- SizeOfRawData:物理大小,节在**PE文件**中该段的占用大小,不足以文件对齐单位则会进行填充。(对齐后的长度)
- PointerToRawData:物理地址,标识该段在**文件中**的偏移位置。
- PointerToRelocations:重定向表的偏移位置。
- PointerToLinenumbers:行号表的偏移位置。
- NumberOfRelocations:重定向表数量。
- NumberOfLinenumbers:行号表数量。
- Characteristics:标识该段的各种属性信息,包括下列常用属性:
- IMAGE_SCN_MEM_READ:可读;
- IMAGE_SCN_MEM_WRITE:可写;
- IMAGE_SCN_MEM_EXECUTE:可执行;
- IMAGE_SCN_CNT_CODE:代码段;
- IMAGE_SCN_CNT_INITIALIZED_DATA:已初始化数据段;
- IMAGE_SCN_CNT_UNINITIALIZED_DATA:未初始化数据段;
- IMAGE_SCN_LNK_INFO:包含附加信息。
*/
这是节表的结构。
这是文件和内存对齐不一致时的情况
由于存在对齐,因此在例如.text和.data段之间,可能会留下足够长的空隙让我们写入shellcode,其实在哪个段写都无所谓,重要的是添加的代码要正确,也就是shellcode地址要正确,OEP要正确,这些都是要经过计算的。
实战
这是节表的信息
可以看到在文件中,.text结束的地址是Raw Size +Raw Offset=0x1400,放到WinHex里看看
还是可以看到有相当长的空闲区可以让我们写的,那么我们就从0X12E0开始添加我们的Shellcode吧。
我们要添加的代码就是
MessageBox(0, 0, 0, 0);
000F1A8D 8B F4 mov esi,esp
000F1A8F 6A 00 push 0
000F1A91 6A 00 push 0
000F1A93 6A 00 push 0
000F1A95 6A 00 push 0
000F1A97 FF 15 F4 B0 0F 00 call dword ptr [__imp__MessageBoxW@16 (0FB0F4h)]
当然MessageBox毕竟是动态链接库里的函数,因此我们需要打开OD或者X32dbg,定位一下MessageBox的具体地址
比如我用X32dbg找到的MessageBox地址就是0x77219B00
OK,开始改
先打个模板。
首先我们要计算的是Call ,也就是计算E8后面的机器码是什么
根据前面说到的公式
X=真正要跳转的地址 - E8要要跳转的地址的下一条指令的地址
真正要跳转的地址为 0x77219B00
Call下一条指令的地址是0x12E0+0x8(4个Push 0) + 0x5(Call的长度)=0x12ED
正如我们之前所说的那样,E8 E9后面跟着的地址的计算,一切都是要以内存的地址来计算
因此在文件中的0X12ED在内存是多少呢?
首先0X12ED在.text段的偏移是 0x12ED- 0x400(.text在文件的偏移)=EED
.text段在内存的起始地址是0x1000,加上偏移就是0x1EED,再加上ImageBase就是0X401EED
所以经过计算
X=真正要跳转的地址 - E8要要跳转的地址的下一条指令的地址
即 0x77219B00 - 0X401EED = 0x76E17C13
换成小端序就是 13 7C E1 76
接下来就是跳回到原来的入口点地址0x1474
0x1474在内存的地址就是0x401474
X=真正要跳转的地址 - E9要要跳转的地址的下一条指令的地址
E9要要跳转的地址的下一条指令的地址(文件中)=0X12ED+0X5=0X12F2
0x12F2在内存中的地址就是(参考之前的算法)0x1EF2,加上ImageBase就是401EF2
因此0X401474-0x401EF2=0xFFFF F582
换成小端序就是 82 F5 FF FF
最后一件事就是修改OEP了,文件中的0x12E0在内存中对应的地址是0x401EE0,所以OEP应该改为1EE0
另存为看看
成功弹出窗口!