XCTF-Fianl-Rev2

- ctf

XCTF-Final-Rev2

Pythonic

印象里这两年 Python 相关的题目遇到了不少,相对来说没有那么困难,主要的困难主要集中在几个问题上:调试困难、反编译困难、字节码难以阅读,这里简单趁这个机会总结一下,当然这里的例题实际上还是挺难得。

怎么确定一个文件是由 Python 打包

不妨对比看看 Detect it Easy 是怎么检查出 Pyinstaller ,代码如下,实际上就是在 .rdata 段去查找有没有特征字符存在,通常来说这是一种基于环境的检查,基于环境的检查能力有限,不过我们还是可以尝试着自己 patch 掉这些信息来掩盖这个事实。

// DIE's signature file

init("packer","PyInstaller");

function detect(bShowType,bShowVersion,bShowOptions)
{
    if(PE.compareOverlay("78da"))
    {
        if(PE.section[".rdata"])
        {
            var nOffset=PE.section[".rdata"].FileOffset;
            var nSize=PE.section[".rdata"].FileSize;
            var nVersionOffset=PE.findString(nOffset,nSize,"PyInstaller: FormatMessageW failed.");
            
            if(nVersionOffset!=-1)
            {
                bDetected=1;
            }
        }
    }

    return result(bShowType,bShowVersion,bShowOptions);
}

这是一个没有修改过的由 Python 打包的 exe, 可以直接看到 PyInstaller 这种字符串,无疑会命中 DIE 的策略。

image-20230403004811946

image-20230403005100150

我们尝试着去 Patch 掉 DIE 的策略检查的字符串(有空的应该可以去给 DIE 提个 Issue 或者 PR?),再看看 Die 的结果,如此我们便轻易的过了 Die 的策略,即使整个软件还有很多 Python 的特征。这里其实敲响了一个警钟,客户端信息可信度不高,基于环境的检测自然会被环境迷惑。

image-20230403011308220

image-20230403011414841

不过看到这里其实会发现一个软验证,python 打包的软件会命中 zlib 的策略,看到了可以往这方面想。

例题

2023-Xctf-我不是病毒

这题就是被抹去了诸如 Pyinstaller 之类特征的 Python 打包程序,不过还是有很多 Python 的特征,比较明显的是 ___PYZ___,这是 Pyinstaller -key 会生成的文件。

image-20230403012039541

拆包

Python 文件的拆包朴实无华,一般使用 pyinstxtractor/pyinstxtractor.py at master · extremecoders-re/pyinstxtractor (github.com) 这个脚本一把梭,注意文件名不可以是中文,Python 版本需要和 打包的版本一致,Python 的版本可以在 可执行的文件中找到对应的 dll。

image-20230403013629178

反编译 main.pyc 会发现这里没什么逻辑,导入了 sign 函数,接着调用,问题转化成了找到 sign.

image-20230403012434556

在提取的时候就会提示,很多东西被加密了,这里可以看到关键的 sign.pyc 也被加密了,下一步就是解密。

image-20230403013615137

解密的详细过程见:[记python逆向 - TLSN - 博客园 (cnblogs.com)](https://www.cnblogs.com/lordtianqiyi/articles/16209125.html) 里面给出了一个解密脚本,拿来主义直接用

其中密钥由 pyimod00_crypto_key.pyc 给出,反汇编即可得到本题的解。

import tinyaes
import zlib
 
CRYPT_BLOCK_SIZE = 16
 
# 从crypt_key.pyc获取key,也可自行反编译获取
key = bytes('HelloHiHowAreYou', 'utf-8')
 
inf = open('sign.pyc.encrypted', 'rb') # 打开加密文件
outf = open('sign.pyc', 'wb') # 输出文件
 
# 按加密块大小进行读取
iv = inf.read(CRYPT_BLOCK_SIZE)
 
cipher = tinyaes.AES(key, iv)
 
# 解密
plaintext = zlib.decompress(cipher.CTR_xcrypt_buffer(inf.read()))
 
# 补pyc头(最后自己补也行)
outf.write(b'\x55\x0d\x00\x00\0\0\0\0\0\0\0\0\0\0\0\0')
 
# 写入解密数据
outf.write(plaintext)
 
inf.close()
outf.close()

于是获得输出 sign.pyc,反编译一下,用 pycdc 或者在线的网站(内核还是 pycdc 不过收费)来获取 python 文件,稍稍手动去一下混淆就会得到下面的结果。这里要注意 pycdc 对 a,b = b,a 这样的交换解析失误,直接运行这的 RC4 是不对的,手动修复一下就可以正常运行,dump 出 shellcode

import hashlib
import base64
import ctypes


def main():
    ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
    myInput = input("您的输入:")
    data_b64 = "9K98jTmDKCXlg9E2kepX4nAi8H0DB57IU57ybV37xjrw2zutw+KnxkoYur3IZzi2ep5tDC6jimCJ7fDpgQ5F3fJu4wHA0LVq9FALbjXN6nMy57KrU8DEloh+Cji3ED3eEl5YWAyb8ktBoyoOkL1c9ASWUPBniHmD7RSqWcNkykt/USjhft9+aV930Jl5VjD6qcXyZTfjnY5MH3u22O9NBEXLj3Y9N5VjEgF2cFJ+Tq7jj92iIlEkNvx8Jl+eH5/hipsonKLTnoLGXs4a0tTQX/uXQOTMBbtd70x04w1Pa0fp+vA9tCw+DXvXj0xmX8c5HMybhpPrwQYDonx7xtS+vRIj/OmU7GxkHOOqYdsGmGdTjTAUEBvZtinOxuR7mZ0r9k+c9da0W93TWm5+2LKNR6OJjmILaJn0lq4foYcfD5+JITDsOD6Vg01yLRG1B4A6OxJ7Rr/DBUabSu2fYf1c4sTFvWgfMV8il6QfJiNMGkVLey1cBPSobenMo+TQC1Ql0//9M4P01sOiwuuVKLvTyDEv6dKO//muVL9S2gq/aZUBWkjj/I5rUJ6Mlt4+jsngmuke9plAjw22fUgz+8uSzn40dhKXfBX/BOCnlwWsMGAefAfoz/XAsoVSG2ioLFmlcYe/WBgaUJEoRUSyv73yiEOTVwIK6EPnDlwRgZZHx2toLu8udpEZ0aKGkex5sn7P8Jf9AbD4/EiQU+FdoJSxGorPSZGvrc4="

    key = hashlib.md5("云南".encode("utf-8")).hexdigest()
    data = base64.b64decode(data_b64)
    cipher_text = b""
    len_key = len(key)
    # init S_Box
    s_box = list(range(256))
    j = 0
    # Init S_Box
    for i in range(256):
        j = (j + s_box[i] + ord(key[i % len_key])) % 256
        #  vv 反汇编错误,实际上这里是 RC4 的 S 盒初始化
        # s_box[i] = s_box[j]
        # s_box[j] = s_box[i]
        s_box[i], s_box[j] = s_box[j], s_box[i]

    m = n = 0
    # XOR
    for q in data:
        m = (m + 1) % 256
        n = (n + s_box[m]) % 256
        # 同理错误
        # s_box[m] = s_box[n]
        # s_box[n] = s_box[m]
        s_box[m], s_box[n] = s_box[n], s_box[m]
        cipher_text += bytes([q ^ s_box[(s_box[m] + s_box[n]) % 256]])

    c_input = ctypes.create_string_buffer(myInput.encode())
    mem_alloc = ctypes.windll.kernel32.VirtualAlloc(
        ctypes.c_int(0),
        ctypes.c_int(len(cipher_text)),
        ctypes.c_int(12288),
        ctypes.c_int(64),
    )
    ctypes.windll.kernel32.RtlMoveMemory(
        ctypes.c_void_p(mem_alloc),
        (ctypes.c_ubyte * len(cipher_text)).from_buffer(bytearray(cipher_text)),
        ctypes.c_size_t(len(cipher_text)),
    )
    thread = ctypes.windll.kernel32.CreateThread(
        ctypes.c_int(0),
        ctypes.c_int(0),
        ctypes.c_void_p(mem_alloc),
        ctypes.byref(c_input),
        ctypes.c_int(0),
        ctypes.pointer(ctypes.c_int(0)),
    )
    ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(thread), ctypes.c_int(-1))
    if (
            c_input.raw
            == b"\xdb\x1b\x00Dy\\C\xcc\x90_\xca.\xb0\xb7m\xab\x11\x9b^h\x90\x1bl\x19\x01\x0c\xeduP6\x0c0\x7f\xc5E-L\xb0\xfb\xba\xf6\x9f\x00"
    ):
        print("是的!你得到了!")
        return None


if __name__ == "__main__":
    main()

dump 出 cipher_text 得到

{81, 232, 0, 0, 0, 0, 89, 72, 129, 193, 97, 1, 0, 0, 85, 72, 137, 229, 72, 131, 236, 104, 72, 137, 77, 152, 199, 69, 252, 0, 0, 0, 0, 233, 49, 1, 0, 0, 139, 69, 252, 193, 224, 4, 72, 152, 72, 139, 85, 152, 72, 1, 208, 72, 137, 69, 240, 72, 184, 1, 219, 186, 51, 35, 1, 219, 186, 72, 137, 69, 160, 72, 184, 255, 238, 221, 204, 187, 170, 153, 136, 72, 137, 69, 168, 72, 184, 239, 205, 171, 144, 120, 86, 52, 18, 72, 137, 69, 176, 72, 184, 186, 220, 254, 33, 67, 101, 135, 9, 72, 137, 69, 184, 72, 139, 69, 240, 72, 139, 0, 72, 137, 69, 232, 72, 139, 69, 240, 72, 139, 64, 8, 72, 137, 69, 224, 72, 184, 192, 187, 111, 171, 119, 3, 124, 235, 72, 137, 69, 216, 72, 184, 239, 190, 173, 222, 13, 240, 173, 11, 72, 137, 69, 208, 72, 199, 69, 200, 0, 0, 0, 0, 235, 127, 72, 139, 69, 232, 72, 193, 224, 8, 72, 137, 194, 72, 139, 69, 176, 72, 1, 194, 72, 139, 77, 232, 72, 139, 69, 216, 72, 1, 200, 72, 49, 194, 72, 139, 69, 232, 72, 193, 232, 10, 72, 137, 193, 72, 139, 69, 184, 72, 1, 200, 72, 49, 208, 72, 41, 69, 224, 72, 139, 69, 224, 72, 193, 224, 8, 72, 137, 194, 72, 139, 69, 160, 72, 1, 194, 72, 139, 77, 216, 72, 139, 69, 224, 72, 1, 200, 72, 49, 194, 72, 139, 69, 224, 72, 193, 232, 10, 72, 137, 193, 72, 139, 69, 168, 72, 1, 200, 72, 49, 208, 72, 41, 69, 232, 72, 139, 69, 208, 72, 41, 69, 216, 72, 131, 69, 200, 1, 72, 131, 125, 200, 63, 15, 134, 118, 255, 255, 255, 72, 139, 69, 240, 72, 139, 85, 232, 72, 137, 16, 72, 139, 69, 240, 72, 131, 192, 8, 72, 139, 85, 224, 72, 137, 16, 144, 131, 69, 252, 1, 131, 125, 252, 11, 15, 142, 197, 254, 255, 255, 72, 131, 196, 104, 93, 89, 19, 45, 239, 197, 133, 72, 183, 185, 107, 151, 30, 51, 174, 0, 39, 61, 1, 135, 228, 208, 161, 110, 65, 89, 91, 206, 249, 238, 144, 92, 65, 174, 91, 6, 4, 186, 214, 131, 243, 10, 63, 162, 60, 255, 167, 103, 240, 110, 13, 2, 131, 222, 224, 175, 5, 27, 91, 21, 4, 55, 133, 233, 252, 61, 193, 245, 231, 61, 59, 227, 129, 22, 225, 192, 43, 104, 237, 12, 203, 161, 134, 59, 150, 195, 7, 3, 233, 200, 247, 163, 104, 183, 40, 98, 202, 104, 230, 204, 147, 157, 65, 66, 119, 147, 46, 155, 235, 94, 213, 116, 152, 199, 174, 139, 97, 102, 248, 253, 19, 93, 75, 41, 40, 251, 201, 193, 54, 64, 13, 26, 20, 145, 20, 125, 35, 174, 155, 130, 10, 139, 197, 132, 41, 205, 74, 219, 102, 67, 16, 221, 44, 3, 204, 94, 136, 122, 119, 231, 48, 112, 43, 57, 105, 91, 184, 10, 128, 33, 1, 73, 52, 164, 22, 59, 254, 165, 105, 223, 237, 58, 180, 94, 129, 143, 114, 73, 61, 210, 121, 123, 115, 85}

接着就是重建 代码

#include "stdio.h"
#include "Windows.h"
#include "stdint.h"

uint8_t shellcode[] = {81, 232, 0, 0, 0, 0, 89, 72, 129, 193, 97, 1, 0, 0, 85, 72, 137, 229, 72, 131, 236, 104, 72, 137, 77, 152, 199...};


typedef void (*VoidFuncPtr)();
int main() {
	PVOID p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (p == NULL) {
		return -1;
	}
	memcpy(p, shellcode, sizeof(shellcode));
	((VoidFuncPtr)p)();
	return 0;
}

有个 SMC ,调试到这个位置创建函数,可以得到一个完整的函数栈,这样就可以拿到全部逻辑。

image-20230404203859248

image-20230404203410783

_WORD *__fastcall sub_90000(__int64 a1)
{
  _WORD *result; // rax
  unsigned __int64 j; // [rsp-48h] [rbp-48h]
  unsigned __int64 v3; // [rsp-38h] [rbp-38h]
  unsigned __int64 v4; // [rsp-30h] [rbp-30h]
  unsigned __int64 v5; // [rsp-28h] [rbp-28h]
  char *v6; // [rsp-20h] [rbp-20h]
  unsigned int v7; // [rsp-1Ch] [rbp-1Ch]
  int i; // [rsp-14h] [rbp-14h]
  unsigned int v9; // [rsp-14h] [rbp-14h]
  unsigned int v10; // [rsp-10h] [rbp-10h]
  int k; // [rsp-Ch] [rbp-Ch]

  for ( i = 0; i <= 11; ++i )                   // SMC Start 12 轮
  {
    v6 = (char *)&loc_90167 + 16 * i;           // SMC
    v5 = *(_QWORD *)v6;
    v4 = *((_QWORD *)v6 + 1);
    v3 = 0xEB7C0377AB6FBBC0ui64;
    for ( j = 0i64; j <= 0x3F; ++j )            // 内部 Logic
    {
      v4 -= (v5 + v3) ^ ((v5 << 8) + 0x1234567890ABCDEFi64) ^ ((v5 >> 10) + 0x987654321FEDCBAi64);
      v5 -= (v3 + v4) ^ ((v4 << 8) - 0x4524FEDCCC4524FFi64) ^ ((v4 >> 10) - 0x7766554433221101i64);
      v3 -= 0xBADF00DDEADBEEFi64;
    }
    *(_QWORD *)v6 = v5;
    result = v6 + 8;
    *((_QWORD *)v6 + 1) = v4;
  }
  for ( k = 0; k <= 20; ++k )                   // Check Logic
  {
    v9 = 2029;
    v10 = *(unsigned __int16 *)(2i64 * k + a1) % 0xD1EFu;
    v7 = 1;
    while ( v9 )
    {
      if ( (v9 & 1) != 0 )
        v7 = v10 * v7 % 0xD1EF;
      v10 = v10 * v10 % 0xD1EF;
      v9 >>= 1;
    }
    result = (_WORD *)(2i64 * k + a1);
    *result = v7;
  }
  return result;
}

对应的 shellcode放了下面,怕大伙逆向不出来,这个直接编译 f5 就可以分析了。

这里有个 a1 是函数的参数,这里要回看一下 Python 代码,注意里面创建线程的时候有个函数,里面这个参数对应的输入的 c-style input 。

image-20230404211550525

后面就是对输入的检查了,经过 20 次的运算结果应该和 python 里的一致。

image-20230404212058609

爆破得解:

#include"stdio.h"

int main()
{
     int check[]{
        219, 27, 0, 68, 121, 92, 67, 204, 144, 95, 202, 46, 176, 183,
        109, 171, 17, 155, 94, 104, 144, 27, 108, 25, 1, 12, 237, 117,
        80, 54, 12, 48, 127, 197, 69, 45, 76, 176, 251, 186, 246, 159, 0
    };
 
     for (int k = 0; k <= 20; ++k)                   // Check Logic
     {
         unsigned int check_code = check[k * 2] | (check[k * 2 + 1] << 8);
         for (size_t m = 0; m < 127; m++)
         {
             for (size_t n = 0; n < 127; n++)
             {
                 unsigned int v9 = 2029;
                 unsigned int v10 = (m | (n << 8)) % 0xD1EFu;
                 unsigned int v7 = 1;
                 while (v9)
                 {
                     if ((v9 & 1) != 0)
                         v7 = v10 * v7 % 0xD1EF;
                     v10 = v10 * v10 % 0xD1EF;
                     v9 >>= 1;
                 }
                 if (v7 == check_code) {
                     printf("%c%c", m,n);
                 }
             }
         };    
     }
}
[0x51, 0xe8, 0x0, 0x0, 0x0, 0x0, 0x59, 0x48, 0x81, 0xc1, 0x61, 0x1, 0x0, 0x0, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x68, 0x48, 0x89, 0x4d, 0x98, 0xc7, 0x45, 0xfc, 0x0, 0x0, 0x0, 0x0, 0xe9, 0x31, 0x1, 0x0, 0x0, 0x8b, 0x45, 0xfc, 0xc1, 0xe0, 0x4, 0x48, 0x98, 0x48, 0x8b, 0x55, 0x98, 0x48, 0x1, 0xd0, 0x48, 0x89, 0x45, 0xf0, 0x48, 0xb8, 0x1, 0xdb, 0xba, 0x33, 0x23, 0x1, 0xdb, 0xba, 0x48, 0x89, 0x45, 0xa0, 0x48, 0xb8, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x48, 0x89, 0x45, 0xa8, 0x48, 0xb8, 0xef, 0xcd, 0xab, 0x90, 0x78, 0x56, 0x34, 0x12, 0x48, 0x89, 0x45, 0xb0, 0x48, 0xb8, 0xba, 0xdc, 0xfe, 0x21, 0x43, 0x65, 0x87, 0x9, 0x48, 0x89, 0x45, 0xb8, 0x48, 0x8b, 0x45, 0xf0, 0x48, 0x8b, 0x0, 0x48, 0x89, 0x45, 0xe8, 0x48, 0x8b, 0x45, 0xf0, 0x48, 0x8b, 0x40, 0x8, 0x48, 0x89, 0x45, 0xe0, 0x48, 0xb8, 0xc0, 0xbb, 0x6f, 0xab, 0x77, 0x3, 0x7c, 0xeb, 0x48, 0x89, 0x45, 0xd8, 0x48, 0xb8, 0xef, 0xbe, 0xad, 0xde, 0xd, 0xf0, 0xad, 0xb, 0x48, 0x89, 0x45, 0xd0, 0x48, 0xc7, 0x45, 0xc8, 0x0, 0x0, 0x0, 0x0, 0xeb, 0x7f, 0x48, 0x8b, 0x45, 0xe8, 0x48, 0xc1, 0xe0, 0x8, 0x48, 0x89, 0xc2, 0x48, 0x8b, 0x45, 0xb0, 0x48, 0x1, 0xc2, 0x48, 0x8b, 0x4d, 0xe8, 0x48, 0x8b, 0x45, 0xd8, 0x48, 0x1, 0xc8, 0x48, 0x31, 0xc2, 0x48, 0x8b, 0x45, 0xe8, 0x48, 0xc1, 0xe8, 0xa, 0x48, 0x89, 0xc1, 0x48, 0x8b, 0x45, 0xb8, 0x48, 0x1, 0xc8, 0x48, 0x31, 0xd0, 0x48, 0x29, 0x45, 0xe0, 0x48, 0x8b, 0x45, 0xe0, 0x48, 0xc1, 0xe0, 0x8, 0x48, 0x89, 0xc2, 0x48, 0x8b, 0x45, 0xa0, 0x48, 0x1, 0xc2, 0x48, 0x8b, 0x4d, 0xd8, 0x48, 0x8b, 0x45, 0xe0, 0x48, 0x1, 0xc8, 0x48, 0x31, 0xc2, 0x48, 0x8b, 0x45, 0xe0, 0x48, 0xc1, 0xe8, 0xa, 0x48, 0x89, 0xc1, 0x48, 0x8b, 0x45, 0xa8, 0x48, 0x1, 0xc8, 0x48, 0x31, 0xd0, 0x48, 0x29, 0x45, 0xe8, 0x48, 0x8b, 0x45, 0xd0, 0x48, 0x29, 0x45, 0xd8, 0x48, 0x83, 0x45, 0xc8, 0x1, 0x48, 0x83, 0x7d, 0xc8, 0x3f, 0xf, 0x86, 0x76, 0xff, 0xff, 0xff, 0x48, 0x8b, 0x45, 0xf0, 0x48, 0x8b, 0x55, 0xe8, 0x48, 0x89, 0x10, 0x48, 0x8b, 0x45, 0xf0, 0x48, 0x83, 0xc0, 0x8, 0x48, 0x8b, 0x55, 0xe0, 0x48, 0x89, 0x10, 0x90, 0x83, 0x45, 0xfc, 0x1, 0x83, 0x7d, 0xfc, 0xb, 0xf, 0x8e, 0xc5, 0xfe, 0xff, 0xff, 0x48, 0x83, 0xc4, 0x68, 0x5d, 0x59, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, 0x48, 0x89, 0x4d, 0x10, 0xc7, 0x45, 0xfc, 0x0, 0x0, 0x0, 0x0, 0xe9, 0x90, 0x0, 0x0, 0x0, 0x8b, 0x45, 0xfc, 0x48, 0x98, 0x48, 0x8d, 0x14, 0x0, 0x48, 0x8b, 0x45, 0x10, 0x48, 0x1, 0xd0, 0xf, 0xb7, 0x0, 0xf, 0xb7, 0xc0, 0x89, 0x45, 0xf8, 0xc7, 0x45, 0xf4, 0xed, 0x7, 0x0, 0x0, 0xc7, 0x45, 0xf0, 0xef, 0xd1, 0x0, 0x0, 0x8b, 0x45, 0xf8, 0xba, 0x0, 0x0, 0x0, 0x0, 0xf7, 0x75, 0xf0, 0x89, 0x55, 0xf8, 0xc7, 0x45, 0xec, 0x1, 0x0, 0x0, 0x0, 0xeb, 0x30, 0x8b, 0x45, 0xf4, 0x83, 0xe0, 0x1, 0x85, 0xc0, 0x74, 0x12, 0x8b, 0x45, 0xec, 0xf, 0xaf, 0x45, 0xf8, 0xba, 0x0, 0x0, 0x0, 0x0, 0xf7, 0x75, 0xf0, 0x89, 0x55, 0xec, 0x8b, 0x45, 0xf8, 0xf, 0xaf, 0xc0, 0xba, 0x0, 0x0, 0x0, 0x0, 0xf7, 0x75, 0xf0, 0x89, 0x55, 0xf8, 0xd1, 0x6d, 0xf4, 0x83, 0x7d, 0xf4, 0x0, 0x75, 0xca, 0x8b, 0x4d, 0xec, 0x8b, 0x45, 0xfc, 0x48, 0x98, 0x48, 0x8d, 0x14, 0x0, 0x48, 0x8b, 0x45, 0x10, 0x48, 0x1, 0xd0, 0x89, 0xca, 0x66, 0x89, 0x10, 0x83, 0x45, 0xfc, 0x1, 0x83, 0x7d, 0xfc, 0x14, 0xf, 0x8e, 0x66, 0xff, 0xff, 0xff, 0x90, 0x90, 0x48, 0x83, 0xc4, 0x20, 0x5d, 0xc3]