April 19, 2022

关于逆向工程核心原理-代码注入

(简称逆向工程第二十七章学习)

代码注入

代码注入是一种向目标进程插入独立运行代码并使之运行的技术,一般调用CreateRemoteThread() API远程线程形式运行插入代码(万能的CreateRemoteThread

所以也被称为线程注入

注意,代码注入以线程过程形式注入,而代码中的数据则以线程参数传入,也就是说代码和数据分别注入

使用代码注入的原因

  1. 占用内存少
  2. 难以查找痕迹(比如恶意代码中就喜欢使用
  3. DLL注入技术主要用于代码量大且复杂的时候,而代码注入则适合用于代码量小且简单

实验环节

填上进程号,注进去咯

image-20220419211655928

审计代码

开审!

main

把PID作为参数传入InjectionCode函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc, char *argv[])
{
DWORD dwPID = 0;

if( argc != 2 )
{
printf("\n USAGE : %s <pid>\n", argv[0]);
return 1;
}

// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;

// code injection
dwPID = (DWORD)atol(argv[1]);
InjectCode(dwPID);

return 0;
}

ThreadProc

  1. 可以先不传递LoadLibrary和GetProcAddress的地址,直接传递MessageBoxA地址也可以

​ 但原则上是要的这两个API,这种方式好处在于可以把相关库准确加载到指定进制

  1. ThreadPrco函数是可以独立运行的代码不是直接引用的硬编码地址
  2. 同时要注意Release/Debug代码所产生的优化是不同的(早已深熟于心
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
// 这个结构体以线程参数的形式传递使用
typedef struct _THREAD_PARAM
{
FARPROC pFunc[2]; // LoadLibraryA(), GetProcAddress()
char szBuf[4][128]; // "user32.dll", "MessageBoxA", "www.reversecore.com", "ReverseCore"
} THREAD_PARAM, *PTHREAD_PARAM;

// LoadLibrary暂时函数原型?
typedef HMODULE (WINAPI *PFLOADLIBRARYA)
(
LPCSTR lpLibFileName
);

// GetProcAddress暂时函数原?
typedef FARPROC (WINAPI *PFGETPROCADDRESS)
(
HMODULE hModule,
LPCSTR lpProcName
);

// MessageBoxA暂时函数原型?
typedef int (WINAPI *PFMESSAGEBOXA)
(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
);

DWORD WINAPI ThreadProc(LPVOID lParam)
{
PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
HMODULE hMod = NULL;
FARPROC pFunc = NULL;

// hMod = LoadLibrary("user32.dll")
hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]); // "user32.dll"
if( !hMod )
return 1;

// pFunc = GetProcAddress(hMod, "MessageBoxA")
pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]); // "MessageBoxA"
if( !pFunc )
return 1;

// MessageBoxA(NULL, "P.Z!", "PPPPZ.NET", MB_OK)
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

return 0;
}

InjectionCode

  1. 先向目标进程写入所需的参数param
  2. 再写入要执行的代码ThreadProc(注意这个大小是两个函数地址相见)
  3. 再使用CreateRemoteThread远程执行ThreadProc 参数是param
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
BOOL InjectCode(DWORD dwPID)
{
HMODULE hMod = NULL;
THREAD_PARAM param = {0,};
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID pRemoteBuf[2] = {0,};
DWORD dwSize = 0;

// 加载指定库返回句柄
hMod = GetModuleHandleA("kernel32.dll");

// 通过库的句柄获取库中的地址获取指定API的值
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
// 放入结构库的必要参数
strcpy_s(param.szBuf[0], "user32.dll");
strcpy_s(param.szBuf[1], "MessageBoxA");
strcpy_s(param.szBuf[2], "www.reversecore.com");
strcpy_s(param.szBuf[3], "ReverseCore");

// 通过进程PID找到进程句柄
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, // dwDesiredAccess
FALSE, // bInheritHandle
dwPID)) ) // dwProcessId
{
printf("OpenProcess() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// 算出结构体的大小
dwSize = sizeof(THREAD_PARAM);
// 开指定进程的指定大小的内存空间,经典VirtualAllocEx开申请内存空间
if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
dwSize, // dwSize
MEM_COMMIT, // flAllocationType
PAGE_READWRITE)) ) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// 在指定进程hProcess的 申请了空间的地址pRemoteBuf 写入数据param 还有大小dwSize
if( !WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[0], // lpBaseAddress
(LPVOID)&param, // lpBuffer
dwSize, // nSize
NULL) ) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// 这边是算出ThreadProc函数大小
// 在MS Visual C++使用C++的Release模式编译程序后,源代码中的函数顺序与二进制代码一样
// 又函数名称就是函数地址,所以InjectionCode - ThreadProc做减法运算,就是ThreadProc函数的大小
dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
// 在指定进程开了个ThreadProc函数大小的内存空间
if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, // hProcess
NULL, // lpAddress
dwSize, // dwSize
MEM_COMMIT, // flAllocationType
PAGE_EXECUTE_READWRITE)) ) // flProtect
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// 将线程函数写入目标进程
if( !WriteProcessMemory(hProcess, // hProcess
pRemoteBuf[1], // lpBaseAddress
(LPVOID)ThreadProc, // lpBuffer
dwSize, // nSize
NULL) ) // [out] lpNumberOfBytesWritten
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// 这里在指定线程 远程执行了pRemoteBuf[1] 参数是pRemoteBuf[0]
if( !(hThread = CreateRemoteThread(hProcess, // hProcess
NULL, // lpThreadAttributes
0, // dwStackSize
(LPTHREAD_START_ROUTINE)pRemoteBuf[1], // dwStackSize
pRemoteBuf[0], // lpParameter
0, // dwCreationFlags
NULL)) ) // lpThreadId
{
printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

Debug InjectionCode

先拖入notepad,然后运行起来,再开启这个如果有新线程就停在线程函数开始的代码位置

image-20220419232051664

再代码注入我们的notepad

image-20220419232219281

一注入就会成功停在线程代码开始处(有时候只是调试器到这,EIP没到这,只要F9几次让EIP到这即可

image-20220419232635947

P.Z!

image-20220419232726473

今天录了*CTF的前两题,难得录的快了一次,要加油把后面的题也录了,今天这里的代码审起来舒服多了,也可能是今天的API都是见过的,比一开始一个API查一次做一下笔记的快多了,好耶!–4.19 PM 11:28

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.