虚拟内存与物理内存
1. 虚拟内存与物理内存关系
可以使用的内存不一定有物理页,必须申请内存了才有物理页。
物理页数量限制由内存条限制
物理内存
pagefile.sys,就是用硬盘存放物理页。
私有内存的申请与释放
windbg里!vad 地址,可以查看那些线性地址被占用。
线性地址分为Private和Mapped
私有内存时当前物理页只归当前进程用,一个物理页被多个进程共享称为Mapped
申请内存的两种方式
- 通过VirtualAlloc/VirtualAllocEx申请的:Private Memory(第一个是自己进程,第二个可以指定ID,远程申请)
- 通过CreateFileMapping映射的: Mapped Memory
内存申请与释放:
LPVOID VirtualAlloc(
LPVOID lpAddress,//要分配的内存区域地址,通常不用指定
DWORD dwSize,//分配的大小,单位自己,但一般申请页的整数倍
DWORD flAllocationType,//分配的类型,常用的是MEM_COMMIT(需要物理内存)和MEM_RESERVE(留出来,并不需要物理内存)
DWORD flProtect//改内存的初始保护属性,就是读写执行权限
);
BOOL WINAPI VirtualFree(
_In_ LPVOID lpAddress,//要释放的内存区域地址,通常不用指定
_In_ SIZE_T dwSize,//内存大小,必须填0
_In_ DWORD dwFreeType//MEM_DECOMMIT是虚拟地址保存,不留物理页,MEM_RELEASE都不保留
);
malloc是从已经申请的现有的内存,挖出来一块(堆内存),这个内存已经由VirtualAlloc申请好了。
malloc–>HeapAlloc
共享内存的申请与释放
//创建一个内核对象,可以为我们准备物理内存
HANDLE WINAPI CreateFileMapping(
_In_ HANDLE hFile,//如果指定文件,不仅可以提供物理内存,还能将次文件关联到这个物理内存,(INVALID_HANDLE_VALUE)-1就是不关联
_In_opt_ LPSECURITY_ATTRIBUTES lpAttributes,//安全描述符,NULL就行
_In_ DWORD flProtect,//保护模式,PAGE_READONLY为只读,PAGE_READWRITE为读写
_In_ DWORD dwMaximumSizeHigh,//高32位,0就行
_In_ DWORD dwMaximumSizeLow,//申请多大一块物理内存
_In_opt_ LPCTSTR lpName//内核对象的名字
);
//返回内核句柄,如果已经创立过了,GetLastError会返回错误,但是返回值本身还是那个内核对象句柄。
//这个函数完了只是物理页准备好了,还没用跟虚拟内存建立联系,所以将物理页与线性地址进行映射
所以还要用到下一个API
LPVOID WINAPI MapViewOfFile(
_In_ HANDLE hFileMappingObject,//映射FIleMap的句柄
_In_ DWORD dwDesiredAccess,//虚拟内存属性,属性要比物理页属性权限低,更严格
_In_ DWORD dwFileOffsetHigh,//NULL
_In_ DWORD dwFileOffsetLow,//从哪里开始映射
_In_ SIZE_T dwNumberOfBytesToMap//物理页创建多少就映射多少
);
返回值就是映射好的虚拟地址句柄
可以映射多个内存
用完之后要关闭映射,用到UnmapViewOfFile(映射的虚拟地址句柄)
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress
);
//关闭句柄
CloseHandle(物理页的内核对象句柄)
Map的使用的时候回挂上物理页
同一个物理页在2个进程映射出的虚拟内存地址可能不一样
文件系统
文件系统是操作系统用于管理磁盘上文件的方法和数据结构,简单点说就是在磁盘上如何组织文件的方法。
表格 | NTFS | FAT32 |
---|---|---|
磁盘分区容量 | 2T(2048G) | 32G |
单个文件容量 | 4G | 最大4G |
EFS加密 | 支持 | 不支持 |
磁盘配额 | 支持 | 不支持 |
内存映射文件
graph LR
文件-->物理内存
物理内存-->文件
物理内存-->进程的虚拟地址
进程的虚拟地址-->物理内存
直接就能在虚拟内存上读写文件
跟共享内存差不多,多了一步,创建文件句柄
CreateFile,然后就是CreateFileMapping(第一个参数就不能为空了,是文件的句柄),然后就是映射到虚拟内存,MapViewOfFIle(CreateFileMapping的句柄),这个函数返回值是句柄,句柄就是虚拟内存的地址
逻辑就是如下图
比如说所有的DLL在物理页上都是只有一个空间的,映射到各个进程。那怎么是修改了一个进程的物理内存,不影响其他的进程呢就用到了写拷贝。
写拷贝
就是MapViewOfFile的参数dwDesiredAccess是FILE_MAP_COPY A copy-on-write view of the file is mapped. 就是当写的时候赋值。
当资源释放写会文件还是原来的dll。
# 静态链接库
生成的头文件和lib都有可能要给别人用。
静态库的缺点
- 使用的静态链接生成的可执行文件体积较大。
- 包含相同的公共代码,造成浪费。
动态链接库
动态库就是dll,ocx,解决掉了静态库的问题。
使用格式
在声明时候
extern “C” _declspec(dllexport) 调用约定 返回值 函数名 (参数列表) 下面是示例:
extern “C” _declspec(dllexport) int fun(int a, int b);
或者要制定函数名,就建一个.def文件
fun @编号 noname 比如 fun@1
使用方法:
1. 先定义函数指针
typedef 返回值类型 (调用约定 *函数指针名)(参数列表)
示例 typedef int(__stdcall *fun)(int, int);
2. 声明函数指针变量示例fun a
3. 加载模块(LoadLibraryA(dll名字))
4. 给指针变量赋值
5. 使用
6. FreeLibrary
隐式链接
不仅要dll,还要拷贝lib
然后还要加一句
pragma comment(lib,”A.lib”)到调用文件中
然后加入函数申明,比如
extern “C”_declspec(dllimport) int Min(int a,int b);
BOOL WINAPI DllMain(
HANDLE hinstDLL,
DWORD dwReason,
LPVOID lpvReserved
);
Main和WinMain的区别:
1. WinMain可能被调用很多次。
DLL_PROCESS_ATTACH (LoadLibrary)调用一次
DLL_PROCESS_DETACH (FreeLibrary) 调用一次
DLL_THREAD_ATTACH 每个进程的每多一个线程调用一次
DLL_THREAD_DETACH 每个进程的每多一个线程离开一次
- 参数不一样,第一个参数当前dll被加载到什么位置,第二个参数被调用的原因。
远程线程
- 线程就是进程的执行实体
- 代码必须通过线程才能执行
HANDLE WINAPI CreateRemoteThread(
_In_ HANDLE hProcess,//进程句柄
_In_ LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_ LPDWORD lpThreadId
);
下面是代码示例
#include "stdafx.h"
#include<windows.h>
BOOL MyCreateRemoteThread(DWORD dwProcessID, DWORD dwProcAddr) {
HANDLE hProcess;
HANDLE hThread;
DWORD dwThreadID;
hProcess = 0;
dwThreadID = 0;
hThread = 0;
//1.获取进程句柄
hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessID);
if (hProcess == NULL) {
OutputDebugString(L"OpenProcess Error!\n");
return FALSE;
}
//2.创建远程线程
hThread=CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwProcAddr, NULL, 0, &dwThreadID);
if (NULL == hThread) {
OutputDebugString(L"CreateRemoteThread Error!\n");
CloseHandle(hProcess);
return FALSE;
}
//3.关闭
CloseHandle(hThread);
CloseHandle(hProcess);
return true;
}
int main()
{
MyCreateRemoteThread(752,0x401090);//别人的线程地址
return 0;
}
远程线程注入
把自己的要执行代码写入个Dll,注入进去。
什么是注入
远程线程注入流程
远程线程注入
把自己的要执行代码写入个Dll,注入进去。
什么是注入
思路就是写一个dll,然后再里面创建一个线程,然后执行你想做的操作,注入程序按照流程,先获取对方进程ID,将在目标进程申请一块内存,写入Dll名字,然后获取kernel32的句柄,用于找到LoadLibraryA(因为Kernel32是内核模块的,同个计算机系统每个进程这个地址一样),找到LoadLibraryA之后把这个函数的地址放入创建远程线程函数在远程进程的地址空间中,该线程的线程函数的起始地址.的参数里,参数就是之前我们申请的放入Dll的地址。
然后我们可以在运行时调试,通过OD观察远程进程内存是否写入我们的dll路径,以及是否创建成功远程线程,如下图
具体代码如下
// 远程线程注入.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include<windows.h>
BOOL LoadDll(DWORD dwProcessID, char *szDllPathName) {//第一个参数进程ID,加载dll的完整路径
BOOL bRet;
HANDLE hProcess;
HANDLE hThread;
DWORD dwLength;
DWORD dwLoadAddr;
LPVOID lpAllocAddr;
DWORD dwThreadID;
HMODULE hModule;
bRet = 0;
dwLoadAddr = 0;
hProcess = 0;
//1.获取进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if (hProcess == NULL) {
OutputDebugString(L"OpenProcess Error!\n");
return FALSE;
}
//2. 计算DLL路径名字长度,并且要加上0结尾的长度
dwLength = strlen(szDllPathName) + 1;
//3. 在目标进程分配内存
lpAllocAddr = VirtualAllocEx(hProcess, NULL, dwLength, MEM_COMMIT, PAGE_READWRITE);
if (lpAllocAddr == NULL) {
OutputDebugString(L"VirtualAllocEx Error!\n");
CloseHandle(hProcess);
return FALSE;
}
//4.拷贝DLL路径名字到目标进程的内存
bRet = WriteProcessMemory(hProcess, lpAllocAddr, szDllPathName, dwLength, NULL);
if (!bRet) {
OutputDebugString(L"WriteProcessMemory Error!\n");
CloseHandle(hProcess);
return FALSE;
}
//5.因为Kernel32,大家都有,所以从自己进程这获取就行,所以这步是火区模块地址
hModule = GetModuleHandle(L"kernel32.dll");
if (!hModule) {
OutputDebugString(L"GetModuleHandle Error!\n");
CloseHandle(hProcess);
return FALSE;
}
//6.获取LoadLibraryA函数地址
dwLoadAddr = (DWORD)GetProcAddress(hModule, "LoadLibraryA");
if (!dwLoadAddr) {
OutputDebugString(L"GetProcAddress Error!\n");
CloseHandle(hProcess);
return FALSE;
}
//7. 创建远程线程,加载Dll
hThread = CreateRemoteThread(hProcess, NULL,0, (LPTHREAD_START_ROUTINE)dwLoadAddr, lpAllocAddr, 0, NULL);
if (!hThread) {
OutputDebugString(L"CreateRemoteThread Error!\n");
CloseHandle(hProcess);
CloseHandle(hModule);
return FALSE;
}
//8.关闭句柄
CloseHandle(hProcess);
return TRUE;
}
BOOL MyCreateRemoteThread(DWORD dwProcessID, DWORD dwProcAddr) {
HANDLE hProcess;
HANDLE hThread;
DWORD dwThreadID;
hProcess = 0;
dwThreadID = 0;
hThread = 0;
//1.获取进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if (hProcess == NULL) {
OutputDebugString(L"OpenProcess Error!\n");
return FALSE;
}
//2.创建远程线程
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwProcAddr, NULL, 0, &dwThreadID);
if (NULL == hThread) {
OutputDebugString(L"CreateRemoteThread Error!\n");
CloseHandle(hProcess);
return FALSE;
}
//3.关闭
CloseHandle(hThread);
CloseHandle(hProcess);
return true;
}
int main()
{
LoadDll(16588, "D:\\yangruiqiDll.dll");//第一个参数是要注入dll的进程,第二个是dll目录
return 0;
}
测试的Dll如下
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
#include <stdio.h>
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
for (;;) {
printf("注入的代码在执行\n");
}
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
进程间通信
模块隐藏
注入的dll隐藏起来
模块是怎么被API遍历出来的
模块隐藏之断链
- TEB(Thread Environment Block),它记录的相关线程信息,每个线程都有自己的TEB,FS:[0]即是当前线程的TEB。
mov eax,fs:[0]
- PEB(Process Enbiroment Block),存放进程信息,每个进程都有自己的PEB,TEB偏移0x30的位置是PEB。
mov eax,fs:[30]
mov PEB,eax
TEB
PEB
nt!_UNICODE_STRING
+0x000 Length : Uint2B
+0x002 MaximumLength : Uint2B
+0x004 Buffer : Ptr32 Uint2B
所以偏移4才是字符串的值
隐藏思路就是GetModule找到想隐藏的模块,然后通过查找PEB找到,然后断链,前一个指向后一个。
进一步隐藏Vad
再进一步隐藏PE特征
再进一步查LoadLibary
所以最后就是代码注入
代码注入
不能有全局变量,常量,毕竟是重定位的数据。
导入的函数也不能。
不能使用系统调用。(只能在本进程找,然后跟系统调用那样传进去)
嵌套函数也不行
注意代码要修正,就是复制一个函数时候,函数起始位置其实是个jump,要找到E9之后修正起始复制的位置
参考资料
[1] 滴水视频