羽夏殼世界——壓縮程式碼的實現
阿新 • • 發佈:2022-04-11
寫在前面
此係列是本人一個字一個字碼出來的,包括程式碼實現和效果截圖。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏殼世界——序 ,方便學習本教程。
壓縮原理
由於展示最基本最簡單的實現,使用壓縮演算法就沒用複雜的。如果使用比較複雜的壓縮演算法,首先你在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 編寫注意事項
在編寫ShellCode
程式碼的時候,請一定保證如下原則,避免一些麻煩,否則會出現出乎意料的錯誤:
- 除了 eax / rax 其他暫存器用到的話,一定要注意儲存好,因為其它函式呼叫有各種呼叫約定,一定不要影響它們,否則會出錯。為什麼要對 eax / rax 區別對待,因為通常來說它只用做返回值,呼叫函式返回結果一定會修改它,所以大可不必。
- 在使用 ASMJIT 生成彙編的時候,使用類似 MOV 的指令的時候,一定要注意如果要寫入多大的資料一定要在目標運算元體現數來,比如要移動 WORD 大小的話,用 ax 就不要用 eax,否則它正常生成彙編指令不報錯,結果和你想生成的程式碼不一樣。
- 一定要注意堆疊平衡,這個是非常重要的東西,在64位尤甚,32位的作業系統也是十分注意堆疊平衡的。
下一篇
羽夏殼世界——匯入表加密的實現