前言

近些日子在很多線上比賽中都遇到了smc文件加密技術,比較出名的有Hgame杭電的比賽,于是我準備實現一下這項技術,但是在網上看了很多文章,發現沒有講的特別詳細的,或者是無法根據他們的方法進行實現這項技術,因此本篇文章就是分享我在學習以及嘗試smc文件加密技術時所遇到的麻煩以及心得。

該篇文章將會從我學習這項技術的視角,講述我屢次失敗的經歷,一點點深入。

SMC局部代碼加密技術簡介:

SMC(Software-Based Memory Encryption)是一種局部代碼加密技術,它可以將一個可執行文件的指定區段進行加密,使得黑客無法直接分析區段內的代碼,從而增加惡意代碼分析難度和降低惡意攻擊成功的可能性。

SMC的基本原理是在編譯可執行文件時,將需要加密的代碼區段(例如函數、代碼塊等)單獨編譯成一個section(段),并將其標記為可讀、可寫、不可執行(readable, writable, non-executable),然后通過某種方式在程序運行時將這個section解密為可執行代碼,并將其標記為可讀、可執行、不可寫(readable, executable, non-writable)。這樣,攻擊者就無法在內存中找到加密的代碼,從而無法直接執行或修改加密的代碼。

SMC技術可以通過多種方式實現,例如修改PE文件的Section Header、使用API Hook實現代碼加密和解密、使用VMProtect等第三方加密工具等。加密時一般采用異或等簡單的加密算法,解密時通過相同的算法對密文進行解密。SMC技術雖然可以提高惡意代碼的抗分析能力,但也會增加代碼運行的開銷和降低代碼運行速度。

具體來說,SMC實現的主要步驟包括:

  1. 1. 讀取PE文件并找到需要加密的代碼段。
  2. 2. 將代碼段的內容進行異或加密,并更新到內存中的代碼段。
  3. 3. 重定向代碼段的內存地址,使得加密后的代碼能夠正確執行。
  4. 4. 執行加密后的代碼段。

SMC的優點在于:

  1. 1. SMC采用的是軟件實現方式,因此不需要硬件支持,可以在任何平臺上運行。
  2. 2. SMC對于程序的執行速度影響較小,因為代碼解密和執行過程都是在內存中進行的。
  3. 3. SMC可以對代碼進行多次加密,增加破解的難度。
  4. 4. SMC可以根據需要對不同的代碼段進行不同的加密方式,從而提高安全性。

然而,SMC的缺點也顯而易見,主要包括:

  1. 1. SMC的實現比較復雜,需要涉及到PE文件結構、內存管理等方面的知識。
  2. 2. SMC需要在運行時動態地解密代碼,因此會對程序的性能產生一定的影響。
  3. 3. SMC只能對靜態的代碼進行加密,對于動態生成的代碼無法進行保護。
  4. 4. SMC對于一些高級的破解技術(如內存分析)可能無法完全保護程序。

綜上所述,SMC是一種局部代碼加密技術,可以提高程序的安全性,但也存在一些局限性。在實際應用中,需要根據具體的情況選擇最合適的保護方案,綜合考慮安全性、性能和可維護性等因素。

[流程圖]

+---------------------+
| 讀取PE文件 |
| 找到代碼段 |
+---------------------+
|
|
v
+---------------------------------+
| 對代碼段進行異或加密 |
| 并更新到內存中的代碼段 |
+---------------------------------+
|
|
v
+---------------------------------+
| 重定向代碼段的內存地址, |
| 使得加密后的代碼能夠正確執行 |
+---------------------------------+
|
|
v
+---------------------+
| 執行加密后的代碼段 |
+---------------------+

[小結一下]

前面說的非常的高端,其實通俗的講就是程序可以自己對自己底層的字節碼進行操作,就是所謂的自解密技術。其在ctf比賽中常見的就是可以將一段關鍵代碼進行某種加密,然后程序運行的時候就直接解密回來,這樣就可以干擾解題者的靜態分析,在免殺方面也是非常好用的技術。可以利用該技術隱藏關鍵代碼。

言歸正傳 如何實現這項技術

說實話,實現這項技術我是踩了非常多的坑的,接下來將會一一分享。

用偽代碼解釋一下該技術:

proc main:
............
IF .運行條件滿足
  CALL DecryptProc (Address of MyProc)//對某個函數代碼解密
  ........
  CALL MyProc                           //調用這個函數
  ........
  CALL EncryptProc (Address of MyProc)//再對代碼進行加密,防止程序被Dump
......
end main

OK,非常明確,首先我是使用了Dev-C++ 6.7.5編譯器,使用的MinGW GCC 9.2.0 32bit Debug的編譯規則。

我們回憶一下該項技術,加入我們需要加密的是函數fun,那么我們首先需要使用指針找到fun的地址,一開始我使用的是int類型的指針,代碼如下:

void fun()
{
    char flag[]="flag{this_is_test}";
    printf("%s",flag);
}
int main ()
{
    
    int *a=(int *)fun;
    for(int i = 0 ; i < 10  ; i++ )
    {
        printf("%x ",*(a++));
    }
}

輸出結果為:

83e58955 45c738ec 616c66e5 e945c767 6968747b 73ed45c7 c773695f 745ff145 c7667365 7d74f545

然后我們把編譯出來的文件放到ida里面觀察

可以發現輸出的內容確實是fun的字節碼,但是由于int在c語言中占用了四個字節,因此是由四個16進制的機器碼根據小端序排列輸出的,那么為了解決這種連續字節碼的問題我們需要找到一個只占用一個字節的指針,首先我想到了char類型,于是我馬上更改代碼,使用char類型的指針,得到了如下的輸出結果。

55 ffffff89 ffffffe5 ffffff83 ffffffec 38 ffffffc7 45 ffffffe5 66

顯然,這里是忽略的char的符號位的問題,有符號char型如果最高位是1,意思是超過了0x7f,當%X格式化輸出的時候,則會將這個類型的值拓展到int型的32位,所以才會出現0xff,被擴展為ffffffff。

一籌莫展之際,我想起了在c語言中還有一種數據類型是只占一個字節的,那就是byte類型的數據,將代碼改成byte類型之后可以發現輸出變得正常了。

輸出為:

55 89 e5 83 ec 38 c7 45 e5 66

這個就是正確的字節碼的形式了。

那么我們需要定位到程序段進行加密了,由于本次只是實驗,我們采取簡單的異或加密方式,異或加密的特點就是加密函數也可以是解密函數,極大的方便了我們此次實驗。我們可以先在ida中看到我們需要加密的程序段的位置。

在ida中我們可以發現我們需要解密的fun函數占用的地址段是0x00401410-00401451,那我們只需要將這一段內存中的機器碼進行異或加密理論上就可以實現smc文件加密技術了。

實現代碼如下:

void fun()
{
    char flag[]="flag{this_is_test}";
    printf("%s",flag);
}
int main ()
{
    
    byte *a=(byte *)fun;
    byte *b = a ;
    for( ; a!=(b+0x401451-0x401410+1) ; a++ )
    {
        *a=*a^3;
    }
    fun();
}

這段代碼直接運行的話會出現內存錯誤,這是因為代碼運行的時候對原本未被加密的fun函數進行了異或處理,導致本來應該是解密的操作變成了加密操作,然后機器無法識別該段內存就出現了內存錯誤,因此在運行代碼前我們需要將文件中的fun函數部分進行加密操作。我這里使用idapython對字節碼進行操作,然后將文件dump出來,完成對文件的加密。

idapython腳本為:

for i in range(0x401410,0x401451):
    patch_byte(i,get_wide_byte(i)^3)

運行后把代碼dump下來,再運行

發現出現內存錯誤告警,猜測可能是dev-c++的編譯器開啟了隨機基地址和數據保護,因此選擇更換編譯器,并關閉隨機基地址選項。這里使用的是visual studio 2019,32位的debug模式進行編譯

但是遺憾的是仍然無法運行,思考了一會兒之后發現可能是該段內存沒有被設置成可讀、可執行、可寫入,導致程序無法識別這段內存了,因此我們改變方法使用程序段的概念,通過對整個程序段進行加密解密,來實現smc技術。

使用的代碼是:

#include
#include
#include
using namespace std;
#include 
#pragma code_seg(".hello")
void Fun1()
{
      char flag[]="flag{this_is_test}";
    printf("%s",flag);
}
#pragma code_seg()
#pragma comment(linker, "/SECTION:.hello,ERW")
void Fun1end()
{
}
void xxor(char* soure, int dLen)   //異或
{
    for (int i = 0; i < dLen;i++)
    {
         soure[i] = soure[i] ^3;
    }
}
void SMC(char* pBuf)     //SMC解密/加密函數
{
    const char* szSecName = ".hello";
    short nSec;
    PIMAGE_DOS_HEADER pDosHeader;
    PIMAGE_NT_HEADERS pNtHeader;
    PIMAGE_SECTION_HEADER pSec;
    pDosHeader = (PIMAGE_DOS_HEADER)pBuf;
    pNtHeader = (PIMAGE_NT_HEADERS)&pBuf[pDosHeader->e_lfanew];
    nSec = pNtHeader->FileHeader.NumberOfSections;
    pSec = (PIMAGE_SECTION_HEADER)&pBuf[sizeof(IMAGE_NT_HEADERS) + pDosHeader->e_lfanew];
    for (int i = 0; i < nSec; i++)
    {
        if (strcmp((char*)&pSec->Name, szSecName) == 0)
        {
            int pack_size;
            char* packStart;
            pack_size = pSec->SizeOfRawData;
            packStart = &pBuf[pSec->VirtualAddress];
            xxor(packStart, pack_size);
            return;
        }
        pSec++;
    }
}
void UnPack()   //解密/加密函數
{
    char* hMod;
    hMod = (char*)GetModuleHandle(0);  //獲得當前的exe模塊地址
    SMC(hMod);
}
int main()
{
   //UnPack();
    UnPack(); //
    Fun1();
    return 0;
}

如此操作后,做一個簡單的驗證看看能不能成功,就是進行兩次調用unpack函數來看看程序能否正常運行,發現程序成功的輸出了flag那么使用程序段的方式是正確的!!

這段代碼實現了一個簡單的SMC自修改代碼技術,主要包括以下幾個部分:

  1. 1. 使用 #pragma code_seg 指令將 Fun1() 函數代碼段定義為一個名為 ".hello" 的新代碼段,使其與其他代碼段隔離開來,方便后面的加密和解密。
  2. 2. 使用 #pragma comment(linker, "/SECTION:.hello,ERW") 指令將 ".hello" 代碼段設置為可讀、可執行、可寫入的,以便后面的加密和解密操作。
  3. 3. 定義 Fun1end() 函數作為 Fun1() 函數的結束點,以便后面的加密操作。
  4. 4. 定義 xxor() 函數用于將指定的字符串進行異或加密/解密。
  5. 5. 定義 SMC() 函數,該函數用于解密指定代碼段的內容。具體操作是遍歷 PE 文件的各個段,找到指定代碼段并對其進行解密。
  6. 6. 定義 UnPack() 函數,該函數用于對當前進程的代碼段進行解密操作。具體操作是獲取當前模塊的句柄,讀取模塊的 PE 文件并對指定代碼段進行解密。
  7. 7. 在 main() 函數中調用 UnPack() 函數進行解密操作,然后調用 Fun1() 函數進行計算。

需要注意的是,這段代碼只是一個簡單的示例,實際應用中可能需要更加復雜的加密和解密方法,以及更多的安全措施來保護代碼的安全性。同時,SMC自修改代碼技術也存在一定的風險和挑戰,需要仔細評估和規劃,謹慎使用。

代碼寫好之后,仍然需要我們自己手動先加密程序,在別的文章中所使用的方法和工具我找了很久都沒有找到,因此決定自己使用ida+idapython來實現對程序的加密,最后dump出程序,然后程序運行時會自己進行解密。

ida中的hello程序段

我們需要的是將所有hello程序段的內容進行加密。

idapython腳本:

for i in range(0x417000,0x4170A4):
    patch_byte(i,get_wide_byte(i)^3)

雖然dump出來的程序能輸出我們程序中的值,但是仍然出現了堆棧不平衡的問題,因此在終端運行程序時仍然會爆出內存錯誤的告警,研究到此時我已經心態崩了,找了很多大牛的博客都沒有詳細提到怎么實現加密程序,那這樣的話只能自己手擼了,這里使用python語言,代碼為:

import pefile
def encrypt_section(pe_file, section_name, xor_key):
    """
    加密PE文件中指定的區段
    """
    # 找到對應的section
    for section in pe_file.sections:
        if section.Name.decode().strip('\x00') == section_name:
            print(f"[*] Found {section_name} section at 0x{section.PointerToRawData:08x}")
            data = section.get_data()
            encrypted_data = bytes([data[i] ^ xor_key for i in range(len(data))])
            pe_file.set_bytes_at_offset(section.PointerToRawData, encrypted_data)
            print(f"[*] Encrypted {len(data)} bytes at 0x{section.PointerToRawData:08x}")
            return
    print(f"[!] {section_name} section not found!")
if __name__ == "__main__":
    filename = "test1.exe"#加密文件的名字,需要在同一根目錄下
    section_name = ".hello"#加密的代碼區段名字
    xor_key = 0x03#異或的值
    print(f"[*] Loading {filename}")
    pe_file = pefile.PE(filename)
    # 加密
    print("[*] Encrypting section")
    encrypt_section(pe_file, section_name, xor_key)
    # 保存文件
    new_filename = filename[:-4] + "_encrypted.exe"
    print(f"[*] Saving as {new_filename}")
    pe_file.write(new_filename)
    pe_file.close()

這段代碼實現了對PE文件中指定的代碼區段進行異或加密的功能,具體解釋如下:

  1. 1. 導入pefile模塊:該模塊提供了解析PE文件格式的功能;
  2. 2. 定義encrypt_section函數:該函數接收三個參數,分別是PE文件對象pe_file、待加密區段名稱section_name和異或值xor_key。函數首先遍歷PE文件中的所有區段,查找名字為section_name的區段;
  3. 3. 對指定的代碼區段進行加密:如果找到了名字為section_name的代碼區段,該函數調用PE文件對象的set_bytes_at_offset方法,將指定區段中的每個字節和異或值異或,得到加密后的數據,并將加密后的數據寫回指定區段。注意,set_bytes_at_offset方法需要傳入一個字節串作為參數,因此需要將加密后的數據轉換為字節串;
  4. 4. main函數:該函數首先指定待加密的PE文件名filename、待加密的區段名稱section_name和異或值xor_key。然后,它創建一個PE文件對象pe_file,讀入PE文件;接著調用encrypt_section函數,對指定區段進行加密;最后,將加密后的文件寫入新的文件中,并關閉PE文件對象。

這段代碼的執行過程如下:

  1. 1. 調用main函數,讀取PE文件test1.exe;
  2. 2. 找到名字為.hello的區段,對其中的每個字節和異或值0x03進行異或,得到加密后的數據;
  3. 3. 將加密后的數據寫回.hello區段,并將加密后的文件保存為test1_encrypted.exe。

腳本完成后,滿懷激動的運行它!

成功了!!

終端也成功的運行出了加密后的程序,我們再到ida中觀察它

成功的無法靜態分析。那么至此我們就成功的實現了該項技術!

CTF實戰

SMC 技術在 CTF 比賽中有很多應用,主要是用來對抗反調試和反編譯等工具的逆向分析。下面是幾個常見的應用場景:

  1. 1. 局部代碼加密:CTF 比賽中有很多加密的二進制程序,利用 SMC 技術可以對程序的關鍵代碼進行加密,增加分析難度,提高程序的安全性。
  2. 2. 加密字符串和常量:CTF 比賽中有很多加密的字符串和常量,這些字符串和常量通常用來存儲關鍵信息,如密鑰、密碼等。利用 SMC 技術可以對這些字符串和常量進行加密,增加分析難度,提高程序的安全性。
  3. 3. 防止調試:CTF 比賽中有很多程序會使用調試器進行逆向分析,利用 SMC 技術可以對程序進行調試器檢測和防御,防止調試器的使用。
  4. 4. 防止反編譯:CTF 比賽中有很多程序會被反編譯,利用 SMC 技術可以對程序進行反編譯檢測和防御,防止程序被反編譯。

總之,SMC 技術在 CTF 比賽中是一個非常有用的技術,可以用來保護程序的安全性,增加分析難度,提高程序的安全性。

[Hgame2023]patchme

點開文件可以看到一個可疑函數對文件地址進行操作,懷疑是smc文件加密技術

跟蹤過去看一看

發現地址爆紅,出現大量沒有被解析的數據段 那么實錘此處就是smc文件加密,那么我們將其異或回去,使用idc或者idapython

運行idapython腳本之后發現本來ida無法識別的匯編代碼變得可以識別了,那么我們聲明所有的未聲明函數

就可以在下面找到輸出flag的方法了

EXP

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef int status;
typedef int selemtype;
int ida_chars[] =
{
    0xFA, 0x28, 0x8A, 0x80, 0x99, 0xD9, 0x16, 0x54,
    0x63, 0xB5, 0x53, 0x49, 0x09, 0x05, 0x85, 0x58,
    0x97, 0x90, 0x66, 0xDC, 0xA0, 0xF3, 0x8C, 0xCE,
    0xBD, 0x4C, 0xF4, 0x54, 0xE8, 0xF3, 0x5C, 0x4C,
    0x31, 0x83, 0x67, 0x16, 0x99, 0xE4, 0x44, 0xD1,
    0xAC, 0x6B, 0x61, 0xDA, 0xD0, 0xBB, 0x55
};
int c[]={
    0x92, 0x4F, 0xEB, 0xED, 0xFC, 0xA2, 0x4F, 0x3B,
    0x16, 0xEA, 0x67, 0x3B, 0x6C, 0x5A, 0xE4, 0x07,
    0xE7, 0xD0, 0x12, 0xBF, 0xC8, 0xAC, 0xE1, 0xAF,
    0xCE, 0x38, 0x91, 0x26, 0xB7, 0xC3, 0x2E, 0x13,
    0x43, 0xE6, 0x11, 0x73, 0xEB, 0x97, 0x21, 0x8E,
    0xC1, 0x0A, 0x54, 0xAE, 0xB5, 0xC9,0x28
};
int main ()
{
    for(int i = 0 ; i <= 46 ; i ++ )
    {
        printf("%c",ida_chars[i]^c[i]);
    }
}