• 我们在哪一颗星上见过 ,以至如此相互思念 ;我们在哪一颗星上相互思念过,以至如此相互深爱
  • 我们在哪一颗星上分别 ,以至如此相互辉映 ;我们在哪一颗星上入睡 ,以至如此唤醒黎明
  • 认识世界 克服困难 洞悉所有 贴近生活 寻找珍爱 感受彼此

恶意代码分析实战:第十二章 隐蔽的恶意代码启动

恶意代码分析实战 云涯 4年前 (2020-09-09) 2352次浏览

为了隐藏自身,恶意代码的编写者开发了多种技术,将恶意代码混淆到正常的Windoes资源空间中。

1. 启动器

启动器也叫加载器,是一种设置自身或其他恶意代码片段以达到即时或将来秘密运行恶意代码。启动器目的是安装一些东西,以使恶意行为对用户隐藏。

启动器通常包含它要加载的恶意代码,最常见情况是在它的资源节中包含一个可执行文件或者DLL。如果资源节被压缩或者加密,则恶意代码必须在加载之前进行资源节提取,常常使用FindReSource,LoadResource,SizeofRecource。

2. 进程注入

进程注入是将代码注入到另一个正在运行的进程中,而被注入的进程会不知不觉运行恶意代码。这种注入也试图绕过基于主机的防火墙和那些针对进程的安全机制。

2.1 DLL注入

DLL注入是进程注入的一种形式。它强迫一个进程加载恶意DLL程序,同时它也是最常使用的秘密加载技术。DLL注入将代码注入到一个远程进程,并让远程进程调用LoadLibrary,从而强制远程进程加载一个DLL程序到它的进程上下文。一旦被感染的进程加载恶意DLL程序, 操作系统会自动调用DllMain函数执行恶意功能.

如下,启动器注入DLL到IE浏览器中,因此DLL与lE浏览器拥有相同访问网络的权限.注入之前, 进程S级防火墙会探测到启动器恶意代码并封锁它, 所以恶意代码不能访问网络.

images

① 为了将恶意DLL 注入到一个主机进程,启动器恶意代码必须获取受害进程句柄. 常用函数是 CreateToolhelp32Snapshot, Process32First, Process32Next来查找进程列表中的目标进程.一旦发现目标进程的PID, 然后使用OpenProcess提取PID, 获取目标进程句柄.

线程入口点可能是LoadLibrary函数地址, 并让恶意DLL名字作为它的参数, 这会触发受害进程恶意DLL名字作为参数调用LoadLibrary, 因此恶意DLL就可以加载到受害进程中.

② 使用VirtualAllocEx为恶意DLL的字符串创建内存空间. 如果提供远程进程句柄, VirtualAllocEx函数在远程进程中分配内存空间.

③ 使用WriteProcessMemory将恶意DLL程序字符串写入到VirtuallAllocEx分配的内存空间中.

④ 使用CreateRemoteThread函数远程创建一个新的线程, 需要传递三个重要参数: OpenProcess获得的进程句柄(hProcess), 注入线程入口点(IpStartAddress), 以及线程参数(hParameter).

images

① OpenProcess获取目标进程句柄

② VirtuallAllocEx远程创建空间

③ WriteProcessMemory将恶意DLL字符串写入内存空间

④ DLL注入启动器目的是用恶意DLL作为参数, 调用CreateRemoteThread创建远程线程LoadLibrary.

2.2 直接注入

直接注入是直接将恶意代码注入到远程进程中. 如果想要注入的代码不对宿主进程产生副作用的前提下成功运行, 直接注入需要大量定制代码.

通常调用两次VirtualAllocEx 和 WriteProcessMemory

第一次调用是分配内存空间并写入远程线程使用的数据; 第二次是调用分配内存空间并且写入远程线程代码.

CreateRemoteThread调用包含远程线程代码的位置(IpStartAddress)和数据(IpParameter)

由于远程线程使用的数据和函数都必须包含在受害进程空间中, 所以正常编译的程序都无法工作.

3. 进程替换

将一个可执行文件重写到一个运行进程的内存空间. 进程替换的关键是以挂起状态创建一个进程. 意味着这个进程将会被载入内存, 但是它的主线程被挂起, 恶意程序恢复主线程则开始执行.

images

images

images

① CreateProcess 创建挂起进程

② ZwUnmapViewOfSection 释放由参数指定所有内存

③ 解除内存映射后,加载器通常执行VirtuallAllocEx为恶意代码分配新的内存

④ WriteProcessMemory将恶意代码每个段写入到受害进程的内存空间

⑤ SetThreadContext函数让入口点指向恶意代码, 让其获得运行

⑥ ResumeTheread函数初始化恶意代码并进行执行

4. 钩子(Hook)注入

钩子注入是一种利用Windoes钩子(Hook)加载恶意代码的方法,恶意代码用它拦截发往某个应用程序的消息. 恶意代码使用挂钩注入,来完成以下两件事情.

  • 保证无论何时拦截到一个特殊消息,恶意代码都会被运行
  • 保证一个特殊的DLL被载入到受害进程的内存空间

images

用户产生的事件被发送到操作系统, 然后 操作系统 再发送这些事件创建的消息 到 注册接收它的线程. 而恶意DLL在线程接收之前进行了拦截.

4.1 本地和远程钩子(Hook)

  • 本地钩子被用来观察和操纵发往进程内部的消息
  • 远程钩子被用来观察和操纵发往一个远程进程的消息

远程钩子有两种形式:

  • 上层远程钩子: 要求钩子例程是DLL程序的一个导出函数. 它被操作系统映射到被挂钩线程或者系统所有线程的进程地址空间.
  • 底层远程钩子: 要求钩子例程被保护在安装钩子的进程中. 这个例程在操作系统获得处理事件的机会前被通知.

4.2 使用钩子的击键记录器

击键可以分别使用 WH_KEYBOARD 和 WH_KEYBOARD_LL 钩子例程类型,来注册上层和底层钩子.

对于 WH_KEYBOARD例程,钩子通常运行在远程进程的上下文空间, 也可以运行在安装钩子的进程空间中.

对于WH_KEYBOARD_LL 例程, 事件直接发送到安装钩子的进程,所以钩子运行在创建钩子进程中.

4.3 使用SetWindowsHookEx

用来执行Windows挂钩主要函数, 拥有如下参数:

idHook : 指定要安装的钩子例程的类型

lpfn : 钩子例程指针

hMod : 对于上层钩子,它用来标识lpfn定义的钩子例程的DLL句柄. 对于底层钩子,它用来标识包含lpfn例程的本地模块句柄.

dwThreadId: 指定与钩子例程关联的线程标识.

4.4 目标线程

指定单线程为目标, 要求 查找进程列表中的目标进程, 如果碰到目标没有运行, 恶意代码要先删除它. 如果一个恶意应用程序挂钩一个经常使用的Windows消息, 它很可能会触发入侵防御系统,所以恶意代码应用程序经常使用一个不常用的消息, 如WH_BT(一个用于计算机训练的消息).

images

① LoadLibraryA加载了恶意的DLL hook.dll

② 局部调用GetNotepadThreadId获取notepad.exe的dwThreadId.

③ 调用SetWindowsHookEx函数发送WH_BT消息.

④ 一旦hook.dll被注入, 它就可以运行DllMain中所有恶意代码, 并且伪装成notepad.exe进程.

5. Detours

Detours是微软1999年开发的一个代码库. 恶意代码修改PE结构, 创建一个名为.detour的段, 它通常位于导出表和调试符号之间. .detour段在新的导入地址表中包含了原始PE头部. 使用Detours库提供了的setdll工具, 恶意代码编写者修改PE头部, 使其指向新的导入表.

images

在①处的.detour段中, 新的导入表包含evil.dll ,如②所示. 现在无论notepad.exe 何时启动, evil.dll 都将被加载.

6. APC注入

线程创建需要系统开销, 所以调用一个现有线程会更加有效.

APC可以让一个线程在它正常执行路径运行之前执行一些其他代码. (在我开始干活之前,我先干些别的)

每个线程都有一个附加的APC队列, 它们在线程处于可警告的等待状态时被处理.

例如它们调用  WaitForSingleObjectEx , WaitForMultipleObjectsEx, SleepEx函数, 这些函数给了线程一个处理等待APC的机会

应用程序在线程可警告等待状态时(未运行之前)排入一个APC队列, 那么线程将从调用APC函数开始. 线程逐个调用APC队列中的所有APC, 当APC队列执行完毕时, 线程才继续沿着它规定的路径执行.

恶意代码用APC抢占可警告等待状态的线程.

APC有两种存在形式:

  • 内核模式APC : 为系统或者驱动生成的APC
  • 用户模式APC: 为应用程序生成的APC

6.1 用户模式下APC注入

用户模式下, 一个线程可以使用API函数QueueUserAPC 排入一个让远程线程调用的函数. 运行用户模式的APC要求线程必须处于可警告等待状态, 因此恶意代码会查看进程中是否有可能进入这个状态的目标线程.

WaitForSingleObjectEx是最常用的API调用

QueueUserAPC有三个参数: pfnAPC, hThread, dwData.  该函数要求句柄为hThread的线程使用参数dwData运行pfnAPC定义的函数.

images

① 获取目标线程, 则使用OpenThread打开线程句柄

② 使用函数QueueUserAPC, 其中pfnAPC被设置为LoadLibraryA, 传递给LoadLibrary的参数存在于dwData中. 意味着一旦这个目标线程处于等待状态, 则LoadLibraryA 会被远程线程调用, 从而导致目标进程加载dbnet.dll

③ svchost.exe是经常被APC注入的线程

6.2 内核模式APC注入

恶意代码驱动和Rootkit也常常希望用户空间中执行代码,但是这并不容易. 因此可以在内核空间中执行APC注入.

恶意驱动创建一个APC, 然后分配用户模式进程中的一个线程运行它. 通常是shellcode

主要使用两个函数使用APC: keInitializeAPC, KeInsertQueueAPC.

images

① 使用keInitializeAPC 进行初始化. 初始化一个KAPC结构, 它必须传递给函数KeInserQueueAPC, 以将APC对象放在目标线程相应的APC队列中.

如果 上图 ①和②分别被设置为非0和1, 则查找用户模式APC

② 一旦KeInserQueueAPC调用成功, APC将被排队运行

7. 程序实操

7.1 Lab 12-1  DLL注入

使用EnumProcesses枚举进程,获取句柄后比较是否是explorer.exe,之后打开explorer.exe,使用VirtualAllocEx在explorer.exe开辟空间,使用WriteProcessMemory将Lab12-01.dll绝对路径字符串写入explorer.exe,再利用CreateRemoteThread在进程explorer.exe内创建远程线程,线程入口是LoadLibraryA,传递给LoadLibraryA的参数是Lab12-01.dll绝对路径字符串,这样就强迫explorer.exe加载了Lab12-01.dll并执行恶意功能。

images

7.2 Lab 12-2  进程替换

在IDA的结构体中Structures中导入_CONTXT(线程结构体),在004011c3的004h处单击鼠标邮件,选中_CONTEXT模板,可以发现004h就是CONTEXT._Ebx,来引用新进程EBX寄存器的,ebx总是包含一个指向进程环境块也就是PEB数据结构。再偏移8,也即是被加载的可执行文件的起始部分指针。通过ReadProcessMemory读取四个字节。再利用NtUnmapViewOfSection释放掉新创建的进程的内存空间。

images

在使用VirtualAllocEx分配可读可写可执行的内存空间,再使用WriteProcessMemory写入数据。加载一个可执行文件到挂起进程。

SetThreadContext让入口点指向恶意代码,再利用ResumeThread初始化恶意代码并执行。

这样就把svchost.exe替换成了恶意代码。

images

7.3 Lab 12-3

程序使用SetWindowsHookExA设置idHook=0D(13)(WH_KEYBOARD_LL)监视键盘,通过查看msdn,知道WH_KEYBOARD_LL回调函数是LowLevelKeyboardProc,可以将fn函数的参数解析为实际的数据结构。

images

LowLevelKeyboardProc第三个参数是KBDLLHOOKSTRUCT,因此在fn函数顶部,将第三个结构修改为KBDLLHOOKSTRUCT,之后就可以解析。

images

在sub_4010C7中,使用CreateFileA创建practicalmalwareanalysis.log用来记录键盘输入。利用GetForegroundWindow选择按键按下去的活动窗口,利用GetWindowTextA获取窗口标题,这样就获取了按键来源。之后虚拟按键作为索引去查找。

images

images

7.4 Lab 12-4

 

7.5 详细解释一下通过HOOK进行DLL注入

消息钩子:Windows操作系统为用户提供了GUI(Graphic User Interface,图形用户界面),它以事件驱动方式工作。

在操作系统中借助键盘、鼠标、选择菜单、按钮、移动鼠标、改变窗口大小与位置等都是事件。

发生这样的事件时,操作系统会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的信息后会执行相应的动作。也就是说,在敲击键盘时,消息会从操作系统移动到应用程序。

所谓的消息钩子就是在此期间偷看这些信息。以键盘输入事件为例,消息的流向如下:

1.发生键盘输入时,WM_KEYDOWN消息被添加到操作系统的消息队列中;

2.操作系统判断这个消息产生于哪个应用程序,并将这个消息从消息队列中取出,添加到相应的应用程序的消息队列中;

3.应用程序从自己的消息队列中取出WM_KEYDOWN消息并调用相应的处理程序。

当我们的钩子程序启用后,操作系统在将消息发送给用用程序前会先发送给每一个注册了相应钩子类型的钩子函数。钩子函数可以对这一消息做出想要的处理(修改、拦截等等)。

多个消息钩子将按照安装钩子的先后顺序被调用,对同一事件消息可安装多个钩子处理过程,这些消息钩子在一起组成了”钩链”。消息在钩链之间传递时任一钩子函数拦截了消息,接下来的钩子函数(包括应用程序)将都不再收到该消息。

使用API函数SetWindowsHookEx()把一个应用程序定义的钩子子程安装到钩子链表中

SetWindowsHookEx函数总是在Hook链的开头安装Hook子程。当指定类型的Hook监视的事件发生时,系统就调用与这个Hook关联的 Hook链的开头的Hook子程。每一个Hook链中的Hook子程都决定是否把这个事件传递到下一个Hook子程。Hook子程传递事件到下一个 Hook子程需要调用CallNextHookEx函数。

线程勾子用于监视指定线程的事件消息。线程勾子一般在当前线程或者当前线程派生的线程内。
系统勾子监视系统中的所有线程的事件消息。
因为系统勾子会影响系统中所有的应用程序,所以勾子函数必须放在独立的动态链接库(DLL) 中。
系统自动将包含”钩子回调函数”的DLL映射到受钩子函数影响的所有进程的地址空间中,即将这个DLL注入了那些进程。
当SetWindowsHookEx函数调用成功后,当某个进程生成这一类型的消息时,操作系统会判断这个进程是否被安装了钩子,如果安装了钩子,操作系统会将相关的dll文件强行注入到这个进程中并将该dll的锁计数器递增1。然后再调用安装的钩子函数。
接下来是代码层次解释如何使用HOOk进行DLL注入:
这是单独的程序,用来加载DLL:
#include "stdafx.h"
#include <Windows.h>
#include<TlHelp32.h>
#include<iostream>

using namespace std;

BOOL InjectDllBySetWindowsHook(ULONG32 ulTargetProcessID);
DWORD getThreadID(ULONG32 ulTargetProcessID);

int main()
{

ULONG32 ulTargetProcessID;
cout << "请输入目标进程ID:";
cin >> ulTargetProcessID;

if (!InjectDllBySetWindowsHook(ulTargetProcessID))
{
cout << "Set Hook Unsuccess!\r\n" << endl;
return 0;
}
cout << "Inject Success!\r\n" << endl;
return 0;
return 0;
}


BOOL InjectDllBySetWindowsHook(ULONG32 ulTargetProcessID)   //ulTargetProcessID是目标进程ID
{
HANDLE TargetProcessHandle = NULL;
TargetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ulTargetProcessID);  //打开目标进程句柄

if (NULL == TargetProcessHandle)
{
printf("Couldn't get Target Process Handle\r\n");
return FALSE;
}

HMODULE DllModule = LoadLibrary(L"Dll.dll");    //加载恶意的DLL

if (DllModule == NULL)
{
printf("cannt find dll\r\n");
return FALSE;
}

//获取Dll中导出的函数的地址
HOOKPROC Sub_1Address = NULL;
Sub_1Address = (HOOKPROC)GetProcAddress(DllModule, "MyMessageProcess");  //获取DLL中的导出函数,也就是钩子函数
if (Sub_1Address == NULL)
{
printf("cannt found MyMessageProcess");
return FALSE;
}

DWORD ThreadID = getThreadID(ulTargetProcessID);  //获取目标进程的目标线程ID

HHOOK Handle = SetWindowsHookEx(WH_KEYBOARD,      //使用SetWindowsHookEx获取键盘消息,然后钩子函数是Sub_1Address,
Sub_1Address, DllModule, ThreadID);               //也即是DLL里的MyMessageProcess处理键盘消息的函数
                                                  // 然后钩子回调句柄设置成了DLLModule,线程ID设置为刚刚获取到的目标线程ID

if (Handle == NULL)
{
printf("cannt hook\r\n");
return FALSE;
}
printf("hook success\r\n");
getchar();
getchar();
getchar();
UnhookWindowsHookEx(Handle);

FreeLibrary(DllModule);
}


DWORD getThreadID(ULONG32 ulTargetProcessID)                    //获取线程ID
{
HANDLE Handle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (Handle != INVALID_HANDLE_VALUE)
{
THREADENTRY32 te;
te.dwSize = sizeof(te);
if (Thread32First(Handle, &te))
{
do
{
if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID))
{
if (te.th32OwnerProcessID == ulTargetProcessID)
{
HANDLE hThread = OpenThread(READ_CONTROL, FALSE, te.th32ThreadID);
if (!hThread)
{
printf("Couldn't get thread handle\r\n");
}
else
{
return te.th32ThreadID;
}
}
}
} while (Thread32Next(Handle, &te));
}
}
CloseHandle(Handle);
return (DWORD)0;
}
#pragma data_seg(SHARD_SEG_NAME) 
static HHOOK g_hHook;
#pragma data_seg()

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
MessageBox(NULL, L"Inject Success!", L"1", 0);
}

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

extern "C"

__declspec(dllexport)LRESULT MyMessageProcess(int Code, WPARAM wParam, LPARAM lParam)
{
// 
//你自己对消息的处理 
// 
MessageBox(NULL, L"GetMessage!", L"Message", 0);
return CallNextHookEx(g_hHook, Code, wParam, lParam);
}

 

 


云涯历险记 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:恶意代码分析实战:第十二章 隐蔽的恶意代码启动
喜欢 (0)