PE文件本地DLL注入实现

  之前写过一个利用远程线程注入DLL的工具,大致过程如下:

  1.使用OpenProcessToken、LookupPrivilegeValue、AdjustTokenPrivileges函数修改当前进程的的访问令牌,获得调试权限。

  2.OpenProcess打开目标进程,用VirtualAllocEx在目标进程申请一段内存空间,WriteProcessMemory将要注入DLL的路径字符串写入申请的内存里。

  3.最后调用GetProcAddress获取LoadLibraryA(W)的地址,CreateRemoteThread执行远程线程,成功在目标进程注入DLL。

  远程注入的好处是动态的,它不修改文件本身,所有的改动都是在内存里,申请分配写入销毁,这就是说我们在注入DLL的时候目标程序已经在运行中了,但有时又需要注入代码执行在程序之前,有什么办法呢?很自然的想到通过修改本地PE文件的相关数据从而达成我们的目的。在Windows上所有EXE、DLL、OCX甚至SYS都是PE文件,所谓PE是微软制定的一种可移植的文件格式,其结构如图:

  DOS头的0x3C偏移处的DWORD值指向PE头,对于Win32程序,结构如下:

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

  其中OptionalHeader.AddressOfEntryPoint是程序入口点,是系统装载程序完毕后跳转执行第一条指令的地址,这是一个内存偏移地址,程序基址由OptionalHeader.BaseOfCode指明。我们要做的就是修改OEP,先执行我们的代码,然后再跳转回原OEP,那么这些代码应该写入在哪里?PE文件中数据都是按节存放的,节中数据具有相同的属性,如可读可写可执行等等,PE格式规定节的物理大小必须是OptionalHeader.FileAlignment(一般为512)的倍数,不足的用0补齐,这些用0对齐的数据就是代码写入的地方。FileHeader.NumberOfSections表示节的数目,PE文头后面紧跟着节头信息,结构如下:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[8];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

  SizeOfRawData是对齐后的物理大小,Misc.VirtualSize是节的实际大小,两者一减,就是磁盘上对齐0的数量,PointerToRawData是节的文件偏移,Characteristics是节属性。好了,有了这些信息就能遍历节表,找到代码节,计算节剩余空间(即对齐的0数据),代码如下:

// 节的数目
int Sections = lpNtHead->FileHeader.NumberOfSections;
IMAGE_SECTION_HEADER *lpSections = (IMAGE_SECTION_HEADER*)((DWORD)&(lpNtHead->OptionalHeader) + lpNtHead->FileHeader.SizeOfOptionalHeader);
DWORD lpCodeOffset = 0;
DWORD dwCodeSize = 0;
DWORD dwFreeSize = 0;
// 遍历所有节头
for(int i=0;i<Sections;i++) {
	// 查找代码节
	if((lpSections[i].Characteristics & IMAGE_SCN_CNT_CODE) &&
		(lpSections[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) &&
		(lpSections[i].Characteristics & IMAGE_SCN_MEM_READ)) {
		// 获得代码节的相对虚拟地址并转换成文件偏移地址
		lpCodeOffset = (DWORD)lpSections[i].VirtualAddress;
		lpCodeOffset = RVAToFileOffset(lpDosHead,lpCodeOffset);
		lpCodeOffset += (DWORD)lpDosHead;
		// 计算节的剩余空间
		dwCodeSize = lpSections[i].Misc.VirtualSize;
		if(lpSections[i].SizeOfRawData > dwCodeSize) {
			dwFreeSize = lpSections[i].SizeOfRawData - dwCodeSize;
		} else {
			dwFreeSize = 0;
			continue;
		}
		break;
	}
}

  找到可用的代码节,再判断一下剩余空间能否容下写入代码,如果符合条件就可以在lpCodeOffset + dwCodeSize处写入代码了:调用LoadLibraryA(W)函数,载入注入的DLL文件,然后跳转回原OEP程序继续执行,类似下面的:

__asm {
	pushad
	pushfd
	lea eax,szDllName
	push eax
	call lpLoadLibraryA
	popfd
	popad
	jmp oldOEP
}

  szDllName存放着DLL名字,可以在代码节剩余空间里分配,压入栈的是字符串的虚拟地址(RVA+映像基址),这个我们转换一下就行了,不过还有一个小问题:EXE和DLL不同,总是能成功映射首选基址不存在基址冲突的,但从Vista以后,系统出现一种新的“地址空间布局随机化”机制,如果一个EXE程序存在基址重定位信息并且没有IMAGE_FILE_RELOCS_STRIPPED标志,EXE基址会被随机化,如此一来之前压入的szDllName地址就不正确了,解决这问题最简单的办法就是强制EXE程序必须映射到首选基址上,FileHeader.Characteristics |= IMAGE_FILE_RELOCS_STRIPPED成功搞定!

  好了,就剩最后一步了,LoadLibraryA(W)的地址怎么找?从导入表里获取,于是循环导入表查找kernel32.dll然后……,此处省略N字还是直接上代码简洁明了:

IMAGE_DATA_DIRECTORY *pImportDirectory = &(lpNtHead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);
DWORD lpBase = pImportDirectory->VirtualAddress;
lpBase = (DWORD)lpDosHead + RVAToFileOffset(lpDosHead,lpBase);
IMAGE_IMPORT_DESCRIPTOR *pImport = (IMAGE_IMPORT_DESCRIPTOR*)lpBase;
char *buf;
DWORD *dwAddress;
DWORD lpLoadLibraryA,lpLoadLibraryW,lpLoadLibraryExA,lpLoadLibraryExW;

lpLoadLibraryA = lpLoadLibraryW = lpLoadLibraryExA = lpLoadLibraryExW = NULL;

for(int i=0;;i++) {
	if(0 == pImport[i].Characteristics &&
		0 == pImport[i].Name) {
		break;
	}

	buf = (char*)((DWORD)lpDosHead + RVAToFileOffset(lpDosHead,pImport[i].Name));
	if(0 == stricmp(buf,"kernel32.dll")) {
		lpBase = pImport[i].FirstThunk;
		lpBase = (DWORD)lpDosHead + RVAToFileOffset(lpDosHead,lpBase);
		dwAddress = (DWORD*)lpBase;
		int pos = 0;
		while(0 != (*dwAddress)) {
			// 使用名称导入而非序号
			if(!((*dwAddress) & 0x80000000)) {
				// 2字节序号 + ASCII字符串(函数名)
				buf = (char*)((DWORD)lpDosHead + RVAToFileOffset(lpDosHead,(*dwAddress)));
				buf += 2;
				if(0 == strcmp(buf,"LoadLibraryA")) {
					lpLoadLibraryA = pImport[i].FirstThunk+pos*4;
				}
				if(0 == strcmp(buf,"LoadLibraryW")) {
					lpLoadLibraryW = pImport[i].FirstThunk+pos*4;
				}
				if(0 == strcmp(buf,"LoadLibraryExA")) {
					lpLoadLibraryExA = pImport[i].FirstThunk+pos*4;
				}
				if(0 == strcmp(buf,"LoadLibraryExW")) {
					lpLoadLibraryExW = pImport[i].FirstThunk+pos*4;
				}
			}
			dwAddress++;
			pos++;
		}
		break;
	}
}

  终于写完了,好像感觉有点虎头蛇尾敲打点击这里下载实例工具。如果代码节剩余空间不足可以使用本工具先添加一个任意大小的新节。

  最后,以此纪念我第一篇CSDN博文。微笑

远程注入DLL方法有很多种,也是很多木马病毒所使用的隐藏进程的方法,因为通过程序加载的DLL在进程管理器是没有显示的.这里介绍一种用 CreateRemoteThread 远程建立线程的方式注入DLL. 首先,我们要提升自己的权限,因为远程注入必不可免的要访问到目标进程的内存空间,如果没有足够的系统权限,将无法作任何事.下面是这个函数是用来提升我们想要的权限用的. function EnableDebugPriv: Boolean; var hToken: THandle; tp: TTokenPrivileges; rl: Cardinal; begin Result := false; //打开进程令牌环 OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, hToken); //获得进程本地唯一ID if LookupPrivilegeValue(nil, 'SeDebugPrivilege', tp.Privileges[0].Luid) then begin tp.PrivilegeCount := 1; tp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; //调整权限 Result := AdjustTokenPrivileges(hToken, false, tp, SizeOf(tp), nil, rl); end; end; 关于 OpenProcessToken() 和 AdjustTokenPrivileges() 两个 API 的简单介绍: OpenProcessToken():获得进程访问令牌的句柄. function OpenProcessToken( ProcessHandle: THandle; //要修改访问权限的进程句柄 DesiredAccess: DWORD; //指定你要进行的操作类型 var TokenHandle: THandle//返回的访问令牌指针 ): BOOL; AdjustTokenPrivileges() :调整进程的权限. function AdjustTokenPrivileges( TokenHandle: THandle; // 访问令牌的句柄 DisableAllPrivileges: BOOL; // 决定是进行权限修改还是除能(Disable)所有权限 const NewState: TTokenPrivileges; { 指明要修改的权限,是一个指向TOKEN_PRIVILEGES结构的指针,该结构包含一个数组, 数据组的每个项指明了权限的类型和要进行的操作; } BufferLength: DWORD; //结构PreviousState的长度,如果PreviousState为空,该参数应为 0 var PreviousState: TTokenPrivileges; // 指向TOKEN_PRIVILEGES结构的指针,存放修改前的访问权限的信息 var ReturnLength: DWORD //实际PreviousState结构返回的大小 ) : BOOL; 远程注入DLL其实是通过 CreateRemoteThread 建立一个远程线程调用 LoadLibrary 函数来加载我们指定的DLL,可是如何能让远程线程知道我要加载DLL呢,要知道在Win32系统下,每个进程都拥有自己的4G虚拟地址空间,各个进程之间都是相互独立的。所我们需要在远程进程的内存空间里申请一块内存空间,写入我们的需要注入DLL 的路径. 需要用到的 API 函数有: OpenProcess():打开目标进程,得到目标进程的操作权限,详细参看MSDN function OpenProcess( dwDesiredAccess: DWORD; // 希望获得的访问权限 bInheritHandle: BOOL; // 指明是否希望所获得的句柄可以继承 dwProcessId: DWORD // 要访问的进程ID ): THandle; VirtualAllocEx():用于在目标进程内存空间中申请内存空间以写入DLL文件名 function VirtualAllocEx( hProcess: THandle; // 申请内存所在的进程句柄 lpAddress: Pointer; // 保留页面的内存地址;一般用nil自动分配 dwSize, // 欲分配的内存大小,字节单位;注意实际分 配的内存大小是页内存大小的整数倍 flAllocationType: DWORD; flProtect: DWORD ): Pointer; WriteProcessMemory():往申请到的空间中写入DLL文件名 function WriteProcessMemory( hProcess: THandle; //要写入内存数据的目标进程句柄 const lpBaseAddress: Pointer; //要写入的目标进程的内存指针, 需以 VirtualAllocEx() 来申请 lpBuffer: Pointer; //要写入的数据 nSize: DWORD; //写入数据的大小 var lpNumberOfBytesWritten: DWORD //实际写入的大小 ): BOOL; 然后就可以调用 CreateRemoteThread 建立远程线程调用 LoadLibrary 函数来加载我们指定的DLL. CreateRemoteThread() //在一个远程进程中建立线程 function CreateRemoteThread( hProcess: THandle; //远程进程的句柄 lpThreadAttributes: Pointer; //线程安全描述字,指向SECURITY_ATTRIBUTES结构的指针 dwStackSize: DWORD; //线程栈大小,以字节表示 lpStartAddress: TFNThreadStartRoutine; // 一个TFNThreadStartRoutine类型的指针,指向在远程进程中执行的函数地址 lpParameter: Pointer; //传入参数的指针 dwCreationFlags: DWORD; //创建线程的其它标志 var lpThreadId: DWORD //线程身份标志,如果为0, 则不返回 ): THandle; 整个远程注入DLL的具体实现代码如下: function InjectDll(const DllFullPath: string; const dwRemoteProcessId: Cardinal): Boolean; var hRemoteProcess, hRemoteThread: THandle; pszLibFileRemote: Pointer; pszLibAFilename: PwideChar; pfnStartAddr: TFNThreadStartRoutine; memSize, WriteSize, lpThreadId: Cardinal; begin Result := false; // 调整权限,使程序可以访问其他进程的内存空间 if EnableDebugPriv then begin //打开远程线程 PROCESS_ALL_ACCESS 参数表示打开所有的权限 hRemoteProcess := OpenProcess(PROCESS_ALL_ACCESS, false, dwRemoteProcessId); try // 为注入dll文件路径分配内存大小,由于为WideChar,故要乘2 GetMem(pszLibAFilename, Length(DllFullPath) * 2 + 1); // 之所以要转换成 WideChar, 是因为当DLL位于有中文字符的路径下时不会出错 StringToWideChar(DllFullPath, pszLibAFilename, Length(DllFullPath) * 2 + 1); // 计算 pszLibAFilename 的长度,注意,是以字节为单元的长度 memSize := (1 + lstrlenW(pszLibAFilename)) * SizeOf(WCHAR); //使用VirtualAllocEx函数在远程进程的内存地址空间分配DLL文件名空间 pszLibFileRemote := VirtualAllocEx(hRemoteProcess, nil, memSize, MEM_COMMIT, PAGE_READWRITE); if Assigned(pszLibFileRemote) then begin //使用WriteProcessMemory函数将DLL的路径名写入到远程进程的内存空间 if WriteProcessMemory(hRemoteProcess, pszLibFileRemote, pszLibAFilename, memSize, WriteSize) and (WriteSize = memSize) then begin lpThreadId := 0; // 计算LoadLibraryW的入口地址 pfnStartAddr := GetProcAddress(LoadLibrary('Kernel32.dll'), 'LoadLibraryW'); // 启动远程线程LoadLbraryW,通过远程线程调用创建新的线程 hRemoteThread := CreateRemoteThread(hRemoteProcess, nil, 0, pfnStartAddr, pszLibFileRemote, 0, lpThreadId); // 如果执行成功返回 True; if (hRemoteThread 0) then Result := true; // 释放句柄 CloseHandle(hRemoteThread); end; end; finally // 释放句柄 CloseHandle(hRemoteProcess); end; end; end; 接下来要说的是如何卸载注入目标进程中的DLL,其实原理和注入DLL是完全相同的,只是远程调用调用的函数不同而已,这里要调用的是FreeLibrary,代码如下: function UnInjectDll(const DllFullPath: string; const dwRemoteProcessId: Cardinal): Boolean; // 进程注入和取消注入其实都差不多,只是运行的函数不同而已 var hRemoteProcess, hRemoteThread: THandle; pszLibFileRemote: PChar; pszLibAFilename: PwideChar; pfnStartAddr: TFNThreadStartRoutine; memSize, WriteSize, lpThreadId, dwHandle: Cardinal; begin Result := false; // 调整权限,使程序可以访问其他进程的内存空间 if EnableDebugPriv then begin //打开远程线程 PROCESS_ALL_ACCESS 参数表示打开所有的权限 hRemoteProcess := OpenProcess(PROCESS_ALL_ACCESS, false, dwRemoteProcessId); try // 为注入dll文件路径分配内存大小,由于为WideChar,故要乘2 GetMem(pszLibAFilename, Length(DllFullPath) * 2 + 1); // 之所以要转换成 WideChar, 是因为当DLL位于有中文字符的路径下时不会出错 StringToWideChar(DllFullPath, pszLibAFilename, Length(DllFullPath) * 2 + 1); // 计算 pszLibAFilename 的长度,注意,是以字节为单元的长度 memSize := (1 + lstrlenW(pszLibAFilename)) * SizeOf(WCHAR); //使用VirtualAllocEx函数在远程进程的内存地址空间分配DLL文件名空间 pszLibFileRemote := VirtualAllocEx(hRemoteProcess, nil, memSize, MEM_COMMIT, PAGE_READWRITE); if Assigned(pszLibFileRemote) then begin //使用WriteProcessMemory函数将DLL的路径名写入到远程进程的内存空间 if WriteProcessMemory(hRemoteProcess, pszLibFileRemote, pszLibAFilename, memSize, WriteSize) and (WriteSize = memSize) then begin // 计算GetModuleHandleW的入口地址 pfnStartAddr := GetProcAddress(LoadLibrary('Kernel32.dll'), 'GetModuleHandleW'); //使目标进程调用GetModuleHandleW,获得DLL在目标进程中的句柄 hRemoteThread := CreateRemoteThread(hRemoteProcess, nil, 0, pfnStartAddr, pszLibFileRemote, 0, lpThreadId); // 等待GetModuleHandle运行完毕 WaitForSingleObject(hRemoteThread, INFINITE); // 获得GetModuleHandle的返回值,存在dwHandle变量中 GetExitCodeThread(hRemoteThread, dwHandle); // 计算FreeLibrary的入口地址 pfnStartAddr := GetProcAddress(LoadLibrary('Kernel32.dll'), 'FreeLibrary'); // 使目标进程调用FreeLibrary,卸载DLL hRemoteThread := CreateRemoteThread(hRemoteProcess, nil, 0, pfnStartAddr, Pointer(dwHandle), 0, lpThreadId); // 等待FreeLibrary卸载完毕 WaitForSingleObject(hRemoteThread, INFINITE); // 如果执行成功返回 True; if hRemoteProcess 0 then Result := true; // 释放目标进程中申请的空间 VirtualFreeEx(hRemoteProcess, pszLibFileRemote, Length(DllFullPath) + 1, MEM_DECOMMIT); // 释放句柄 CloseHandle(hRemoteThread); end; end; finally // 释放句柄 CloseHandle(hRemoteProcess); end; end; end;
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值