天天看點

探究SMC局部代碼加密技術以及在CTF中的運用

作者:iHacking

前言

近些日子在很多線上比賽中都遇到了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. 讀取PE檔案并找到需要加密的代碼段。
  2. 将代碼段的内容進行異或加密,并更新到記憶體中的代碼段。
  3. 重定向代碼段的記憶體位址,使得加密後的代碼能夠正确執行。
  4. 執行加密後的代碼段。

SMC的優點在于:

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

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

  1. SMC的實作比較複雜,需要涉及到PE檔案結構、記憶體管理等方面的知識。
  2. SMC需要在運作時動态地解密代碼,是以會對程式的性能産生一定的影響。
  3. SMC隻能對靜态的代碼進行加密,對于動态生成的代碼無法進行保護。
  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的編譯規則。

【----幫助網安學習,需要網安學習資料關注我,私信回複“資料”免費擷取----】

① 網安學習成長路徑思維導圖

② 60+網安經典常用工具包

③ 100+SRC漏洞分析報告

④ 150+網安攻防實戰技術電子書

⑤ 最權威CISSP 認證考試指南+題庫

⑥ 超1800頁CTF實戰技巧手冊

⑦ 最新網安大廠面試題合集(含答案)

⑧ APP用戶端安全檢測指南(安卓+IOS)

我們回憶一下該項技術,加入我們需要加密的是函數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裡面觀察。

探究SMC局部代碼加密技術以及在CTF中的運用

可以發現輸出的内容确實是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局部代碼加密技術以及在CTF中的運用

但是遺憾的是仍然無法運作,思考了一會兒之後發現可能是該段記憶體沒有被設定成可讀、可執行、可寫入,導緻程式無法識别這段記憶體了,是以我們改變方法使用程式段的概念,通過對整個程式段進行加密解密,來實作smc技術。

使用的代碼是:

#include<Windows.h>
#include<string>
#include<string.h>
using namespace std;
#include <iostream>

#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. 使用 #pragma code_seg 指令将 Fun1() 函數代碼段定義為一個名為 ".hello" 的新代碼段,使其與其他代碼段隔離開來,友善後面的加密和解密。
  2. 使用 #pragma comment(linker, "/SECTION:.hello,ERW") 指令将 ".hello" 代碼段設定為可讀、可執行、可寫入的,以便後面的加密和解密操作。
  3. 定義 Fun1end() 函數作為 Fun1() 函數的結束點,以便後面的加密操作。
  4. 定義 xxor() 函數用于将指定的字元串進行異或加密/解密。
  5. 定義 SMC() 函數,該函數用于解密指定代碼段的内容。具體操作是周遊 PE 檔案的各個段,找到指定代碼段并對其進行解密。
  6. 定義 UnPack() 函數,該函數用于對目前程序的代碼段進行解密操作。具體操作是擷取目前子產品的句柄,讀取子產品的 PE 檔案并對指定代碼段進行解密。
  7. 在 main() 函數中調用 UnPack() 函數進行解密操作,然後調用 Fun1() 函數進行計算。

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

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

ida中的hello程式段

探究SMC局部代碼加密技術以及在CTF中的運用

我們需要的是将所有hello程式段的内容進行加密。

idapython腳本:

for i in range(0x417000,0x4170A4):
    patch_byte(i,get_wide_byte(i)^3)           
探究SMC局部代碼加密技術以及在CTF中的運用

雖然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. 導入pefile子產品:該子產品提供了解析PE檔案格式的功能;
  2. 定義encrypt_section函數:該函數接收三個參數,分别是PE檔案對象pe_file、待加密區段名稱section_name和異或值xor_key。函數首先周遊PE檔案中的所有區段,查找名字為section_name的區段;
  3. 對指定的代碼區段進行加密:如果找到了名字為section_name的代碼區段,該函數調用PE檔案對象的set_bytes_at_offset方法,将指定區段中的每個位元組和異或值異或,得到加密後的資料,并将加密後的資料寫回指定區段。注意,set_bytes_at_offset方法需要傳入一個位元組串作為參數,是以需要将加密後的資料轉換為位元組串;
  4. main函數:該函數首先指定待加密的PE檔案名filename、待加密的區段名稱section_name和異或值xor_key。然後,它建立一個PE檔案對象pe_file,讀入PE檔案;接着調用encrypt_section函數,對指定區段進行加密;最後,将加密後的檔案寫入新的檔案中,并關閉PE檔案對象。

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

  1. 調用main函數,讀取PE檔案test1.exe;
  2. 找到名字為.hello的區段,對其中的每個位元組和異或值0x03進行異或,得到加密後的資料;
  3. 将加密後的資料寫回.hello區段,并将加密後的檔案儲存為test1_encrypted.exe。

腳本完成後,滿懷激動的運作它!

探究SMC局部代碼加密技術以及在CTF中的運用

成功了!!

探究SMC局部代碼加密技術以及在CTF中的運用

終端也成功的運作出了加密後的程式,我們再到ida中觀察它。

探究SMC局部代碼加密技術以及在CTF中的運用

成功的無法靜态分析。那麼至此我們就成功的實作了該項技術!

CTF實戰

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

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

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

[Hgame2023]patchme

點開檔案可以看到一個可疑函數對檔案位址進行操作,懷疑是smc檔案加密技術。

探究SMC局部代碼加密技術以及在CTF中的運用

跟蹤過去看一看。

探究SMC局部代碼加密技術以及在CTF中的運用

發現位址爆紅,出現大量沒有被解析的資料段那麼實錘此處就是smc檔案加密,那麼我們将其異或回去,使用idc或者idapython

探究SMC局部代碼加密技術以及在CTF中的運用

運作idapython腳本之後發現本來ida無法識别的彙編代碼變得可以識别了,那麼我們聲明所有的未聲明函數。

探究SMC局部代碼加密技術以及在CTF中的運用

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

EXP

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<map>
#include<vector>
#include<queue>
#include<stack>
#include<set>
#include<string>
#include<cstring>
#include<list>
#include<stdlib.h>
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]);
    }
}
           

繼續閱讀