1. 程式人生 > 其它 >羽夏殼世界——壓縮程式碼的實現

羽夏殼世界——壓縮程式碼的實現

寫在前面

  此係列是本人一個字一個字碼出來的,包括程式碼實現和效果截圖。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏殼世界——序 ,方便學習本教程。

壓縮原理

  由於展示最基本最簡單的實現,使用壓縮演算法就沒用複雜的。如果使用比較複雜的壓縮演算法,首先你在C++程式碼層面和彙編層面要有配套的程式碼,C++負責壓縮程式碼,彙編負責自我解壓縮,否則你壓縮完了,結果被壓縮後的PE檔案自己又解不了,這就很尷尬。
  我們本專案使用的演算法被稱之為RLE

壓縮演算法,英文全稱是run-length encoding,亦稱行程長度編碼。聽起來高大上,下面我用比較通俗語言就行介紹。
  比如一個字串:AABBCCDDDDEEEEEEEEE,一個二十個字元,我們如何使用該演算法進行壓縮呢?
  好,A有兩個,就用2A表示;B有兩個,用2B表示……最後我們得到下面的字串:2A2B2C4D10E,可以看到長度被進行了壓縮。
  這種演算法有一個比較嚴重的弊端,如果每一組相鄰的字元相同的少於2個,會導致負面影響,導致沒有壓縮效果甚至膨脹,但對於壓縮程式碼比較足夠了。因為裡面會有大量的0xCC0x00這樣的位元組,可以忽略這種演算法的缺陷導致的影響。

壓縮的實現

  既然是使用該方式進行壓縮,首先我們得進行編碼。每一個壓縮塊定義如下:

#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程式碼的時候,請一定保證如下原則,避免一些麻煩,否則會出現出乎意料的錯誤:

  1. 除了 eax / rax 其他暫存器用到的話,一定要注意儲存好,因為其它函式呼叫有各種呼叫約定,一定不要影響它們,否則會出錯。為什麼要對 eax / rax 區別對待,因為通常來說它只用做返回值,呼叫函式返回結果一定會修改它,所以大可不必。
  2. 在使用 ASMJIT 生成彙編的時候,使用類似 MOV 的指令的時候,一定要注意如果要寫入多大的資料一定要在目標運算元體現數來,比如要移動 WORD 大小的話,用 ax 就不要用 eax,否則它正常生成彙編指令不報錯,結果和你想生成的程式碼不一樣。
  3. 一定要注意堆疊平衡,這個是非常重要的東西,在64位尤甚,32位的作業系統也是十分注意堆疊平衡的。

下一篇

  羽夏殼世界——匯入表加密的實現