一次简单的dll注入,学到的还挺多的。
题目来源于合天lab
2.2.1 OpenProcess(processthreadsapi.h)
2.2.2 VirtualAllocEx(memoryapi.h)
2.2.3 WriteProcessMemory(memoryapi.h)
5.1 DllMain()函数及imhacker.cpp文件
动态链接库(英文全称Dynamic-Link-Library,缩写DLL),是在微软Windows操作系统中实现共享函数库概念的一种方式。
这些库函数的扩展名是 ”.dll"、".ocx"(包含ActiveX控制的库)或者 ".drv"(旧式的系统驱动程序)
Dll注入指的是向运行中的进程强制插入特定的DLL文件。从技术细节来说,DLL注入命令让其他进程自行调用LoadLibrary() API,加载(loading)用户指定的DLL文件。DLL注入与一般的DLL加载的区别在于,加载的目标进程是其自身或其他进程。DLL注入是渗透其他进程的最简单有效的方法。
举一个简单的例子。
DLL注入
Imhacker.dll已被强制插入notepad进程(本来notepad并不会加载imhacker.dll)。加载到notepad.exe进程中的imhacker.dll与已经加载到motepad.exe进程中的DLL(kernel32.dll、user32.dll)一样,拥有访问notepad.exe进程内存的(正当的)权限,这样用户就可以做任何想做的事了(比如:向notepad添加通信功能以实现Messenger、文本网络浏览器等)。
由此可见,所谓的dll注入正是让进程A强行加载程序B给定的a.dll,并执行程序B给定的a.dll里面的代码,从而达到B进程控制A进程的目的。
程序B所给定的a.dll原先并不会被程序A主动加载,但是当程序B通过某种手段让程序A“加载”a.dll后, 程序A将会执行a.dll里的代码,此时,a.dll就进入了程序A的地址空间,而a.dll模块的程序逻辑由程序B的开发者设计, 因此程序B的开发者可以对程序A为所欲为。
DLL被加载到进程后会自动运行DllMain() 函数,用户可以把想执行的代码放到DllMain() 函数,每当加载DLL时,添加的代码就会自然而然得到执行。利用该特性可以修复程序Bug,或向程序添加新功能。
向某个进程注入DLL时主要使用以下三种方法:
- 创建远程线程(CreateRemoteThread() API)
- 使用注册表(AppInit_DLLs 值)
- 消息钩取(SetWindowsHookEx() API)
DLL注入分为以下几个步骤:
1.获取注入目标进程句柄(OpenProcess()函数)
2.在目标进程中分配内存,分配的内存能够存放下dll完全路径字符串
3.将dll路径字符串写入刚刚分配的目标程序的内存之中
4.找到目标程序中LoadLibaray的入口地址
5.使用CreateRemoteThread()函数创建远程线程,实现最终注入
2.1.1.1 基本介绍
ProcessExplorer是一种最好的进程监视工具,重要的是它完全免费。!它不仅结合了文件监视和注册表监视两个工具(regmon和directory monitor)的功能,还增加了多项重要的增强功能。
ProcessExplorer能了解看不到的在后台执行的处理程序,能显示已经载入哪些模块,分别是正在被哪些程序使用着,还可显示这些程序所调用的 DLL进程,以及他们所打开的句柄。
2.1.1.2 简单使用
1. Process Explorer的主界面显示的是一种树形结构,准确的显示的进程的父子关系。
通过颜色可以判断此进程处于的状态和类型,是挂起还是正在退出,是服务进程还是普通进程。
- 能显示进程的系统信息,如:
- 显示进程当前的权限。(系统用户权限、网络管理员权限、普通管理员权限)
- 显示进程的文件路径(image path)
- 显示的进程是64位的还是32位的
- 显示进程当前所在的session id
- 显示进程命令行参数
- 显示当前进程所加载的DLL
- 显示当前进程所占用的系统资源句柄
- 操控进程以及显示进程的内部信息(操控进程、杀掉进程、重启进程、挂起进程等)
能随时关闭进程,甚至系统级别的关键进程。
在本次实验中,ProcessExplorer被用来查看正在运行的程序的pid(软件界面上方显示部分)
通过树形结构来查看程序的子进程。
还被用来查看当前进程所加载的DLL( 选择View —> Lower Pane View —> DLLs )
2.1.2.1 基本介绍
Microsoft Visual C++,(简称Visual C++、MSVC、VC++或VC)是Microsoft公司推出的以C++语言为基础的开发Windows环境程序,面向对象的可视化集成编程系统。它不但具有程序框架自动生成、灵活方便的类管理、代码编写和界面设计集成交互操作、可开发多种程序等优点,而且通过的设置就可使其生成的程序框架支持数据库接口、OLE2.0,WinSock网络。
2.1.2.2 简单使用
1. 打开Microsoft Visual C++ 6.0
2. 菜单中选择文件(File)->新建(New...)
3. 在打开的对话框中选择“工程(Projects)”
4. 选择“Win32控制台应用程序(Win32 Console Application)”->填写“工程名称(Project name)”->选择“位置(Location)”->“确定”。此时在该目录下新建了一个“工作空间(Workspace)”。
5. 然后再选择文件(File)->新建(New...),在“文件(FIle)”选项卡下选择选择“C++源文件(C++ Source file)”->填写“文件名称”->“确定”。就建好了一个.cpp文件。
6. 然后在.cpp文件中写入c语言代码。
7. 代码写好以后依次点击“编译(Compile)”->“连接(Build)”->“运行程序(Execute Program)”。 程序就可以运行了。
2.1.3.1 基本介绍
IDLE是开发 python 程序的基本IDE(集成开发环境),具备基本的IDE的功能,是非商业Python开发的不错的选择。
IDLE 总的来说是标准的 Python 发行版。 打开 Idle 后出现一个增强的交互命令行解释器窗口(具有比基本的交互命令提示符更好的剪切、粘贴和回行等功能)。除此之外,还有一个针对 Python 的编辑器(无代码合并,但有语法标签高亮和代码自动完成功能),类浏览器和调试器。 菜单为 TK “剥离”式,也就是点击顶部任意下拉菜单的虚线将会将该菜单提升到它自己的永久窗口中去。特别是 "Edit" 菜单,将其“靠”在桌面一角非常实用。Idle 的调试器提供断点、步进和变量监视功能;但并没有其内存地址和变量内容存数或进行同步和其他分析功能来得优秀。
IDLE为开发人员提供了许多有用的特性,如自动缩进、语法高亮显示、单词自动完成等。在这些功能的帮助下,能够有效地提高程序开发效率。 不同部分颜色不同,即所谓语法高亮显示。默认时,关键字显示为橘红色,注释显示为红色,字符串显示为绿色,解释器的输出显示为蓝色。在输入代码时,会自动应用这些颜色突出显示。语法高亮显示的好处是:可以更容易区分不同的语法元素,从而提高可读性;与此同时,也降低了出错的可能性。
2.2.1 OpenProcess(processthreadsapi.h)
2.2.1.1 原形
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 对过程对象的访问,英译汉-->渴望得到的访问权限(标志)--->获取的权限
BOOL bInheritHandle, // 是否继承句柄
DWORD dwProcessId // 进程标识符 (pid)
);
2.2.1.2 作用
用来打开一个已存在的进程对象,并返回进程的句柄。
2.2.2 VirtualAllocEx(memoryapi.h)
2.2.2.1 原形
LPVOID VirtualAllocEx(
HANDLE hProcess, // 进程的句柄
LPVOID lpAddress, // 内存地址
SIZE_T dwSize, // 内存大小
DWORD flAllocationType, // 内存分配的类型
DWORD flProtect // 对要分配的内存保护
);
2.2.2.2 作用
在指定进程的虚拟空间保留或提交内存区域
2.2.2.3 和VirtualAlloc的区别
VirtualAllocEx可以送一个handle进去,使得分配的空间再指定进程的内存地址空间里。
2.2.3 WriteProcessMemory(memoryapi.h)
2.2.3.1 原形
BOOL WriteProcessMemory(
HANDLE hProcess, // 返回的进程句柄
LPVOID lpBaseAddress, // 要写的内存首地址
LPCVOID lpBuffer, // 指向要写的数据的指针
SIZE_T nSize, // 要写入的字节数(大小)
SIZE_T *lpNumberOfBytesWritten // 指向变量的指针(可选)
);
2.2.3.2 作用
写入某一进程的内存区域,直接写入会出Access Violation错误,故需此函数。要写入的整个区域必须是可访问的,如果不可访问,则该功能将失败。
2.2.4.1 原形
HANDLE CreateRemoteThread(
HANDLE hProcess, // 线程所属进程的进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 一个指向指定了线程的安全属性的结构的指针
SIZE_T dwStackSize, // 线程栈初始大小
LPTHREAD_START_ROUTINE lpStartAddress, // 在远程进程的地址空间中,该线程的线程函数的起始地址.
LPVOID lpParameter, // 传给线程函数的参数
DWORD dwCreationFlags, // 线程的创建标志
LPDWORD lpThreadId // 指向所创建线程ID的指针,如果创建失败,该参数为NULL.
);
2.2.4.2 作用
创建一个在另一个进程的虚拟地址空间中运行的线程。
2.2.5.1 原形
HMODULE LoadLibraryA(
LPCSTR lpLibFileName // 模块的名称。这可以是库模块(.dll文件)或可执行模块(.exe文件)。
);
2.2.5.2 返回值
如果函数成功,则返回值是模块的句柄
如果函数失败,则返回值是NULL。
2.2.5.3 作用
进程调用 LoadLibrary以显式链接到 DLL。 如果函数执行成功,它会将指定的 DLL 映射到调用进程的地址空间中并返回该 DLL 的句柄。
在搜索和以上API函数相关的内容的时候发现,上面这些函数的位置都是位于kernel32.dll中。Kernel32.dll是windows的内核dll,包含的函数用来管理内存,进程以及线程。
但是为什么微软要把这些函数放在kernel32.dll这个DLL中呢?因为DLL的优势非常明显。
1.DLL可以自由的扩展功能,而无需对应用程序进行操作。
2.DLL使项目更加清晰便于管理。
3.DLL只需要被载入内存一次就可以供其他应用程序使用,节省了内存。
4.DLL促进了应用程序本地化
5.DLL有助于解决平台差异
6.DLL可用于特殊目地,比如我们将要使用的SetWindowsHookEx这类挂钩函数。
实验名称:PythonHacking之DLL注入
实验所属系列:计算机病毒分析
测试环境:windows xp sp3
实验工具:ProcessExplorer、VC6.0、IDLE
Python版本:2.7
实验文件:
- 运行成功之后的实验结果
记事本的pid:2676
注入成功
1. 开始把imhacker.cpp里的函数名打错了,DllMain()打成了DLLMain(),在cmd里运行python文件的时候,计算器进程没有出现。
又试了几种方法,看一下运行结果会怎样。
- 将计算器进程关掉,重新运行上述代码,之后计算器进程不再出现
python文件和dll文件都被使用过,再重新使用
- 再写一个相同的cpp文件,生成dll文件,还是使用之前的python文件,注入的目标进程不变,重新在cmd里运行,计算器又出现。
dll文件变,python文件不变
- 再写一个相同的python文件,使用之前注入时使用过的dll文件,注入的目标进程不变,在cmd里运行,计算器不出现。
dll不变,python文件变
- dll文件和python文件皆为使用过的,换一个进程进行注入,计算器子进程重新出现
更换pid进行注入
5.1 DllMain()函数及imhacker.cpp文件
每个DLL都可以又一个入口点函数DllMain(),系统会在不同时刻调用此函数。
DllMain() 的三个参数:
BOOL APIENTRY DllMain( HMODULE hModule, // 模块句柄
DWORD ul_reason_for_call, // 调用原因
LPVOID lpReserved // 参数保留
)
解释一下第二个参数:
ul_reason_for_call参数:指明了DLL被调用的原因,可以有以下4个取值:
DLL_PROCESS_ATTACH: // DLL被某个程序加载
DLL_THREAD_ATTACH: // DLL被某个线程加载
DLL_THREAD_DETACH: // DLL被某个线程卸载
DLL_PROCESS_DETACH: //DLL被某个程序卸载
此实验中生成dll文件的cpp代码:
// imhacker.cpp
#inlcude "windows.h"
BOOL WINAPT DllMain(HINSTANCE hMoudle,DWORD why_call,LPVOID lpReserved)
{
switch(why_call) // 根据调用原因选择不不同的加载方式
{
case DLL_PROCESS_ATTACH: // DLL被某个程序加载
WinExec("calc.exe",SW_SHOWNORMAL);
case DLL_THREAD_ATTACH: // DLL被某个线程加载
case DLL_THREAD_DETACH: // DLL被某个线程卸载
case DLL_PROCESS_DETACH: //DLL被某个程序卸载
break;
}
return TRUE;
}
在上面实验平台验证中写到了遇到的各种问题,在写这一部分的时候得到了解答。
在开始实验时,把函数名字DllMain()错打成DLLMain()了,执行python文件的时候没有出现注入的迹象(即没有出现计算器)。
- 将计算器进程关掉,重新运行上述代码,之后计算器进程不再出现。
现分析原因:
- 函数名字打错:
DllMain函数是DLL模块的默认入口点。当Windows加载DLL模块时调用这一函数。系统首先调用全局对象的构造函数,然后调用全局函数 DLLMain。
DLLMain函数不仅在将DLL链接加载到进程时被调用,在DLL模块与进程分离时(以及其它时候)也被调用。
DLL程序入口点函数:DllMain(),注意:大小写是区别的(仅导出资源的DLL可以没有DllMain函数)。
- 计算器进程不再出现:
DLL_PROCESS_ATTACH:
当DLL被进程 <<第一次>> 调用时,导致DllMain函数被调用,
同时ul_reason_for_call的值为DLL_PROCESS_ATTACH(DLL被某个程序加载),
如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,
不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数,即计算器进程不会再被调用。
又发现:
如果将要调用的进程放在PROCESS_ATTACH只出一次
放在DLL_THREAD_ATTACH就每次都有了
- 其余的问题就可以在上述解释中得到解答。
问题:再写一个相同的cpp文件,生成dll文件,还是使用之前的python文件,注入的目标进程不变,重新在cmd里运行,计算器进程又出现。
猜测:
上面提到
再次调用此DLL,只会增加DLL使用次数,不会再调用DllMain函数。
但是换了一个DLL调用,即使内容相同,但是是不同的文件,兴许就会重新调用DllMain函数。然后调用注入的计算器进程。
PROCESS_ALL_ACCESS=0x001F0FFF
#1. 获取注入目标进程句柄
kernel32 =windll.kernel32
AimPid =sys.argv[1]
dll_path= sys.argv[2]
dll_path_len=len(dll_path)
handle_aim=kernel32.OpenProcess(PROCESS_ALL_ACCESS,False,int(AimPid))
5.2.1.1 sys.argv
执行python程序:如 python my.py --version -y
那么 sys.argv[0]指的是 my.py,sys.argv[1]指的是--version,sys.argv[2]指的是-y
在这里,由于执行命令时python文件后面时是进程的PID和dll文件的路径,所以
AimPid =sys.argv[1] // AimPid = 进程的PID
dll_path= sys.argv[2] // dll_path = dll文件的路径
5.2.1.2 OpenProcess
OpenProcess(获取的权限,是否继承句柄,进程标识符)
由于在2.2 API函数模块提到了injector.py文件的一些函数以及函数的参数,但是没有详细讲,所以下面的模块中虽然都提到了一些函数,但是只详细讲一下其中的一些参数。
- PROCESS_ALL_ACCESS
在2.2 API函数那里提到了这个函数,在这里需要提一下函数的第一个参数。
PROCESS_ALL_ACCESS = 0x001F0FFF :
#define PROCESS_ALL_ACCESS
(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFF)
PROCESS_ALL_ACCESS = (0x000F0000L | 0x00100000L | 0xFFF)
PROCESS_ALL_ACCESS: 给予进程所有可能允许的权限
- 返回值
OpenProcess函数执行成功将根据传入参数的PID返回该PID进程的句柄.
MEMORY_ALLC=(0x1000|0x2000)
PAGE_READWRITE = 0x04
dll_path= sys.argv[2]
dll_path_len=len(dll_path)
handle_aim=kernel32.OpenProcess(PROCESS_ALL_ACCESS,False,int(AimPid)) # 目标进程句柄
#2. 在目标进程中分配内存,分配的内存能够下存放dll完全路径字符串
dll_address=kernel32.VirtualAllocEx(handle_aim,0,dll_path_len,MEMORY_ALLC,PAGE_READWRITE)
5.2.2.1 VirtualAllocEx
VirtualAllocEx(申请内存所在的进程句柄,保留页面的内存地址,想要分配的内存大小,内存分配的类型,对要分配的页面区域的内存保护)
- MEMORY_ALLC
函数的第四个参数有四种类型,类型和类型的值分别是:
类型 | MEM_COMMIT | MEM_RESERVE | MEM_RESET | MEM_RESET_UNDO |
值 | 0x00001000 | 0x00002000 | 0x00080000 | 0x1000000 |
在这里,第四个参数的值是:MEMORY_ALLC=(0x1000|0x2000),所以这里用到了
MEM_COMMIT(0x00001000) 和MEM_RESERVE(0x00002000),这俩类型的作用反别是:
MEM_COMMIT |
为指定的保留内存页分配内存费用(从内存的总大小和磁盘上的页面文件的总容量)。该函数还保证当调用者以后最初访问内存时,内容将为零。除非/直到实际访问虚拟地址,否则不会分配实际的物理页面。
MEM_RESERVE |
保留进程的虚拟地址空间范围,而不在内存或磁盘上的页面文件中分配任何实际的物理存储。
举个栗子:也就是基本上是对操作系统说:“嘿,拜托,我需要这个连续的虚拟内存页块,你能给我一个符合我需求的内存地址吗?” 操作系统会计算保留块的位置. 但它还没有分配任何东西.
在这里使用MEM_RESERVE | MEM_COMMIT的方式,作用是:一步一步保留和提交页面,并且不必两次调用`VirtualAlloc()’API。
2.PAGE_READWRITE
位于VirtualAllocEx第五个参数的位置,对要分配的页面区域的内存保护。如果正在提交页面,则可以指定任何一个内存保护常量。
内存保护常量可以有13种,但不能为空,这里的值为 0x04,这里的作用是:
PAGE_READWRITE(0x04):启用对页面的提交区域的只读或读/写访问权限。如果启用了“数据执行保护“,则尝试在提交的区域中执行代码会导致访问冲突。
handle_aim=kernel32.OpenProcess(PROCESS_ALL_ACCESS,False,int(AimPid)) # 目标进程句柄
dll_address=kernel32.VirtualAllocEx(handle_aim,0,dll_path_len,MEMORY_ALLC,PAGE_READWRITE) # dll文件的路径
#3. 将dll路径字符串写入刚刚分配的目标程序的内存之中
null_zero=c_int(0)
kernel32.WriteProcessMemory(handle_aim,dll_address,dll_path,dll_path_len,byref(null_zero))
5.2.3.1 WriteProcessMemory
WriteProcessMemory(由OpenProcess返回的进程句柄,要写的内存首地址,指向缓冲区的指针,指定进程的字节数,指向变量的指针)
ByRef:按地址传递
#4. 找到目标程序中LoadLibaray的入口地址
handle_kernel32=kernel32.GetModuleHandleA("kernel32.dll")
Load_address=kernel32.GetProcAddress(handle_kernel32,"LoadLibraryA")
5.2.4.1 GetModuleHandleA
返回值是指定模块的句柄。
5.2.4.2 GetProcAddress
获取DLL中导出函数的地址。即获取LoadLibrary的入口地址。
#5. 创建远程线程,实现最终注入
thread_id=c_ulong(0)
kernel32.CreateRemoteThread(handle_aim,None,0,Load_address,dll_address,0,byref(thread_id))
5.2.5.1 CreateRemoteThread(返回新线程的句柄)
CreateRemoteThread(句柄,NULL-->线程获取默认的安全描述符,堆栈的初始大小,进程中线程的起始地址,dll文件路径,控制线程创建的标志--> 0 -->线程创建后立即运行,指向接收线程标识符的变量的指针)
#injector.py
PROCESS_ALL_ACCESS=0x001F0FFF
MEMORY_ALLC=(0x1000|0x2000)
PAGE_READWRITE = 0x04
#1. 获取注入目标进程句柄
kernel32 =windll.kernel32
AimPid =sys.argv[1]
dll_path= sys.argv[2]
dll_path_len=len(dll_path)
handle_aim=kernel32.OpenProcess(PROCESS_ALL_ACCESS,False,int(AimPid))
#2. 在目标进程中分配内存,分配的内存能够下存放dll完全路径字符串
dll_address=kernel32.VirtualAllocEx(handle_aim,0,dll_path_len,MEMORY_ALLC,PAGE_READWRITE)
#3. 将dll路径字符串写入刚刚分配的目标程序的内存之中
null_zero=c_int(0)
kernel32.WriteProcessMemory(handle_aim,dll_address,dll_path,dll_path_len,byref(null_zero))
#4. 找到目标程序中LoadLibaray的入口地址
handle_kernel32=kernel32.GetModuleHandleA("kernel32.dll")
Load_address=kernel32.GetProcAddress(handle_kernel32,"LoadLibraryA")
#5. 创建远程线程,实现最终注入
thread_id=c_ulong(0)
kernel32.CreateRemoteThread(handle_aim,None,0,Load_address,dll_address,0,byref(thread_id))
参考资料:
《逆向工程核心原理》
CSDN许多文章
MSDN资料