写个壳

[TOC]

已有

1
2
3
4
压缩区段
加密区段
清除数据目录表
修复重定位

目标

1
2
3
1.能够突破看雪的虚拟化检测dbg插件
2.FUD “FUD”代表恶意软件完全不可被检测到的意思
3.从攻击者角度来研究病毒查杀技术

注意事项

1
2
3
4
5
6
7
8
9
10
11
1.   最核心的就是伪装,完全模拟正常PE程序。

2. 壳代码没有异常行为,不触发杀软的检测规则。

3. 傀儡文件的OEP代码尽量避免影响被加壳程序的正常执行

4. 核心功能为减少暴露面,提高做免杀的速度

5. 通过混淆引擎混淆壳代码,达到混淆后无特征代码。

6. 支持壳上壳,通过加其他VM壳处理内存特征码、反分析。

不能使用多线程来进行解压缩 因为在shell代码中线程化是一个非常糟糕的想法!

基础和高级

一般的加壳都是下面这些步骤

1
2
3
4
5
6
1.使用加壳器给被加壳程序添加新区段。
2.加密/压缩被加壳程序。
3.将stub的代码段移植到新区段。 stub.dll
4.将被加壳程序的OEP记录到share.h中。
5.将被加壳程序的EP设置到新区段。
6.保存为新文件。
1
2
3
4
5
6
7
8
9
10
在PE文件中开辟空间用于存储补丁代码
复制扩展PE头中的数据目录表(DataDirectory)的内容到存储补丁的空间中,复制完后清空原本的数据目录表
修正复制过去的数据目录表的内容
对修正完的数据目录表之后的数据(不包括补丁部分)进行加密
修改PE文件的程序入口为补丁代码
补丁代码还原前面被清空的数据目录表
补丁代码解密先前加密的数据
补丁代码加载导入表中需要导入的DLL
补丁代码修正IAT
补丁代码执行完后返回原本的程序入口
  • 加壳的难点在于将数据目录项复制到补丁代码部分,并修正复制后的数据目录项
  • 加壳中加密的核心是加密算法

一般壳都是带压缩的壳

压缩是一个比较复杂的过程,对于一个主要功能的加密的壳来说,压缩也有一定的加密效果,如果使用了一些加密库加密,即使你压缩了,会发现加壳后的文件比没加壳之前还要大!

vmp用不好,就变成了第三类壳:增积壳
作用:虽然没有虚拟化保护软件关键函数,但是可以增加软件体积,吓唬破解者,顺便增加检测虚拟机或是调试器附加的功能

很多常见的壳都用汇编写的,确实,汇编确实可以写出很多短小精悍、骚操作的代码,这是C++所没有的,但是C++支持内联汇编,在一定程度上弥补了它的不足。

加密节(除了tls和rsrc)

比如加密压缩的过程中每次可以随机使用不同的加密压缩算法,比如调用rar,zip,upx的压缩算法,比如使用DES、3DES、AES 、 RSA、DSA 、SHA-1、MD5等加密算法……或者这些都随机调用,每次生成算法都不同

压缩

哈夫曼树 upx

到现在我比较想写的是unishox2压缩算法siara-cc/Unishox2: Compression for Unicode short strings (github.com)和base-N压缩算法莫迪克|关于计算机安全的随机帖子 (wordpress.com)

反调试

参考VMP

学保护模式 ——》 为了突破UAC (也能学到反调试)

反调试 –》为了学加壳等

学tenprotect的保护机制 –》反向学习反调试(因为会学到Windbg双机调试的保护、ValidAccessMask清零的保护和DebugPort清零的保护)

对于PE结构的表的获取结构体 还有一种是利用特征码的获取结构体(只能获取某个内核api) 显然就没那么高操作 pe表获取结构体也会引用其中内容。所以pe结构是一个值得反复学习的东西

常规的反调试都是getparentpresent和beginndebuged来获取是否被调试。

2019年有一个文章用的是NtqueryInformationProcess(这个函数可以同时在0环和3环运行)的第二个参数传入ProcessDeubgpory并获取传出的nDebugPort来判断是否被调试

写出了好的反调试方法,可以放在壳代码的各个角落,检测到调试就马上退出程序,多放置几个阴人位置,这样就能增加破解的难度了!

在32位程序中,有人把钩子挂到64位的”ntdll.dll”上,然后来反反调试,可以用crc校验或者检测关键字节来反反反调试

https://www.52pojie.cn/thread-1277269-1-1.html

一般虚拟机沙箱的网卡还有文件默认设置 进程少等特点 可以用来检测一下

30个反调试方法(2016年)

https://github.com/wanttobeno/AntiDebuggers/blob/master/Tencent2016D.cpp

1

https://github.com/strivexjun/XAntiDebug/blob/master/XAntiDebug/XAntiDebug.cpp

anti-debug-popf

1

anti-debug-int2d

1

当然直接ollvm可以直接反调试

加密iat

IAT(导入地址表 import address table)每个元素的地址就是内存窗口的地址

这时候我们就需要去加密这些地址来干扰调试器对winapi的获取

加密原理

1
2
3
4
5
6
遍历导入表获取每个函数的IAT地址(对应上图内存栏中地址的值)
取出IAT地址的内容,就是函数的地址(上图内存栏中数值的值),把该函数地址进行加密后得到一个数据
申请一段内存,其中存放解密上述的数据得到真地址,然后调用该地址的代码。
把申请的内存地址放入IAT地址对应的数值中。
完成以上步骤后IAT就被加密了,当然第3步当中可以进行适当的混淆和加花指令别人就更加看不出来了。

hash单向散列(数值加密) 加密api函数的字符串 (因为一些有经验的逆向师会对字符串比较敏感)

众所周知1个字节是8位,这代表他表示2的8次方个数,也就是256种可能,如果我们把它的一个数据代表一个系统中的函数(API),相当于给函数一个序号,那么1个字节就能存储256个函数的信息,那2个字节就能存储2的16次方也就是65536个API函数,这真是大大的好消息, windows系统中的API函数也就几千个,2个字节存储其全部API函数信息真是绰绰有余。

而让这2个字节的数据代表一个函数,这个数据我们称它为Hash值,因此需要设计一个算法。我在这设计是方法是定义一个2字节类型(short)的数据,分别把nHash值先左移11位再右移5位后相加,再加上API函数中一个字符的Ascii码,以此循环遍历完整个API函数的所有字符,得到一个我们需要的Hash值。在之前写壳基础篇中提到过壳代码中的API是动态获取的,那么我们在动态获取的时候使用Hash值更能提高隐蔽性,使破解者不易发现我们所要使用的是哪个函数。
具体Hash加密代码如下:

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
#include "pch.h"
#include <iostream>

int main()
{
while (true)
{
//用于保存Hash值
unsigned short nHash = 0;
char arr[50] = {}, *p;
p = arr;
printf("请输入API: ");
scanf_s("%s", arr, 50);
while (*p)
{
//先左移11位再右移5位相加后再加上该字符的Ascii
nHash = ((nHash << 11) | (nHash >> 5));
nHash = nHash + *p;
p++;
}
printf("Hash值为:0x%X\n", nHash);
}
return 0;
}

使用纯汇编来在壳代码中写入解密函数的好处

1
2
3
4
5
6
1.代码少
2.更能锻炼基本功
3.可以用到一些骚操作
4.壳主要是指针的操作(加壳程序还得算上文件IO),汇编相对于C/C++操纵指针起来更加得心应手。
5.尤其是汇编可以直接操纵堆栈,由此有了两大优势:一、方便数据寻址,二、方便堆栈平衡。

hash加密(要有密钥的那种)

动态加密

加入动态解密的壳,这无疑是强度较高的壳了,它能够在目标程序运行起来之后,动态的对代码段进行解密。先运行一段代码解密后一部分的代码,然后再运行解密后的代码,可以往复循环,这样破解者只能看见运行着的代码的附近的代码,隔得远的代码处于加密状态,这样就需要花费大量的时间才能破解了,当然想要实现这种高强度,还是需要花费很多时间去设计的,而且要求我们对x86汇编语言有比较深刻理解,

解密代码才是动态解密中的核心点,重中之重。因为加密代码全部去加密就可。解密代码的话就将代码分段分时解密

这里要说一下GetPC技术,GetPC技术翻译为中文也就是获取指针计数器。在x86汇编中实际上就是获取当前代码EIP的技术。我这用的是call 指令,call xxx指令相当于 push 下一行代码的EIP + jmp xxx。 那么我们直接把XXX改为下一行指令的地址就能获取当前EIP

其他操作

在遍历还原导入表时,并没有直接将API的地址填入到IAT里,而是将节表5的地址,从起始位置开始,每隔16个字节,将地址填入到IAT里,然后在对应的节表5地址上填入push 真实函数地址 + retn的汇编指令。这样一来,原PE程序运行调用API时,就会跳到节表5里面,再从节表5里面跳到真实API地址,直接干掉了x64dbg的脱壳导入表自动修复功能。

虚拟壳(技术要求过高 以后写 “如果虚拟壳写的好直接拿去卖也不用工作 年入50w+”)

虚拟技术应用到壳的领域,设计了一套虚拟机引擎,将原始的汇编代码转译成虚拟机指令,要理解原始的汇编代码,就必须对其虚拟机引擎进行研究,而这极大地增加了破解和逆向的难度及成本。

随机花指令构造器

构造花指令,可以使用无条件跳转,比如ret、call、jmp来跳转,也可以使用有条件跳转,跳转的越多,能给人造成的困扰越大。

兼容

image-20221030200744494

xp-win11 x64 x32 都得兼容

内核型加壳器 .sys的

伪装OEP

压缩资源

注意 是压缩资源 不是压缩区段

换句话说就是加壳压缩

用到的有:

哈夫曼编码(好像已启用) UPX源码 IEXPRESS UPX ASProtect WinRAR NSPack DarkCrypt

alib原理

1:当压缩算法扫描到中间某段位置时,如何和前面的内容进行快速比较。

1
2
3
首先回答第一个问题,很简单,用哈希,笔者采用了FNV哈希算法,效果挺好的,用c++stl中的hash不知道效果如何,不过其实后来发现,哈希冲突在这里并不严重,一个好的哈希只能带来速度和内存的提升了,没办法提升压缩率的。然后我们如何解决哈希冲突呢,hash冲突这里其实是重点,所表达的就是可能出现了相同字节串。
我们先说这个算法如何工作,首先我们只对定长的几个字节算hash值,例如我们只算5个字节的hash值,然后保存hash,之后每扫描一个字节,取出当前位置往后5个字节,算hash,和前面的比较,如果hash值相同,则在进一步比较每个字节内容,hash可以很快的知道内容是不一样的,没有办法知道内容是一样的,所以我们还需要实际的去比较每个字节,这意味着,我们在保存hash的时候,还需要保存算得这个hash的字节在整个文件中的index,我们取出上一个相同hash值的字节的起始index,和当前位置往后比,一直比到文件结束或着出现不同,这里是一个重点,如:abcdefgabcabcabcabc,想象一下,这里如何压缩呢,好的做法是:abcdefg3339,第一组33代表offet是3,长度是3,第二个39offet是3(就是第一个03),长度是9,所以,相同字节串可以涵盖本身(39包含了自己)。
回到前面,我们应该如何设计保存hash的结构了,首先为了快速索引,应该用一个数组或者vector保存hash,既hash值作为下标,这样可以快速索引,如何解决hash冲突呢,easy,不解决,只保存,如我们构造一个长度为1000的数组,把数组分成100块,每块有10个元素,产生的hash值在0-99中间,现在算的hash值为1,那么在第一块的第一个位置填入此时的index,往后又算出hash1时,则在第1块的第二个位置填入,所以,这里的块数作为hash索引, 一块里面有多少个则代表可填入相同hash值的数量,所以如果数量很多,那么就覆盖前面的,这里可以用一个环形缓冲区实现。

2:如何区分当前是压缩的内容,还是未压缩的内容。

1
回答第二个问题,如何分别是原来未压缩的数据,还是保存的offsetlength呢,用VLQ编码,此处请goolge vlq编码,此外在实际验证中发现,offsetlength采用vlq编码后,往往length比较小,采用vlq至少需要一个字节,所以可以考虑将length编入offset中,既offset低位保存length,再节省空间。

哈夫曼编码

左节点右节点相加提供给单链

1
2
3
4
5
6
7
首先需要给出文件压缩和下面将要提到的文件解压缩的公共头文件
1.根据值和频率统计结果
2.利用结果创建哈夫曼树,得到相应的每个字符的哈夫曼编码
3.将数据写入文件
(1)校验头filehead
(2)将字符值和频率写入文件中
解压缩时重新创建hafuman数来译码

lzma (lz系列)

采用马尔科夫链主要利用马尔科夫随机过程来消除原始文件中的基于上下文的冗余(如英文中字母Q后面紧接的字母为U的概率远较其它字母大),而不仅仅是哈夫曼编码中单纯的基于字符出现的随机统计概率

就是abcdefgabc转换成abcdefg73

quicklz 号称世界上最快的压缩算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::string quicklz_compress(const std::string& src)
{
qlz_state_compress state;
memset(&state, 0, sizeof(qlz_state_compress));
std::string dst;
char buffer[4096 + 1024];
for(size_t pos = 0;pos<src.size();pos+=4096) {
size_t len = src.size() - pos;
len = len > 4096 ? 4096 : len;
len = qlz_compress(src.data() + pos, buffer, len, &state);
dst.append(buffer,len);
}
return dst;
}

在压缩的过程中不断地读入3个字节,然后根据这3个字节得到一个hash值,根据这个hash值就可以找到offset,这个offset就是上次这个hash值出现的位置,而通过cache可以判断出这次出现的和最近一次出现相同hash值的时候的3个字节是不是相同(可能hash相同而实际的值不同)。

level1 四个字节四个字节的hash对比 获取重复值

level2 对比4个offset的hash 选取其中最长的当模板

level3

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
if(matchlen == 3 && offset <= 63)
{
*dst = (unsigned char)(offset << 2);
dst++;
}
else if (matchlen == 3 && offset <= 16383)
{
ui32 f = (ui32)((offset << 2) | 1);
fast_write(f, dst, 2);
dst += 2;
}
else if (matchlen <= 18 && offset <= 1023)
{
ui32 f = ((matchlen - 3) << 2) | ((ui32)offset << 6) | 2;
fast_write(f, dst, 2);
dst += 2;
}
else if(matchlen <= 33)
{
ui32 f = ((matchlen - 2) << 2) | ((ui32)offset << 7) | 3;
fast_write(f, dst, 3);
dst += 3;
}
else
{
ui32 f = ((matchlen - 3) << 7) | ((ui32)offset << 15) | 3;
fast_write(f, dst, 4);
dst += 4;
}

在level=3的时候与前两个的不同之处是最低的两位统一作为标志

http://www.wjhsh.net/xumaojun-p-8541618.html

图形化

壳代码纯shellcode开发

将壳代码编译为Shellcode代码,方便移植、混淆、特征码定位。Shellcode开发方法论坛搜索即可,非常多的帖子。

API字符串隐藏

Shellcode编程的常规编写技巧,将API字符串转为HASH,壳代码通过HASH来获取API地址。此技术主要用于缩短Shellcode体积、干扰分析人员分析。

Shellcode动态获取外部参数

参考”图-CodeLoader数据结构”可以发现壳代码CodeLoaderCode(EntryPoint函数生成在壳代码CodeLoaderCode的首部)是储存在PARAM_CODE_LOADER结构体的尾部。

因此只需要动态定位到&EntryPoint函数的内存地址,然后减去sizeof(PARAM_CODE_LOADER)就可以获取到加壳器传递的参数数据了。

img

简单图片隐写技术(最大化的压缩体积)

入口点模糊技术

QVM引擎已经对主流编译器编译的程序,从入口点==(OEP)==开始提取==一段==特征代码作为判断依据,因此只要修改入口点代码QVM引擎就会报“HEUR/Malware.QVM20.Gen”。

杀软检测入口点代码的绕过方法:

1、 使用壳代码完全伪造主流编译器编译的程序的入口点特征

2、 不修改入口点开始处的代码,而是在QVM引擎提取的入口点特征代码的==尾部劫持执行流==(此壳就是使用的这个方法)img

内存加载PE&支持壳上壳

​ 当前使用的内存中加载PE技术,是通过将”被加壳程序”内存展开后,覆盖到当前进程的ImageBase处,随后修复”被加壳程序”的IAT表,设置区段属性,最终调用”被加壳程序”的OEP将执行权限交给”被加壳程序”。这种写壳方式的天生就支持壳上壳功能。

​ 壳上壳的功能主要是为了躲避内存查杀。可使用VMP、TMD、SE等虚拟化壳的代码虚拟化功能来模糊化被加壳程序的内存特征,当然使用自写的VM、混淆引擎更好,==不过写一个稳定、兼容性好的VM、混淆引擎耗时太长。==

加多重壳需要注意关闭内层壳的校验基址,如下图的VMP:img

兼容性较好的修复IAT表方法

使用双向链表

​ 兼容性较好的修复导入表(IAT)方法,优先使用INT表来获取API地址。解决有些编译器编译的程序导入表(IAT)不规范的问题(Delphi)。

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
//获取导入表首地址
IMAGE_DOS_HEADER* pDosHdr = (IMAGE_DOS_HEADER*)pImageBaseAddr;
IMAGE_NT_HEADERS* pNtHdr = (IMAGE_NT_HEADERS*)(pDosHdr->e_lfanew + (DWORD)pImageBaseAddr);
IMAGE_DATA_DIRECTORY* pDataDirHdr = (IMAGE_DATA_DIRECTORY*)pNtHdr->OptionalHeader.DataDirectory;
IMAGE_IMPORT_DESCRIPTOR* pImport = (IMAGE_IMPORT_DESCRIPTOR*)(pDataDirHdr[1].VirtualAddress + (DWORD)pImageBaseAddr);

while (pImport->OriginalFirstThunk != 0 || pImport->FirstThunk != 0)
{
//获得当前DLL名
char* chName = (char*)(pImport->Name + (DWORD)pImageBaseAddr);

//加载模块
HMODULE hModule = p_LoadLibraryExA(chName, 0, 0);

//如果有INT表则通过INT表来修复IAT表
DWORD* pReferenceTab = nullptr;
(pImport->OriginalFirstThunk == 0x0) || (pImport->OriginalFirstThunk == 0xFFFFFFFF) ?
pReferenceTab = (DWORD*)(pImport->FirstThunk + (DWORD)pImageBaseAddr) :
pReferenceTab = (DWORD*)(pImport->OriginalFirstThunk + (DWORD)pImageBaseAddr);

//被修复的IAT表
DWORD* pIatTab = (DWORD*)(pImport->FirstThunk + (DWORD)pImageBaseAddr);

DWORD dwIatIndex = 0;
while (pReferenceTab[dwIatIndex] != 0)
{
//判断是什么方式导入 <序号> <名称>
if ((pReferenceTab[dwIatIndex] & 0x80000000) == 0) //最高为1是序号导入
{
IMAGE_IMPORT_BY_NAME* pByName = (IMAGE_IMPORT_BY_NAME*)
(pReferenceTab[dwIatIndex] + (DWORD)pImageBaseAddr);

//获取到的API地址
pIatTab[dwIatIndex] = (DWORD)p_GetProcAddress(hModule, pByName->Name);
}
else
{
DWORD dwIndex = pReferenceTab[dwIatIndex] & 0x7FFFFFFF;
//获取到的API地址
DWORD dwApiAddr = (DWORD)p_GetProcAddress(hModule, (char*)dwIndex);
pIatTab[dwIatIndex] = (DWORD)dwApiAddr;
}
++dwIatIndex;
}
//指向下一个结构体
pImport += 1;
}

长时间执行垃圾指令进行沙箱逃逸

反注射

  • 使用EnumProcessModulesEx (32位、64位和所有选项)枚举模块
  • 使用工具帮助枚举模块32
  • 使用LdrEnumerateLoadedModules枚举流程LDR结构
  • 直接列举流程LDR结构
  • 具有GetModuleInformation的行走记忆
  • 隐藏模块的行走记忆

反dumping

  • 从内存中擦除PE头
  • SizeOfImage

定时攻击[反沙盒]

https://github.com/LordNoteworthy/al-khaser#antidebug

人类互动/通用[反沙盒]

反虚拟化/全系统仿真

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
### 注册表项工件**

### **文件系统工件**

### **目录工件**

### **记忆假象**

### **mac地址**

### **虚拟设备**

### **硬件设备信息**

### **DLL导出和加载的DLL**

### **系统固件表**

### **WMI**

### **中央处理器

Anti-Disassembly

  • 以恒定条件跳跃
  • 目标相同的跳转指令
  • 不可能拆卸
  • 函数指针
  • 返回指针滥用

其它操作

如果在外壳程序中,有复杂的操作,要用到容器,比如矢量、链表或者树,不能用Windows提供的标准模板库,因为这里面说不一定就会用到API,从而导致程序出错。
可以仿照标准模板库设计自己的库,比如设计一个vector容器

https://www.52pojie.cn/thread-1277269-1-1.html

在外壳里面怎么获取随机数的问题,可以直接逆向(直接改写源码)srand()和rand()两个函数,
以下的随机数函数就是照搬srand()和rand()

垃圾指令构造器的设计非常简单,难点在于垃圾指令的选择,有些指令是不能作为垃圾指令的,改变普通寄存器的指令不能用,比如AAA指令,会改变eax寄存器的值。具体参考Intel手册。

但是=======================================================

其实也不是不能用,可以对上下文分析看哪些寄存器暂时未被使用,然后对其赋值。如果有 CALL/JMP 可以暂时先跳过。

而且感觉插入 CALL/JMP 后很容易破坏自动分析工具?可以在垃圾代码中插入一堆看上去有用实际上没用流程控制/空函数调用。

1
2
3
4
5
6
; 比如这里可以放入操作 eax/ecx 的指令,如 add eax, ebx; call dword[nop]
mov eax, 1
; 比如这里可以放入操作 ecx 的指令,如 add ecx, eax
xor eax, ebx
; 这里 可以放入操作 ecx 的指令
mov ecx, 1

也可以插一段算法,比如趁着某个寄存器还未被使用的时候修改,然后用算法还原到之前的状态:

1
2
3
4
5
6
7
mov eax, 1
; 插入 add eax, 3
mov ecx, 1
; 插入 dec eax; sub eax, 2
; 插入这两段后,eax 就变回原来的值了(我记得 VMP 也有类似的处理,不过更复杂?)
; 也可以利用此时 ecx 为 1(常量赋值),把这一段改成 add eax, ecx; sub eax, 4
xor eax, ebx

免杀方法

当前主流的技术,需要有源代码才能操作。通过修改病毒的特征字符串、动态API调用、修改编译环境、套程序外壳(MFC、SDK、QT)等

输入表(IAT)免杀法

发式引擎会扫描目标程序的输入表中是否包含指定的函数特征序列(函数调用特征码)。

解决方案:(本段摘自未知作者)

  • 1、 输入表函数移位法:这是最早也是比较简单的输人表免杀方法了,虽然效果已经不像当年那么好了,但却是学习免杀过程必须要掌握的基础知识。我们使用C32打开一个EXE文件,找到输入表段,找到我们定位出的特征输入表,比如ShellExecuteA就是,我们将其使用OO填充,然后在附近找到一片空白区域,将刚才找到的代码再粘贴到空白区域中,并记下新函数的地址,ShellExecuteA字符串最前面的那个“S”的地址减2,即00078925,这样就实现了转移,但是要想让程序知道我们转移的函数,我们还得告诉输入表,刚才那个地址是文件偏移地址,但不是内存地址,我们需要利用OC计算出新的输入表函数ShellExecuteA的内存位置,并在LoadPE中修改才行。

  • 2、 输入表函数对调法:这个方法的原理就是将输入表函数名长度相同的函数在C32中进行对调,只有长度一样才不会出错,然后在LoadPE中做相应的修改即可。比如被查杀的函数是OpenFileA,存在于A.dll文件中,我们在b.dll中找到了一个GetATimeA函数,这两个函数名称长度一样,我们在C32中做了静态对换之后,还要将它们的RVA进行对换

  • 3、 手工重建输入表:关于输入表的重建,我想大家都非常熟悉了吧,这算是比较复杂的一种方法了,不过免杀效果非常好,这也是必须要掌握的方法哦!这个方法其实就是添加一个新区段,再把原来的输入表移到我们新建的区段上,重建主要是针对杀毒软件定位到大片输入表函数。

  • 4、 输入表隐藏法:将输入表加密隐藏,然后内存解密修复输入表。属于保护壳常用的技术。

代码混淆、加花:通过对特征代码进行膨胀、乱序来干扰启发式引擎的分析,以及提升人工提取特征码的难度。

入口模糊技术

内存加载执行PE文件:壳的基本技术,论坛资料很多。

机器学习引擎(以360QVM为例),绕过方式如下:

模拟正常程序的PE结构(该免杀壳方案能有效针对该引擎,或许还能污染机器学习引擎的分析结果)

常见QVM引擎报毒原因:

HEUR/Malware.QVM06.Gen 一般情况下加数字签名可过HEUR/Malware.QVM07.Gen 一般情况下换资源HEUR/Malware.QVM13.Gen 加壳了HEUR/Malware.QVM19.Gen 杀壳HEUR/Malware.QVM20.Gen 改变了入口点HEUR/Malware.QVM27.Gen 输入表HEUR/Malware.QVM18.Gen 加花HEUR/Malware.QVM05.Gen 加资源,改入口点

沙箱(虚拟机)行为分析引擎,绕过方式如下:

简介:所谓“沙箱”安全技术,是指以计算机系统为基础对恶意软件的行为与特征进行分析并最终检测出恶意代码的方案。

解决方案:

1、通过检测沙箱(虚拟机)与物理机的差异化(参考:https://bbs.pediy.com/thread-225735.htm),检测到沙箱(虚拟机)则不执行恶意代码。

2、延时180+秒(效果比较好)加载恶意代码,沙箱(虚拟机)的检测结束后无法探测到恶意行为。笔者比较推崇此方法,因为此方法针对的是所有反病毒厂商的沙箱(虚拟机)检测。

3、挖掘开机启动程序的代码执行漏洞,配合白加黑技术来执行敏感行为。

主动防御:

简介:主动防御是基于程序行为自主分析判断的实时防护技术,不以病毒的特征码作为判断病毒的依据,而是从最原始的病毒定义出发,直接将程序的行为作为判断病毒的依据。

360主动防御模块经过做黑灰兄弟们的不懈努力,已经非常完善了,绝大部分常规、非常规的行为绕过方式均已被拦截,并弹出一个默认阻止的小框框。

1、继续挖掘非常规方法绕过主防的拦截,主防未监控到的区域。

2、白程序(包含在杀软白名单库中的程序)加黑程序方式来执行高危行为,写启动项、键盘记录等。(不过要注意的是360白程序判定逻辑,灰程序加载的白程序 = 灰程序,因此需要绕过主防的程序执行链监控)

3 免杀壳开发(by: AYZRxx)

3.1 免杀壳核心思想-伪装

经过10多年的发展,反病毒引擎已经在误报&查毒粒度之间取了一个比较好的平衡,常规的免杀技术(特征码免杀、源码免杀)处理成本越来越高。不过反病毒引擎天然存在某些”缺陷”,例如正常软件会加商业保护壳,导致会受到商业壳的制约,无法将所有壳标记为病毒。

由于内存执行”被加壳程序”是壳的基础行为,==而内存执行PE这个”壳的基础行为”可以很好的将”被加壳程序”的特征码隐藏起来。==因此编写一款无特征码壳是一个非常好的反杀软查杀(特征码、启发式)的方案。

\1. ==模拟正常PE程序结构, 模拟正常PE程序结构, 模拟正常PE程序结构==

\2. 特征代码最小化,并且==被查杀后==可通过==混淆引擎==来混淆壳代码,达到快速变种、快速免杀的效果。

\3. 笔者不建议进行任何可能提高程序熵值的操作,==尽可能将壳程序的PE格式、数据结构、代码执行顺序与正常程序保持一致。==

3.2 免杀壳的编写框架说明

这个免杀壳的代码主要分为三部分:

加壳器:这部分代码用来将被加壳程序、傀儡程序、壳代码拼装处理,组合生成一个免杀的PE文件。

CodeLoader(壳代码):这部分代码用来反杀毒引擎、内存加载执行PeLoader,需要编译为Shellcode代码。

PeLoader(壳代码):这部分代码用来内存执行Shelled(被加壳程序),需要编译为Shellcode代码。

1
2
3
4
5
6
7
8
9
10
11
Ø  Shelled:被加壳程序

Ø Pepput:傀儡程序,用来伪装成正常PE文件,植入壳代码的载体

Ø CodeLoaderCode:壳代码

Ø PeLoaderCode:壳代码

Ø CodeLoader:作用是反调试、反沙箱(虚拟机)、加载执行PeLoader(只有这段代码暴露在杀毒引擎的检测范围之内,只需要对这段代码做混淆即可快速免杀)

Ø PeLoader:作用是加载执行Shelled(被加壳程序)
1
2
3
4
5
6
7
8
9
1、 加壳后只有CodeLoader代码暴露在杀毒引擎(静态)检测范围内,混淆前代码只有1KB左右大小,由于可定位的特征代码少,特征码免杀较为简单。

2、 传统的Shellcode使用自解密的方式来模糊特征,需要代码段内存具有可写属性,该操作会导致启发式引擎报毒。CodeLoader代码则使用了代码混淆技术来达到免于杀毒引擎查杀的效果,代码段内存在PE结构种无需具有可写属性。

3、 并且只需要在CodeLoader代码加入检测沙箱、虚拟机的功能代码,绕过杀毒引擎的行为检测,就做到了基本的无特征码化、无行为化。

4、 无需创建傀儡进程,远程写内存这种高危行为杀毒软件是不允许的。

5、 内存加载PE文件就是老生常谈了,不再做过多的阐述,论坛里面有大量的优秀文章可供参考。

先看看别人的代码分析分析

Peprotect

stub

stub.cpp(植入的.cpp)

1
2
3
4
5
6
7
8
9
10
1.汇编找到kernel.dll
2.kernel.dll获取各类函数地址
3.填充iat
4.修复exe重定位
5.对加壳器(加密部分)的解密
6.执行TLS回调 //当这个进程处于TIS保护状态时(默认处于),需要重装TLS以进行修改
7.调用窗口函数验证密码(跟像是一种勒索软件)(看下有没有不需要窗口的但是能使用密码的)
8.解压
9.混淆函数(花指令)
10.壳程序

dllmain.cpp

1
2
3
4
5
switch
创建进程
创建线程
关闭线程
关闭进程

peprotect

peprotect.cpp

peprotectdl.cpp

用来设置对话框

pack

dllmain.cpp

1
2
3
4
5
switch
创建进程
创建线程
关闭线程
关闭进程

PE.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DWORD CPe::GetOepRva() 获取目标程序的入口点Rva
bool CPe::ReadTargetFile 读取要加密文件到内存
DWORD CPe::RvaToOffset(DWORD Rva) 用于将PE文件的rva转为文件偏移
DWORD CPe::AddSection 添加区段
DWORD CPe::GetFirstNewSectionRva() 获取第一个新区段的rva
void CPe::SetNewOep(DWORD dwNewOep) 设置新的程序入口点
void CPe::SaveNewFile(char* pPath) 保存文件
DWORD CPe::CalcAlignment(DWORD dwSize , DWORD dwAlignment) 获取对齐后的大小
void CPe::FixDllRloc(PCHAR pStubBuf, PCHAR pStub) 根据新区段的地址修复dll的重定位[dll是加载到内存的,这里根据默认加载基址,新添加的节区的rva以及和原节区开始的差值来重新设置.text的重定位]
void CPe::Encryption() 对代码段进行加密
void CPe::CancleRandomBase() 去除重定位
DWORD CPe::GetImportTableRva() 获取导入表的rva
DWORD CPe::GetRelocRva() 获取重定位表的rva
void CPe::ChangeImportTable() 对导入表进行更改
DWORD CPe::GetImageBase() 获取目标程序加载基址
void CPe::SetMemWritable() 设置每个区段为可写状态
void CPe::ChangeReloc(PCHAR pBuf) 对于动态加载基址,需要将stub的重定位区段(.reloc)修改后保存,将PE重定位信息指针指向该地址(新区段)
DWORD CPe::GetNewSectionRva() 如果要添加一个新区段,获得这个新区段的rva
DWORD CPe::GetLastSectionRva() 获取最后一个段的rva
void CPe::EnCompression(PPACKINFO & pPackInfo) 压缩区段 压缩在加密区段之后
PCHAR CPe::Compress(PVOID pSource, IN long InLength, OUT long & OutLength) 调用压缩库
BOOL CPe::ModifyTlsTable(PPACKINFO & pPackInfo) 修改Tls表
void CPe::SetTls(DWORD NewSectionRva, PCHAR pStubBuf, PPACKINFO pPackInfo) 设置stub的tls表

Pack.cpp

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
加壳
extern "C" _declspec(dllexport) c++调用c模块
1.调用LoadLibrary将stub.dll载入到内存
2.GetprocAddress在内存中找到和stub.dll通讯的 g_PackInfo(stub.dll中设置了g_PackInfo的信息)
3.调用PE.cpp中的ReaTargetFile将pPath加密读取到内存
4.bTlsUseful = obj.ModifyTlsTable(pPackInfo)获取TLS信息
5.obj.Encryption();对代码段进行加密
6.obj.EnCompression(pPackInfo);压缩区段
7.获取stub.dll的内存大小和节区头(也就是要拷贝的头部)
8.设置加壳信息
pPackInfo->TargetOepRva = obj.GetOepRva() 获取进程OEP
ImageBase = obj.GetImageBase(); 获取目标程序加载基址Iamgebase
获取目标程序重定位表rva和导入表的rva ImportTableRva = obj.GetImportTableRva() RelocRva = obj.GetRelocRva()
9.获得Stub.dll模块中Start函数的相对虚拟地址:VA-Stub.dll基址
10.由于直接在本进程中修改会影响进程,所以将dll拷贝一份到pStubBuf memcpy_s
11.obj.FixDllRloc(pStubBuf, (PCHAR)hStub); 修复dll文件重定位,这里第二个参数应该传入Stub.dll模块基址hStub,因为这是dll加载时重定位的依据
12.把stub.dll的代码段.text添加为目标程序的新区段
13.SetTls
14.//obj.CancleRandomBase() 可以选择去掉重定位
// 或者将stub的重定位区段粘到最后面,将重定位项指向之,但是这之前也必须FixDllRloc,使其适应新的PE文件
obj.ChangeReloc(pStubBuf);
15. 把目标程序的OEP设置为stub中的start函数 obj.SetNewOep(dwNewOep);
16. 设置每个区段可写 obj.SetMemWritable();
17. 对IAT进行加密 obj.ChangeImportTable();
18. 释放改dll FreeLibrary(hStub);
19.保存成文件

Packer-master

stub.dll

aplib.lib– 壳压缩引擎

info.h – c++文件系统 管理写读

dllmain.cpp (直接将创建线程和功能都写在里面了)(用到了内存管理)

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
1.#define IMAGE_SIZEOF_BASE_RELOCATION (sizeof(IMAGE_BASE_RELOCATION))  如果SDK不支持的话 这样定义基址重定位表  
2.#define DLL_SAMPLE_API __declspec(dllexport) DLL导入类
3.打印日志位置,修改这个地方 LOG_PATH L"D:/log.txt"
4.生成自动删除文件名称
5.是否开始反调试代码 ANTI_REVERSE
6.默认打开日志 SUPPORT_LOG
7.自定义malloc模块 MemoryNode *node = (MemoryNode *)s_apier.VirtualAlloc(NULL, len + sizeof(MemoryNode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);//这个是内存管理 这个 (MemoryNode *)s_apier是类
8.自定义mfree模块 调用了virtualfree
9.自定义pmemcpy
10.自定义ccpy sstrlen memsetZero
11.getNtHeader
12.getImageSectionHeader
13.自定义 *int_to_str mprintf
14.beingdebugged反调试
15.定义pUnhandledExceptionFilter 异处理异常筛选器
16.利用SetUnhandledExceptionFilter和pUnhandledExceptionFilter自定义一个异常处理反调试器isDebug1
17._declspec (thread) LPCTSTR g_strTLS = L"Stub TLS DATA"声明一个线程本地变量
18.创建TLS段 自然还是用到了C语言
19.照旧汇编获取kernel32,不过好像更高级了一点
20.GetGPAFunAddr函数定义 里面获取DOS头、NT头 获取导出表项 获取导出表详细信息 处理以函数名查找函数地址的请求,循环获取ENT中的函数名,并与传入值对比对,如能匹配上则在EAT中以指定序号作为索引,并取出其地址值。
21.初始化必要的函数信息 void initFunction()
22.获取当前运行的进程地址 *getExePath()
23.生成临时bat文件字符串,使用mfree删除 *getTempDelBatFilePath()
24.自删除逻辑 deleteSelf()
25.自定义InitTLS表 (IMAGE_TLS_DIRECTORY中的地址就是虚拟地址直接用)
26.IAT重写 recoverIAT
27.重定位OFFSET结构 typedef struct _TYPEOFFSET
28.修复原始重定位表 定位结构体(不一定需要) fixRelocation 还是调用了内存管理
29.decompress 压缩 需要拷贝偏移
30.isPasswordCorrect
31.createWindowButton
32.处理信息 WinProc
33.checkPassword
34.isTimeout
35.主函数oid __declspec(naked) pMain()
解压数据decompress()
修复重定向fixRelocation()
检测密码
是否有限制时间
恢复IAT
看是否有TLS函数 如果有 则调用
转交控制权

packer.exe

InputInfo.cpp : 实现文件

1
2
1.对话框
2.消息处理程序

PackerDlg.cpp :对话框

1
2
3
4
用于应用程序“关于”菜单项的 CAboutDlg 对话框
CPackerDlg 对话框
CPackerDlg 消息处理程序
当用户拖动最小化窗口时系统调用此函数取得光标

pictureEx.cpp :界面装载图片

util.cpp

1
2
生成内存文件  不适用win32 CreateFileMapStruct
校验是否是32位文件 isPEEXE32

Task.cpp

1

loading.cpp

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
Task::SetPEStruct(char *fileBuf, PEstruct &peStruct) 设置PE结构
Task::RVA2FA(char* lpFileBuffer, int RVA) ROA转FOA
Task::GetExpVarAddr(const char * strVarName) 获取扩展节地址?
Task::StoreSectionInfo(char *bufFile, std::vector<pSecInfo*>&vec) 储存节信息
Task::Align(DWORD dwAlign, DWORD dwValue) 不知道干嘛的
GetSecInfoByRVA(DWORD dwRVA, DWORD dwAlign, std::vector<pSecInfo*>&vec) 获取SecInfo
Task::GetTargetImageSize 获取节大小
Task::GetPressSize 获取需要解压的大小
CopyPressData 赋值解压的数据
CompressData 压缩数据
Task::AddSec 用于AddTargetSection
Task::Start 创建FILEmap结构 引用Stub.dll 这只PE结构
Task::GetResRVA 用于修正资源表
Task::FixRsrc 修正资源表
GetSecInfoByName 通过名称获取节信息
SetPressDataDir 设置解压地址
ClearDataDir
fixStubRelocation 修复Stub重定位
Task::Pack 打包的主程序
SetDateAndPassword
SetGlobalVar
SaveFile
CopyToTargetFile
AddTargetSection
CopyToDestMemory 复制到目标内存

内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

GuiShou_Pack-master

这个是分阶段写的 可能有助于理解

阶段1–基础功能实现

main.cpp —大部分函数调用的CPeFileoper.cpp

1
2
3
4
5
6
7
8
9
10
11
12
1.CPeFileOper m_Pe 选择PE文件操作类对象
2.char* pTargetBuff = m_Pe.GetFileData 打开被加壳程序
3.m_Pe.LoadStub(&stub)加载stub.dll
4.m_Pe.Encrypt加密被加壳程序的代码段
5.AddSection GetSection 添加新区段
6.FixStubRelocation GetSection GetOptionHeader GetSection修复重定位
7.GetOptionHeader(pTargetBuff)->AddressOfEntryPoint; 保存目标文件的OEP到stub的全局变量中
8.memcpy 将stub.dll的代码段复制到新加的GuiShou段中
9.修改OEP OEP=start(VA)-dll加载基址-段首RVA+新区段的段首RVA
10.去掉随机基址
11.SavePEFile保存被加壳的程序

CPeFileoper.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
OpenPeFile 打开PE文件
GetFileData 获取文件内容和大小
GetDosHeader 获取Dos
GetNtHeader 获取Nt头
GetFileHead 获取文件头
GetOptionHeader 获取可选头
GetLastSection 获取最后一个区段
AlignMent 计算对齐后的大小
GetSection 获取指定名字的区段头
AddSection 添加一个新的区段
SavePEFile 将文件保存到指定路径
LoadStub 加载stub.dll
Encrypt 加密目标程序的代码段
FixStubRelocation 修复stub.dll的重定位表

stub.cpp

1
2
3
Decrypt  解密代码段
GetApis 获取API函数地址 kernel获取LoadLibrary和VirtualProtect
Start dll的OEP获取函数API 解密代码段 跳转到原始的OEP

阶段2–增加弹框

stub.cpp

1
2
3
4
kernel32和user32获取各种相关api
pWcscmp 自己实现的一个字符串比较函数
AlertPasswordBox 密码弹框
WndPrco 窗口回调函数

main.cpp 不需要改

CPeFileoper.cpp 不需要改

3.0 增加反调试

stub.cpp

1
2
3
AntiDebug  反调试 调用pfnFindWindowW
MixFun 混淆函数 汇编
Start()函数里多了AntiDebug()

main.cpp

1
没变

4.0 增加AES加密

stub文件夹多了个AES.cpp

stub.cpp

1
decrypt函数里面添加了aes解密的方法 InvCipher

stub.h

1
unsigned char key[16] = {};//解密密钥

CPeFileOper.cpp

1
2
3
4
encrypt函数增加了aes加密
AES aes(key);
aes.Cipher(pTargetText, dwTargetTextSize);
CompressPE 压缩PE文件

5.0 增加Tls回调函数的调用

1
//用于获取非rsrc/tls段的总大小 没写 = = 

6.0 增加花指令

stub.dll

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
FusedFunc
_asm
{
jmp label1
label2 :
_emit 0xeb; //跳到下面的call
_emit 0x04;
CALL DWORD PTR DS : [EAX + EBX * 2 + 0x123402EB]; //执行EB 02 也就是跳到下一句

// call Init;// 获取一些基本函数的地址

// call下一条,用于获得eip
_emit 0xE8;
_emit 0x00;
_emit 0x00;
_emit 0x00;
_emit 0x00;
//-------跳到下面的call
_emit 0xEB;
_emit 0x0E;

//-------花
PUSH 0x0;
PUSH 0x0;
MOV EAX, DWORD PTR FS : [0];
PUSH EAX;
//-------花


// fused:
//作用push下一条语句的地址
//pop eax;
//add eax, 0x1b;
/*push eax;*/
CALL DWORD PTR DS : [EAX + EBX * 2 + 0x5019C083];

push funcAddress; //这里如果是参数传入的需要注意上面的add eax,??的??
retn;

jmp label3

// 花
_emit 0xE8;
_emit 0x00;
_emit 0x00;
_emit 0x00;
_emit 0x00;
// 花


label1:
jmp label2
label3 :
}

callTls Tls回调函数
pfnGetMoudleHandleA 获取当前程序的加载基址
GetOptionHeader 获取Tls表
壳程序

这段花指令和壳程序我在各大代码看了不下7遍:joy:

7.0 增加全部区段加密

stub.dll

1
2
3
SetFileHeaderProtect  设置属性可写
FixImportTable_Normal 修复IAT
RecoverDataDir 恢复数据目录表

CPeFileOper.cpp

1
2
3
4
5
6
7
8
9
10
11
12
encrypt函数加密所有区段
//修改属性为可写
DWORD dwOldAttr = 0;
VirtualProtect(pTargetSection, dwTargetSize, PAGE_EXECUTE_READWRITE, &dwOldAttr);
//加密目标区段
aes.Cipher(pTargetSection, dwTargetSize);
//修改回原来的属性
VirtualProtect(pTargetSection, dwTargetSize, dwOldAttr, &dwOldAttr);

ClearDataDir 清除数据目录表

删除了 CompressPE 压缩PE文件

main.cpp

1
m_Pe.ClearDataDir(pTargetBuff, stub);

8.0 增加IAT加密 加花

stub.cpp

1
2
EncryptFun     在里面再写一个加密函数  异或加密
EncodeIAT 加密IAT 调用EncryptFun加密impaddress

CPeFileOper.cpp

1
没变

8.0 增加IAT加密 未加花

对比加花 stub.cpp

加花未加花
在壳程序函数里//获取函数的API地址
FusedFunc((DWORD)GetApis);

//解密代码段
FusedFunc((DWORD)Decrypt);

//恢复数据目录表
FusedFunc((DWORD)RecoverDataDir);

//修复IAT
FusedFunc((DWORD)FixImportTable_Normal);

//反调试
FusedFunc((DWORD)AntiDebug);

//密码弹框
FusedFunc((DWORD)AlertPasswordBox);

//调用Tls回调函数
FusedFunc((DWORD)CallTls);

//加密IAT
FusedFunc((DWORD)EncodeIAT);
不执行加壳函数
在start函数后执行壳 FusedFunc((DWORD)AllFunc);
//获取函数的API地址
GetApis();
//解密代码段
Decrypt();
//恢复数据目录表
RecoverDataDir();
//修复IAT
FixImportTable_Normal();
//反调试
AntiDebug();
//密码弹框
AlertPasswordBox();
//调用Tls回调函数
CallTls();
//加密IAT
EncodeIAT();
所有函数都调用了FusedFunc函数
FusedFunc函数加花函数 需要自己再改改

9.完整体

stub.dll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Decrypt  解密代码段
GetApis 获取API函数地址 kernel获取LoadLibrary和VirtualProtect
Start dll的OEP获取函数API 解密代码段 跳转到原始的OEP
kernel32和user32获取各种相关api
pWcscmp 自己实现的一个字符串比较函数
AlertPasswordBox 密码弹框
WndPrco 窗口回调函数
AntiDebug 反调试 调用pfnFindWindowW
MixFun 混淆函数 汇编
Start()函数里多了AntiDebug()
unsigned char key[16] = {};//解密密钥
//用于获取非rsrc/tls段的总大小 没写 = =
callTls Tls回调函数
pfnGetMoudleHandleA 获取当前程序的加载基址
GetOptionHeader 获取Tls
增加花指令函数funcfund
SetFileHeaderProtect 设置属性可写
FixImportTable_Normal 修复IAT
RecoverDataDir 恢复数据目录表
EncryptFun 在里面再写一个加密函数 异或加密
EncodeIAT 加密IAT 调用EncryptFun加密impaddress

AtomPepacker

ArgsTest

判断args输入命令

hasher

应该是使用hash加密(散列替换)api函数字符串

image-20221029124004927

DLLPP64Stub

General.c

就是自定义一些字符转换

Utils.c

InitializeDirectNtCalls 初始化调用nt结构

GetDllFromKnownDlls 调用kernel.dll获取各类dll

hSection = NtOpenSection 获取NT节句柄

RefreshNtdll 需要时重新分配ntdll

GetModuleHandleH(自定义api哈希库) 从peb的ldr的pdte的InMemoryOrderModuleList.Flink 中枚举全部加载模块 然后获取模块句柄

LoadLibraryH(自定义api哈希库) 将指定的模块加载到调用进程的地址空间中。指定的模块可能会导致其他模块被加载。对于其他加载选项

无CRT导入

RtlInitUnicodeString 初始化设备名称指针。

IoDeleteSymbolicLink 例程从系统中删除符号链接

IoDeleteDevice 删除驱动

Dbgprint extension 显示以前发送到dbgprint缓冲区的字符串。

Shell_Protect-main(虚拟壳)

这个就有点难了 之后分析 别一口吃成胖子 先自己写个简单的加壳 然后改进 然后写这个虚拟化壳

就连压缩的代码都不一样 虚拟化实在太强了

compressiondata

1
2
3
4
5
6
7
在压缩之前要先进行Vmencode加密
自定义加密次数
获取VM的其实地址
线性反汇编来求大小
添加一个区段给压缩后的数据使用
调用VMP

反汇编引擎

用的是capstone

  capstone 可以说是所有反汇编引擎中集大成者,对于它我要多费点口水,因为我对他是又爱又恨。capstone是基于LLVM框架中的MC组件部分移植过来,所以LLVM支持的CPU构架,capstone也都支持。
  它支持的CPU构架有:

Arm, Arm64 (Armv8), M68K, Mips, PowerPC, Sparc, SystemZ, XCore & X86 (include X86_64)

而且Capstone对X86构架的指令集支持是最全的,这一点是其他引擎都比不上的,其支持的X86扩展指令集有:

3dnow, 3dnowa, x86_64, adx, aes, atom, avx, avx2, avx512cd, avx512er, avx512f, avx512pf, bmi, bmi2, fma, fma4, fsgsbase, lzcnt, mmx, sha, slm, sse, sse2, sse3, sse4.1, sse4.2, sse4a, ssse3, tbm, xop.

原来这东西是提供给虚拟机使用的

反汇编stub文件->每条汇编挂钩handler->当eip执行地址则进入虚拟机处理

然后compresstiondata会调用跟这个vm进行vmentry

这个就用来compressiondata前进行vm的解密

之后我们分析这些源码有哪些是相通的函数 然后来看一下函数有没有细致差别

Peprotect和guishou_pack-master的sutb.dll代码差不多

那我们进行pack-master和guishou_pack-master的stub.dll的相似函数的内容分析

pack-masterguishou_pack-master
#头#include “aplib.h”压缩引擎
info.h
AES.h
define IMAGE_SIZEOF_BASE_RELOCATION 定义基址重定位表
define LOG_PATH L”D:/log.txt” 打印日志设置
define ANTI_REVERSE 反调试设置
define SUPPORT_LOG 打开默认日志
定义一堆内存数据流结构体和自定义的内容管理函数
开始getNtHeader和getImageSectionHeader获取各种头
又写了两个内容管理函数
isdebug反调试
又定义一些七了八了的内容管理函数
根据pUnhandledExceptionFilter和pUnhandledExceptionFilter1使用汇编编写debug1
TlsCallBack
汇编获取kernelbetter (kernel+GetGPAFunAddr)的汇编版hKernel32;
GetGPAFunAddrDOS头\Nt头->导出表项->导出表详细信息->根据函数名查找详细地址值
利用GetGPAFunAddr获取API地址利用hKernel32获取地址
编写getExePath获取进程地址
利用getExePath实现那些文件的自删除 不知道有个鬼用
总结除了多了解压缩函数和内存管理啥也没有使用汇编定位api使代码更好写
而且还做了加密 加壳 花指令等函数

上面这个的话我感觉还是用汇编获取api更好用一点

stub.dll里面的函数InitTls和callTLS的区别

InitTls(pack-master)callTLS(guishou_pack-master)
获取pTlsCallBack和pStubCallBack的虚拟地址(DWORD)pfnGetMoudleHandleA(NULL);获取程序的句柄加载基址
直接用虚拟地址获取TLScalbackeGetOptionHeader((char*)dwBase)->DataDirectory[9].VirtualAddress;获取内存地址
pTlsTab = (PIMAGE_TLS_DIRECTORY)(dwTlsRva + dwBase);TLs表的句柄加载基址和内存地址
nTlsCallBacks = (DWORD)pTlsTab->AddressOfCallBacks;如果获取TLS表成功 引用AddressOfCallBacks;
使用汇编call nTlsCallBacks

修复IAT(都是用的动态修复 因为计算机每次重启.系统dll映射到exe程序所在的地址都会变,IAT必须动态修复).修复IAT表需要用到”LoadLibraryA”和GetProcAddress两个函数,这两个函数存在于KERNEL32这个DLL中,可以通过FS寄存器找到进程环境块PEB,得到kernel32的地址,找到GetProcAddress函数所在地址,再通过GetProcAddress找到LoadLibraryA的地址。

recoverIATFixImportTable_Normal
//获取当前程序的加载基址<br/HMODULE hModule = s_apier.GetModuleHandleW(NULL);
然后做出了判断是否获取成功
//设置文件属性为可写
SetFileHeaderProtect(true);
获取NT头进而获取加载基址//获取当前程序的加载基址
DWORD ImageBase = (DWORD)pfnGetMoudleHandleA(NULL);
//导入表
lpImportTable = (IMAGE_IMPORT_DESCRIPTOR*)((DWORD)lpImageBase + g_globalVar.dwIATVirtualAddress);
这里的dwIATVirtualAddress 是自己定义的结构体
//导入表=导入表偏移+加载基址
IMAGE_IMPORT_DESCRIPTOR* pImp = (IMAGE_IMPORT_DESCRIPTOR*)(GetOptionHeader((char*)ImageBase)->DataDirectory[1].VirtualAddress + ImageBase);
获取IAT,这是需要将函数地址写入的地方,但是相比右边缺少了校验,默认为获取PINT根据导入表获取Int 如果没有INT就获取IAT
后面跟右边差不多 我感觉右边的代码更好写且好用一点// 加载dll hImpModule = (HMODULE)pLoadLibraryA((char*)(pImp->Name + ImageBase));
while导入函数地址u1.Function if (IMAGE_SNAP_BY_ORDINAL)判断导入的方式、序号还是名称IMAGE_SNAP_BY_ORDINAL(pInt->u1.Ordinal 来获取impaddress
pVirtualProtect保护piat的导入函数地址u1.Function
将刚才获取到的pint的impaddress赋值给pIat->u1.Function

修复原始重定位表 (只有pepacker里面有)

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
int fixRelocation()
{
DWORD dwImageBase;
PIMAGE_BASE_RELOCATION pReloc;

if (g_globalVar.dwRelocationRva == 0)
return 0;

dwImageBase = (DWORD)s_apier.GetModuleHandleW(NULL);
if (dwImageBase == NULL) {
LOGGER_MESSAGE("GetModuleHandleW failed");
return -1;
}

pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)dwImageBase + g_globalVar.dwRelocationRva);
while (pReloc->VirtualAddress)
{
PTYPEOFFSET pTypeOffset = (PTYPEOFFSET)(pReloc + 1);
DWORD dwNumber = (pReloc->SizeOfBlock - IMAGE_SIZEOF_BASE_RELOCATION) / 2;
for (size_t i = 0; i < dwNumber; i++)
{
if (*(PWORD)(&pTypeOffset[i]) == 0)
{
break;
}
DWORD dwRVA = pTypeOffset[i].offset + pReloc->VirtualAddress;
DWORD dwAddressOfReloc = *(PDWORD)(dwImageBase + dwRVA);

//设置修复后的重定向数据
*(PDWORD)((DWORD)dwImageBase + dwRVA) = dwAddressOfReloc - g_globalVar.dwOrignalImageBase + dwImageBase;
}
pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pReloc + pReloc->SizeOfBlock);
}
return 0;
}

pepacker也多了decompress解压缩函数

修复重定向估计也是为了配合解压使用的

那我们直接改guisuo_packer的代码 compress代码

1
压缩函数

开始写

写个压缩区段 先分析一下流程

1
2
3
4
5
6
7
8
9
10
11
压缩区段
1.获取文件头的大小,并获取除资源段.rsrc和线程本地存储.tls之外的区段的文件总大小
2.读取要压缩的段到内存
3.压缩 调用compress函数
compress函数
调用apsafe_pack压缩函数
4.保存.rsrc .tls段到内存空间
5.设置压缩信息到信息结构体
6. 申请新空间,使m_pNewBuf指向它,将m_pBuf文件头拷贝
7. 添加.compres

再看一下encrypt加密区段函数

1
2
3
4
5
6
7
8
9
10
11
获取区段数量
获取第一个区段
pStub.pStubConf用于保存数据
if判断资源段和tls段不加密并跳过无效的区段
else
开始加密所有区段
获取区段的首地址和大小
修改属性为可写
加密目标区段
修改回原来的属性
保存数据到共享信息结构体

为了更好的编写和使用 得写一个函数用于获取rsrc和tls段的信息

好麻烦啊 直接在peprotect写加密吧

stub

  • 解密代码段

  • 加密iat

  • 反调试 x3个 (后面可以嵌套ollvm)

  • 解压和压缩区段

  • 密码弹框

  • 调用Tls回调函数 (用于反调试)

  • 修复exe重定位

  • 恢复数据目录表

  • 混淆函数

  • 修复iat

  • 加花函数

先看一下什么时候需要修复iat

修复iat 换句话来说就是修复导入表 importtable 但是都常见于脱壳

因为原来的程序在获取完IAT后就把字符删掉了,文件偏移里的字符串指针没有了,就没法自动获取地址了。所以必须脱壳之后必须把IAT修复好

那么解密代码段话就是类似一个脱壳的过程 可以会导致rva的偏移,这就需要我门修复iat去重新获取。

那么我们也要对应的去改一下pack.cpp

接下来看下要不要对那个虚拟壳进行改写学习

抹去PE指纹(不过这类常规方法只能抹去内存中的pe头)

1
2
3
4
5
6
7
8
9
10
11
12
void clearpeheader(){ 
hshellModule = GetModuleHandle("hshell.dll");
VirtualProtect((LPVOID)hshellModule, 1024, PAGE_READWRITE, &dwOldProtect);
//抹去PE头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hshellModule;
pDosHeader->e_magic = 0;
PIMAGE_NT_HEADERS pNtheader = (PIMAGE_NT_HEADERS)hshellModule;
//抹去MZ头
pNtheader->Signature = 0;
VirtualProtect((LPVOID)hshellModule,1024,, &dwOldProtect)

}

有没有写入文件的方法

都不能运行:angry: 改addshelltool

接下来改高级的

  1. 兼容x64

error1

__declspec(naked) naked 无法识别的扩展特性

__declspec(naked) 用来将汇编语言嵌入到c语言中,手工处理堆栈,即使是裸函数 我们也能运行image-20221123195145733

原来是这样

原因是x64不支持使用内联汇编代码

1.一个方法是直接把汇编代码转换成.asm文件然后再去引入

2.参考vmpshell的代码

做了判断函数

#ifdef _WIN64

直接 __stdcall之前的kernel 和分配好的函数

else //win32

重新定义获取puGetModule和MyGetProcAddress函数

//都是用来给下面sheller_code调用

我们就用第二套办法先试试

我们的目的都是为了获取windowsapi函数 但是在win64的话不需要先获取基址

而是可以直接 void _stdcall调用,所以我们只要知道我们需要哪些函数然后去定义就行image-20221123204518797

由于这个pGetProcAddress 我们自己定义的 x64下也没问题 所以我们直接用x64获取kernelbase就可

写到一半 发现好麻烦 除非之前已经定义过api了

我们用第一种方法

参考这个

https://blog.csdn.net/Giser_D/article/details/90670974

在asm定义好了函数 所以在只要写好了头文件和cpp 就不再需要声明

怪不得 vmshell的x64代码频繁用到 __stdcall

都是从asm里面调用的(但是是在cpp里面调用)

所以根本就没有第二种方法

所以我们需要改的就需要这三个东西

1
2
3
1.asm.h
2.test.asm
3.cpp

也就是说这个_stdcall写在cpp里面就可

只要stud.h引用了STUD_H 之后cpp调用stud.h就可image-20221124131530810

与kernelbase的也只有这一段 后面的都可以用pGetProcAddress和pLoadLibraryExA获取

这一段的是getkernelbase-> pgetprocaddress -> 获取各类函数

而这些函数我们都可以用汇编直接获取

  • pLoadLibraryExA typedef定义 调用getprocaddress获取 汇编没有
  • pGetProcAddress 汇编有 但是我们代码也自己定义了
  • pExitProcess typedef定义 调用getprocaddress获取 汇编没有
  • pVirtualProtect typedef定义 调用getprocaddress获取 汇编没有
  • pGetLastError typedef定义 调用getprocaddress获取 汇编没有
  • pVirtualAlloc typedef定义 调用getprocaddress获取 汇编没有
  • pVirtualFree typedef定义 调用getprocaddress获取 汇编没有
  • pVirtualQuery typedef定义 调用getprocaddress获取 汇编没有

汇编有的

1
2
3
4
5
puGetModule
MyGetProcAddress
CodeExecEntry
剩下的都是vm里的 没啥用

只能直接获取LoadLibrary函数 看下怎么用的

1
2
3
4
5
6
7
8
pLoadLibraryExA = (FnLoadLibraryExA)MyGetProcAddress(g_stud.s_Krenel32, 0xC0D83287);
pExitProcess = (FnExitProcess)MyGetProcAddress(g_stud.s_Krenel32, 0x4FD18963);
pVirtualProtect = (FnVirtualProtect)MyGetProcAddress(g_stud.s_Krenel32, 0xEF64A41E);
pGetLastError = (FnGetLastError)MyGetProcAddress(g_stud.s_Krenel32, 0x12F461BB);
pVirtualAlloc = (FnVirtualAlloc)MyGetProcAddress(g_stud.s_Krenel32, 0x1EDE5967);
pVirtualFree = (FnVirtualFree)MyGetProcAddress(g_stud.s_Krenel32, 0x6144AA05);


找了半天找不到直接asm获取kernelbase的办法 可能需要重构直接获取api了

判断PE 64 DLL NET

1
2
3
4
5
bool isPE  = in_pe_dos_header->e_magic == IMAGE_DOS_SIGNATURE;
bool is64 = in_pe_nt_header->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 &&
in_pe_nt_header->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC;
bool isDLL = in_pe_nt_header->FileHeader.Characteristics & IMAGE_FILE_DLL;
bool isNET = in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size != 0;

写个壳
http://example.com/写个壳.html
Author
CDxiaodong
Posted on
December 6, 2022
Licensed under