修改PEB伪装进程 最近看了Windows黑客编程技术详解,里面有一个进程伪装的方法就是修改PEB
0x01 PEB(进程环境块) 简单说就是存放进程运行时的数据的一块内存就行,至于复杂的还是看看网上的说法吧
https://zh.wikipedia.org/wiki/%E8%BF%9B%E7%A8%8B%E7%8E%AF%E5%A2%83%E5%9D%97
PEB里面存放着命令行参数和路径名称,当然不止这两个还有很多
这次只用到这两个所以只关注这两个就行(路径名称好像没用到..)
0x02 实现过程 前面说了PEB就是一块可读可写的内存,那么只要找到指向这个内存的指针,然后把命令行和路径名称都修改掉,这样任务管理器就会读到已经修改掉的参数,显示出来的就是修改之后的参数了
先说一下结论吧,按照书中的代码写出来,运行然后发现没有修改成功..
经过这几次的编程经验猜测和系统位数有关
32位程序:32位任务管理器修改成功,64位任务管理器未修改成功
64位程序:32位任务管理器显示为空,64位任务管理器修改成功
写的时候还挺坑的,不知道咋回事只能修改命令行,哪怕是x32dbg里面去修改路径名称,任务管理器读到的还是正常的,只能说可能任务管理器读的地方不是这个指针了
先看一下需要用到的结构和函数
1 2 3 4 5 6 7 typedef struct _PROCESS_BASIC_INFORMATION { PVOID Reserved1; PPEB PebBaseAddress; PVOID Reserved2[2 ]; ULONG_PTR UniqueProcessId; PVOID Reserved3; } PROCESS_BASIC_INFORMATION;
这个结构里面只需要用到PPEB结构
可以到这个宏定义里面查看,PPEB是指向PEB结构的指针,所以本质是PEB结构
现在再来看一下PEB这个结构
PEB 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _PEB { BYTE Reserved1[2 ]; BYTE BeingDebugged; BYTE Reserved2[1 ]; PVOID Reserved3[2 ]; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID Reserved4[3 ]; PVOID AtlThunkSListPtr; PVOID Reserved5; ULONG Reserved6; PVOID Reserved7; ULONG Reserved8; ULONG AtlThunkSListPtr32; PVOID Reserved9[45 ]; BYTE Reserved10[96 ]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved11[128 ]; PVOID Reserved12[1 ]; ULONG SessionId; } PEB, *PPEB;
这里需要关注的是PRTL_USER_PROCESS_PARAMETERS结构,另外的都用不到
像这种Reserved参数都是保留参数,或许以后版本的Windows会用到
32位系统PEB结构用汇编可以用fs:[0x30]定位到
64位系统用gs:[0x60]定位到
这里先提一下后面需要用到
PRTL_USER_PROCESS_PARAMETERS 1 2 3 4 5 6 typedef struct _RTL_USER_PROCESS_PARAMETERS { BYTE Reserved1[16 ]; PVOID Reserved2[10 ]; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
ImagePathName 路径名称..改了任务管理器不会变..不知道为啥可能不从这里读了(希望知道的师傅带带)
CommandLine 命令行
UNICODE_STRIN这也是个结构,需要了解一下内部构造
UNICODE_STRING 1 2 3 4 5 typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING;
Length 当前长度,比如命令行就是当前命令行长度,这个可以不修改
MaximumLength 应该是最长长度,没用到过所以不清楚
Buffer 指向参数的指针,如果是命令行就是指向命令行的指针
到这里需要的结构已经全部了解完了
1 2 3 4 5 6 7 typedef NTSTATUS (WINAPI* typedef_NtQueryInformationProcess) ( HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength ) ;
这个函数其实简单理解就是从进程内存中读出指定的东西,然后放入事先定义好的结构体中,具体是读出来什么需要看第二个参数
ProcessHandle 进程句柄
PROCESSINFOCLASS 要检索的信息进程类型(这里用很多宏定义,可以查看官方文档,这次需要用到的是ProcessBasicInformation参数,检索指向PEB的指针)
ProcessInformation 指向前面定义好的PROCESS_BASIC_INFORMATION结构的指针
ProcessInformationLength PROCESS_BASIC_INFORMATION结构的大小
ReturnLength 实际返回的大小
到这里需要用到的函数函数也写了
下面是书上的代码(改过的,意思差不多,毕竟书上只写了一个函数而且NtQueryInformationProcess的结构也没给出来)
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 #include <stdio.h> #include <Windows.h> #include <winternl.h> #include <string> typedef NTSTATUS (WINAPI* typedef_NtQueryInformationProcess) ( HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength ) ;int main () { HMODULE Ntdll = LoadLibraryA("ntdll.dll" ); HANDLE hProcess = GetCurrentProcess(); PROCESS_BASIC_INFORMATION pbi = { 0 }; PEB peb = { 0 }; RTL_USER_PROCESS_PARAMETERS Param = { 0 }; if (hProcess == NULL ) { printf ("OpenProcess ErrorCode: %d" , GetLastError()); return -1 ; } wchar_t lpCmd[] = L"explorer.exe" ; ULONG len = 0 ; typedef_NtQueryInformationProcess NtQueryInformationProcess = (typedef_NtQueryInformationProcess)GetProcAddress(Ntdll, "NtQueryInformationProcess" ); NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof (pbi), &len); if (!NT_SUCCESS(status)) { printf ("Fail" ); return -1 ; } USHORT usCmdLen = 2 + 2 * wcslen(lpCmd); ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof (peb), NULL ); ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof (Param), NULL ); BOOL ret32 = WriteProcessMemory(hProcess, pbi.PebBaseAddress->ProcessParameters->CommandLine.Buffer, lpCmd, sizeof (lpCmd), NULL ); return 0 ; }
书上还多了一个改长度,不过长度不一定要改的所以就不写了
0x03 踩坑 上面说了相同位数的程序只能在相同位数的任务管理器上面显示出来
那一个32位程序修改了PEB,高版本的windows默认打开64位的任务管理器,那不是相当于没修改吗
观察发现正常执行程序,在任务管理器出现的命令行都会被双引号包裹起来
在64位任务管理器中显示的是未修改的,那肯定64位任务管理器读的指针和32位管理器不一样,打开任务管理器右键创建转储文件
可以得到当前进程的内存,打开010Editor搜索被双引号包裹的命令行,发现确实有两个,这里双引号很重要,如果没有双引号会显示很多无关的,比如当前路径和窗口标题都有可能和未包裹双引号的命令行一样
搜索要使用Unicode字符,因为在内存中存放用的是这个字符
那就说明的确是有两个任务管理器读的是不同地方的命令行,那现在只要能确定另一个命令行指针就可以修改了,接下去的步骤就是找到这个指针
这里本来想直接相减得到两个命令行的偏移的,但是实际是不行的
网上搜索一下发现这方法的确是有..但是很复杂,又要从dll里面去取函数
这里文章就不放了看到的师傅有兴趣可以搜索一下,或者直接搜索NtWow64ReadVirtualMemory64函数基本都是这个的文章
这文章基本就把所有结构都重写了一遍,每个结构还区别32位和64位,整个代码看下来基本都是结构
我这种懒人肯定是不能接受的…
于是开始找简单的方法,PEB结构在汇编中用fs:[0x30]定位
0x04 旧思路 写着写着发现原本想写的这个思路不是最好的..
先写一下原本的思路吧
本来都是凑的,首先发现了32位程序里面64位的PEB地址在fs:[0x30]-0x1000
然后想在PEB结构里面找一些有用的信息
32位PEB 首先看32位的PEB,用程序把已经确定的指针输出出来看看能不能找到有用的信息
发现前四位和fs:[0x30]+0x18的一样,说明命令行就在这块内存里面,那只需要遍历这块内存肯定可以找到命令行,而且又有双引号这么明显的特征,之前在dump出来的内存中看到了,整个内存只有两个双引号包裹的命令行,所以不用担心读到类似的字符串
64位PEB
在fs:[0x30]-0x1000+0x30是包含命令行的那块内存,循环对比就行,最后使用内嵌汇编写出来这样的一个函数
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 LPVOID peb64 (HANDLE hProcess, WCHAR *cmd, int pathlen) { DWORD Version = GetVersion(); PVOID PEB64; if (((DWORD)HIBYTE(LOWORD(Version))) >= 2 ) { __asm { mov eax, fs: [0x30 ] sub eax, 0x1000 add eax, 0x30 mov eax, [eax] add eax, 0x2000 MOV PEB64, eax } } else { __asm { mov eax, fs: [0x30 ] add eax, 0x1000 add eax, 0x30 mov eax, [eax] add eax, 0x2000 MOV PEB64, eax } } LPVOID path = VirtualAlloc(NULL , pathlen, MEM_COMMIT, PAGE_READWRITE); for (int i = 0 ; i < 0x1000 ; i++) { ReadProcessMemory(hProcess, (LPVOID)((int )PEB64+i), path, pathlen, NULL ); if (!memcmp (cmd, path, pathlen)) { printf ("%S\n" , path); printf ("%p\n" , (LPVOID)((int )PEB64 + i)); return (LPVOID)((int )PEB64 + i); } }
HANDLE hProcess 要改的进程的句柄
WCHAR *cmd 这个参数使用32位PEB读取出来的命令行,因为没修改之前两个肯定是一样的
int pathlen 命令行的长度,因为最后读内存要设置一个长度,对*cmd直接用获取字符串长度的函数好像不太行,最后想了个办法,用GetModuleFileNameA获取程序的路径,因为这个获取到的是char类型,Unicode都是占两个字节的,所以最后的长度要成x2,再加上两个双引号需要+4,就是命令行的长度了,顺便贴一下获取长度的代码,当然现在看起来就有点..呆
windows7是fs:[0x30]+0x1000
windows10是fs:[0x30]-0x1000
所以前面还加了判断版本
1 2 3 char path[MAX_PATH];GetModuleFileNameA(NULL , path, sizeof (path)); int filenamelen = strlen (path)* 2 + 4 ;
可以看到这个方法就是凑的,在写这篇文章的时候发现了比这个好的方法
0x05 新思路 其实不能算是好的方法,前面写出这个方法就是对结构的不熟悉,明明已经知道PEB结构了,当时写出来的时候还沾沾自喜,现在想想有点丢人
下面是获取命令行指针的过程
PROCESS_BASIC_INFORMATION->PebBaseAddress->ProcessParameters->CommandLine->Buffer
fs:[0x30]对应的就是PebBaseAddress
下面就可以观察PEB结构通过偏移来定位指针的位置
1 2 3 4 5 BYTE Reserved1[2 ]; BYTE BeingDebugged; BYTE Reserved2[1 ]; PVOID Reserved3[2 ]; PPEB_LDR_DATA Ldr;
BYTE是一个字节,32位指针PVOID是四个字节,PPEB_LDR_DATA也是一个指针是四个字节
第一个BYTE是一个数组有两个元素,所以是两个字节,第四个参数有两个指针所以要加两个4
偏移就是2+1+1+4+4+4=0x10,那么下一个地址就是PebBaseAddress
这里的00F91ED0就是指向PebBaseAddress的地址
在x32dbg中过去,然后观察PRTL_USER_PROCESS_PARAMETERS结构
1 2 3 4 BYTE Reserved1[16 ]; PVOID Reserved2[10 ]; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine;
16字节+10个指针=16+40
这里都是十进制加法要转成16进制的
PebBaseAddress+0x10+0x28就是ImagePathName的位置
1 2 3 USHORT Length; USHORT MaximumLength; PWSTR Buffer;
USHORT就是short,short占两个字节
第一个红框就是ImagePathName这个结构里面的参数,44就是Length,46就是MaximumLength 后面这个指针就是指向路径名称的位置的指针
第二个红框就是CommandLine,后面都是一样的
00F923DE就是指向命令行的指针,可以过去看一下
那这样就可以准确定位到位置了,不需要像上一种方法循环才可以找到
当然现在关键是64位的,64位和32位的区别就是64位的指针占8个字节
那么PebBaseAddress的偏移就是2+1+1+4+8+8+8=0x20
这里有点疑问,0104后面的0000好像不在这个结构里面,用windbg查看也是没有说这个位置是什么意思,希望知道的师傅告知一下,提前谢谢,包括后面的UNICODE_STRING结构有点问题
这个指针有8位,Windows是小端所以是0000000000DD24A0,但是32位只能取后四个字节,后四个直接就是PRTL_USER_PROCESS_PARAMETERS的地址
接下来就和上面那步一样16+80=0x10+0x50,00DD24A0+0x10+0x50就是路径名称的结构了
USHORT这边和上面一样多了0000,指针是8个字节,需要+0x10可以到命令行的结构
过程知道了之后剩下的就用汇编来实现了
1 2 3 4 MOV EAX, FS: [0x30] - 0x1000 ;32位PEB的位置 SUB EAX, 0x1000 ;定位64位PEB MOV EAX, [EAX + 0x4 + 0x18 + 0x4] ;PRTL_USER_PROCESS_PARAMETERS MOV EAX, [EAX + 0X10 + 0X50 + 0x10 + 0x8] ;CommandLine.Buffer
贴一下最后的代码
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 64 65 66 67 68 #include <stdio.h> #include <Windows.h> #include <winternl.h> #include <string> #pragma warning (disable: 4996) LPVOID peb64 () { DWORD Version = GetVersion(); PVOID PEB64; if (((DWORD)HIBYTE(LOWORD(Version))) >= 2 ) { __asm { MOV EAX, FS:[0x30 ] SUB EAX, 0x1000 MOV EAX, [EAX + 0x4 + 0x18 + 0x4 ] MOV EAX, [EAX + 0x10 + 0X50 + 0x10 + 0x8 ] MOV PEB64,EAX } } else { __asm { MOV EAX, FS: [0x30 ] ADD EAX, 0X1000 MOV EAX, [EAX + 0x4 + 0x18 + 0x4 ] MOV EAX, [EAX + 0x10 + 0X50 + 0x10 + 0x8 ] MOV PEB64, EAX } } return PEB64; } typedef NTSTATUS (WINAPI* typedef_NtQueryInformationProcess) ( HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength ) ;int main () { HMODULE Ntdll = LoadLibraryA("ntdll.dll" ); HANDLE hProcess = GetCurrentProcess(); PROCESS_BASIC_INFORMATION pbi = { 0 }; RTL_USER_PROCESS_PARAMETERS Param = { 0 }; wchar_t lpwszCmd[] = L"explorer.exe" ; PEB peb = { 0 }; if (hProcess == NULL ) { printf ("OpenProcess ErrorCode: %d" , GetLastError()); return -1 ; } ULONG len = 0 ; typedef_NtQueryInformationProcess NtQueryInformationProcess = (typedef_NtQueryInformationProcess)GetProcAddress(Ntdll, "NtQueryInformationProcess" ); NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof (pbi), &len); if (!NT_SUCCESS(status)) { printf ("Fail" ); return -1 ; } USHORT usCmdLen = 2 + 2 * wcslen(lpwszCmd); ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof (peb), NULL ); ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof (Param), NULL ); WCHAR *cmd = peb.ProcessParameters->CommandLine.Buffer; LPVOID rwgcmd = peb64(); BOOL ret = WriteProcessMemory(hProcess, rwgcmd, lpwszCmd, sizeof (lpwszCmd), NULL ); BOOL ret32 = WriteProcessMemory(hProcess, pbi.PebBaseAddress->ProcessParameters->CommandLine.Buffer, lpwszCmd, sizeof (lpwszCmd), NULL ); return 0 ; }
当然如果要修改别的进程,可以先获取PEB的指针转成int然后加加减减最后转回指针类型就行了
0x06 总结一下 也没啥感觉自己写的文章话有点多,简单说就是语文表达能力不好,但是在写文章的过程中也学到了新的方法,当时写出这个代码来的时候感觉自己已经懂了,现在回头看发现还有很多细节不是很清楚,写这篇文章也是温故而知新,当然文章中的还有些问题要是学习到了会在后续的文章中补充