
1. 從“菜雞”到入門為什么逆向工程繞不開編碼與加密剛接觸逆向工程的朋友常常會卡在一個看似基礎實則至關重要的環節面對程序里一堆“亂碼”或者經過變換的數據完全無從下手。你興致勃勃地打開調試器跟到一個關鍵函數發現它傳進去的是一串“5L2g5aW977yM5oiR5Lus5LiA5Liq5a2X56ym5Liy”傳出來的卻是“你好這是一段中文”。或者你攔截到一個網絡包里面的關鍵參數是一長串毫無規律的十六進制字符經過某個算法處理后才變成可讀的明文。沒錯這就是編碼和加密算法在“作祟”。對于逆向分析來說它們就像是鎖住寶藏的第一道也是最常見的一道門。不理解這些算法你的逆向之路就永遠在門口打轉。我剛開始學逆向的時候也吃過不少虧。曾經為了分析一個軟件的注冊機制對著一段經過Base64編碼后又用自定義XOR處理的數據折騰了一整天最后才發現如果我先認出Base64的特征五分鐘就能搞定。所以今天我就把自己在逆向過程中對常見編碼和加密算法的識別、分析與逆向經驗總結下來。這不僅僅是CTF比賽中的常客更是商業軟件、移動應用、網絡協議分析中每天都會遇到的“老朋友”。我們會重點聊聊Base64、SM4這些高頻出現的算法以及如何從二進制代碼或腳本中快速識別它們并找到破解或繞過的思路。無論你是分析一個安卓APK的so庫還是解密一段網頁的JavaScript這些知識都是你的必備工具箱。2. 逆向思維下的編碼算法不只是“轉碼”那么簡單在正向開發中編碼Encoding主要用于數據表示、傳輸或存儲比如把二進制數據變成可打印的文本。但在逆向工程師眼里編碼算法常常被用作一種輕量級的“混淆”或“偽裝”手段。識別它們是還原數據真實面貌的第一步。2.1 Base64逆向分析中的“老熟人”與它的變種們Base64恐怕是逆向中最最常見的編碼了。它的核心特征非常明顯字符集通常由A-Z, a-z, 0-9, , /以及填充符組成輸出字符串的長度通常是4的倍數。如何在逆向中快速識別Base64靜態字符串掃描在IDA Pro、Ghidra等反編譯工具中直接搜索上述字符集構成的字符串。如果發現一段較長的、僅由這些字符組成的字符串大概率是Base64編碼的數據。動態行為觀察在調試時如果發現程序調用了諸如base64_encode、base64_decode或類似命名的函數或者數據流經過一個函數后從二進制/十六進制變成了可打印的ASCII字符串或反之就要高度懷疑。特征碼識別標準的Base64編碼/解碼函數有其固定的操作流程如按6位分組、查表替換。在一些簡單的程序中可能會直接內聯實現。你可以通過識別其常量表比如一個包含64個字符的數組來定位。逆向實戰中的Base64“花活”單純的Base64解碼很簡單但實際軟件中不會這么直接。多層嵌套編碼這是CTF和某些軟件中常用的技巧。比如一段數據先被Base64編碼結果再被Base64編碼一次甚至多次。在逆向時你需要觀察數據解碼后的結果是否仍然是Base64特征字符串。我常用的方法是寫個小腳本循環嘗試解碼直到結果不再符合Base64字符集為止。import base64 data “5L2g5aW977yM5oiR5Lus5LiA5Liq5a2X56ym5Liy” # 示例數據 while True: try: # 嘗試解碼并檢查是否為ascii可打印或utf-8 decoded base64.b64decode(data) # 嘗試轉換為字符串如果失敗則可能仍是二進制或另一層base64 try: text decoded.decode(‘utf-8’) print(f“解碼成功: {text}”) # 可以進一步檢查text是否還是base64字符串特征 if all(c in ‘ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/’ for c in text): print(“解碼后結果仍具有Base64特征可能為多層嵌套。”) data text # 繼續循環 else: break except UnicodeDecodeError: print(“解碼結果為二進制數據:”, decoded.hex()) break except Exception as e: print(“解碼失敗:”, e) break自定義碼表這是增強隱蔽性的常用手段。程序不使用標準的ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/碼表而是將其打亂。例如某些游戲或軟件使用自定義的64個字符作為碼表。逆向時關鍵就是找到這個碼表。你需要在反匯編代碼中尋找一個長度為64的常量字符數組它就是解密的鑰匙。結合其他簡單變換Base64編碼后可能再進行一次簡單的XOR異或或加減操作。這需要你在動態調試中觀察Base64解碼函數輸出后數據是否立即被另一個小函數處理。注意Base64只是一種編碼不是加密它沒有密鑰其“安全性”完全依賴于算法的隱蔽性如自定義碼表。一旦識別出來還原就是分分鐘的事。不要被它的外表唬住。2.2 十六進制Hex與URL編碼網絡數據抓包的好伙伴這兩種編碼在逆向網絡協議、分析HTTP/HTTPS請求響應時極為常見。十六進制編碼將每個字節轉換為兩個字符的0-9, a-f表示。例如字節0xAB變成字符串”AB”或”ab”。在逆向中如果你看到字符串長度是偶數且字符僅在0-9a-fA-F范圍內那很可能就是Hex編碼。Python的binascii.hexlify()和unhexlify()是處理它的利器。URL編碼Percent-Encoding為了在URL中安全傳輸特殊字符將其轉換為%后跟兩個十六進制數字的形式如空格變成%20中文常用UTF-8編碼后再進行此操作如“中”可能變成%E4%B8%AD。在逆向爬蟲或分析API時經常需要解碼URL參數。識別特征就是包含大量的%符號。逆向技巧很多網絡庫如libcurl或語言內置庫如Java的URLEncoder會直接調用相關函數。在反編譯代碼中搜索%字符串處理邏輯或定位hex、urlencode、percent等關鍵字可以幫助你快速找到相關代碼段。2.3 Unicode與字符串編碼解決亂碼問題的鑰匙在逆向跨平臺或國際化軟件時字符串編碼問題會讓你頭疼。程序內部可能使用UTF-8、UTF-16LE、UTF-16BE或GBK等。識別特征UTF-8英文字符單字節中文字符通常為3字節。在內存中ASCII部分正常中文部分每個字節都大于0x80。UTF-16LE在x86 Windows環境下非常常見。每個字符占2字節或4字節。英文字符ASCII會在內存中呈現為類似a\x00b\x00c\x00的形式ASCII碼后跟一個零字節。在IDA中你可能會看到字符串被識別為aWideString。GBK中文Windows傳統編碼。一個中文占2字節。逆向中的應用當你從內存或文件中dump出一段字符串數據顯示為亂碼時第一反應就是嘗試不同的編碼解碼。在Python中你可以簡單地嘗試data b’\xd6\xd0\xce\xc4’ # 示例字節數據 encodings [‘gbk’, ‘utf-8’, ‘utf-16-le’, ‘utf-16-be’] for enc in encodings: try: print(f“{enc}: {data.decode(enc)}”) except UnicodeDecodeError: pass在靜態分析中注意觀察程序調用的字符串處理API如Windows的MultiByteToWideChar/WideCharToMultiByte或Linux下的iconv相關函數它們指明了編碼轉換的發生點。3. 對稱加密算法逆向找到那個“唯一的鑰匙”對稱加密算法在軟件保護、通信加密、本地數據存儲中應用極廣。逆向的目標往往不是破解算法本身現代加密算法在數學上是安全的而是找到那個被隱藏或硬編碼在程序中的密鑰Key或者理解其使用模式如ECB、CBC從而能夠模擬加密解密過程。3.1 國密SM4算法識別與分析SM4是我國商用密碼標準中的分組加密算法在金融、政務及一些國內軟件中越來越常見。它和AES類似是分組密碼分組長度128位密鑰長度128位。如何在二進制代碼中識別SM4查找常量表SM4算法內部使用一個固定的S盒Substitution Box和FK、CK常量。這是最可靠的指紋。SM4的S盒是一個256字節的固定表。如果你在反匯編代碼的數據段.data或.rdata中發現一個256字節的、內容固定的數組可以將其與標準SM4 S盒進行比對。標準SM4 S盒的前幾個字節是{0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, …}。函數特征SM4的輪函數操作涉及S盒查表、循環移位和異或。在反編譯代碼中你可能會看到大量的查表操作byte ptr [table_base index]、32位整數的循環左移rol指令或((x n) | (x (32-n)))的代碼模式以及異或xor操作。字符串與導入表如果程序使用了開源密碼庫如GMSSLBouncyCastle的國密Provider可能會在字符串或導入函數中留下SM4、GMSSL、BC等痕跡。對于Java程序可以查找SM4Engine、SM4Cipher等類名。SM4工作模式ECB vs CBC識別出算法后確定其工作模式同樣關鍵它直接影響到逆向和攻擊的難度。ECB模式最簡單的模式相同的明文塊加密后得到相同的密文塊。在代碼中它通常表現為直接對每個128位分組調用加密函數沒有額外的初始化向量IV處理。安全性較低在逆向中如果你能獲得一段已知明文及其對應的密文可能有助于分析或驗證密鑰。CBC模式更常用的模式每個明文塊先與前一個密文塊異或后再加密。需要一個初始化向量IV。在代碼中你會看到在加密/解密循環開始前有一個IV被加載或生成并且在處理每個分組時有一個與上一分組密文或解密中間結果進行異或的操作。逆向時必須同時找到Key和IV才能正確解密。逆向實戰心得 對于像SM4、AES這類標準強加密算法直接通過逆向分析從算法中推導出密鑰幾乎不可能除非實現有嚴重漏洞。我們的主要思路是密鑰硬編碼在程序的字符串、常量數組或資源文件中搜索可能的密鑰。密鑰可能是16字節128位的十六進制字符串或可打印字符。使用findcrypt-yara等IDA插件可以幫助識別常見算法的常量。密鑰派生密鑰可能不是直接存儲而是通過一個密碼Password和鹽值Salt經過KDF密鑰派生函數如PBKDF2計算得出。你需要找到生成密鑰的那個函數并嘗試輸入已知的測試密碼觀察生成的密鑰是否與加密所用的一致。內存dump在程序運行時密鑰必然會被加載到內存中。如果加密/解密操作發生在你的調試會話期間你可以在調用加密函數如SM4_Encrypt時通過調試器查看傳入的密鑰參數或者在該函數入口處設置斷點從寄存器或棧中提取密鑰。白盒密碼在一些高保護場景中可能會使用白盒密碼技術將密鑰與算法混淆在一起。逆向這種實現極其復雜通常需要深厚的密碼學和程序分析功底。遇到這種情況可能需要考慮其他攻擊面如協議層、輸入驗證等。3.2 其他常見對稱加密算法特征速查除了SM4逆向中還常遇到AES識別方法與SM4類似查找其S盒256字節和輪常量Rcon數組。AES的S盒是公開的前幾個字節為{0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, …}。同樣需要注意ECB、CBC等模式。DES/3DESDES的S盒是8個4x16的查找表共512位。3DES是DES的三次應用。在較老的金融系統或遺留軟件中常見。RC4流密碼。特征包括一個256字節的S盒初始化過程for i in range(256): S[i]i和偽隨機子密碼生成算法。代碼中通常有兩個索引變量i和j在循環更新。TEA/XTEA非常簡單的小型分組密碼核心操作是循環、加法、異或和移位通常直接以內聯代碼實現沒有明顯的S盒。其魔數常數0x9e3779b9黃金比例的倒數是一個關鍵識別特征。4. 非對稱加密與哈希算法逆向中的驗證與簽名在逆向中非對稱加密如RSA和哈希算法如SM3、MD5、SHA系列通常不用于直接加密大量數據而是用于簽名驗證、密鑰交換或完整性校驗。4.1 哈希算法識別與對抗哈希算法將任意長度數據映射為固定長度的摘要。在軟件中常用于驗證文件完整性、密碼存儲加鹽哈希或生成挑戰碼。識別特征常量每種哈希算法都有固定的初始化向量IV。例如MD5的IV是0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476。SHA-1、SHA-256也都有各自的IV常量。在數據段找到這些常量基本就能確定算法。函數名/字符串動態鏈接庫可能導出MD5_Init、SHA256_Update等函數。字符串中也可能出現算法名。輸出長度MD5輸出128位16字節SHA-1輸出160位20字節SHA-256輸出256位32字節SM3輸出256位32字節。如果你看到程序在比較一個固定長度的二進制串可以猜測其算法。逆向場景與應對注冊碼/許可證驗證軟件可能用哈希算法計算機器特征如硬盤序列號、MAC地址生成一個“機器碼”然后要求用戶輸入對應的“注冊碼”。這個注冊碼可能是機器碼經過某種變換如與一個固定密鑰拼接后再哈希或使用非對稱算法簽名的結果。你需要找到生成機器碼和驗證注冊碼的代碼邏輯。API請求簽名很多網絡應用如akamai盾、hcaptcha的后端交互或一些APP的API會對請求參數進行哈希通常用HMAC或簽名防止篡改。逆向的目標是找到生成簽名的密鑰和算法以便能自己構造合法請求。這通常需要分析JavaScriptWeb逆向或移動端的Native代碼Android/iOS逆向。密碼驗證服務器不存儲明文密碼存儲的是加鹽哈希值。客戶端登錄時對用戶輸入的密碼進行同樣的哈希運算后發送。在逆向客戶端時你需要找到哈希函數和鹽值這可能用于制作離線密碼破解工具或進行撞庫測試需在法律允許范圍內。實操心得對于哈希由于其單向性逆向的目標幾乎從不可能是“解密”哈希值而是識別算法、找到鹽值、并能夠復現計算過程。例如如果你發現一個游戲客戶端將用戶密碼與一個硬編碼的字符串拼接后做MD5那么你就可以用同樣的方式生成任意賬號的密碼哈希可能用于本地模擬登錄測試。4.2 非對稱加密以RSA為例在逆向中的角色RSA算法在逆向中常見于軟件激活、許可證驗證、通信密鑰交換等場景。識別點大數運算代碼中會出現非常大的整數幾十上百字節以及模冪運算a^b mod n。可能會調用大數庫如OpenSSL的BN函數。密鑰數據在程序數據段可能會發現PEM格式的證書字符串以—–BEGIN PUBLIC KEY—–開頭或者直接存儲著模數n和公鑰指數e通常為65537的二進制大整數。功能上下文代碼在驗證一個簽名使用公鑰解密一段數據與計算出的哈希值對比或者用公鑰加密一小段數據如會話密鑰。逆向策略 對于RSA私鑰通常不會出現在客戶端。因此逆向的目標通常是提取或繞過公鑰驗證如果你只是想讓程序認為簽名有效可以嘗試修改驗證函數的跳轉指令Patch或者找到一個有效的簽名并硬編碼到程序中。分析密鑰交換過程在通信協議中客戶端可能用服務器的公鑰加密一個隨機生成的對稱密鑰。你需要理解這個流程以便在中間人攻擊或模擬客戶端時能夠生成合法的加密數據。尋找弱密鑰或舊版本漏洞極少數情況下程序可能使用強度不足的RSA密鑰如512位或者使用的隨機數生成器有缺陷。但這需要專業的密碼學分析工具和知識。5. 實戰逆向流程與工具鏈配合理論說了這么多我們來看一個綜合性的簡化實戰流程假設我們要分析一個Android Native So庫中的加密函數。5.1 靜態分析定位入口使用IDA Pro/Ghidra加載So文件首先進行反編譯等待自動分析完成。字符串搜索在字符串窗口中搜索base64、encrypt、decrypt、AES、SM4、MD5、SHA等關鍵詞。注意中英文。查找密碼學常量使用FindCrypt等插件或手動在數據段瀏覽尋找大的、看起來隨機的常量數組可能是S盒、IV、魔數。交叉引用分析找到上述字符串或常量的引用位置跳轉到相關函數。這很可能就是加密/解密/編碼/哈希的入口函數。5.2 動態調試驗證猜想靜態分析只能給出可能的位置動態調試才能看清具體的數據流和參數。使用Frida進行Hook這是移動端逆向的神器。你可以寫一個Frida腳本Hook你懷疑的加密函數。// 示例Hook一個名為 native_encrypt 的JNI函數 Java.perform(function() { var targetClass Java.use(“com.example.app.CryptoHelper”); // 假設有一個native方法叫 encryptData targetClass.encryptData.overload(‘[B’, ‘[B’).implementation function(input, key) { console.log(“[] encryptData called!”); console.log(“Input (hex):”, bytesToHex(input)); console.log(“Key (hex):”, bytesToHex(key)); var result this.encryptData(input, key); // 調用原函數 console.log(“Result (hex):”, bytesToHex(result)); return result; }; function bytesToHex(bytes) { /* 轉換函數 */ } });通過Hook你可以直接看到函數傳入的明文、密鑰和輸出的密文從而100%確認該函數的功能。使用IDA Pro遠程調試對于更底層的Native代碼邏輯分析可以將IDA連接到手機上的調試服務器如android_server在關鍵函數地址下斷點單步跟蹤寄存器、內存和棧的變化觀察算法每一步的執行細節。這對于分析自定義或混淆過的算法尤其有用。5.3 算法還原與模擬一旦通過動態調試確認了算法、密鑰、IV和模式下一步就是用自己的代碼復現這個過程。提取關鍵參數從內存或代碼中提取出密鑰、IV、S盒如果是自定義的、工作模式。選擇對應庫在Python中可以使用pycryptodome或cryptography庫來實現標準算法AES, SM4等。對于自定義編碼或簡單變換自己實現即可。編寫模擬代碼from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 # 假設從逆向中獲取到的信息 key b’your_16_byte_key!!!’ # 16字節密鑰 iv b’initial_vector_16b’ # 16字節IV (CBC模式需要) cipher_text_hex “加密后的十六進制字符串” cipher AES.new(key, AES.MODE_CBC, iv) cipher_text_bytes bytes.fromhex(cipher_text_hex) plain_text_padded cipher.decrypt(cipher_text_bytes) plain_text unpad(plain_text_padded, AES.block_size) # 去除填充 print(“Decrypted:”, plain_text.decode())驗證用你的模擬代碼去解密一段已知的密文看是否能得到正確的明文。或者用你的代碼加密一段數據看是否與目標程序產生的結果一致。6. 常見問題與排查技巧實錄在實際逆向過程中你會遇到各種奇怪的問題。這里記錄一些我踩過的坑和解決思路。問題1明明找到了加密函數Hook時卻捕獲不到調用可能原因1函數名混淆。So庫中的函數名可能被混淆成a、b、c或無意義字符串。此時靜態分析找到的函數地址可能不準或者Hook腳本使用的函數簽名不對。解決使用函數地址進行Hook而不是函數名。在IDA中查看函數的起始地址在Frida中使用Module.findExportByName(null, offset)或絕對地址進行Hook。可能原因2調用時機過早。加密函數可能在APP啟動或某個初始化階段就被調用你的Frida腳本附著上去時已經晚了。解決使用frida -U -f com.package.name –no-pause在APP啟動早期就注入腳本或者修改APP的啟動方式確保腳本最先加載。可能原因3多線程調用。函數可能在非主線程調用導致Frida默認的Java.perform上下文不對。解決確保Hook代碼在Java.perform內并且考慮使用setImmediate或檢查線程。問題2解密出來的數據開頭或結尾有亂碼可能原因1填充問題。分組加密需要填充。常見的填充方式有PKCS#7、ZeroPadding等。如果解密時使用的填充方式與加密時不一致就會導致最后一塊數據錯誤。解決嘗試不同的填充方式。觀察解密后數據的最后幾個字節它們可能就是填充值。例如PKCS#7填充的字節值就是填充的長度。可能原因2編碼問題。解密出的數據可能是二進制數據你直接當成UTF-8解碼就會亂碼。解決先不要解碼打印十六進制形式hex()查看。它可能是圖片數據、序列化對象如Protobuf或其他結構化二進制數據。可能原因3IV錯誤。CBC模式需要正確的IV。如果IV獲取錯誤第一個解密塊會是亂碼但后續塊可能正確錯誤傳播。解決仔細檢查動態調試中傳入的IV值。問題3算法似乎是標準的但用自己的庫實現結果總是不對可能原因1工作模式或參數細節。除了ECB、CBC還有CFB、OFB、CTR等模式。確認模式是否正確。另外一些實現可能使用“CBC with no padding”而你的庫默認使用了填充。可能原因2密鑰或IV處理。程序可能對原始的密鑰字符串進行了預處理比如先做一次MD5哈希取前16字節作為AES密鑰。解決在調試器中在加密函數入口處不僅打印傳入的“密鑰”參數還要打印實際參與加密運算的密鑰數據即函數內部處理后的結果進行對比。可能原因3字節序問題。特別是在處理多字節整數如AES的輪密鑰或從文件中讀取密鑰時大端序和小端序可能會搞錯。問題4遇到未知的、看起來像自定義的加密/編碼怎么辦第一步動態跟蹤。這是最有效的方法。在調試器中單步跟蹤記錄下輸入數據經過每一個操作異或、加減、移位、查表后的變化。用紙筆或文本編輯器記下每一步的中間狀態。第二步尋找規律。觀察這些操作是否可逆異或、加減同一個數是可逆的查表如果有反向表也是可逆的。嘗試用窮舉小數據如單個字節0x00, 0x01, 0xFF輸入觀察輸出來推斷查表的內容或運算的性質。第三步嘗試模擬。根據記錄的步驟用高級語言Python復現這個過程。先從最簡單的部分開始逐步增加復雜度并與調試結果對比。第四步利用符號執行或污點分析高級技巧。對于復雜的混淆可以使用如Angr、Triton等框架進行自動化分析但這需要較高的學習成本。逆向編碼和加密算法就像是在解一個設計者留下的謎題。它考驗的不僅是你的技術知識更是耐心、觀察力和邏輯推理能力。從最基礎的Base64識別開始逐步深入到復雜的自定義算法這個過程本身就是極大的樂趣和成就感來源。記住沒有無法分析的程序只有還沒找到的突破口。每次成功還原一個算法你的工具箱里就多了一件利器下次再遇到類似的保護就能更快地直擊要害。