逆核系列No.12--汇编代码注入

本节我们依旧探讨代码注入(CodeInject),相比先前代码注入的篇章,不同的是,这次我们借助OllyDbg的汇编工程,注入的代码为汇编指令字节码的形式写入我们的代码。

汇编代码编写

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
004010ED    55               PUSH EBP
004010EE 8BEC MOV EBP,ESP
004010F0 8B75 08 MOV ESI,DWORD PTR SS:[EBP+8] ; ESI = pParam 使用注入器注入目标进程会获取
004010F3 68 6C6C0000 PUSH 6C6C
004010F8 68 33322E64 PUSH 642E3233
004010FD 68 75736572 PUSH 72657375
00401102 54 PUSH ESP ; - "user32.dll"
00401103 FF16 CALL DWORD PTR DS:[ESI] ; DS:[esi]中是API地址 LoadLibraryA("user32.dll")
00401105 68 6F784100 PUSH 41786F
0040110A 68 61676542 PUSH 42656761
0040110F 68 4D657373 PUSH 7373654D
00401114 54 PUSH ESP ; - "MessageBoxA"
00401115 50 PUSH EAX ; - hMod
00401116 FF56 04 CALL DWORD PTR DS:[ESI+4] ; GetProcAddress(hMod, "MessageBoxA")
00401119 6A 00 PUSH 0 ; - MB_OK (0)
0040111B E8 0C000000 CALL 0040112C
00401120 <ASCII> ; - "ReverseCore", 0
0040112C E8 14000000 CALL 00401145
00401131 <ASCII> ; - "www.reversecore.com", 0
00401145 6A 00 PUSH 0 ; - hWnd (0)
00401147 FFD0 CALL EAX ; MessageBoxA(0, "www.reversecore.com", "ReverseCore", 0)
00401149 33C0 XOR EAX,EAX
0040114B 8BE5 MOV ESP,EBP
0040114D 5D POP EBP
0040114E C3 RETN

其实代码的完成的功能是上节的ThreadProc的函数功能,为了方便理解,在po一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DWORD WINAPI ThreadProc(LPVOID lParam)
{
PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
HMODULE hMod = NULL;
FARPROC pFunc = NULL;

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

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

// MessageBoxA()
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

return 0;
}

其中在函数体内部用到的一些字符串以及kernel32的API都是设置好的,并且由结构体lParam提交的。

回到汇编代码的编写,这里对重要的进行说明:

1
004010F0    8B75 08          MOV ESI,DWORD PTR SS:[EBP+8]

这里是由函数调用决定的,可以知道函数ThreadProc 的参数是通过栈传递的,且只有一个参数,因此通过访问SS:[EBP+8]就是参数lParam的地址了。lParam参数如下:

1
2
3
4
5
6
7
8
hMod = GetModuleHandleA("kernel32.dll");		
// set THREAD_PARAM
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");
1
2
3
4
5
004010F3    68 6C6C0000      PUSH 6C6C                      
004010F8 68 33322E64 PUSH 642E3233
004010FD 68 75736572 PUSH 72657375
00401102 54 PUSH ESP ; - "user32.dll"
00401103 FF16 CALL DWORD PTR DS:[ESI]

代码前四行完成的是将构造出字符串user32.dll,由于是入栈的操作,因此当前的esp指向的是字符串user32.dll的首地址,此时CALL DWORD PTR DS:[ESI]此时会完成LoadLibraryA(user32.dll)

此时,Eax存放的是user32.dll的句柄。

1
2
3
4
5
6
00401105    68 6F784100      PUSH 41786F
0040110A 68 61676542 PUSH 42656761
0040110F 68 4D657373 PUSH 7373654D
00401114 54 PUSH ESP ; - "MessageBoxA"
00401115 50 PUSH EAX ; - hMod
00401116 FF56 04 CALL DWORD PTR DS:[ESI+4]

同样的手法,安排栈中元素,设置esp指向将要使用的字符串MessageBoxA首地址,再传入先前LoadLibraryA(user32.dll)获取到的user32.dll的句柄(存放在EAX中)。随后调用DS:[ESI+4] ,这里取到的是传递给ThreadProc的参数结构体中的GetProcAddress,所以上述的汇编代码完成的GetProcAddress(hMod, "MessageBoxA")获取到user32.MessageBoxA API的地址。

PS:这里可以好好学习通过push操作,在栈中放置参数的手法。

经过上述的代码后,EAX存放的是user32.MessageBoxA API的地址。接下来就该调用了。

1
2
3
4
5
6
7
00401119    6A 00            PUSH 0                             ; - MB_OK (0)
0040111B E8 0C000000 CALL 0040112C
00401120 <ASCII> ; - "ReverseCore", 0
0040112C E8 14000000 CALL 00401145
00401131 <ASCII> ; - "www.reversecore.com", 0
00401145 6A 00 PUSH 0 ; - hWnd (0)
00401147 FFD0 CALL EAX

这里初次看到时感觉设置参数的方式很惊艳,是通过call指令完成参数入栈的,原理如何?

需要了解call指令的运作细节,call指令会将下一条指令的地址进行入栈(十分关键)。然后跑去执行call的主体,随后当call主体进行retn时,将先前入栈的指令地址接着往下的执行流程。

0040111B处执行了CALL 0040112C,此时会将下一个“指令”地址进行入栈,但由于下一条“指令”并非指令,而是设置好的字符串内容,此时的入栈相当于MessageBoxA参数字符串ReverseCore入栈了。

随后会跳向call的主体内容的位置0040112C,然后系统又发现了一个call指令,同样的操作,将MessageBoxA参数又一字符串www.reversecore.com入栈了。当然这里观察call的地址会发现猫腻,CALL 00401145,直接往下就是正常执行了,最后调用CALL EAX(MessageBoxA)

至此,需要的汇编指令编写完毕,只需在调用这段代码前,保证参数lParam入栈即可。

CodeInject2源码分析

在开始分析CodeInject2源码前,需要做一些准备工作。

编写完上述代码,我们只需要保存他的字节码即可

获取字节码

OD随意载入一个不需要的程序(这里我复制了一份notepad.exe),

敲完ThreadProc的汇编代码之后,选择复制到可执行文件,在接下来的窗口中保存:

保存修改到ThreadProc.exe,在使用OD打开。

在数据窗口跟踪这些指令,然后完整的复制到文件ThreadProc.txt文件中

去除不必要的内容,地址,注释部分,并在每个字节前面加0x,得到字节码

1
2
3
4
5
6
7
8
9
10
0x55, 0x8B, 0xEC, 0x8B, 0x75, 0x08, 0x68, 0x6C, 0x6C, 0x00,
0x00, 0x68, 0x33, 0x32, 0x2E, 0x64, 0x68, 0x75, 0x73, 0x65,
0x72, 0x54, 0xFF, 0x16, 0x68, 0x6F, 0x78, 0x41, 0x00, 0x68,
0x61, 0x67, 0x65, 0x42, 0x68, 0x4D, 0x65, 0x73, 0x73, 0x54,
0x50, 0xFF, 0x56, 0x04, 0x6A, 0x00, 0xE8, 0x0C, 0x00, 0x00,
0x00, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6F,
0x72, 0x65, 0x00, 0xE8, 0x14, 0x00, 0x00, 0x00, 0x77, 0x77,
0x77, 0x2E, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x63,
0x6F, 0x72, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x6A, 0x00,
0xFF, 0xD0, 0x33, 0xC0, 0x8B, 0xE5, 0x5D, 0xC3

在保证参数lParam入栈的情况下,这段代码是可以直接上CPU运行的。

CodeInject2.main
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;
}

和先前文章的一致,不做赘述。

关注InjectCode函数即可:

CodeInject2.InjectCode
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
BOOL InjectCode(DWORD dwPID)
{
HMODULE hMod = NULL;
THREAD_PARAM param = {0,};
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID pRemoteBuf[2] = {0,};

hMod = GetModuleHandleA("kernel32.dll");

// set THREAD_PARAM
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

// Open Process
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE, dwPID)) )
{
printf("OpenProcess() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// Allocation for THREAD_PARAM
if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, sizeof(THREAD_PARAM), MEM_COMMIT, PAGE_READWRITE)) )
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !WriteProcessMemory(hProcess, pRemoteBuf[0],
(LPVOID)&param, sizeof(THREAD_PARAM), NULL) )
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

// Allocation for ThreadProc()
if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, sizeof(g_InjectionCode),
MEM_COMMIT, PAGE_EXECUTE_READWRITE)) )
{
printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)&g_InjectionCode,
sizeof(g_InjectionCode), NULL) )
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

if( !(hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1],pRemoteBuf[0], 0, NULL)) )
{
printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}

各位读者回想一下,关于ThreadProc的参数部分是一样的,但ThreadProc代码的主体部分在上节我们是怎么操作的?我们是在开辟出需要的空间(需要计算,这一点依赖于原代码的顺序在编译后顺序保持一致的特性,这带来了诸多不便,使得不通用)之后写入的内容也是依赖于这一点的。

在使用汇编字节码注入中,我们是这样进行的:

1
2
3
4
5
6
if( !WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)&g_InjectionCode,
sizeof(g_InjectionCode), NULL) )
{
printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
return FALSE;
}

其中g_InjectionCode是字节码部分,在CodeInject2源码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
BYTE g_InjectionCode[] = 
{
0x55, 0x8B, 0xEC, 0x8B, 0x75, 0x08, 0x68, 0x6C, 0x6C, 0x00,
0x00, 0x68, 0x33, 0x32, 0x2E, 0x64, 0x68, 0x75, 0x73, 0x65,
0x72, 0x54, 0xFF, 0x16, 0x68, 0x6F, 0x78, 0x41, 0x00, 0x68,
0x61, 0x67, 0x65, 0x42, 0x68, 0x4D, 0x65, 0x73, 0x73, 0x54,
0x50, 0xFF, 0x56, 0x04, 0x6A, 0x00, 0xE8, 0x0C, 0x00, 0x00,
0x00, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6F,
0x72, 0x65, 0x00, 0xE8, 0x14, 0x00, 0x00, 0x00, 0x77, 0x77,
0x77, 0x2E, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x63,
0x6F, 0x72, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x6A, 0x00,
0xFF, 0xD0, 0x33, 0xC0, 0x8B, 0xE5, 0x5D, 0xC3
};

可以看到,写入的size大小是独立计算的,写入的代码也是独立的。(不需要依赖于编译后代码先后顺序与源代码一致的特性)。

启动CodeInject2.exe并输入notepad.exe的PID,后续都是自动完成的。

回顾

本节关于参数设定,由两个很有趣的点值的学习,一个是通过push入栈,结束了将当前栈顶地址保存出来。

第二个是使用call完成参数的设置。

Author: Victory+
Link: https://cvjark.github.io/2022/05/07/逆核系列No-12-汇编代码注入/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.