文章首发先知:https://xz.aliyun.com/t/14937
本公众号技术文章仅供参考! 文章仅用于学习交流,请勿利用文章中的技术对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责。 |
【_LIST_ENTRY详解】shellcode免杀之动态获取API
11.1 动态获取API的意义
首先我们要知道什么是导入表,在PE结构中导入表用于描述程序在运行时需要使用的外部函数和库,操作系统会根据导入表中的信息来定位并加载所需的 DLL,并将程序与这些外部函数进行链接。
举个例子:
这里代码如下图所示,我使用VirtualAlloc
去申请了一块内存执行shellcode
然后我们用工具查看我们的PE文件,在导入表中就看到了Kernel32.dll
中存在VirtualAlloc
函数,那么我们的编写的程序当调用这个函数的时候,就会通过我们的导入表获取到该函数的相关信息,然后计算得出在内存中的地址,最后去调用
所以如果杀软如果对导入表进行一些检测,就能够发现我们使用了敏感函数,那么有什么办法能让其不显示在导入表中呢?当然,当我们自己用代码完成相关dll地址的寻找,以及对应函数的寻找,那么就不会体现在导入表中了
-
这里补充一下什么是kernel32.dll呢
kernel32.dll
是 Windows 中最基本和核心的动态链接库之一,包含了大量的系统级别函数,并且所有的Windows 程序,无论是窗口程序还是控制台程序,都会引用并依赖于kernel32.dll
所以这表明,只要我们运行一个PE那么它的内存中都会加载Kernel32.dll
-
除了kernel32.dll,还有一个ntdll.dll
ntdll.dll
是一个更底层的动态链接库,提供了与操作系统内部核心相关的函数和服务。事实上,kernel32.dll
中的一些函数最终会调用到ntdll.dll
中的函数,因为ntdll.dll
提供了更为底层的系统调用接口,甚至可以访问操作系统的内核态(Ring 0)既然所有的Windows 程序都会将kernel32.dll加载到内存中,而kernel32.dll又最终会调用到ntdll.dll,所以其实所有的Windows 程序也会将ntdll.dll加载到内存
我们正常去调用一个DLL中的某个函数代码如下
HMODULE hModule = LoadLibrary("kernel32.dll");
FARPROC funcAddress = GetProcAddress(hModule, "XXXX函数名");
所以我们只需要自己编写代码得到kernel32.dll的地址,以及去拿到我们kernel32.dll
的函数GetProcAddress
后,我们再想获取任何函数就简单了
11.2 TEB和PEB相关知识详解
动态函数获取,依赖的就是TEB和PEB
TEB:
TEB指的是线程环境块" Thread Environment Block ",用于存储线程状态信息和线程所需的各种数据。每个线程都有一个对应的TEB结构体,并且 TEB 结构的其中一个成员就是 PEB。
TEB在x64程序中,地址为GS寄存器的值
其实从上方的描述不难看出,我们使用TEB的唯一作用就是通过它找到PEB
PEB:
进程环境块,包含系统与当前进程关联的用户模式下的所有参数,比如载入了的DLL的名字,进程开始处的参数,堆地址,检查当前进程是否处于调试状态以及DLL的镜像基地址等,当然涉及PEB整体概念完全理解后,应用就比较简单了,这里主要讲解的是动态获取API,当然PEB也可以用来进程伪装和反调试
因为前面讲了我们所有PE程序,都会将kernel32.dll和ntdll.dll载入内存,所以我们就可以利用PEB来找到相关dll的地址,然后获取对应函数了
OK,我们整体流程如下(我们可以去这个网站了解代码结构https://www.vergiliusproject.com/或者通过Windbg分析本机程序):
以64为例,我们找下TEB,然后发现在TEB的偏移0x60
处就是PEB了
我们跟进PEB,然后看0x18位置是一个Ldr指针,它是一个_PEB_LDR_DATA结构体,它里面存储了各种与模块加载相关的信息,如进程加载的所有模块(DLL)的信息
我们跟进Ldr,然后发现在偏移为0x10,0x20,0x30的地方有三个双向链表,而这三个双向链表里面就存着我们想要的dll信息了,我们可以通过他们获取dll地址,这三个都能获取,只不过是他们存dll的顺序不同
这三个双向链表都是这个结构_LIST_ENTRY
,如下图,要注意的是在上图中三个双向链表0x10
,0x20
,0x30
地址处确实都为下面的结构只占16个字节
紧接着上图中Flink指向链表的下一个结构,而Blink指向上一个结构,而他们所指向的不单单是_LIST_ENTRY
了,而是下图中_LDR_DATA_TABLE_ENTRY
里面的对应的_LIST_ENTRY
的Flink
如果没理解,没有关系,直接看下面这张图
接下来我们去调试来验证一下
我这里以计算器calc.exe
为例子,拖到x64dbg,然后双击GS寄存器,就跳转到了对应TEB的地址,然后我们找偏移是60的地方,对应地址是000000AE5BF5C060
,这个就是我们PEB的地址了
我们按ctrl+G后输入PEB地址,跳转过去,在PEB中我们找偏移为0x18的地址,就是Ldr了,如下图,对应的值00007FFFBD434380
就是LDR了
同样我们继续跳转过去,然后对应偏位为10的地方就是我们内存加载所有pe的双向链表InLoadOrderModuleList
了,对应地址为链表的开头结构为_LIST_ENTRY
,然后对应Flink地址为000001B1739458E0
,这个地址就对应第一个_LDR_DATA_TABLE_ENTRY
结构了
同样我们跳到第一个_LDR_DATA_TABLE_ENTRY
去,然后去找他偏移为0x48的地方,就是它的FullDllName结构,而这个结构的后8个字节就是名字的地址,也就是0x50的地方是它的名称所在地址,这里是000001B1739452A0
我们继续跳转过去,就知道对应名称了,这里是calc自己
然后我们回过头来继续看刚刚那张图,如下,现在可以联想到一个事情,就是既然它是个环形,那么入口的那个_LIST_ENTRY
的Flink指向下第一个内存加载模块,而Blink指向最后一个加载模块,而且我们知道LDR_DATA_TABLE_ENTRY
结构的长度为0x138,既然如此我们就可以通过它来计算这个程序运行后往内存中加载了多少个模块
同样我这里还是以刚刚那个程序为例子,然后找到第一个_LIST_ENTRY如下,接下来我们用(Blink-Flink)%0x138
,那么我这里应该是000001B17394D390-000001B1739458E0
%0x138,结果是多少呢?
计算结果如下,对应10进制也就是100,但是实际上我们还得加1,因为我们Blink-Flink,这两个代表最后一个LDR_DATA_TABLE_ENTRY的起始地址后第一个LDR_DATA_TABLE_ENTRY的起始地址,所以我们计算出来的结果是缺少最后一个LDR_DATA_TABLE_ENTRY的,所以要加1(小学数学),所以代表calc运行后加载了101个某块
接下来我们用Process Monitor
验证一下,将过滤器按照如下设置(我们在最新win11中启动calc.exe,最后其实会去执行CalculatorApp.exe
)
然后运行,得出结果101
11.3 获取API
前面已经了解了TEB和PEB相关的基础知识,然后我们这里还需要知道一下,因为我们的pe程序都会加载exe本身以及kernel.dll和ntdll.dll,所以他们在双向链表里面位置是固定的,例如kernel32在InInitializationOrderModuleList
里是第三个模块,所以我们可以通过汇编来找到它的地址
.code
getknel proc
mov rax,gs:[60h] ;rax为PEB
mov rax,[rax+18h] ;rax为ldr
mov rax,[rax+30h] ;rax为InInitializationOrderModuleList的Flink
mov rax,[rax] ;第一个模块
mov rax,[rax] ;第二个模块
mov rax,[rax+10h] ;第三个模块的InInitializationOrderLinks后面偏移0x10,就是第三个模块的DllBase
ret
getknel endp
end
我们的C语言代码如下:
#include <Windows.h>
#include <stdio.h>
//简单说就是从汇编里获取函数
extern "C" PVOID64 getknel();
//声明函数指针
typedef LPVOID(WINAPI* pVAlloc)(LPVOID, DWORD, DWORD, DWORD);
PVOID myGetAddress(PVOID pBaseAddress, PCHAR pszFunctionName)
{
PVOID get_address = 0;
ULONG ulFunctionIndex = 0;
// 声明DOS头,指针此时指向PE文件kernel32第一个结构DOS头的开始位置,同时也是kernel32开始位置
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
// 通过e_lfanew找到NT头
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
// 通过可选PE头的数据目录表找到导出表地址
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
// 获取导出函数个数
ULONG ulNumberOfNames = pExportTable->NumberOfNames;
// 导出函数地址表
PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
PCHAR lpName = NULL;
// 遍历导出表,找到我们想要找到的导出函数并返回其地址
for (ULONG i = 0; i < ulNumberOfNames; i++)
{
lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]);
if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName)))
{
USHORT uHint = *(USHORT*)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint);
get_address = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr);
break;
}
}
return get_address;
}
int main()
{
char shellcode[] = "";
//将VirtualAlloc地址赋值给指针
pVAlloc VAlloc = (pVAlloc)myGetAddress(getknel(), (PCHAR)"VirtualAlloc");
void* exec = VAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();
}
然后我们可以正常执行
然后我们这时候如果查看我们的导入表,里面是没有VirtualAlloc
的
11.4 编译汇编代码
这里补一下在VS code里面执行汇编需要做的操作
在我们的项目上右键,然后选择生成依赖项
--生成自定义
勾选masm
紧接着在我们asm文件上右-->属性,常规这里选择项类型为自定义生成工具
然后在命令行和输出这里,写下如下配置
ml64 /c %(fileName).asm
%(fileName).obj;%(Outputs)
接下来编译运行,就可以了
原文始发于微信公众号(小惜渗透):bypass5 -【_LIST_ENTRY详解】shellcode免杀之动态获取API
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论