此系列是本人一个字一个字码出来的,包括代码实现和效果截图。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
压缩原理你如果是从中间插过来看的,请仔细阅读 羽夏壳世界——序 ,方便学习本教程。
由于展示最基本最简单的实现,使用压缩算法就没用复杂的。如果使用比较复杂的压缩算法,首先你在C++
代码层面和汇编层面要有配套的代码,C++
负责压缩代码,汇编负责自我解压缩,否则你压缩完了,结果被压缩后的PE
文件自己又解不了,这就很尴尬。
我们本项目使用的算法被称之为RLE
压缩算法,英文全称是run-length encoding
,亦称行程长度编码。听起来高大上,下面我用比较通俗语言就行介绍。
比如一个字符串:AABBCCDDDDEEEEEEEEE
,一个二十个字符,我们如何使用该算法进行压缩呢?
好,A
有两个,就用2A
表示;B
有两个,用2B
表示……最后我们得到下面的字符串:2A2B2C4D10E
,可以看到长度被进行了压缩。
这种算法有一个比较严重的弊端,如果每一组相邻的字符相同的少于2个,会导致负面影响,导致没有压缩效果甚至膨胀,但对于压缩代码比较足够了。因为里面会有大量的0xCC
和0x00
这样的字节,可以忽略这种算法的缺陷导致的影响。
既然是使用该方式进行压缩,首先我们得进行编码。每一个压缩块定义如下:
#pragma pack(1)
struct codata
{
BYTE code;
BYTE count;
};
#pragma pack()
可以看出每一个压缩块的大小为双字,低字节放着是重复的代码,高字节放着是重复的个数。
如果你细心的发现,这个压缩块最多一次放0xFF
大小的重复字符,这个得考虑到,否则解压缩的时候会发生错误,与压缩代码相关的代码如下:
BOOL CWingProtect::CompressSeciton(BOOL NeedReloc, BOOL FakeCode)
{
using namespace asmjit;
if (_lasterror != ParserError::Success) return FALSE;
#pragma pack(1)
struct codata
{
BYTE code;
BYTE count;
} cdata{};
#pragma pack()
list<codata> datas;
auto p = (BYTE*)OFFSET(packedPE, peinfo.PCodeSection->PointerToRawData);
auto length = peinfo.PCodeSection->SizeOfRawData;
CodeHolder holder;
CodeHolder jmpholder;
//开始进行压缩
for (UINT i = 0; i < length; i++, p++)
{
cdata.count = 1;
cdata.code = *p;
while (true)
{
if (cdata.count < 0xFF && i + 1 < length && *(p + 1) == cdata.code)
{
cdata.count++;
i++;
p++;
}
else
{
datas.push_back(cdata);
break;
}
}
}
auto wingSection = peinfo.WingSection;
auto buffer = GetPointerByOffset(peinfo.WingSecitonBuffer, peinfo.PointerOfWingSeciton);
encryptInfo.CompressedData = (UINT)peinfo.PointerOfWingSeciton;
BYTE* shellcode;
INT3264 codesize;
INT3264 datasize;
Environment envX64(Arch::kX64);
// gs:[0x60]
x86::Mem memX64;
memX64.setSegment(x86::gs);
memX64.setOffset(0x60);
Environment envX86(Arch::kX86);
// fs:[0x30]
x86::Mem memX86;
memX86.setSegment(x86::fs);
memX86.setOffset(0x30);
auto rvabase = peinfo.AnalysisInfo.MinAvailableVirtualAddress;
#define AddRVABase(offset) ((UINT)offset + (UINT)rvabase)
if (is64bit)
{
//生成汇编代码
holder.init(envX64);
x86::Assembler a(&holder);
Label loop = a.newLabel();
Label loop_d = a.newLabel();
a.push(x86::rsi);
a.push(x86::rdi);
a.push(x86::rcx);
a.push(x86::rdx);
a.mov(x86::rax, memX64);
a.mov(x86::rdx, x86::qword_ptr(x86::rax, 0x10));
a.mov(x86::rsi, AddRVABase(encryptInfo.CompressedData));
a.add(x86::rsi, x86::rdx);
a.mov(x86::rdi, peinfo.PCodeSection->VirtualAddress);
a.add(x86::rdi, x86::rdx);
a.mov(x86::rcx, x86::qword_ptr(x86::rsi));
a.add(x86::rsi, 8);
a.xor_(x86::eax, x86::eax);
a.bind(loop);
a.mov(x86::ax, x86::word_ptr(x86::rsi));
a.bind(loop_d);
if (FakeCode) FakeProtect(a);
a.mov(x86::byte_ptr(x86::rdi), x86::al);
a.inc(x86::rdi);
a.dec(x86::ah);
a.test(x86::ah, x86::ah);
a.jnz(loop_d);
a.add(x86::rsi, 2);
a.dec(x86::rcx);
a.test(x86::rcx, x86::rcx);
a.jnz(loop);
a.mov(x86::rax, x86::rdx); //此时执行完毕后 rax 存放的是 ImageBase
a.pop(x86::rdx);
a.pop(x86::rcx);
a.pop(x86::rdi);
a.pop(x86::rsi);
//确保此时 rax 或 eax 存放的是 ImageBase ,否则是未定义行为
if (NeedReloc)
RelocationSection(a);
a.ret();
shellcode = a.bufferData();
codesize = holder.codeSize();
datasize = datas.size() * sizeof(codata) + sizeof(INT64);
}
else
{
holder.init(envX86);
x86::Assembler a(&holder);
Label loop = a.newLabel();
Label loop_d = a.newLabel();
a.push(x86::esi);
a.push(x86::edi);
a.push(x86::ecx);
a.push(x86::edx);
a.mov(x86::eax, memX86);
a.mov(x86::edx, x86::qword_ptr(x86::eax, 0x8));
a.mov(x86::esi, AddRVABase(encryptInfo.CompressedData));
a.add(x86::esi, x86::edx);
a.mov(x86::edi, peinfo.PCodeSection->VirtualAddress);
a.add(x86::edi, x86::edx);
a.mov(x86::ecx, x86::dword_ptr(x86::esi));
a.add(x86::esi, 8);
a.xor_(x86::eax, x86::eax);
a.bind(loop);
a.mov(x86::ax, x86::word_ptr(x86::rsi));
a.bind(loop_d);
if (FakeCode) FakeProtect(a);
a.mov(x86::byte_ptr(x86::edi), x86::al);
a.inc(x86::edi);
a.dec(x86::ah);
a.test(x86::ah, x86::ah);
a.jnz(loop_d);
a.add(x86::esi, 2);
a.dec(x86::ecx);
a.test(x86::ecx, x86::ecx);
a.jnz(loop);
a.mov(x86::eax, x86::edx); //此时执行完毕后 rax 存放的是 ImageBase
a.pop(x86::edx);
a.pop(x86::ecx);
a.pop(x86::edi);
a.pop(x86::esi);
//确保此时 rax 或 eax 存放的是 ImageBase ,否则是未定义行为
if (NeedReloc)
RelocationSection(a);
a.ret();
shellcode = a.bufferData();
codesize = holder.codeSize();
datasize = datas.size() * sizeof(codata) + sizeof(INT32);
}
encryptInfo.ShellCodeDeCompress = (UINT)(encryptInfo.CompressedData + datasize);
peinfo.PointerOfWingSeciton += (datasize + codesize);
codata* pc;
if (is64bit)
{
auto bd = (INT64*)buffer;
*bd = (INT64)datas.size();
pc = (codata*)(bd + 1);
}
else
{
auto bd = (INT32*)buffer;
*bd = (INT32)datas.size();
pc = (codata*)(bd + 1);
}
//生成数据
for (auto i = datas.begin(); i != datas.end(); i++, pc++)
{
*pc = *i;
}
memcpy_s(pc, codesize, shellcode, codesize); //拷贝 shellcode
//清空代码段
::memset((LPVOID)OFFSET(packedPE, peinfo.PCodeSection->PointerToRawData), 0, peinfo.PCodeSection->SizeOfRawData);
auto tmp = (PIMAGE_SECTION_HEADER)TranModPEWapper(peinfo.PCodeSection);
tmp->Characteristics |= IMAGE_SCN_MEM_WRITE;
return TRUE;
}
对于以上代码你可能有一些疑问,我这里说一下:
为什么有重定位的相关代码生成操作,这个原因我在上一篇说了,这里就不赘述了。
为什么将被压缩的代码清空?因为压缩之后源代码还在,不清理的话这个和没被压缩有什么区别。
为什么将代码块改为可写?因为我需要写啊,类似的原因在上一篇说过了。
怎么用代码实现压缩和写ShellCode
进行解密,这里就不唠叨了。
在编写ShellCode
代码的时候,请一定保证如下原则,避免一些麻烦,否则会出现出乎意料的错误:
- 除了 eax / rax 其他寄存器用到的话,一定要注意保存好,因为其它函数调用有各种调用约定,一定不要影响它们,否则会出错。为什么要对 eax / rax 区别对待,因为通常来说它只用做返回值,调用函数返回结果一定会修改它,所以大可不必。
- 在使用 ASMJIT 生成汇编的时候,使用类似 MOV 的指令的时候,一定要注意如果要写入多大的数据一定要在目标操作数体现数来,比如要移动 WORD 大小的话,用 ax 就不要用 eax,否则它正常生成汇编指令不报错,结果和你想生成的代码不一样。
- 一定要注意堆栈平衡,这个是非常重要的东西,在64位尤甚,32位的操作系统也是十分注意堆栈平衡的。
羽夏壳世界——导入表加密的实现
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可