s0m1ng

二进制学习中

前言

DLL(Dynamic Link Library,动态链接库)是 Windows 下的一种可执行模块,
可以被多个程序同时加载使用。可以导出函数

常见用途:

  • 封装公共函数(比如数学库、图形库)

  • 插件系统(比如浏览器插件)

  • 逆向工程与注入(CTF、安全研究中常用)

DLL基础:

DllMain:

  • dll没有 mainWinMain

  • 它有一个可选的 DllMain 入口点函数。这个函数不是给普通用户调用的,而是操作系统加载器在特定事件发生时(DLL 被加载、卸载、进程创建线程、线程结束)自动调用的。

  • 它的主要目的是进行初始化和清理工作(例如,创建/销毁全局对象、初始化线程本地存储 TLS)。如果不需要这些,完全可以不实现 DllMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH: // DLL被映射到进程的地址空间
// 初始化代码,例如创建互斥体、加载资源
break;
case DLL_THREAD_ATTACH: // 进程创建了一个新线程
// 线程相关的初始化
break;
case DLL_THREAD_DETACH: // 线程正常退出
// 线程相关的清理
break;
case DLL_PROCESS_DETACH: // DLL被从进程的地址空间卸载
// 清理代码,例如释放资源
break;
}
return TRUE; // 返回TRUE表示成功
}

导出函数,有两种方式:

写.def文件

1
2
3
4
5
6
; mylib.def
LIBRARY "MyLibrary"
EXPORTS
AddFunction @1
MultiplyFunction @2
MyExportedVariable DATA ; 导出变量需要DATA关键字

然后在编译时链接这个文件。这种方式可以精确控制导出函数的名字和序号。

使用关键字(更常见)

可在如下所示的函数声明中使用 __declspec(dllexport) 关键字。

1
2
3
4
5
__declspec(dllexport) double WINAPI my_C_export(double x)
{
/* Modify x and return it. */
return x * 2.0;
}

必须在声明的最左侧添加 __declspec(dllexport) 关键字。 这种方法的优点是该函数不需要在 DEF 文件中列出,并且导出状态与定义一致。

如果要避免使用 C++ 名称修饰来提供 C++ 函数,必须按如下方式声明函数。

1
2
3
4
5
6
extern "C"
__declspec(dllexport) double WINAPI my_undecorated_Cpp_export(double x)
{
// Modify x and return it.
return x * 2.0;
}

链接器将使该函数显示为 my_undecorated_Cpp_export,即源代码中显示的名称,没有任何修饰。

编写一个dll并编译

mydll.h

1
2
3
4
5
6
#pragma once


__declspec(dllexport) int add(int a, int b);

__declspec(dllexport) int mul(int a, int b);t b);

这里我们下面的源文件是.c,所以不加extern “C”

mydll.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "mydll.h"
#include <stdio.h>
#include <windows.h>
int add(int a, int b)
{
return a + b;
}
int mul(int a, int b)
{
return a * b;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
return TRUE;
}

编译指令:

1
gcc -shared -o mydll.dll mydll.c -Wl,--out-implib,libmydll.a

解释一下参数:

  • -shared:告诉 gcc 生成动态链接库

  • -o mydll.dll:输出 DLL 文件

  • -Wl,--out-implib,libmydll.a:同时生成一个静态导入库(方便别人链接)

这里的静态导入库的作用是可以把dll和exe合成一个文件。方便发布,一般我们直接gcc -shared -o mydll.dll mydll.c就可以

main.c(测试函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<windows.h>
typedef int (*add_t)(int,int);
typedef int (*mul_t)(int,int);
int main()
{
HMODULE h = LoadLibraryA("C:\\Users\\Lenovo\\OneDrive\\Desktop\\c++andpy\\cpp\\mydll\\mydll.dll"); // 或者写绝对路径
if (!h) {
printf("LoadLibrary failed: %lu\n", GetLastError());
return 1;
}
add_t add = (add_t)GetProcAddress(h, "add");
mul_t mul = (mul_t)GetProcAddress(h, "mul");
int a=10,b=20;
int sum;
sum=add(a,b);
printf("sum=%d\n",sum);
int m=mul(a,b);
printf("mul=%d\n",m);
return 0;
}

运行结果:

1
2
sum=30
mul=200

DLL调试:

dll调试如果用vscode的话太逆天,掌握不好注入器和被注入exe之间的关系,用vs就很轻松

资源管理器

右键我们的文件夹,点最下面的属性

编译dll

然后配置类型需要改成动态库.dll

目标exe

在调试的行那,右边的命令放要注入的exe路径,然后在我们的dll对应的.c文件那直接像正常的.c文件那样下断点就可以了

断点

然后直接运行这个dll对应的.c源程序,在exe文件的进程空间导入dll文件(dll注入或loadlibrary)后,我们就可以正常调试了

DLL注入

前言:

windows窗口编程就是创造一个窗口并实现消息循环,我们需要逆向的是窗口消息处理函数

流程:

入口点:

窗口的入口点是WinMain()函数

1
2
3
4
5
6
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPreInstance,
LPSTR lpCmdeLine,
int nCmdShow
)
  • HINSTANCE hInstance :

    • 含义:当前模块(process module)的实例句柄。

    • 现代 Windows(32/64 位)中 HINSTANCEHMODULE 等价,表示模块基址(即 exe 或 dll 的加载基址)。也就是说 HINSTANCE == HMODULE

    • 常用场景:作为资源加载的句柄参数(LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APP))FindResource(hInstance, ...) 等);也常用来创建窗口类 WNDCLASSEX.hInstance

    • 获取方式:程序入口 WinMain 会直接给出;也可以在任意地方用 GetModuleHandle(NULL) 获取当前可执行模块句柄,或 GetModuleHandle("mylib.dll") 获取指定 DLL 的句柄。

    • x86/x64 区别:无差别。都是指向模块基址的句柄(底层为值型,大小随平台指针宽度而定)。

    • 只有在16位可执行程序里会有差别,因为16位同时多开exe,并不是独立的分割内存,而如今都是每个进程分配4gb

  • HINSTANCE hPreInstance 已废弃,填null即可

  • LPSTR lpcmdeline:含义:指向以空字符结尾的命令行字符串(char*,ANSI 版 WinMain 使用)。

  • int nCmdShow:含义:程序窗口的初始显示状态,由操作系统或调用者(例如从快捷方式的“运行方式”设置或 ShowWindow 参数)传入。通常传给 ShowWindow(hwnd, nCmdShow)

1.创建一个窗口类

1
2
3
WNDCLASSW myClass = { 0 };
myClass.lpszClassName = L"51hook";
myClass.lpfnWndProc = WindowProc;

其中WNDCLASSW的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct tagWNDCLASSEX {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc; // 窗口过程指针
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;

其中最重要的就是 WNDPROC lpfnWndProc; // 窗口过程指针。做逆向只需要学会它的用法

2.注册窗口类

RegisterClassW(&myClass);

3.创建窗口

用 CreateWindowW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//3.创建窗口
HWND hwindow = CreateWindowW(
myClass.lpszClassName,

L"51hook",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
0,
CW_USEDEFAULT,
0,
NULL,
NULL,
hInstance,
0
);

函数定义:

1
2
3
4
5
6
7
8
9
10
11
12
HWND CreateWindowEx(
DWORD dwExStyle,
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x, int y, int nWidth, int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);

  • dwExStyle/dwStyle:窗口扩展样式 / 样式(WS、WS_EX

  • lpClassName:类名(或 Atom)

  • lpWindowName:窗口标题(Caption)

  • hMenu:菜单或子控件 ID(对于子窗口/控件是控件 ID)

  • lpParam:传递给 WM_CREATECREATESTRUCT*lpCreateParams 字段(常用于传递指针)
    逆向:

  • CreateWindowEx 返回的 HWND 常会被存到全局或成员变量中,搜索 mov [global], eax(x86)或 mov [rip+..], rax(x64)可定位。

  • 若使用类 atom(整数),lpClassName 参数的高位为小值。

补充:winapi中A版本和W版本(就是函数后的字母)区别只有参数可不可以是unicode的区别,W版本可以用unicode编码,比如说可以写字符串L’这是unicode编码’

4.显示窗口

ShowWindow(hwindow, SW_SHOWNORMAL);

1
2
BOOL ShowWindow(HWND hWnd, int nCmdShow);
BOOL UpdateWindow(HWND hWnd);
  • ShowWindow 常在窗口创建后调用以显示窗口(nCmdShow=SW_SHOW, SW_SHOWNORMAL 等)

  • UpdateWindow 触发 WM_PAINT(如果需要)

5.消息循环

1
2
3
4
5
6
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

GetMessage:

获取消息,如果不是WM_quit就接着循环

TranslateMessage / DispatchMessage

  • TranslateMessage:将虚拟键消息转换为字符消息(WM_KEYDOWN -> WM_CHAR)

  • DispatchMessage:把消息派发到目标窗口的 WndProc(最终调用 CallWindowProc

6.分析WndProc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LRESULT CALLBACK WindowProc(   //消息处理函数
_In_ HWND hwnd, //窗口句柄
_In_ UINT uMsg, // 接收的消息
_In_ WPARAM wParam, //小参数,低6位放
_In_ LPARAM lParam //long 参数,高16位放鼠标x坐标,低16位放鼠标y坐标
) {
switch (uMsg)
{
case WM_CREATE:
MessageBoxW(hwnd, L"窗口创建了", L"提示", MB_OK);
break;
case WM_CLOSE:
MessageBoxW(hwnd, L"窗口关闭了", L"提示", MB_OK);
DestroyWindow(hwnd);
PostQuitMessage(0);
break;
default:
break;
}
return DefWindowProcW(hwnd,uMsg,wParam,lParam);
}

其中函数参数:

参数 类型 典型用途 注意点
HWND hwnd 窗口句柄 唯一标识窗口实例 用于区分不同窗口
UINT uMsg 消息ID 指定当前消息类型 用于 switch 分发
WPARAM wParam 辅助参数 消息相关状态/标志/ID 含义因消息而异
LPARAM lParam 扩展参数 附加数据,如坐标、句柄 依赖消息类型解释

最常见的wparam和lparam的用法:

消息类型 wParam 含义
WM_KEYDOWN 虚拟键码(VK_XXX)
WM_LBUTTONDOWN 鼠标按键状态(MK_CONTROL / MK_SHIFT 等标志)
WM_COMMAND 高16位是通知码(如按钮点击),低16位是控件ID(父窗口内的代号)
WM_TIMER 定时器ID
消息 lParam 含义
WM_MOUSEMOVE(鼠标移动), WM_LBUTTONDOWN(鼠标左键被按下) 等 低16位 = X坐标, 高16位 = Y坐标
WM_COMMAND(操作菜单、控件,快捷键、加速键,或程序调用 SendMessage(hwnd, WM_COMMAND, …) 时,系统就会发送这个消息给对应的窗口过程。) 如果来自控件(子窗口):lParam 是该控件的 HWND。如果来自菜单/加速键:lParam 为 0(因此可以用来区分)

具体消息和处理逆向我们还需要进一步积累,这里只是起抛砖引玉的作用

实际例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<Windows.h>
#include<iostream>
LRESULT CALLBACK WindowProc( //消息处理函数
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
) {
switch (uMsg)
{
case WM_CREATE:
MessageBoxW(hwnd, L"窗口创建了", L"提示", MB_OK);
break;
case WM_CLOSE:
MessageBoxW(hwnd, L"窗口关闭了", L"提示", MB_OK);
DestroyWindow(hwnd);
PostQuitMessage(0);
break;
default:
break;
}
return DefWindowProcW(hwnd,uMsg,wParam,lParam);
}
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPreInstance,
LPSTR lpCmdeLine,
int nCmdShow
)
{
//1.创建一个窗口类
WNDCLASSW myClass = { 0 };
myClass.lpszClassName = L"51hook";
myClass.lpfnWndProc = WindowProc;
//2.注册窗口类
RegisterClassW(&myClass);
//3.创建窗口
HWND hwindow = CreateWindowW(
myClass.lpszClassName,

L"51hook",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
0,
CW_USEDEFAULT,
0,
NULL,
NULL,
hInstance,
0
);
//4.显示窗口
ShowWindow(hwindow, SW_SHOWNORMAL);

//5.获取消息
MSG msg = { 0 };
while (GetMessageW(&msg, 0,0,0)) {
DispatchMessageW(&msg);//分发消息给消息处理函数
}

return 0;
}

前言:

pe中的hook技术是指通过 DLL 注入(例如 CreateRemoteThread 注入、SetWindowsHookEx、frida-gadget 等实现方式)将代码注入目标进程,替换目标exe文件中的某个函数为自己的函数,而在这过程中我们没办法获得目标exe源码,只能用另一个进程去操作目标进程。本篇记录了IAT HOOK和inline hook。在日常生活中hook更多还是用frida的插桩技术实现(封装好了),不过底层hook的机制还是要了解,因为frida一旦被全方位检测拦截就没招了

注意:代码来自文末链接,但他讲的有问题,我把正确的代码改进后放在我的文章了

IAT hook

原理:

进程运行时IAT表里存储了我们函数的真实地址rva,我们如果用注入的dll更改IAT里的地址为我们dll里实现的函数地址,就实现了hook。相当于elf文件中的got表劫持

头文件:

1
2
3
4
5
6
7
8
9
#pragma once
#include<Windows.h>
#define IMAGE_ORDINAL_FLAG32 0x80000000
DWORD* g_iatAddr = NULL;
DWORD* g_unHookAddr = NULL;

BOOL InstallHook(); //安装钩子
BOOL UninstallHook(); //卸载钩子
DWORD* GetIatAddr(const char* dllName, const char* dllFuncName);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include "hookMain.h"

int WINAPI HookMessageBoxW( //必须指定调用约定,否则注入时会弹错误窗口
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType
)
{
int result = MessageBoxA(0, "51hook", "提示", MB_OK);
return result;
}

BOOL InstallHook() //安装钩子
{
DWORD dwOldProtect = 0;
VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);
*g_iatAddr = (DWORD)HookMessageBoxW;
VirtualProtect(g_iatAddr, 4, dwOldProtect, &dwOldProtect);
return TRUE;
}

BOOL UninstallHook() //卸载钩子
{
DWORD dwOldProtect = 0;
VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);
*g_iatAddr = (DWORD)g_unHookAddr;
VirtualProtect(g_iatAddr, 4, dwOldProtect, &dwOldProtect);
return TRUE;
}

DWORD* GetIatAddr(const char* dllName, const char* dllFuncName, WORD ordinal)
{
HMODULE hModule = GetModuleHandleA(NULL);
DWORD dwhModule = (DWORD)hModule;

PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(dwhModule + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionHeader = &pNtHeader->OptionalHeader;
PIMAGE_IMPORT_DESCRIPTOR pImageImportTable =
(PIMAGE_IMPORT_DESCRIPTOR)(dwhModule + pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

while (pImageImportTable->Name)
{
char* iatDllName = (char*)(dwhModule + pImageImportTable->Name);
if (_stricmp(iatDllName, dllName) == 0)
{
PIMAGE_THUNK_DATA pInt = (PIMAGE_THUNK_DATA)(dwhModule + pImageImportTable->OriginalFirstThunk);
PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(dwhModule + pImageImportTable->FirstThunk);

while (pInt->u1.AddressOfData)
{
// 判断是否按序号导入
if (pInt->u1.Ordinal & IMAGE_ORDINAL_FLAG32)
{
WORD importOrdinal = (WORD)(pInt->u1.Ordinal & 0xFFFF);
if (importOrdinal == ordinal)
{
return (DWORD*)pIat;
}
}
else
{
PIMAGE_IMPORT_BY_NAME pImportName =
(PIMAGE_IMPORT_BY_NAME)(dwhModule + pInt->u1.AddressOfData);
if (_stricmp(pImportName->Name, dllFuncName) == 0)
{
return (DWORD*)pIat;
}
}
++pInt;
++pIat;
}
}
++pImageImportTable;
}

return NULL;
}


BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD callReason, LPVOID lpReservered)
{
if (callReason == DLL_PROCESS_ATTACH)
{
/**
* 1 获取iat表
* 2 保存要hook的函数地址
* 3 安装钩子
*/
g_iatAddr = GetIatAddr("user32.dll", "MessageBoxW"0);
g_unHookAddr = (DWORD*)* g_iatAddr;
InstallHook();
}
else if (callReason == DLL_PROCESS_DETACH)
{
UninstallHook();
}
return TRUE;
}

我们可以随便写个主进程函数,然后把这个dll文件注入看能否hook成功。注入dll的方式有很多,可以看我注入dll的那篇。当然最简单是用github上别人造好的轮子来搞dll注入。这里放一个我觉得还不错的项目GitHub - Joe1sn/S-inject: 支持x86/x64的DLL和Shellcode 的Windows注入的免杀工具,支持图形化界面

1
2
3
4
5
6
7
8
9
10
11
#include<Windows.h>
int main()
{

HMODULE hModule = LoadLibraryA("IATHook.dll");//把
if (hModule)
{
MessageBoxW(0, L"hello world", L"hello world", MB_OK);
}
return 0;
}

inline hook

上面的IAT表有个问题,就是不在导入表里的函数你没办法hook啊,所以inline hook就是用来解决这个问题的。

  • 原理:

直接修改目标函数的前几条机器指令(通常是函数入口),替换成跳转指令(如 b 或 ldr pc, [addr]);

  • 优点:

可以 Hook 几乎任意函数(导出或非导出、静态或动态)

精细控制,适合保护/加壳/代码注入等底层用途

  • 缺点:

对 CPU 架构高度依赖(ARM64、ARMv7)

对汇编、内存保护、缓存等有要求(必须关闭写保护)

稳定性较低,不当使用可能 crash

代码:

main.h

1
2
3
4
5
6
7
8
9
10
11
#pragma once
#include<iostream>
#include<Windows.h>
int WINAPI MyMessageBoxW(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType);
BOOL InstallHook();
BOOL UnInstallHook();

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include"main.h"

DWORD g_unhookfun = NULL;
char g_oldcode[5] = { 0 };//函数的前五个字节也就是旧函数的地址
char g_newcode[5] = { 0xE9 };

//思路
//1、找到我们要HOOK函数地址
//2、保存要HOOK的函数的前5个字节
//3、计算目标函数距离jmp指令的一条指令的偏移offset
//4、改变函数的前5个字节改成OXE9 offset

//1、初始化函数:进行hook的初始工作找到hook函数保存函数的前五个字节,计算出偏移值。保存改变后的前5个字节
//2、安装钩子
//3、卸载钩子
//4、自定义函数

int WINAPI MyMessageBoxW(
_In_opt_ HWND hWnd,
_In_opt_ LPCWSTR lpText,
_In_opt_ LPCWSTR lpCaption,
_In_ UINT uType)
{
UnInstallHook();
int result = MessageBoxW(hWnd, L"51HOOK", lpCaption, uType);
InstallHook();
return result;
}

//安装钩子
BOOL InstallHook()
{
DWORD oldProtect = 0;
if (g_unhookfun==0)
{
return FALSE;
}
VirtualProtect((DWORD*)g_unhookfun, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((DWORD*)g_unhookfun, g_newcode, 5);
VirtualProtect((DWORD*)g_unhookfun, 5, oldProtect, &oldProtect);
return TRUE;
}

//卸载钩子
BOOL UnInstallHook()
{
DWORD oldProtect = 0;
if (g_unhookfun == 0)
{
return FALSE;
}
VirtualProtect((DWORD*)g_unhookfun, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((DWORD*)g_unhookfun, g_oldcode, 5);
VirtualProtect((DWORD*)g_unhookfun, 5, oldProtect, &oldProtect);
return TRUE;
}

//初始化函数
BOOL InitHook()
{
//找到要Hook函数的地址
HMODULE hModule = LoadLibraryA("user32.dll");
if (hModule == 0)
{
return 0;
}
g_unhookfun = (DWORD)GetProcAddress(hModule, "MessageBoxW");
//保存函数的前五个字节(旧函数的地址)
memcpy(g_oldcode, (char*)g_unhookfun, 5);
//计算偏移
DWORD offset = (DWORD)MyMessageBoxW - (g_unhookfun + 5);
//保存hook后的五个字节(新函数的地址)
memcpy(&g_newcode[1], &offset, 4);
return TRUE;
}

BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD callReason, LPVOID lpReserved)
{
if (callReason == DLL_PROCESS_ATTACH)
{
InitHook();
InstallHook();
}
else if (callReason == DLL_PROCESS_DETACH)
{
UnInstallHook();
}
return TRUE;
}

Reference:

【【保姆级教程】16 节吃透 Windows PE 文件格式!从解析到 Hook 攻防全覆盖】https://www.bilibili.com/video/BV1cXT4z7Etf?p=8&vd_source=ef1be23ebedc3f547905767af45d9f93

前言:

pe文件指在windows平台上的可执行文件(.exe,.dll,.com)了解他们的结构虽然对做题没什么用,但如果想开发新型外挂,防御新型外挂都是基于底层原理创新的

pe文件结构总览:

pe文件结构

地址的基本概念

  • VA(Virtual Address):虚拟地址
    PE 文件映射到内存空间时,数据在内存空间中对应的地址。

  • ImageBase:映射基址
    PE 文件在内存空间中的映射起始位置,是个 VA 地址。

  • RVA(Relative Virtual Address):相对虚拟地址
    PE 文件在内存中的 VA 相对于 ImageBase 的偏移量。

  • FOA(File Offset Address,FOA):文件偏移地址
    PE 文件在磁盘上存放时,数据相对于文件开头位置的偏移量,文件偏移地址等于文件地址。

转换关系:

  • VA = ImageBase + RVA

  • RVA-节区段首地址的RVA=FOA-节区段首地址的FOA

pe文件格式

DOS_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

这是dos头在微软的定义,我们只需要了解

(1)e_magic: DOS 映像文件格式标记,与 MS-DOS 兼容的 PE 文件都将该值设为 0x4D5A,对应的 ASCII 字符为:MZ。
(2)e_ip: DOS 代码的初始化指令入口。
(3)e_cs: DOS 代码的初始化代码段入口。
(4)e_lfanew:PE 文件头 _IMAGE_NT_HEADERS 结构的 FA 偏移地址,即指向 _IMAGE_NT_HEADERS 结构。

DOS_STUB

该结构未在 winnt.h 中定义,其内容随着链接时使用的链接器不同而不同,通常用于保存在 DOS 环境中的可执行代码。
例如:该结构中的代码用于显示字符串:“This program cannot run in DOS mode”。

NT_HEADER

位于 e_lfanew 处,结构

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // "PE\0\0"
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS;
  • Signature 必须是 PE\0\0(0x00004550)。

  • FileHeader 是 pe文件头,OptionalHeader(尽管名字叫 optional)几乎对可执行文件必需,包含入口点、ImageBase、节对齐等。

FILE_HEADER

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

要掌握的只有:

(1)Machine:平台类型,映像文件只能在指定的平台或模拟指定平台的系统上运行。在 winnt.h 中定义的 Machine 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // Useful for indicating we want to interact with the host and not a WoW guest.
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33 0x01d3
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
#define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE

(2)NumberOfSections:Section 的数目,即 Section Table 数组的元素个数。Windows Loader 限制 Section 的数目为 96 。
(3)TimeDateStamp:文件创建的日期和时间。
(4)PointerToSymbolTable:PE符号表的 RVA 偏移量,如果 PE符号表不存在,则该值为 0 。
(5)NumberOfSymbols:PE符号表中的符号个数。
(6)SizeOfOptionalHeader:_IMAGE_OPTIONAL_HEADER 结构的大小,对于 obj 文件,该值为 0 。
(7)Characteristics:PE 文件的属性。在 winnt.h 中定义的 Characteristics 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved external references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Aggressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.

OPTIONAL_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;
#else
typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER;
#endif

需要记的:

  • Magic 指示 PE32(0x10b)或 PE32+(0x20b,用于 x64)。PE32+ 没有某些 32-bit 字段(如 BaseOfData)。

  • 重要字段:

    • AddressOfEntryPoint(RVA) — 程序入口点(EP)。

    • ImageBase — 默认加载基址(x86 常 0x400000,x64 常 0x140000000)。

    • SectionAlignment 内存对齐粒度,即 PE 文件映射到内存时的对齐粒度;默认值为系统页面大小 0x1000(4KB);该值必须大于或等于 FileAligment 的值。

    • FileAlignment — 磁盘对齐粒度,即 PE 文件在磁盘中存储时的对齐粒度;默认值为磁盘页面大小 0x200(512B);如果 SectionAlignment 的值小于系统页面大小,则该值必须与 SectionAlignment 的值相同。

    • SizeOfImage — 映像在内存中的总大小(按 SectionAlignment 对齐)。

    • SizeOfHeaders — 所有头部(包括节表)在文件中的合占大小(按 FileAlignment 对齐)。

    • DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] — 数据目录数组,指向导入表/导出表/资源/重定位/证书等。每个目录是 (RVA, Size)。这是进入各种表的门。

Section Table(节表 / Section Headers)

IMAGE_NT_HEADERS 之后,紧跟 NumberOfSectionsIMAGE_SECTION_HEADER。每个节描述文件和内存中的一个区域(例如 .text, .rdata, .data, .rsrc 等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8]; // 节名(如 ".text")
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; // RVA(节在内存中的起始 RVA)
DWORD SizeOfRawData; // 文件中该节占用字节数(按 FileAlignment)
DWORD PointerToRawData; // 文件偏移(file offset)到节数据
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 可读/可写/可执行 等标志
} IMAGE_SECTION_HEADER;

常见节:

  • .text — 代码段(可执行、只读)。

  • .rdata — 只读数据(导出表、字符串、常量)。

  • .data — 已初始化的可读写数据。

  • .bss / uninitialized data — 在 PE 通常以 VirtualSize 指定但 SizeOfRawData 可能为 0。

  • .rsrc — 资源(图标、对话框、版本信息等)。

  • .reloc — 基址重定位表(如果启用了 ASLR 或 ImageBase 不是默认值时需要)。

  • .pdata / .xdata(x64 异常/函数表)等。

可选文件头中的数据目录表:

Data Directory 位于 IMAGE_OPTIONAL_HEADER 内,是一个固定长度的数组(通常 16 项,IMAGE_NUMBEROF_DIRECTORY_ENTRIES)。每项结构如下:

1
2
3
4
5
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // RVA(或对于某些目录是 file offset 特例)
DWORD Size;
} IMAGE_DATA_DIRECTORY;

我们只需要记住里面的导出表,导入表,重定位表

导出表:

位于 数据目录表第 0 项
IMAGE_DIRECTORY_ENTRY_EXPORT = 0

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 通常为0,保留字段
DWORD TimeDateStamp; // 时间戳(编译时间)
WORD MajorVersion; // 主版本号(可选)
WORD MinorVersion; // 次版本号(可选)
DWORD Name; // 模块名字符串的 RVA(如 "KERNEL32.dll")
DWORD Base; // 导出序号起始值(通常为1)
DWORD NumberOfFunctions; // EAT (Export Address Table) 的函数总数
DWORD NumberOfNames; // 按名称导出的函数数量
DWORD AddressOfFunctions; // RVA → DWORD 数组(EAT),每项为函数的 RVA
DWORD AddressOfNames; // RVA → DWORD 数组,保存函数名的 RVA
DWORD AddressOfNameOrdinals; // RVA → WORD 数组,保存函数名对应的序号索引
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Name 导出表文件名首地址
Base 导出函数起始序号
NumberOfFunctions是dll文件中导出函数的个数:最大的序号-最小序号+1
NumberOfNames以名称导出函数的个数:即在dll文件中函数后面不加noname的数量

首先我们要知道dll文件通常怎么编写导出哪些函数,一般是用.def文件存储函数和序号,编译时指定这个def文件

1
2
3
4
5
LIBRARY mydll
EXPORTS
sum @2
Add @3 NONAME
mul @7

上面这个例子里NumberOfFunctions就是6=7-2+1;

NumberOfNames就是2

在DLL文件中如何找到要用的函数呢?

AddressOfNames存的是函数名称起始位置的偏移。
AddressOfNameOrdinals存的是序号,加上Base等于dll文件中函数后面的序号。
AddressOfFunctions存的是真正函数存储位置的偏移。

从右向左看

要找到MessageBoxW的函数地址,首先从AddressOfNames在AddressOfNameOrdinals中的索引找到MessageBoxW的序号,在AddressOfFunctions按序号找到地址。

导出表

导入表

一个文件只有一个导出表,有多个导入表

INT:导入名称表,无论在文件中还是在内存中都是指向函数的名称

IAT: 导入地址表,在文件中时,与INT是一样的指向函数名称,在内存中保存的是函数实际地址

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 原先叫 OriginalFirstThunk
DWORD OriginalFirstThunk; // 指向 IMAGE_THUNK_DATA 数组
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 时间戳(若为绑定导入,则非零)
DWORD ForwarderChain; // 转发器链表索引(一般为 0)
DWORD Name; // 导入模块名字符串的 RVA(如 "USER32.dll")
DWORD FirstThunk; // 指向 IAT(IMAGE_THUNK_DATA 数组)
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // 转发字符串 RVA
DWORD Function; // 实际函数地址(IAT 填充后)
DWORD Ordinal; // 按序号导入时,高位标志+序号
DWORD AddressOfData; // 指向 IMAGE_IMPORT_BY_NAME 的 RVA
} u1;
} IMAGE_THUNK_DATA32;

1
2
3
4
5
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 建议序号(可加快查找速度)
CHAR Name[1]; // 函数名字符串(以 '\0' 结束)
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

这三个结构体之间的关系可以用图表示

导入表

_IMAGE_THUNK_DATA32四个字段的工作方式:

u1.AddressOfData

按名称导入时的初始状态。这是最常见的导入方式。它是一个 RVA,指向 IMAGE_IMPORT_BY_NAME 结构体。

Loader 执行时:

  1. 检查高位标志(是否为按序号导入)

  2. 如果按名称导入 → 从 AddressOfData 找到字符串 "MessageBoxA"

  3. 调用 GetProcAddress("MessageBoxA")

  4. 把查到的实际函数地址 写回到同一个 thunk 里
    → 此时该 DWORD 的含义变成了 Function

u1.Function
阶段 字段含义
程序加载前 AddressOfData(指向函数名结构)
程序徐加载后 Function(函数实际地址)

所以你用 IDA 或 PE-Bear 打开导入表时,
可以看到一列是函数地址(已经被修正),那就是 u1.Function 的值。

u1.Ordinal

如果是 按序号导入(而不是按名称),
那么 IMAGE_THUNK_DATA 的最高位会被置为 1,这个时候序号是ordinal的低16位

Loader 检查后,会直接按序号查找导出表中的对应函数。

u1.ForwarderString

具体工作流程

  1. 程序启动,系统加载器解析其导入表。

  2. 加载器看到需要从 Old.dll 导入一个函数。

  3. 加载器检查该函数对应的 IMAGE_THUNK_DATA 结构。

  4. 如果发现这个条目被标记为一个转发(Loader 看到字符串里有 .,就知道它是转发函数),那么 u1.ForwarderString 字段中存储的值就是一个 RVA。

  5. 这个 RVA 指向 PE 文件内部的一个字符串,这个字符串就是转发的目标,例如 "NewDLL.NewFunction"

  6. 加载器于是会转而加载 NewDLL.dll,获取 NewFunction 的地址,并填充到程序的 IAT 中。

重定位表

为什么需要重定位表:

假设你编译了一个 DLL:

1
编译期设定的镜像基址 (ImageBase) = 0x10000000

代码里可能存在这样的指令:

1
mov eax, [0x10003000]  ; 访问全局变量的绝对地址

但是当系统加载这个 DLL 时,如果地址 0x10000000 已经被别的模块占用,
Windows 就会把它加载到另一个位置,比如 0x20000000

那么所有访问 0x10003000 的指令都错了!

重定位表的任务:
告诉系统:“文件里哪些地方用了绝对地址”,好让 Loader 在加载时给它们加上偏移量修正

重定位表的结构层级:

整体结构是由若干个 重定位块 (Base Relocation Block) 组成。

1
2
3
4
5
6
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 该块对应的页基址(相对整个镜像)
DWORD SizeOfBlock; // 该块的大小(包括头和所有偏移项)
// 后面紧跟若干个 WORD 类型的重定位项(类型 + 偏移)
} IMAGE_BASE_RELOCATION;
//采取这种基址加偏移的存储结构优点是能节省空间,这种结构只需要8+4n字节可以存n个,但不是这种结构需要8n字节才能存n个

重定位项 (WORD) 结构

每个 WORD 包含两个部分(共 16 位):

位段 名称 含义
高 4 位 Type 重定位类型
低 12 位 Offset 该页内偏移

type类型在微软中的定义:

Type 值 名称 用途
0 IMAGE_REL_BASED_ABSOLUTE 无效项(跳过/对齐用)
1 IMAGE_REL_BASED_HIGH 高16位修正(16位系统遗留)
2 IMAGE_REL_BASED_LOW 低16位修正
3 IMAGE_REL_BASED_HIGHLOW 32位绝对地址修正(最常用)
10 IMAGE_REL_BASED_DIR64 64位绝对地址修正

在内存中的结构:

重定位表

重定位表的工作原理(Windows Loader 处理流程)

1.系统加载映像文件:

  • 期望基址 = ImageBase(例如 0x10000000)

  • 实际加载地址 = LoadBase(例如 0x20000000)

2.计算偏移差:

1
Delta = LoadBase - ImageBase;   // = 0x10000000

3.遍历每个重定位块:

  • 找到 IMAGE_BASE_RELOCATION.VirtualAddress

  • 遍历其中的所有 WORD

4.按类型修正目标地址:

  • 如果类型是 IMAGE_REL_BASED_HIGHLOW
1
2
DWORD* pAddr = (DWORD*)(imageBase + VirtualAddress + Offset);
*pAddr += Delta;

5.加载器修正完这些地址后:

  • 所有全局变量、函数指针都指向正确的绝对地址;

  • .reloc 区域在内存中可以被释放(某些加载器会保留用于卸载)

阶段 内容
编译期 生成以固定 ImageBase 链接的可执行文件
加载期 如果装入地址 ≠ ImageBase,则触发重定位
.reloc 记录所有需要修改绝对地址的地方
Loader 根据差值修正每个位置的值
类型 HIGHLOW(32位) 或 DIR64(64位)

TLS表

什么是TLS?
TLS是 Thread Local Storage的缩写线程局部存储。主要是为了解决多线程中变量同步的问题。

tls变量:

TLS变量只需要定义一次,类似全局变量,但定义完后每一个线程都能获取TLS变量的副本,解决了不能同步访问TLS的问题。节约了时间和成本。

tls回调函数:

1
2
3
4
5
typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK)(
PVOID DllHandle,
DWORD Reason, // DLL_PROCESS_ATTACH / DLL_THREAD_ATTACH / DLL_THREAD_DETACH / DLL_PROCESS_DETACH
PVOID Reserved
);

它会在进程附加(1),线程附加(2),线程脱离(3),进程脱离(0)时调用(小括号内数字指这四个状态对应整数):

  • 回调会在 Loader 设置好 TLS 数据后被调用。因此回调内部可以读取/写入静态 TLS 变量(以每线程视图访问)。

  • 调用时机

    • 当模块被装载(process attach)时,Loader 为当前存在的线程分配/初始化 TLS 模板(把模板拷贝给每个线程),然后调用模块的 TLS 回调,回调通常以 DLL_PROCESS_ATTACHReason

    • 当一个新线程被创建时,Loader 会为该线程拷贝 TLS 模板并调用已加载模块的 TLS 回调(DLL_THREAD_ATTACH)。

    • 当线程退出时,Loader 会先调用 DLL_THREAD_DETACH 回调,然后释放该线程的 TLS 数据。

    • 当模块卸载或进程退出时,会按顺序调用 DLL_PROCESS_DETACH 回调。

  • 执行时的约束

    • TLS 回调在 Loader Lock 下执行(与 DllMain 的执行约束类似),因此在回调中调用可能导致死锁的 API(如 LoadLibrary、某些同步函数)可能不安全。

    • 回调可能在非常早的阶段执行(在 DllMain 被调用之前),所以某些运行时/全局初始化可能尚未完成。

  • 多个回调AddressOfCallBacks 指向的回调数组中回调按数组顺序被调用(从低地址到高地址),数组以 NULL 结束。多个模块的回调调用顺序涉及模块加载顺序。

代码(包含tls变量和回调函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<Windows.h>
#include<iostream>
#pragma comment(linker,"/INCLUDE:__tls_used") //要声明连接tls
_declspec(thread) int g_number = 100; //tls变量
HANDLE hEvent = NULL;

DWORD WINAPI threadProc1(LPVOID lparam)
{
g_number = 200;
printf("threadProc1 g_number=%d\n", g_number);
SetEvent(hEvent);
return 0;
}

DWORD WINAPI threadProc2(LPVOID lparam)
{
WaitForSingleObject(hEvent, -1);
printf("threadProc2 g_number=%d\n", g_number);
return 0;
}

void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
printf("TLS函数执行了\n");
}

#pragma data_seg(".CRT$XLX")
//存储回调函数地址
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { t_TlsCallBack_A,0 };
#pragma data_seg()

int main()
{
hEvent = CreateEventA(NULL, FALSE, FALSE, NULL);
HANDLE hThread1 = CreateThread(NULL, NULL, threadProc1, NULL, NULL, NULL);
HANDLE hThread2 = CreateThread(NULL, NULL, threadProc2, NULL, NULL, NULL);
WaitForSingleObject(hThread1,-1);
WaitForSingleObject(hThread2,-1);
CloseHandle(hEvent);
system("pause");
return 0;
}



tls反调试:

既然我们知道了TLS是最先执行的,那么我们在TLS回调函数中加上判断是否被调试的API,若被调试直接在OEP之前终止程序,即可做到反调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<Windows.h>
#include<iostream>
#pragma comment(linker,"/INCLUDE:__tls_used")
void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (Reason == DLL_PROCESS_ATTACH)
{
BOOL result = FALSE;
HANDLE hNewHandle = 0;
DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hNewHandle, NULL, NULL, DUPLICATE_SAME_ACCESS);
CheckRemoteDebuggerPresent(hNewHandle, &result);//微软提供的API 判断该文件有没有被调试
if (result)
{
MessageBoxA(0, "程序被调试了!", "警告", MB_OK);
ExitProcess(0);
}
}
return;
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1,0 };
#pragma data_seg()

int main()
{
printf("main函数执行了");
system("pause");
return 0;
}

tls表

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_TLS_DIRECTORY {
DWORD StartAddressOfRawData; // TLS 数据的起始 VA(虚拟地址)
DWORD EndAddressOfRawData; // TLS 数据的结束 VA(虚拟地址)
DWORD AddressOfIndex; // 存放 TLS 索引的指针(VA)
DWORD AddressOfCallBacks; // TLS 回调函数数组的指针(VA)
DWORD SizeOfZeroFill; // 填充 0 的大小
DWORD Characteristics; // 特性标志,一般为0
} IMAGE_TLS_DIRECTORY32;

pe文件结构代码:

文件结构:

头文件:Main.cpp,CPeUtil.h

源文件:CPeUtil.cpp

CPeUtil.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#include "CPeUtil.h"

CPeUtil::CPeUtil()
{
FileBuff=NULL;
FileSize=0;
pDosHeader = NULL;
pNtHeaders = NULL;
pFileHeader = NULL;
pOptionHeader = NULL;
}

CPeUtil::~CPeUtil()
{
if (FileBuff)
{
delete[]FileBuff;
FileBuff = NULL;
}
}
//载入文件
BOOL CPeUtil::loadFile(const char* patch)
{
HANDLE hFile = CreateFileA(patch, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile==0)
{
return FALSE;
}
//私有成员变量获取文件大小并初始化缓冲区
FileSize = GetFileSize(hFile, 0);
FileBuff = new char[FileSize]{0};
DWORD realReadBytes = 0;
//是否读取成功
BOOL readSuccess =ReadFile(hFile,FileBuff,FileSize,&realReadBytes,0);
if (readSuccess==0)
{
return FALSE;
}
if (InitPeInfo())
{
CloseHandle(hFile);
return TRUE;
}
return FALSE;
}

//加载文件后初始化不同头位置
BOOL CPeUtil::InitPeInfo()
{
//用以下两个判断该文件是否为PE文件
pDosHeader = (PIMAGE_DOS_HEADER)FileBuff;
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
return FALSE;
}
pNtHeaders = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + FileBuff);
if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
return FALSE;
}
pFileHeader = &pNtHeaders->FileHeader;
pOptionHeader = &pNtHeaders->OptionalHeader;

return TRUE;
}

//输出区段头
void CPeUtil::PrintSectionHeaders()
{
PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders);//获取第一个区段头地址
//遍历不同区段
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
char name[9]{ 0 };
memcpy_s(name, 9, pSectionHeaders->Name, 8);
printf("区段名称:%s\n", name);
pSectionHeaders++;
}
}

//解析导出表
void CPeUtil::GetExportTable()
{
IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[0];
PIMAGE_EXPORT_DIRECTORY pexport = (PIMAGE_EXPORT_DIRECTORY)RvaToFoa(directory.VirtualAddress);
char *dllName = RvaToFoa(pexport->Name)+FileBuff;
printf("文件名称:%s\n", dllName);
//遍历不同函数的地址
DWORD* funaddr = (DWORD*)(RvaToFoa(pexport->AddressOfFunctions) + FileBuff);
WORD* peot = (WORD*)(RvaToFoa(pexport->AddressOfNameOrdinals) + FileBuff);
DWORD* pent = (DWORD*)(RvaToFoa(pexport->AddressOfNames) + FileBuff);
for (int i = 0; i < pexport->NumberOfFunctions; i++)
{
printf("函数地址为:%x\n",*funaddr);
for (int j = 0; j < pexport->NumberOfNames; j++)
{
if (peot[j]==i)
{
char* funName = RvaToFoa(pent[j])+FileBuff;
printf("函数名称为:%s\n", funName);
break;
}
}
funaddr++;
}

}

//获取导入表
void CPeUtil::GetImportTables()
{
//导入表也是数据目录表的一部分,作为第二个
IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[1];
//获取真正导入表地址
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(directory.VirtualAddress) + FileBuff);
//判断联合体中是否有数据
while (pImport->OriginalFirstThunk)
{
char* dllName = RvaToFoa(pImport->Name) + FileBuff;
printf("dll文件名称为:%s\n", dllName);
PIMAGE_THUNK_DATA pThunkData = (PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk) + FileBuff);
//判断联合体中是否有数据
while (pThunkData->u1.Function)
{
//判断是按序号导入还是按名称导入
if (pThunkData->u1.Ordinal & 0x80000000)
{
printf("按序号导入:%d\n", pThunkData->u1.Ordinal & 0x7FFFFFFF);
}
else
{
PIMAGE_IMPORT_BY_NAME importName = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pThunkData->u1.AddressOfData) + FileBuff);
printf("按名称导入:%s\n", importName->Name);
}
pThunkData++;
}
pImport++;
}
}

//RVA转化FOA
DWORD CPeUtil::RvaToFoa(DWORD rva)
{

PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders);//获取第一个区段头地址
//遍历不同区段
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
if (rva >= pSectionHeaders->VirtualAddress && rva < pSectionHeaders->VirtualAddress + pSectionHeaders->Misc.VirtualSize)
{
//数据的FOA=数据的RVA-区段的RVA+区段的FOA
return rva - pSectionHeaders->VirtualAddress + pSectionHeaders->PointerToRawData;
}
pSectionHeaders++;
}
return 0;
}



CPeUtil.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma once
#include<Windows.h>
#include<iostream>
class CPeUtil {
public:
CPeUtil();
~CPeUtil();
BOOL loadFile(const char* patch);
BOOL InitPeInfo();
void PrintSectionHeaders();
void GetExportTable();
void GetImportTables();
private:
char* FileBuff;
DWORD FileSize;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_FILE_HEADER pFileHeader;
PIMAGE_OPTIONAL_HEADER pOptionHeader;
DWORD RvaToFoa(DWORD rva);
};


main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
#include"CPeUtil.h"

int main()
{
CPeUtil peUtil;
BOOL ifSuccess = peUtil.loadFile("D:\\code\\VisualStudio2022\\FirstDLL\\Debug\\FirstDLL.dll");
if (ifSuccess)
{
peUtil.GetImportTables();
//peUtil.GetExportTable();
//peUtil.PrintSectionHeaders();
return 0;
}
printf("加载PE文件失败!\n");
return 0;
}

Reference:

https://blog.csdn.net/weixin_44143678/article/details/120044602?spm=1001.2014.3001.5506

【【保姆级教程】16 节吃透 Windows PE 文件格式!从解析到 Hook 攻防全覆盖】https://www.bilibili.com/video/BV1cXT4z7Etf?p=8&vd_source=ef1be23ebedc3f547905767af45d9f93

VEH

这是 SEH 的一个增强扩展,通过 AddVectoredExceptionHandler 添加。VEH 处理器会在调试器和所有 SEH 处理器之前被调用。它们更像是一种“通知”机制,可以观察或拦截进程中的所有异常。

与veh有关的函数

函数名 作用 参数解释
AddVectoredExceptionHandler 注册一个新的向量化异常处理函数(VEH)。注册后,当线程出现异常(例如访问违规、除零、RaiseException)时,系统会回调你提供的函数。 ULONG FirstHandler:是否把此 handler 放在最前面。 1 → 高优先级(最先被调用) 0 → 低优先级(放在队列后面)PVECTORED_EXCEPTION_HANDLER VectoredHandler:回调函数地址,函数类型为 LONG CALLBACK handler(EXCEPTION_POINTERS* ExceptionInfo)。返回值控制异常是否继续传播。
RemoveVectoredExceptionHandler 移除已注册的 VEH。 PVOID HandlerHandleAddVectoredExceptionHandler 返回的句柄。
RaiseException 主动抛出一个软件异常,会触发 VEH。 DWORD dwExceptionCode:异常码(自定义或系统定义)。DWORD dwExceptionFlags:是否可继续执行。0 表示可继续,EXCEPTION_NONCONTINUABLE 表示不可继续。DWORD nNumberOfArguments:额外参数个数。*_const ULONG_PTR _lpArguments__:异常参数数组(可选)。
EXCEPTION_POINTERS VEH 的回调参数结构,包含异常上下文。 成员有两个:ExceptionRecord:描述异常的详细信息(代码、参数、地址等)。ContextRecord:保存异常发生时 CPU 的寄存器状态(Rip/Eip, Rsp/Esp, Rax, Rcx…)。可读写!修改后返回 EXCEPTION_CONTINUE_EXECUTION 可以改变执行流。

正向实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// veh_demo.cpp
#include <windows.h>
#include <iostream>
using namespace std;

// ======================= 1. 被“隐藏”的函数 =======================
void SecretFunc()
{
cout << "[SecretFunc] VEH 修改 RIP 后跳转到这里执行!" << endl;
cout << "[SecretFunc] 恭喜,VEH 已成功拦截并重定向执行流!" << endl;
ExitProcess(0);
}

// ======================= 2. VEH 回调函数 =======================
LONG CALLBACK MyVectoredHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
cout << "[VEH] 异常捕获!" << endl;
cout << " ExceptionCode: 0x" << hex << ExceptionInfo->ExceptionRecord->ExceptionCode << endl;
cout << " ExceptionAddress: " << ExceptionInfo->ExceptionRecord->ExceptionAddress << endl;

// 判断是不是我们自己触发的异常(0xDEADC0DE)
if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xDEADC0DE)
{
cout << "[VEH] 捕获到自定义异常,修改上下文..." << endl;

#ifdef _M_X64
ExceptionInfo->ContextRecord->Rip = (DWORD64)&SecretFunc; // x64
#else
ExceptionInfo->ContextRecord->Eip = (DWORD)&SecretFunc; // x86
#endif

// 返回 EXCEPTION_CONTINUE_EXECUTION,让程序从修改后的地址继续执行
return EXCEPTION_CONTINUE_EXECUTION;
}

// 其他异常交给系统处理
return EXCEPTION_CONTINUE_SEARCH;
}

// ======================= 3. 主函数 =======================
int main()
{
cout << "[Main] 注册 VEH..." << endl;

// 注册 VEH,优先级高
PVOID hHandler = AddVectoredExceptionHandler(1, MyVectoredHandler);
if (!hHandler)
{
cerr << "AddVectoredExceptionHandler failed! error=" << GetLastError() << endl;
return 1;
}

cout << "[Main] 准备触发自定义异常..." << endl;

// 手动触发异常
RaiseException(0xDEADC0DE, 0, 0, nullptr);

cout << "[Main] 如果看到这行,说明 VEH 没有拦截执行流。" << endl;

// 移除 VEH
RemoveVectoredExceptionHandler(hHandler);
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10

[Main] 注册 VEH...
[Main] 准备触发自定义异常...
[VEH] 异常捕获!
ExceptionCode: 0xdeadc0de
ExceptionAddress: 00007FF6F8B21000
[VEH] 捕获到自定义异常,修改上下文...
[SecretFunc] VEH 修改 RIP 后跳转到这里执行!
[SecretFunc] 恭喜,VEH 已成功拦截并重定向执行流!

总结:

函数 用途 常用返回
AddVectoredExceptionHandler 注册 VEH 返回 handler 句柄
RemoveVectoredExceptionHandler 移除 VEH 成功返回非 0
RaiseException 主动抛出异常 会调用 VEH
MyVectoredHandler 回调处理异常 返回 CONTINUE_EXECUTION 或 CONTINUE_SEARCH
ExceptionInfo->ContextRecord 保存寄存器上下文 可修改以改变执行流

SEH

结构化异常处理(SEH)是 C 的Microsoft扩展,C++用于处理某些异常代码情况(如硬件故障)正常。 尽管 Windows 和 Microsoft C++支持 SEH,但我们建议在 C++ 代码中使用 ISO 标准C++异常处理。 它使代码更具可移植性和灵活性。 但是,若要维护现有代码或特定类型的程序,仍可能需要使用 SEH。

异常出现流程:

首先异常被交给内核态 / 最底层**

当 CPU 检测到一个错误(如无效内存访问),它会中断当前进程,并将控制权交给 Windows 内核。内核会为进程创建一个异常记录EXCEPTION_RECORD),其中包含异常代码、地址等信息。然后内核会查看进程是否正在被调试。

  • 如果进程被调试:内核将异常事件发送给调试器(第一机会异常)。调试器可以决定处理这个异常(继续执行)或不处理。

  • 如果进程未被调试,或调试器不处理:内核开始在用户态中寻找能处理这个异常的函数。

如果异常未能被处理,则在用户态等待被veh处理,若无veh,则交给seh

如果链式seh,veh未能处理

  • 当进程中发生异常时,此时会调用系统的kernel32!UnhandledExceptionFIlter()API。
  • 该API会运行系统的最后一个异常处理器——Top Level Exception FilterLast Exception Filter(通常行为是弹出错误消息框、终止进程)。
  • kernel32!UnhandledExceptionFilter()调用了ntdll!QueryInformationProcess(ProcessDebugPort)。来判断是否正在调试进程。如果正在进行调试,则将异常传递给调试器。否则系统异常处理器终止进程。

SEH结构体

1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
// 链表以Next成员为FFFFFFFF的结构体结束,表示链表的最后一个结点
PEXCEPTION_REGISTRATION_RECORD Next;
// Handler:异常处理函数
PEXCEPTION_DISPOSITION Handler;
} EX

SEH语法

1
2
3
4
5
try-except-statement :
  __try compound-statement __except ( filter-expression ) compound-statement

try-finally-statement :
  __try compound-statement __finally compound-statement

正向实例:

(a) __try / __except - 异常处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>
#include <excpt.h>
#include <stdio.h>

int main() {
__try {
// 可能会引发异常的代码
int* p = NULL;
*p = 42; // 这将引发一个访问违规异常 (EXCEPTION_ACCESS_VIOLATION)
}
__except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
// 异常过滤器返回 EXCEPTION_EXECUTE_HANDLER 时,执行这个块
printf("Caught an access violation exception!\n");
// 这里可以进行错误恢复、清理、记录日志等操作
}

printf("Program continues after handling the exception.\n");
return 0;
}
  • __try:包含可能出错的代码。

  • __except:异常处理程序。它是否能执行取决于其括号内的“异常过滤器表达式”

  • 异常过滤器表达式:这是一个必须返回以下三个值之一的表达式:

    • EXCEPTION_EXECUTE_HANDLER (1): 执行处理程序。系统会展开堆栈(清理 __try 块中已构造的局部 C++ 对象可能会成为问题),然后跳转到 __except 块。

    • EXCEPTION_CONTINUE_SEARCH (0): 不处理。系统继续向上一个(外层)的异常处理程序寻找能处理的 __except 块。

    • EXCEPTION_CONTINUE_EXECUTION (-1): 继续执行。从异常发生处重新开始执行。极其危险! 除非你能在过滤器里修复导致异常的问题(如虚拟内存分配),否则通常会立刻再次触发同一个异常,导致死循环。

其中的GetExceptionCode()函数值包含EXCEPTION_ACCESS_VIOLATION, EXCEPTION_INT_DIVIDE_BY_ZERO, EXCEPTION_STACK_OVERFLOW等等,对应不同出错类型

(b) __try / __finally - 终止处理程序

这种结构不处理异常,而是保证无论 __try 块是如何退出的(正常执行完毕、returngotobreak 或由于异常),__finally 块中的代码一定会被执行。用于实现资源清理(如关闭文件、释放锁)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HANDLE hFile = INVALID_HANDLE_VALUE;

__try {
hFile = CreateFileA("test.txt", ...);
if (hFile == INVALID_HANDLE_VALUE) {
__leave; // 跳转到 __finally 块的另一种方式
}
// 对文件进行一些操作,可能会引发异常
SomeRiskyOperation(hFile);
}
__finally {
// 无论上面如何退出,这里都会执行
if (hFile != INVALID_HANDLE_VALUE) {
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
}
}
// 执行完 __finally 后,异常(如果有)会继续向外传播

逆向实战:

注意:32位pe和64位peSEH使用方式不同,注意甄别

1.32位例题:

1
2
3
4
5
6
7
.text:00401140                 push    ebp
.text:00401141 mov ebp, esp
.text:00401143 push 0FFFFFFFEh
.text:00401145 push offset stru_403758
.text:0040114A push offset SEH_401140
.text:0040114F mov eax, large fs:0
.text:00401155 push eax

在使用SEH的函数汇编你会看到这样一段

第一第二行是创建函数的基本操作,这里不多解释,第三行0xFFFFFFFE叫做Trylevel/enclosing``

-1 (0xFFFFFFFF) 表示:函数中没有任何 try/except(即编译器没生成 ScopeTable)。

-2 (0xFFFFFFFE) 表示:函数有 ScopeTable,但当前没有任何激活的 try 块

所以翻译过来就是目前这个seh只有一层(还没进入try),具体进入try的部分见什么修改了Trylevel,如下最后是try结束

1
2
3
4
5
6
7
8
.text:004011B3                 mov     [ebp+ms_exc.registration.TryLevel], 0 //try开始
.text:004011BA mov [ebp+var_38], 0
.text:004011C1 mov eax, [ebp+var_1C]
.text:004011C4 mov edx, [ebp+var_24]
.text:004011C7 mov ecx, [ebp+var_20]
.text:004011CA mov ebx, [ebp+arg_0]
.text:004011CD div [ebp+var_38] //明显除0异常
.text:004011D0 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh //try结束

第4行push offset stru_403758

  • 把指向 .rdata 中 scope table(你之前贴的 stru_403758) 的地址压栈。

  • 这个表包含了 filter/handler 的地址、cookie 偏移等,运行时的异常处理器用它来决定哪个 try/except(或 finally)块应该响应当前异常

第5行push offset SEH_401140

  • 把一个“handler 地址”或“该函数专用的异常处理 stub”的地址压栈。IDA 给它取名为 SEH_401140(或许是个局部的 handler/veneer)。

  • 当异常发生并且运行时走到这个注册记录时,系统会调用这个 handler(这个 handler 通常是编译器生成的代码 / 运行时枢纽,它会读取 scope table,调用相应的 filter/handler 函数

第6行mov eax, large fs:0

  • fs:[0] 读取当前线程的 SEH 链表头(在 x86 Windows 中,FS 段基址指向 TIB,TIB 的第一个 dword 就是 SEH 链表头)。large 是汇编器的语法,表示读取完整的 32 位值。

  • 把当前链表头(即“之前的注册记录”的指针)读出来保存到 EAX。

第7行push eax

  • 把旧的 fs:[0](即之前的链表头)压栈 —— 这就是新注册记录的 Next 字段(保存链表的前驱,以便函数退出时能恢复)。

  • 在压栈/设置 fs:[0] 后,新的记录就会被插到链表最前面,变成当前活动的异常注册记录。

退出函数时解除seh

1
2
3
4
5
6
7
8
9
10
11
loc_401268:
mov ecx, [ebp+ms_exc.registration.Next]
mov large fs:0, ecx ; 恢复 fs:[0] = 上一个 SEH 节点
pop ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
retn

总结逆向流程:

我们需要在stru_403758找到相应的过滤函数,和处理异常函数,该题中

1
2
3
4
5
6
7
8
                stru_403758     dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00403758 ; DATA XREF: sub_401140+5↑o
.rdata:0040375C dd 0 ; GSCookieXOROffset
.rdata:00403760 dd 0FFFFFFB0h ; EHCookieOffset
.rdata:00403764 dd 0 ; EHCookieXOROffset
.rdata:00403768 dd 0FFFFFFFEh ; ScopeRecord.EnclosingLevel
.rdata:0040376C dd offset loc_4011D9 ; ScopeRecord.FilterFunc
.rdata:00403770 dd offset loc_4011DF ; ScopeRecord.HandlerFunc

前3个没什么用,第四个是我们上面的Trylevel,第5个是过滤函数,第6个是我们的处理函数,也就是ctf中反调试替换掉的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
               loc_4011D9:                             ; DATA XREF: .rdata:stru_403758↓o
.text:004011D9 mov eax, 1
.text:004011DE retn
.text:004011DF ; ---------------------------------------------------------------------------
.text:004011DF
.text:004011DF loc_4011DF: ; DATA XREF: .rdata:stru_403758↓o
.text:004011DF mov esp, [ebp+ms_exc.old_esp]
.text:004011E2 mov edi, [ebp+var_24]
.text:004011E5 mov ecx, edi
.text:004011E7 shr ecx, 4
.text:004011EA mov eax, edi
.text:004011EC shl eax, 5
.text:004011EF xor ecx, eax
.text:004011F1 add ecx, edi
.text:004011F3 mov eax, [ebp+arg_0]
.text:004011F6 mov eax, [eax]
.text:004011F8 add eax, [ebp+var_20]
.text:004011FB xor ecx, eax
.text:004011FD xor [ebp+var_1C], ecx
.text:00401200 push offset Buffer ; "Something happend..."
.text:00401205 call ds:puts
.text:0040120B add esp, 4
.text:0040120E mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:00401215 mov esi, [ebp+var_28]

这里过滤函数返回值保存在eax中,这里也就是返回了1,说明EXCEPTION_EXECUTE_HANDLER要处理这个异常,把原函数逻辑替换为下面的处理函数然后就可以接着进行逆向分析了,try中遇到error,那条出错指令汇编跳过,执行exception指令,然后执行try{}下面的语句。

前言:

逆向工程中往往使用aes加密的程序不是用的查表法aes,就是用的白盒aes,让你难以分析,傻乎乎的使用aes基础算法很容易被破解

查表法实现的aes:

在前一篇文章中我们详细讲解了AES的每一轮中具体的四层结构,以加密过程为例分别是:字节代换层、行移位层、列混淆层和轮密钥加层

对于查表法实现,就是要将每一轮中的前三层操作(字节代换层、行移位层和列混淆层)合并为查找表。

查表法的核心思想是将字节代换层、ShiftRows层和MixColumn层融合为查找表:每个表的大小是32 bits(4字节)乘以256项,一般称为T盒(T-Box)或T表。加密过程4个表(Te),解密过程4个表(Td),共8个。每一轮操作都通过16次查表产生。

算法中定义T表实现为:

1
2
3
4
5
T0[x] = (2·S[x])<<24 | (S[x])<<16 | (S[x])<<8 | (3·S[x])
T1[x] = (3·S[x])<<24 | (2·S[x])<<16 | (S[x])<<8 | (S[x])
T2[x] = (S[x])<<24 | (3·S[x])<<16 | (2·S[x])<<8 | (S[x])
T3[x] = (S[x])<<24 | (S[x])<<16 | (3·S[x])<<8 | (2·S[x])

其中:

  • S[x] 是 S-box 输出;

  • 2·S[x] 表示有限域 GF(2^8) 下的乘法(即 xtime 运算)。

原理:

由于aes加密流程中的字节代换和行移位可随意更改顺序而不影响加密结果,所以,我们把行移位放在最前面

查表法aes

先不管行移位,把行移位之后的矩阵状态设为

只看第一列的变化,设

1
2
3
4
5
a0' = S[a0]
a1' = S[a1]
a2' = S[a2]
a3' = S[a3]

1
2
3
4
5
6
c0 = 2·a0' ⊕ 3·a1'1·a2' ⊕ 1·a3'
c1 = 1·a0' ⊕ 2·a1'3·a2' ⊕ 1·a3'
c2 = 1·a0' ⊕ 1·a1'2·a2' ⊕ 3·a3'
c3 = 3·a0' ⊕ 1·a1'1·a2' ⊕ 2·a3'


1
2
3
4
5
T0[x] = (2·S[x], S[x], S[x], 3·S[x])   // 合并为一个 32 位值
T1[x] = (3·S[x], 2·S[x], S[x], S[x])
T2[x] = (S[x], 3·S[x], 2·S[x], S[x])
T3[x] = (S[x], S[x], 3·S[x], 2·S[x])

所以

1
2
3
4
5
T0[a0] = [2·S[a0], S[a0], S[a0], 3·S[a0]]
T1[a1] = [3·S[a1], 2·S[a1], S[a1], S[a1]]
T2[a2] = [S[a2], 3·S[a2], 2·S[a2], S[a2]]
T3[a3] = [S[a3], S[a3], 3·S[a3], 2·S[a3]]

于是

1
c0c1c2c3 = T0[a0] ⊕ T1[a1] ⊕ T2[a2] ⊕ T3[a3]

现在再来考虑行移位的影响,只要把a1-a15的对应标号改掉就行

1
2
3
4
t0 = T0[a0] ^ T1[a5] ^ T2[a10] ^ T3[a15] ^ roundKey[0];
t1 = T0[a4] ^ T1[a9] ^ T2[a14] ^ T3[a3] ^ roundKey[1];
t2 = T0[a8] ^ T1[a13]^ T2[a2] ^ T3[a7] ^ roundKey[2];
t3 = T0[a12]^ T1[a1] ^ T2[a6] ^ T3[a11] ^ roundKey[3];

是最后加密的结果

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
#include <stdlib.h>
#ifndef AES_H
#define AES_H
#include <stdint.h>
typedef struct AES_Key {
uint32_t* ek; // AES加密轮密钥
uint32_t* dk; // AES 解密轮密钥
uint32_t nr; //加密轮数
} AES_Key;
int AES_KeyInit(uint8_t* key, AES_Key* aes_key, size_t bits);
void AES_Encrypt(uint8_t* plaintext, uint8_t* ciphertext, AES_Key aes_key);
void AES_Decrypt(uint8_t* ciphertext, uint8_t* plaintext, AES_Key aes_key);
void AES_KeyDelete(AES_Key aes_key);
#endif
static const uint8_t Sbox[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B,
0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0,
0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26,
0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2,
0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0,
0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED,
0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F,
0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5,
0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC,
0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14,
0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C,
0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D,
0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F,
0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E,
0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11,
0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F,
0xB0, 0x54, 0xBB, 0x16 };
static const unsigned char SboxIV[256] = {
0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB,
0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB,
0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E,
0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25,
0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92,
0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84,
0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06,
0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B,
0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73,
0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E,
0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B,
0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4,
0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F,
0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF,
0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61,
0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D
};
static const uint32_t TE[256] = {
0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd,
0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d,
0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d,
0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b,
0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7,
0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a,
0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4,
0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f,
0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1,
0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d,
0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e,
0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb,
0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e,
0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c,
0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46,
0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a,
0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7,
0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81,
0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe,
0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504,
0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a,
0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f,
0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2,
0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395,
0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e,
0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c,
0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256,
0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4,
0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4,
0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7,
0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa,
0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818,
0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1,
0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21,
0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42,
0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12,
0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158,
0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133,
0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22,
0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a,
0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631,
0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11,
0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a };
static const uint32_t TD[256] = {
0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1,
0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25,
0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67,
0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6,
0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3,
0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd,
0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182,
0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94,
0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2,
0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5,
0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492,
0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a,
0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa,
0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46,
0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997,
0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb,
0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48,
0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927,
0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f,
0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16,
0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad,
0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd,
0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc,
0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120,
0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3,
0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422,
0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1,
0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4,
0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8,
0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3,
0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4,
0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6,
0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331,
0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815,
0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d,
0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f,
0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252,
0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89,
0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f,
0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86,
0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c,
0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190,
0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 };
#define rotr32(value, shift) ((value >> shift) ^ (value << (32 - shift)))
int AES_KeyInit(uint8_t* key, AES_Key* aes_key, size_t bits) {
uint32_t Rcon[10] = { 0x01, 0x02, 0x04, 0x08, 0x10,
0x20, 0x40, 0x80, 0x1B, 0x36 }; //轮常数
uint32_t nr = 10 + (bits - 128) / 32; //加密轮数 Nr
uint32_t nk = bits / 32; //密钥字数 Nk
uint32_t tmp, tmp1;
aes_key->nr = nr;
//-----------malloc-------------
uint32_t* w = (uint32_t*)malloc(sizeof(uint32_t) * 4 * (nr + 1));
if (w == (void*)0) {
return 0;
}
uint32_t* d = (uint32_t*)malloc(sizeof(uint32_t) * 4 * (nr + 1));
if (d == (void*)0) {
free(d);
return 0;
}
//--------------Load as BigEndian---------------
for (int i = 0; i < nk; i++) {//将总的bits,每32个(四个字节)分一组,每一组用大端序来进行表示
w[i] = (key[4 * i + 0] << 24) | (key[4 * i + 1] << 16) |
(key[4 * i + 2] << 8) | (key[4 * i + 3]);
}
//------------KeyExpand-----------------
for (int i = nk; i < 4 * (nr + 1); i++) {
tmp = w[i - 1];
if (i % nk == 0) {
/* tmp = SubWord(RotWord(w[i-1])) */
tmp1 = tmp;
tmp = Sbox[(tmp1 >> 24) & 0xFF];
tmp |= Sbox[(tmp1 >> 0) & 0xFF] << 8;
tmp |= Sbox[(tmp1 >> 8) & 0xFF] << 16;
tmp |= (Sbox[(tmp1 >> 16) & 0xFF] ^ Rcon[i / nk - 1]) << 24;
}
else if (nk > 6 && i % nk == 4) {
/* temp = SubWord(w[i-1]) */
tmp1 = tmp;
tmp = Sbox[(tmp1 >> 0) & 0xFF];
tmp |= Sbox[(tmp1 >> 8) & 0xFF] << 8;
tmp |= Sbox[(tmp1 >> 16) & 0xFF] << 16;
tmp |= Sbox[(tmp1 >> 24) & 0xFF] << 24;
}
w[i] = w[i - nk] ^ tmp;
}
aes_key->ek = w;
//------------TransKey-----------
for (int i = 0; i < 4; i++) {
d[i] = w[i];
}
for (int i = 4; i < 4 * nr; i++) {
//-----------MixCol IV-----------
d[i] = TD[Sbox[(w[i] >> 24) & 0xFF]];
tmp = TD[Sbox[(w[i] >> 16) & 0xFF]];
d[i] ^= rotr32(tmp, 8);
tmp = TD[Sbox[(w[i] >> 8) & 0xFF]];
d[i] ^= rotr32(tmp, 16);
tmp = TD[Sbox[(w[i] >> 0) & 0xFF]];
d[i] ^= rotr32(tmp, 24);
}
for (int i = 0; i < 4; i++) {
d[4 * nr + i] = w[4 * nr + i];
}
aes_key->dk = d;
return 1;
}
void AES_KeyDelete(AES_Key aes_key) {
free(aes_key.ek);
free(aes_key.dk);
}

void AES_Encrypt(uint8_t* plaintext, uint8_t* ciphertext, AES_Key aes_key) {
uint32_t s[4];
uint32_t t[4];
uint32_t tmp;
//------------Load as BigEndian------------------
for (int i = 0; i < 4; i++) {
s[i] = (plaintext[4 * i + 0] << 24) | (plaintext[4 * i + 1] << 16) |
(plaintext[4 * i + 2] << 8) | (plaintext[4 * i + 3]);
}
//----------------AddRoundKey----------------
s[0] ^= aes_key.ek[0];
s[1] ^= aes_key.ek[1];
s[2] ^= aes_key.ek[2];
s[3] ^= aes_key.ek[3];
for (int i = 1; i < aes_key.nr; i++) {
//-------ShiftRow + SubByte + MixCol-------------
// t0
t[0] = TE[(s[0] >> 24) & 0xFF];
tmp = TE[(s[1] >> 16) & 0xFF];
t[0] ^= rotr32(tmp, 8);
tmp = TE[(s[2] >> 8) & 0xFF];
t[0] ^= rotr32(tmp, 16);
tmp = TE[(s[3] >> 0) & 0xFF];
t[0] ^= rotr32(tmp, 24);
// t1
t[1] = TE[(s[1] >> 24) & 0xFF];
tmp = TE[(s[2] >> 16) & 0xFF];
t[1] ^= rotr32(tmp, 8);
tmp = TE[(s[3] >> 8) & 0xFF];
t[1] ^= rotr32(tmp, 16);
tmp = TE[(s[0] >> 0) & 0xFF];
t[1] ^= rotr32(tmp, 24);
// t2
t[2] = TE[(s[2] >> 24) & 0xFF];
tmp = TE[(s[3] >> 16) & 0xFF];
t[2] ^= rotr32(tmp, 8);
tmp = TE[(s[0] >> 8) & 0xFF];
t[2] ^= rotr32(tmp, 16);
tmp = TE[(s[1] >> 0) & 0xFF];
t[2] ^= rotr32(tmp, 24);
// t3
t[3] = TE[(s[3] >> 24) & 0xFF];
tmp = TE[(s[0] >> 16) & 0xFF];
t[3] ^= rotr32(tmp, 8);
tmp = TE[(s[1] >> 8) & 0xFF];
t[3] ^= rotr32(tmp, 16);
tmp = TE[(s[2] >> 0) & 0xFF];
t[3] ^= rotr32(tmp, 24);
//-------------AddRoundKey---------------
s[0] = t[0] ^ aes_key.ek[4 * i + 0];
s[1] = t[1] ^ aes_key.ek[4 * i + 1];
s[2] = t[2] ^ aes_key.ek[4 * i + 2];
s[3] = t[3] ^ aes_key.ek[4 * i + 3];
}
//------------ShiftRow + SubByte-----------
// t0
t[0] = Sbox[(s[0] >> 24) & 0xFF] << 24;
t[0] |= Sbox[(s[1] >> 16) & 0xFF] << 16;
t[0] |= Sbox[(s[2] >> 8) & 0xFF] << 8;
t[0] |= Sbox[(s[3] >> 0) & 0xFF] << 0;
// t1
t[1] = Sbox[(s[1] >> 24) & 0xFF] << 24;
t[1] |= Sbox[(s[2] >> 16) & 0xFF] << 16;
t[1] |= Sbox[(s[3] >> 8) & 0xFF] << 8;
t[1] |= Sbox[(s[0] >> 0) & 0xFF] << 0;
// t2
t[2] = Sbox[(s[2] >> 24) & 0xFF] << 24;
t[2] |= Sbox[(s[3] >> 16) & 0xFF] << 16;
t[2] |= Sbox[(s[0] >> 8) & 0xFF] << 8;
t[2] |= Sbox[(s[1] >> 0) & 0xFF] << 0;
// t3
t[3] = Sbox[(s[3] >> 24) & 0xFF] << 24;
t[3] |= Sbox[(s[0] >> 16) & 0xFF] << 16;
t[3] |= Sbox[(s[1] >> 8) & 0xFF] << 8;
t[3] |= Sbox[(s[2] >> 0) & 0xFF] << 0;
//------------AddRoundKey-------------
s[0] = t[0] ^ aes_key.ek[4 * aes_key.nr + 0];
s[1] = t[1] ^ aes_key.ek[4 * aes_key.nr + 1];
s[2] = t[2] ^ aes_key.ek[4 * aes_key.nr + 2];
s[3] = t[3] ^ aes_key.ek[4 * aes_key.nr + 3];
//-----------Store as BigEndian--------------
for (int i = 0; i < 4; i++) {
ciphertext[4 * i + 0] = (s[i] >> 24) & 0xFF;
ciphertext[4 * i + 1] = (s[i] >> 16) & 0xFF;
ciphertext[4 * i + 2] = (s[i] >> 8) & 0xFF;
ciphertext[4 * i + 3] = (s[i] >> 0) & 0xFF;
}
}

void AES_Decrypt(uint8_t* ciphertext, uint8_t* plaintext, AES_Key aes_key) {
uint32_t s[4];
uint32_t t[4];
uint32_t tmp;
//------------Load as BigEndian------------------
for (int i = 0; i < 4; i++) {
s[i] = (ciphertext[4 * i + 0] << 24) | (ciphertext[4 * i + 1] << 16) |
(ciphertext[4 * i + 2] << 8) | (ciphertext[4 * i + 3]);
}
//----------------AddRoundKey----------------
s[0] ^= aes_key.dk[4 * aes_key.nr + 0];
s[1] ^= aes_key.dk[4 * aes_key.nr + 1];
s[2] ^= aes_key.dk[4 * aes_key.nr + 2];
s[3] ^= aes_key.dk[4 * aes_key.nr + 3];
for (int i = aes_key.nr - 1; i > 0; i--) {
//-------ShiftRow IV + SubByte IV + MixCol IV-------------
// t0
t[0] = TD[(s[0] >> 24) & 0xFF];
tmp = TD[(s[3] >> 16) & 0xFF];
t[0] ^= rotr32(tmp, 8);
tmp = TD[(s[2] >> 8) & 0xFF];
t[0] ^= rotr32(tmp, 16);
tmp = TD[(s[1] >> 0) & 0xFF];
t[0] ^= rotr32(tmp, 24);
// t1
t[1] = TD[(s[1] >> 24) & 0xFF];
tmp = TD[(s[0] >> 16) & 0xFF];
t[1] ^= rotr32(tmp, 8);
tmp = TD[(s[3] >> 8) & 0xFF];
t[1] ^= rotr32(tmp, 16);
tmp = TD[(s[2] >> 0) & 0xFF];
t[1] ^= rotr32(tmp, 24);
// t2
t[2] = TD[(s[2] >> 24) & 0xFF];
tmp = TD[(s[1] >> 16) & 0xFF];
t[2] ^= rotr32(tmp, 8);
tmp = TD[(s[0] >> 8) & 0xFF];
t[2] ^= rotr32(tmp, 16);
tmp = TD[(s[3] >> 0) & 0xFF];
t[2] ^= rotr32(tmp, 24);
// t3
t[3] = TD[(s[3] >> 24) & 0xFF];
tmp = TD[(s[2] >> 16) & 0xFF];
t[3] ^= rotr32(tmp, 8);
tmp = TD[(s[1] >> 8) & 0xFF];
t[3] ^= rotr32(tmp, 16);
tmp = TD[(s[0] >> 0) & 0xFF];
t[3] ^= rotr32(tmp, 24);
//-------------AddRoundKey---------------
s[0] = t[0] ^ aes_key.dk[4 * i + 0];
s[1] = t[1] ^ aes_key.dk[4 * i + 1];
s[2] = t[2] ^ aes_key.dk[4 * i + 2];
s[3] = t[3] ^ aes_key.dk[4 * i + 3];
}
//------------ShiftRow + SubByte-----------
// t0
t[0] = SboxIV[(s[0] >> 24) & 0xFF] << 24;
t[0] |= SboxIV[(s[3] >> 16) & 0xFF] << 16;
t[0] |= SboxIV[(s[2] >> 8) & 0xFF] << 8;
t[0] |= SboxIV[(s[1] >> 0) & 0xFF] << 0;
// t1
t[1] = SboxIV[(s[1] >> 24) & 0xFF] << 24;
t[1] |= SboxIV[(s[0] >> 16) & 0xFF] << 16;
t[1] |= SboxIV[(s[3] >> 8) & 0xFF] << 8;
t[1] |= SboxIV[(s[2] >> 0) & 0xFF] << 0;
// t2
t[2] = SboxIV[(s[2] >> 24) & 0xFF] << 24;
t[2] |= SboxIV[(s[1] >> 16) & 0xFF] << 16;
t[2] |= SboxIV[(s[0] >> 8) & 0xFF] << 8;
t[2] |= SboxIV[(s[3] >> 0) & 0xFF] << 0;
// t3
t[3] = SboxIV[(s[3] >> 24) & 0xFF] << 24;
t[3] |= SboxIV[(s[2] >> 16) & 0xFF] << 16;
t[3] |= SboxIV[(s[1] >> 8) & 0xFF] << 8;
t[3] |= SboxIV[(s[0] >> 0) & 0xFF] << 0;
//------------AddRoundKey-------------
s[0] = t[0] ^ aes_key.dk[0];
s[1] = t[1] ^ aes_key.dk[1];
s[2] = t[2] ^ aes_key.dk[2];
s[3] = t[3] ^ aes_key.dk[3];
//-----------Store as BigEndian--------------
for (int i = 0; i < 4; i++) {
plaintext[4 * i + 0] = (s[i] >> 24) & 0xFF;
plaintext[4 * i + 1] = (s[i] >> 16) & 0xFF;
plaintext[4 * i + 2] = (s[i] >> 8) & 0xFF;
plaintext[4 * i + 3] = (s[i] >> 0) & 0xFF;
}
}
int main() {
AES_Key aes_key;
// 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17
// 18 19 1a 1b 1c 1d 1e 1f
uint8_t key[256 / 8] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f };
// 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
uint8_t plaintext[16] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff };
uint8_t ciphertext[16];
//-----------AES 128-------------------
int success = AES_KeyInit(key, &aes_key, 128);
if (success) {
printf("-----------AES 128-------------\n");
// 69 c4 e0 d8 6a 7b 04 30 d8 cd b7 80 70 b4 c5 5a
AES_Encrypt(plaintext, ciphertext, aes_key);
for (int i = 0; i < 16; i++) {
printf("%02x ", ciphertext[i]);
}
printf("\n");
// 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
AES_Decrypt(ciphertext, plaintext, aes_key);
for (int i = 0; i < 16; i++) {
printf("%02x ", plaintext[i]);
}
printf("\n");
AES_KeyDelete(aes_key);
}
//-----------AES 196-------------------
success = AES_KeyInit(key, &aes_key, 196);
if (success) {
printf("-----------AES 196-------------\n");
// dd a9 7c a4 86 4c df e0 6e af 70 a0 ec 0d 71 91
AES_Encrypt(plaintext, ciphertext, aes_key);
for (int i = 0; i < 16; i++) {
printf("%02x ", ciphertext[i]);
}
printf("\n");
// 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
AES_Decrypt(ciphertext, plaintext, aes_key);
for (int i = 0; i < 16; i++) {
printf("%02x ", plaintext[i]);
}
printf("\n");
AES_KeyDelete(aes_key);
}
//------------AES 256-----------------
success = AES_KeyInit(key, &aes_key, 256);
if (success) {
printf("-----------AES 256-------------\n");
// 8e a2 b7 ca 51 67 45 bf ea fc 49 90 4b 49 60 89
AES_Encrypt(plaintext, ciphertext, aes_key);
for (int i = 0; i < 16; i++) {
printf("%02x ", ciphertext[i]);
}
printf("\n");
// 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
AES_Decrypt(ciphertext, plaintext, aes_key);
for (int i = 0; i < 16; i++) {
printf("%02x ", plaintext[i]);
}
printf("\n");
AES_KeyDelete(aes_key);
}
return 0;
}

注意点:

加解密密钥在查表法aes中不同

逆向时拿到其中一个可以推导另一个

1
2
3
4
dk[0]  = ek[10];                     // 最后一轮密钥直接用
dk[10] = ek[0]; // 第一轮密钥直接用
dk[i] = InvMixColumns(ek[10 - i]); // 中间轮要过一次逆MixColumns

不需要研究透算法逻辑,只需要知道大致原理和遇到时能识别出这是aes变种就可以了

白盒aes

白盒aes算是在逆向中最常见到的了,特点是隐藏密钥。

查表法 AES 提供了“把轮操作变成查表”的思路,白盒 AES 则利用这个查表方法隐藏密钥,

白盒 AES 和 “查表法 AES” 算法是同一回事,可以把白盒AES看成查表AES的加强版(把密钥也混淆进表里了),一些密码学库会在 AES_init 的时候预处理并展开 key;如果 key 是字面量,还可能在编译时进行常量计算。这样编译后的可执行文件中就没有明文 key 了。

DFA攻击

白盒aes最主要的破解方式就是dfa攻击

我们需要的攻击条件:

原理:

在白盒攻击模型中,我们可以通过DBI工具(比如Frida),Debuggger(比如IDA),修改二进制文件本身 (SO patch)来实现对 中一个字节的更改,这可以称为引导、诱发一个错误。 因此差分故障攻击或差分错误攻击都是DFA合适的名字,下面修改明文中中第一个字节的值

首先是初始轮密钥加,错误限于这一个字节

DFA1

然后是第一轮的字节替换,错误限于这一个字节

DFA2

然后是第一轮的循环左移,因为是第一行,所以没动。

DFA3

然后是第一轮的列混淆步骤,结果的第m行第n列的值等于矩阵A的第m行的元素与矩阵B的第n列对应元素乘积之和,因此结果中第一列的每一个元素都受到矩阵B(即下图左边)第一列中每个元素的影响。因而,一个字节的错误被扩散到了一整列。或者说,正常情况和故障情况在第一轮列混淆结束后,有四个字节的值不同。

DFA4

然后是第一轮的轮密钥加,它只作用用当前字节,不会将差异扩散出去。

DFA5

可以看到,在一轮循环后,一个字节的故障,被扩散到了四个字节上。继续第二轮。
第二轮的字节替换

DFA6

第二轮的循环左移,需要注意到,虽然差异还是四个字节,但被扩散到不同的四列去了。

DFA7

第二轮的列混淆,每列存在的差异扩散到整列,这导致state的全部字节都与原先有差异。

DFA8

所以DFA攻击就是从第9轮攻击的行移位和列混淆中间更改1个数据,创造差错点,然后从最后拿到密文来分析故障结果(有四个差错点),由于aes一次可加密16个字节,所以可以得出16种不同的故障情况,那我们就可以通过数学间的关系,把密钥反解出来

攻击实现

在调试时更改第九轮对应字节即可,明文要求输入的话直接全输入\x00,在第9轮进行故障注入,假设正常明文(无故障)加密结果为0x8df4e9aac5c7573a27d8d055d6e4d64b

注入时把第一个字节改为0x10,第十轮结束后结果:

8d f4 e9 aa c5 c7 57 3a 27 d8 d0 55 d6 e4 d6 4b
da f4 e9 aa c5 c7 57 c9 27 d8 53 55 d6 37 d6 4b
确实有4个字节不一样。以此类推,得到16个不一样的带差错的密文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
daf4e9aac5c757c927d85355d637d64b
47f4e9aac5c7577d27d8a655d61ed64b
79f4e9aac5c7572a27d89855d62ad64b
30f4e9aac5c7570b27d86555d6a5d64b
8d7de9aac8c7573a27d8d09ed6e4be4b
8d5ce9aa43c7573a27d8d04cd6e4054b
8d0de9aaddc7573a27d8d060d6e4234b
8dabe9aacac7573a27d8d009d6e4484b
8df48caac598573a62d8d055d6e4d636
8df4bbaac5f4573acdd8d055d6e4d693
8df47aaac576573ac1d8d055d6e4d61c
8df444aac5c8573a23d8d055d6e4d6fb
8df4e9e0c5c7b73a2768d055ade4d64b
8df4e9f2c5c7063a27a4d055dfe4d64b
8df4e942c5c7793a275ed05535e4d64b
8df4e98fc5c7fa3a2778d055b3e4d64b

有了这个以后我们就可以还原得到第十轮的密钥了,这里使用phoenixAES工具,先安装:

1
pip install phoenixAES
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
import phoenixAES

with open('tracefile', 'wb') as t:
t.write("""
8df4e9aac5c7573a27d8d055d6e4d64b
daf4e9aac5c757c927d85355d637d64b
47f4e9aac5c7577d27d8a655d61ed64b
79f4e9aac5c7572a27d89855d62ad64b
30f4e9aac5c7570b27d86555d6a5d64b
8d7de9aac8c7573a27d8d09ed6e4be4b
8d5ce9aa43c7573a27d8d04cd6e4054b
8d0de9aaddc7573a27d8d060d6e4234b
8dabe9aacac7573a27d8d009d6e4484b
8df48caac598573a62d8d055d6e4d636
8df4bbaac5f4573acdd8d055d6e4d693
8df47aaac576573ac1d8d055d6e4d61c
8df444aac5c8573a23d8d055d6e4d6fb
8df4e9e0c5c7b73a2768d055ade4d64b
8df4e9f2c5c7063a27a4d055dfe4d64b
8df4e942c5c7793a275ed05535e4d64b
8df4e98fc5c7fa3a2778d055b3e4d64b
""".encode('utf8'))
phoenixAES.crack_file('tracefile', [], True, False, 3)

一共写入了17行数据到文件,其中第一行为正确的密文,剩余16行都是故障密文,最终通过crack_file即可得到第10轮密钥:

1
2
Last round key #N found:
D014F9A8C9EE2589E13F0CC8B6630CA6

还原最初密钥:

接下来用开头DFA攻击第三个工具里的aes_keyschedule.c,在本地编译后运行

1
./aes_keyschedule 5D432583B2AA833FC22D53130FDA904C 10

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
./aes_keyschedule D014F9A8C9EE2589E13F0CC8B6630CA6 10
K00: 2B7E151628AED2A6ABF7158809CF4F3C
K01: A0FAFE1788542CB123A339392A6C7605
K02: F2C295F27A96B9435935807A7359F67F
K03: 3D80477D4716FE3E1E237E446D7A883B
K04: EF44A541A8525B7FB671253BDB0BAD00
K05: D4D1C6F87C839D87CAF2B8BC11F915BC
K06: 6D88A37A110B3EFDDBF98641CA0093FD
K07: 4E54F70E5F5FC9F384A64FB24EA6DC4F
K08: EAD27321B58DBAD2312BF5607F8D292F
K09: AC7766F319FADC2128D12941575C006E
K10: D014F9A8C9EE2589E13F0CC8B6630CA6

即可得到密钥为2B7E151628AED2A6ABF7158809CF4F3C

最后附上白盒aes的实现代码,有兴趣的可以自己看一看GitHub - Nexus-TYF/Xiao-Lai-White-box-AES: A Xiao-Lai's white-box AES implementation.

Reference

https://zhuanlan.zhihu.com/p/42264499

找回消失的密钥 —- DFA分析白盒AES算法 - 奋飞安全

https://www.zskkk.cn/posts/15785/#%E8%BF%98%E5%8E%9F%E5%AF%86%E6%96%87

前言:

aes在逆向有很多应用,尤其是现在越来越多软件加密逻辑都选择aes,所以总结一下aes的算法和在ctf逆向中的考点

aes算法基础

aes结构

aes最重要的一个特征就是输入是128位分组,输出也是128位分组,但其中key分128位,192位,256位三种版本

AES 类型 密钥长度 轮数 (Nr) 密钥字数 (Nk)
AES-128 128 位 10 4
AES-192 192 位 12 6
AES-256 256 位 14 8

aes分组后16个字节是按矩阵方式排列

aes字节约定

加密总流程:

总结构

最终轮和前面9轮的区别是没有第三个步骤列混合

初始变换(Initial round)

初始变换就是输入的16个字节和密钥(不确定几位)进行密钥扩展后生成的(16位)异或的结果

展开为矩阵形式:

其中:

  • 为明文状态矩阵中第 个字节
  • 为轮密钥矩阵中第 个字节
  • 表示按字节异或(XOR)运算

循环运算:

字节代换:

字节代换

字节代换就是把第一步初始变换后的16字节矩阵块用s表代换,例如矩阵左上角的数是十六进制19,那就要代换成s盒中第1行第9列,查表可知是d4,以此类推

结果:

字节代换结果

行移位:

ShiftRows 操作在状态矩阵上进行,规则如下:

  • 第 0 行不变
  • 第 1 行循环左移 1 字节
  • 第 2 行循环左移 2 字节
  • 第 3 行循环左移 3 字节

原始状态矩阵:

经过行移位后:

每一行的移位规律如下:

行号 移位字节数 移位方向
0 0 不变
1 1 左移
2 2 左移
3 3 左移

列混淆:

列混淆

左乘一个确定的矩阵,但是这里的乘法不是普通乘法

列混淆中的乘法

其中乘法在有限域 上进行:

  • 表示按字节左移一位(若最高位为 1,则再与 0x1B 异或)

注:与0x1B异或是因为要构造有限域构造出来的多项式把模之后的结果,学逆向不用学那么深,只需要知道aes的加密解密和漏洞攻击就可以了

轮密钥加:

AddRoundKey 是 AES 每一轮中最简单但最关键的操作之一。
它将 状态矩阵 (State)轮密钥矩阵 (RoundKey) 按字节异或(XOR):

设状态矩阵为:

轮密钥矩阵为:

异或后得到:

按字节运算公式

每个字节的计算方式为:

密钥扩展:

初始只有128/192/256位的密钥是怎么更新的呢,这就涉及到密钥扩展

这是初始状态,这里以128位密钥为例,先全部填入前4列,设第5列为Wi

密钥扩展初始

  1. 初始部分:
  1. 递推部分:(适用于所有aes算法)

对于

函数定义

RotWord:
循环左移 1 字节

字循环

SubWord:
对 4 个字节分别进行 S-box 替代

Rcon:
轮常数,仅作用于字的第一个字节:

其中









最终结果

密钥最终结果

解密方式

就是把上述过程反过来一遍(解密的第一轮没有列混合逆向)

加解密

其中的轮密钥加只需要每轮相同状态的W[i,i+3]就可以

逆列混淆

逆列混淆

逆行移位:

就是逆向移位就可以,很简单

逆s表

逆s表

查表即可

逆向题型:

普通AES

找到密钥,找到密文,逆向脚本,进行解密。

加解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)

# inv_s_box = tuple([s_box.index(i) for i in range(256)])
inv_s_box = (
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)


def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]


def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]


def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]


def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]

def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]


# learned from https://web.archive.org/web/20100626212235/http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)


def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)


def mix_columns(s):
for i in range(4):
mix_single_column(s[i])


def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v

mix_columns(s)


r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)


def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]

def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return bytes(sum(matrix, []))

def xor_bytes(a, b):
""" Returns a new byte array with the elements xor'ed. """
return bytes(i^j for i, j in zip(a, b))

def inc_bytes(a):
""" Returns a new byte array with the value increment by 1 """
out = list(a)
for i in reversed(range(len(out))):
if out[i] == 0xFF:
out[i] = 0
else:
out[i] += 1
break
return bytes(out)

def pad(plaintext):
"""
Pads the given plaintext with PKCS#7 padding to a multiple of 16 bytes.
Note that if the plaintext size is a multiple of 16,
a whole block will be added.
"""
padding_len = 16 - (len(plaintext) % 16)
padding = bytes([padding_len] * padding_len)
return plaintext + padding

def unpad(plaintext):
"""
Removes a PKCS#7 padding, returning the unpadded text and ensuring the
padding was correct.
"""
padding_len = plaintext[-1]
assert padding_len > 0
message, padding = plaintext[:-padding_len], plaintext[-padding_len:]
assert all(p == padding_len for p in padding)
return message

def split_blocks(message, block_size=16, require_padding=True):
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]


class AES:
"""
Class for AES-128 encryption with CBC mode and PKCS#7.

This is a raw implementation of AES, without key stretching or IV
management. Unless you need that, please use `encrypt` and `decrypt`.
"""
rounds_by_key_size = {16: 10, 24: 12, 32: 14}
def __init__(self, master_key):
"""
Initializes the object with a given key.
"""
assert len(master_key) in AES.rounds_by_key_size
self.n_rounds = AES.rounds_by_key_size[len(master_key)]
self._key_matrices = self._expand_key(master_key)

def _expand_key(self, master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4

i = 1
# expand round: (rounds+1)*4
while len(key_columns) < (self.n_rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])

# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]

# XOR with equivalent word from previous iteration.
word = xor_bytes(word, key_columns[-iteration_size])
key_columns.append(list(word))

# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]

def encrypt_ecb(self, ciphertext):
assert len(ciphertext) >= 16
assert len(ciphertext) % 16 == 0

result = b''
for i in range(0, len(ciphertext), 16):
result += self.encrypt_ecb_block(ciphertext[i:i+16])
return result

def encrypt_ecb_block(self, plaintext):
"""
Encrypts a single block of 16 byte long plaintext.
"""
assert len(plaintext) == 16

plain_state = bytes2matrix(plaintext)

add_round_key(plain_state, self._key_matrices[0])

for i in range(1, self.n_rounds):
sub_bytes(plain_state)
shift_rows(plain_state)
mix_columns(plain_state)
add_round_key(plain_state, self._key_matrices[i])

sub_bytes(plain_state)
shift_rows(plain_state)
add_round_key(plain_state, self._key_matrices[-1])

return matrix2bytes(plain_state)

def decrypt_ecb(self, ciphertext):
assert len(ciphertext) >= 16
assert len(ciphertext) % 16 == 0

result = b''
for i in range(0, len(ciphertext), 16):
result += self.decrypt_ecb_block(ciphertext[i:i+16])
return result


def decrypt_ecb_block(self, ciphertext):
"""
Decrypts a single block of 16 byte long ciphertext.
"""
assert len(ciphertext) == 16

cipher_state = bytes2matrix(ciphertext)

add_round_key(cipher_state, self._key_matrices[-1])
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)

for i in range(self.n_rounds - 1, 0, -1):
add_round_key(cipher_state, self._key_matrices[i])
inv_mix_columns(cipher_state)
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)

add_round_key(cipher_state, self._key_matrices[0])

return matrix2bytes(cipher_state)

def encrypt_cbc(self, plaintext, iv):
"""
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
assert len(iv) == 16

plaintext = pad(plaintext)

blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext):
# CBC mode encrypt: encrypt(plaintext_block XOR previous)
block = self.encrypt_ecb_block(xor_bytes(plaintext_block, previous))
blocks.append(block)
previous = block

return b''.join(blocks)

def decrypt_cbc(self, ciphertext, iv):
"""
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
assert len(iv) == 16

blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
# CBC mode decrypt: previous XOR decrypt(ciphertext)
blocks.append(xor_bytes(previous, self.decrypt_ecb_block(ciphertext_block)))
previous = ciphertext_block

return unpad(b''.join(blocks))

def encrypt_pcbc(self, plaintext, iv):
"""
Encrypts `plaintext` using PCBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
assert len(iv) == 16

plaintext = pad(plaintext)

blocks = []
prev_ciphertext = iv
prev_plaintext = bytes(16)
for plaintext_block in split_blocks(plaintext):
# PCBC mode encrypt: encrypt(plaintext_block XOR (prev_ciphertext XOR prev_plaintext))
ciphertext_block = self.encrypt_ecb_block(xor_bytes(plaintext_block, xor_bytes(prev_ciphertext, prev_plaintext)))
blocks.append(ciphertext_block)
prev_ciphertext = ciphertext_block
prev_plaintext = plaintext_block

return b''.join(blocks)

def decrypt_pcbc(self, ciphertext, iv):
"""
Decrypts `ciphertext` using PCBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
assert len(iv) == 16

blocks = []
prev_ciphertext = iv
prev_plaintext = bytes(16)
for ciphertext_block in split_blocks(ciphertext):
# PCBC mode decrypt: (prev_plaintext XOR prev_ciphertext) XOR decrypt(ciphertext_block)
plaintext_block = xor_bytes(xor_bytes(prev_ciphertext, prev_plaintext), self.decrypt_ecb_block(ciphertext_block))
blocks.append(plaintext_block)
prev_ciphertext = ciphertext_block
prev_plaintext = plaintext_block

return unpad(b''.join(blocks))

def encrypt_cfb(self, plaintext, iv):
"""
Encrypts `plaintext` with the given initialization vector (iv).
"""
assert len(iv) == 16

blocks = []
prev_ciphertext = iv
for plaintext_block in split_blocks(plaintext, require_padding=False):
# CFB mode encrypt: plaintext_block XOR encrypt(prev_ciphertext)
ciphertext_block = xor_bytes(plaintext_block, self.encrypt_ecb_block(prev_ciphertext))
blocks.append(ciphertext_block)
prev_ciphertext = ciphertext_block

return b''.join(blocks)

def decrypt_cfb(self, ciphertext, iv):
"""
Decrypts `ciphertext` with the given initialization vector (iv).
"""
assert len(iv) == 16

blocks = []
prev_ciphertext = iv
for ciphertext_block in split_blocks(ciphertext, require_padding=False):
# CFB mode decrypt: ciphertext XOR decrypt(prev_ciphertext)
plaintext_block = xor_bytes(ciphertext_block, self.encrypt_ecb_block(prev_ciphertext))
blocks.append(plaintext_block)
prev_ciphertext = ciphertext_block

return b''.join(blocks)

def encrypt_ofb(self, plaintext, iv):
"""
Encrypts `plaintext` using OFB mode initialization vector (iv).
"""
assert len(iv) == 16

blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext, require_padding=False):
# OFB mode encrypt: plaintext_block XOR encrypt(previous)
block = self.encrypt_ecb_block(previous)
ciphertext_block = xor_bytes(plaintext_block, block)
blocks.append(ciphertext_block)
previous = block

return b''.join(blocks)

def decrypt_ofb(self, ciphertext, iv):
"""
Decrypts `ciphertext` using OFB mode initialization vector (iv).
"""
assert len(iv) == 16

blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext, require_padding=False):
# OFB mode decrypt: ciphertext XOR encrypt(previous)
block = self.encrypt_ecb_block(previous)
plaintext_block = xor_bytes(ciphertext_block, block)
blocks.append(plaintext_block)
previous = block

return b''.join(blocks)

def encrypt_ctr(self, plaintext, iv):
"""
Encrypts `plaintext` using CTR mode with the given nounce/IV.
"""
assert len(iv) == 16

blocks = []
nonce = iv
for plaintext_block in split_blocks(plaintext, require_padding=False):
# CTR mode encrypt: plaintext_block XOR encrypt(nonce)
block = xor_bytes(plaintext_block, self.encrypt_ecb_block(nonce))
blocks.append(block)
nonce = inc_bytes(nonce)

return b''.join(blocks)

def decrypt_ctr(self, ciphertext, iv):
"""
Decrypts `ciphertext` using CTR mode with the given nounce/IV.
"""
assert len(iv) == 16

blocks = []
nonce = iv
for ciphertext_block in split_blocks(ciphertext, require_padding=False):
# CTR mode decrypt: ciphertext XOR encrypt(nonce)
block = xor_bytes(ciphertext_block, self.encrypt_ecb_block(nonce))
blocks.append(block)
nonce = inc_bytes(nonce)

return b''.join(blocks)


def AES_ecb_encrypt(data: bytes, key: bytes):
a = AES(key)
return a.encrypt_ecb(data)

def AES_ecb_decrypt(data: bytes, key: bytes):
a = AES(key)
return a.decrypt_ecb(data)

def AES_cbc_encrypt(data: bytes, key: bytes, iv: bytes):
a = AES(key)
return a.encrypt_cbc(data, iv)

def AES_cbc_decrypt(data: bytes, key: bytes, iv: bytes):
a = AES(key)
return a.decrypt_cbc(data, iv)

白盒AES

项目 普通 AES(黑盒) 白盒 AES
密钥 独立变量、明确定义 被混入查表中,不可直接访问
算法结构 明确的五步(SubBytes 等) 各步骤混淆成查表和线性映射
可移植性 高(用同样的密钥随处运行) 低(查表与密钥绑定)
安全假设 攻击者看不到内部 攻击者能看到全部

目前市面上大多数app都是基于白盒aes开发的,还有查表法实现的aes真的严格按照上面讲的aes流程走的很少,白盒aes东西太多了,而且还有很多攻击手法,就放在”逆向中的AES(二)“里讲好了

参考资料:

【AES加密算法】| AES加密过程详解| 对称加密| Rijndael-128| 密码学| 信息安全_哔哩哔哩_bilibili

密码学——AES/DES加密算法原理介绍 - 枫のBlog

分组密码工作模式

工作模式对算法本身结构没有影响,影响的是明文密文

ECB:电子密码本模式(electronic codebook mode)

ecb

从ECB的工作原理可以看出,如果明文数据在等分后,两块数据相同则会产生相同的加密数据块,这会辅助攻击者快速判断加密算法的工作模式,而将攻击资源聚集在破解某一块数据即可,一旦成功则意味着全文破解,大大提升了攻击效率。

CBC:密码分组链接模式(cipher block chaining Triple)

cbc

cbc的解密

dcbc

CBC模式相比ECB实现了更好的模式隐藏,但因为其将密文引入运算,加解密操作无法并行操作。同时引入的IV向量,并且还需要加、解密双方共同知晓方可。

CFB:密文反馈模式(Cipher FeedBack)

cfb

与CBC模式类似,但不同的地方在于,CFB模式先生成密码流字典,然后用密码字典与明文进行异或操作并最终生成密文。后一分组的密码字典的生成需要前一分组的密文参与运算。

cfb结构

其中s位可任意,不同s位加密结果不同,默认s是算法块规定长度,例des是64,aes是128

OFB:输出反馈模式(Output Feedbaek)

ofb

OFB和CFB一样,明文块可自定义长度

CTR:计数器模式(counter mode)

ctr

加密模式总结:

模式 全称 优点 缺点 是否需 IV 是否可并行加密 是否可并行解密 是否适合流加密
ECB Electronic Codebook 实现简单;可并行加解密 同块明文→同块密文,易被模式识别(最不安全)
CBC Cipher Block Chaining 同块明文不同IV→不同密文;常用于文件加密 无法并行加密;需填充;IV重用会泄密
CFB Cipher Feedback 不需填充;可加密任意长度数据;适合流式 错误传播严重;速度略慢
OFB Output Feedback 不需填充;错误不会传播;适合流加密 同IV下重用密钥极危险;同步要求高
CTR Counter 可随机访问块;可并行加解密;性能优 计数器不能重用,否则致命泄密 是(计数器)

加密模式攻击

CBC反转字节攻击:

已知密文,和明文。可以在不知道key的情况下,肆意更改明文的值,比如网站验证权限,可以把传进去的密文修改,从而使明文从’user’到’admin’,可能能绕过权限

设A是第N-1块的密文一个字节,B是第N块密文解密后的中间值的对应部分字节,C是第N块明文对应字节,X是想要修改的字节值

公式右边是明文变化,左边括号内是输入密文的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from Crypto.Cipher import AES
import uuid
import binascii


BS=AES.block_size #分组长度
key=b'test' #密钥
iv=uuid.uuid4().bytes #随机初始向量
pad=lambda s: s+((BS-len(s)%BS)*chr(BS-len(s)%BS)).encode() #Pkcs5Padding
data=b'1234567890abcdefabcdef1234567890' #明文M

#加密
def enc(data):
aes=AES.new(pad(key),AES.MODE_CBC,iv)
ciphertext=aes.encrypt(pad(data))
ciphertext=binascii.b2a_hex(ciphertext)
return ciphertext

#解密
def dec(c):
c=binascii.a2b_hex(c)
aes=AES.new(pad(key),AES.MODE_CBC,iv)
data=aes.decrypt(c)
return data

#测试CBC翻转
def CBC_test(c):
c=bytearray(binascii.a2b_hex(c))
c[0]=c[0]^ord('a')^ord('A') #c[0]为第一组的密文字符,a为第二组相应位置的明文字符,A是我们想要的明文字符
c=binascii.b2a_hex(c)
return c

print("ciphertext:",enc(data))
print("data:",dec(enc(data)))
print("CBC Attack:",dec(CBC_test(enc(data))))

1
2
3
ciphertext: b'ffa645d1b5e40afbbae47de053a66f978fa0a824e99864a7e8baf38ceccda613c304883f11fc0857c1bb7603f859798e'
data: b'1234567890abcdefabcdef1234567890\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
CBC Attack:b':8O<\xe7\x04\xd8v\xe8Q\xfe\xa5I\xc9c]Abcdef1234567890\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'

Padding Oracle Attack

已知条件:如果明文的padding格式出错服务端会提示某个特定状态码,密文,iv(IV经常会随着密文一起发送。常见的做法是将IV作为一个前缀,附着在密文的前面)

效果:在不清楚 key 的前提下解密任意给定的密文。

poa

原理:

我们有密文,解密后的中间状态(Intermediary Value)我们不知道,爆破每个字节的iv值,当服务端不报错时,说明padding正确,如上图所示,我们知道爆破的第8个字节,就肯定直接padding为0x1,也知道我们此时爆破的iv是多少,就可以推出正确的中间状态(Intermediary Value)是多少,依次类推,把所有字节的中间状态都算出来后,就可以用初始iv异或中间状态得到明文。

如果已知多组密文解密:

从前往后进行解密

如果已知多组明文加密:

先从最后一组开始,爆破最后一组的intermediary并构造出iv,然后将本组的iv当作前一组的密文,以此类推。由此我们可以得到构造密文的步骤

  1. 从最后一组开始,爆破出该组的intermediary并构造出iv,然后将本组的iv当作前一组的密文
  2. 爆破前一组的intermediary并构造出iv,然后将本组的iv当作前一组的密文
  3. 最后会得到第一组的iv,至此我们已经构造出了所有合法密文以及iv

reference:

CBC字节翻转攻击&Padding Oracle Attack原理解析 - 枫のBlog