Encryption

減少對鍊式 64 位異或進行暴力攻擊的密鑰空間

  • April 30, 2015

我有一個數據塊,我知道它是使用鍊式 64 位 xor 加密的(對不起,如果這沒有意義,我不熟悉加密術語)。我對明文應該是什麼樣子略知一二,也知道用於解密數據的算法。

我對數據的了解:

  • 6 x 0x20 大小的“塊”,總大小為 0xC0
  • 第一個塊包含一個 Windows DLL 名稱(例如 foobar.dll),其大小寫未知(並且可能是混合的)。該名稱可能是典型 Windows 安裝中存在的 DLL 的名稱。
  • 其他五個塊每個都包含由上述 DLL 導出的過程的名稱。該名稱可能以大寫字母開頭,因為它是系統 DLL 中的導出。
  • 所有字元串都必須以 null 結尾。
  • 所有字元串都是“窄的”(即每個字元一個字節)。

解密算法如下:

extern unsigned int* ObfuscatedData;
extern unsigned int k1;
extern unsigned int k2;
unsigned int i = 0x18;
do
{
 ObfuscatedData[0] ^= k1;
 ObfuscatedData[1] ^= k2;
 k1 = ObfuscatedData[0];
 k2 = ObfuscatedData[1];
 ObfuscatedData += 2;
 --i;
}
while ( i );

這是我編寫的一個簡單的蠻力測試應用程序,用於測量窮舉攻擊需要多長時間(可以說,它的速度慢得令人無法接受)。它包含我要解密的數據塊(以及我自己生成的一個測試塊,以驗證我的程式碼是否有效)。

#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>

// Code analysis only analyzes single threaded code.
#pragma warning(disable : 6993)

bool ValidateString(std::uint8_t const* beg, std::uint8_t const* end)
{
 // If there are any non-alphanumeric characters (other than a period, due to
 // module name) in the string then it's probably garabge.
 for (std::ptrdiff_t i = 0; i < end - beg && beg[i]; ++i)
 {
   auto const c = beg[i];
   if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
         (c >= 'a' && c <= 'z') || c == '.'))
   {
     return false;
   }
 }

 return true;
}

int main(int /*argc*/, char* /*argv*/ [])
{
 // 6 x 0x20 chunks each containing an obfuscated null-terminated string. The
 // first chunk contains a module name, the rest contain procedure names.
 std::uint8_t const kObfuscatedData[] = {
   0x2E, 0x6B, 0x62, 0x33, 0xF2, 0x0D, 0xB1, 0x96, 0x33, 0x09, 0x1A, 0x74,
   0x79, 0x62, 0xE3, 0x67, 0x72, 0x80, 0x2D, 0x1E, 0xA9, 0x18, 0x0E, 0x4E,
   0xF4, 0xA4, 0x22, 0xF2, 0x68, 0xBE, 0x92, 0x5E, 0xA7, 0x3E, 0x17, 0xBD,
   0xAC, 0xD1, 0x64, 0x20, 0x45, 0xB8, 0x31, 0x1D, 0xE0, 0xD3, 0x86, 0x3C,
   0xC2, 0x4C, 0x75, 0x86, 0xE1, 0x25, 0x9F, 0x19, 0xD1, 0xFC, 0xE3, 0x80,
   0xE3, 0x20, 0x6E, 0x66, 0x56, 0x08, 0xA7, 0x09, 0xE5, 0xD6, 0x64, 0x4E,
   0x20, 0x24, 0x11, 0x2D, 0x16, 0x00, 0x13, 0x37, 0x0A, 0x3C, 0x11, 0x0B,
   0x1C, 0x11, 0x72, 0xAD, 0x99, 0x8B, 0x05, 0x96, 0x25, 0xD4, 0x21, 0x28,
   0xB3, 0x93, 0x05, 0xBD, 0x38, 0xD8, 0x55, 0xC6, 0x23, 0x1C, 0x31, 0xE2,
   0xFB, 0xFD, 0x25, 0xAD, 0x29, 0x4B, 0xFE, 0xA0, 0xC8, 0x3F, 0x3B, 0x50,
   0x4C, 0xDB, 0xF4, 0xEB, 0xDD, 0x78, 0x00, 0x43, 0x46, 0x8C, 0x3B, 0xB5,
   0xEE, 0xAC, 0x0E, 0xDB, 0x37, 0x76, 0x3B, 0xC7, 0x52, 0x74, 0x09, 0x28,
   0x2F, 0xDF, 0x32, 0x1E, 0x75, 0x94, 0xE7, 0x7D, 0x21, 0x77, 0xB2, 0xA2,
   0x93, 0xCD, 0x04, 0xAB, 0x39, 0xDE, 0xBB, 0x6A, 0xBD, 0x2C, 0xFD, 0xFE,
   0x45, 0x50, 0xCC, 0xC6, 0x97, 0x25, 0x26, 0xE0, 0xDA, 0xA6, 0x4B, 0x1A,
   0x11, 0x8C, 0x22, 0x84, 0x73, 0x13, 0x35, 0xF4, 0xA4, 0x80, 0x9C, 0xBA};

#if 0
 // Test data.
 // Chunk 0: SomeModuleName.dLl
 // Chunk 1: FooBarBaz
 // Chunk 2: StupidApiName
 // Chunk 3: ReallyLongStupidApiName
 // Chunk 4: Blah
 // Chunk 5: Wat
 // Key: a = 0x55555557 b = 0xDEADBEEF
 std::uint8_t const kObfuscatedData[] = {
   0x04, 0x3A, 0x38, 0x30, 0xA2, 0xD1, 0xC9, 0xAB, 0x3F, 0x0A, 0x23, 0x04,
   0x20, 0x0A, 0x4A, 0x11, 0x20, 0x09, 0x4E, 0x7F, 0xC4, 0x7D, 0x20, 0x2A,
   0xB8, 0xC8, 0x22, 0xEC, 0xC1, 0xA6, 0x9C, 0x10, 0xB2, 0xCB, 0x4D, 0xB0,
   0x09, 0xCC, 0xD0, 0x3F, 0x3C, 0x6F, 0xD7, 0x5F, 0x81, 0xA1, 0xC4, 0x5D,
   0xB8, 0x4C, 0xCD, 0x9B, 0x01, 0xF6, 0x19, 0x25, 0x13, 0xB0, 0x96, 0x06,
   0x02, 0x05, 0xF1, 0x7F, 0x82, 0x88, 0x96, 0xF0, 0x8A, 0x44, 0x2F, 0x16,
   0x3A, 0x3A, 0x14, 0x1D, 0x0C, 0x64, 0x52, 0x47, 0x63, 0x72, 0x70, 0x66,
   0x79, 0x11, 0x61, 0x9A, 0x93, 0xB7, 0x14, 0x9D, 0x39, 0xC5, 0x53, 0x85,
   0xCB, 0xEE, 0x64, 0xFA, 0x49, 0xAD, 0x6D, 0x47, 0x3C, 0x02, 0x32, 0x18,
   0x19, 0x09, 0x25, 0x0B, 0x2F, 0x17, 0x3A, 0x3A, 0x14, 0x1D, 0x0C, 0x64,
   0x0D, 0xAB, 0x9D, 0xA5, 0xBC, 0x15, 0x65, 0x43, 0x0E, 0xB7, 0x95, 0x83,
   0xDD, 0xD4, 0x0E, 0x98, 0x75, 0x1A, 0x5A, 0xAF, 0x52, 0xD8, 0x07, 0xF3,
   0x18, 0xA9, 0x09, 0xD9, 0x27, 0xE0, 0xEE, 0x55, 0x0E, 0xA8, 0x80, 0xBC,
   0xE6, 0x59, 0xE3, 0xD6, 0x76, 0x16, 0xC6, 0xA2, 0x2E, 0xE1, 0xF9, 0x55,
   0x12, 0x31, 0xB8, 0xC6, 0x2A, 0x09, 0xDB, 0x1E, 0x9F, 0xF6, 0x87, 0xDC,
   0x86, 0xA9, 0x04, 0x64, 0xA9, 0xB5, 0x7E, 0xEE, 0xB5, 0x0C, 0xBE, 0x3E};
#endif

 static_assert(sizeof(kObfuscatedData) == 0xC0, "Invalid data size.");

#pragma omp parallel for
 for (std::int64_t a = 0; a < static_cast<std::uint32_t>(-1); ++a)
 {
   for (std::uint32_t b = 0; b < static_cast<std::uint32_t>(-1); ++b)
   {
     std::uint8_t deobfuscated_data[sizeof(kObfuscatedData)];

     std::uint64_t k =
       static_cast<std::uint64_t>(b) << 32 | static_cast<std::uint64_t>(a);
     for (std::uint32_t i = 0;
          i < sizeof(kObfuscatedData) / sizeof(std::uint64_t);
          ++i)
     {
       auto const o =
         reinterpret_cast<std::uint64_t const*>(&kObfuscatedData[0]);
       auto const d = reinterpret_cast<std::uint64_t*>(&deobfuscated_data[0]);
       d[i] = (k ^= o[i]);
     }

     // Check the chunks which are expected to be procedure names first.
     std::uint32_t kChunkLen = 0x20;
     std::uint32_t kNumChunks = sizeof(kObfuscatedData) / kChunkLen;
     bool valid = true;
     for (std::uint32_t i = 1; i < kNumChunks; ++i)
     {
       // Assume that the first character must be uppercase. Not 100%
       // guaranteed but this simple check results in a large speedup and is
       // probably true.
       auto const chunk_beg = &deobfuscated_data[kChunkLen * i];
       auto const chunk_end = chunk_beg + kChunkLen;
       if (*chunk_beg < 'A' || *chunk_beg > 'Z' ||
           !ValidateString(chunk_beg, chunk_end))
       {
         valid = false;
         break;
       }
     }

     if (!valid)
     {
       continue;
     }

     // Check the module name chunk.
     auto const chunk_beg = &deobfuscated_data[0];
     auto const chunk_end = chunk_beg + kChunkLen;
     if (!ValidateString(chunk_beg, chunk_end))
     {
       continue;
     }

     // Technically the module name passed to LoadLibrary doesn't have to end
     // in .DLL, but the binary the data was extracted from always puts it so
     // I'm assuming it continues to do so here.
     auto const s = reinterpret_cast<char const*>(chunk_beg);
     if (std::strstr(s, ".DLL") || std::strstr(s, ".dll") ||
         std::strstr(s, ".Dll") || std::strstr(s, ".dLl") ||
         std::strstr(s, ".dlL") || std::strstr(s, ".DlL"))
     {
       std::printf("Found candidate key. a = 0x%08X, b = 0x%08X.\n",
                   static_cast<std::uint32_t>(a),
                   b);

       for (std::uint32_t i = 0; i < kNumChunks; ++i)
       {
         std::printf("Chunk %u: %s\n", i, &s[kChunkLen * i]);
       }
     }
   }

   std::printf("Finished outer loop pass for a = 0x%08X.\n",
               static_cast<std::uint32_t>(a));
 }
}

最後我的問題……有什麼辦法可以減少密鑰空間,或者以某種方式只是執行“更智能”的攻擊?我的實現非常幼稚,所以我懷疑/希望有更好的方法來完成我正在嘗試做的事情。

有很多方法可以攻擊它。

首先要注意的是,如果您知道索引處明文的值 $ i $ ,然後可以推導出索引處所有明文字節的值 $ i+8k $ ; 對於所有整數 $ i $ (!).

這是因為明文和密文字節之間的關係是 $ P_{i} = C_{i} \oplus P_{i-8} $ ; 如果你知道的話 $ P_{i-8} $ (當然,你知道 $ C_{i} $ ), 你知道 $ P_i $ (並且,如果需要,您也可以倒退)。

這給了我們第一個明顯的攻擊方法:你說明文的開頭是一個眾所周知的 DLL 名稱;好吧,繼續列出系統上的所有 DLL,並嘗試將每個名稱的前 8 個字節作為明文的前 8 個字節,用它來解密其餘的密文,看看它是否有意義(根據您了解的有關明文的規則)。如果是這樣,那麼,你已經做到了。

至於大小寫,好吧,ASCII 中大小寫的區別是第 5 位;顯而易見的方法是假設一種情況,如果你得到一個以 0x20(空格)而不是 0x00 結尾的字元串,那麼你得到的相應初始字元的情況是錯誤的。

如果這不起作用,還有其他可能的方法;讓我們先從簡單的開始

引用自:https://crypto.stackexchange.com/questions/25358