找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3659

积分

0

好友

501

主题
发表于 昨天 21:25 | 查看: 9| 回复: 0

记录一次完整的协议逆向实践,针对阿里CTF2026中一道名为license的题目,从ELF加载、数据包结构、多层解密解压,到最终构造远程利用链获取FLAG。整个流程涉及多种现代加密与压缩技术的组合应用,极具实战参考价值。

协议流程概览

该程序的通信协议设计复杂,层层嵌套,完整流程如下:

  1. 数据包 = 长度(4字节)+ nonce(12字节)+ 数据流(长度-12字节)
  2. 数据流使用AES-GCM解密
  3. 解密后进行zstd解压缩
  4. Base64解码
  5. Protobuf解析出四个字段:enc_datapasswordsaltsha256_hash
  6. 使用魔改AES-160解密(CBC模式 + PKCS#7填充),密钥由passwordsalt经PBKDF2-HMAC-SHA256生成48字节,前28字节为密钥,后20字节为IV
  7. 解密后为JSON格式,需包含license_codesign字段,其中sign为RSA-4096签名
  8. 全部验证通过后打印FLAG环境变量

ELF加载与动态通信

程序licensebuild_token之间存在动态通信机制。启动时,license会连接build_token以获取运行所需的数据。通过调试可观察到如下日志输出:

listening on 0.0.0.0:12345 for key exchange... press Ctrl+C to stop
[127.0.0.1:55012] connected, waiting for rsa public key...
[127.0.0.1:55012] received rsa public key, encrypting...
[127.0.0.1:55012] sending package...
[127.0.0.1:55012] package sent successfully...

本地可通过以下命令模拟服务端:

./build_token -p 'r&FGW9RpqTc*aqof' -s -l 0.0.0.0:12345

调试发现,主程序在运行过程中会动态加载由build_token发送的ELF代码段并执行,这是典型的运行时加载技术,增加了静态分析难度。

数据包结构分析

程序首先生成一个UUID作为license_code,随后要求用户输入两段数据:一段4字节,一段12字节。第三处输入长度为第一处减去12(含回车符),因此实际可用长度不可控。为便于测试,可在调试时手动修改长度限制。

最终数据包结构为:

  • 前4字节表示总长度(小端序)
  • 接着12字节为nonce
  • 剩余部分为加密后的数据流

AES-GCM解密实现

数据流使用AES-GCM模式加密。通过对sub_403D10函数的分析,结合CPU是否支持AVX指令集进行分支选择,最终提取出用于密钥扩展的初始密钥:

AES_KEY = bytes.fromhex("f6778d8728d8f17ce8c5c81f45c3d5fd869ca851b7575be540776f4f26c1140d")

GCM模式包含CTR加密与GHASH认证两部分。调试中可见对16个零字节的加密过程,符合AES-256-GCM特征。验证时还会检查数据流末尾16字节是否等于GHASH计算结果。

核心加密逻辑如下:

void __fastcall sub_40C1B0(__m128i *_RDI, const __m128i *a2, _OWORD *a3)
{
  _XMM0 = _mm_xor_si128(_mm_loadu_si128(a2), *_RDI);
  __asm
  {
    aesenc  xmm0, xmmword ptr [rdi+10h]
    aesenc  xmm0, xmmword ptr [rdi+20h]
    aesenc  xmm0, xmmword ptr [rdi+30h]
    aesenc  xmm0, xmmword ptr [rdi+40h]
    aesenc  xmm0, xmmword ptr [rdi+50h]
    aesenc  xmm0, xmmword ptr [rdi+60h]
    aesenc  xmm0, xmmword ptr [rdi+70h]
    aesenc  xmm0, xmmword ptr [rdi+80h]
    aesenc  xmm0, xmmword ptr [rdi+90h]
    aesenc  xmm0, xmmword ptr [rdi+0A0h]
    aesenc  xmm0, xmmword ptr [rdi+0B0h]
    aesenc  xmm0, xmmword ptr [rdi+0C0h]
    aesenc  xmm0, xmmword ptr [rdi+0D0h]
    aesenclast xmm0, xmmword ptr [rdi+0E0h]
  }
  *a3 = _XMM0;
}

zstd解压缩处理

解密后数据需满足长度不小于13字节的要求。若解压失败,会抛出错误信息Unknown frame descriptor。进一步分析发现,zstd头部魔数为28 B5 2F FD,缺少此标识即报错。

使用Python的zstandard库可轻松完成压缩:

import zstandard as zstd

def zstd_compress(data):
    return zstd.compress(data)

Base64与Protobuf解析

Base64解码环节明显,多处查表操作且对padding敏感。上层逻辑显示解压后的数据需为Base64编码字符串。

紧接着是Protobuf解析,字段定义如下:

  • enc_data:加密后的数据
  • password:用于密钥派生
  • salt:盐值
  • sha256_hash:原始数据SHA256哈希值

通过分析sub_40B960函数,确定字段编号与类型映射关系,符合Protobuf wire format规范,例如:

  • enc_data → field 1, type LengthDelimited
  • password → field 2, type LengthDelimited
  • salt → field 3, type LengthDelimited
  • sha256_hash → field 4, type LengthDelimited

魔改AES-160解密分析

核心解密算法为一种基于AES思想但参数不同的变体——Rijndael-160(分组大小20字节)。其主要魔改点包括:

S-Box生成方式不同

标准AES S-Box基于有限域GF(2^8)上的逆元运算,而本题使用自定义逻辑生成:

def xtime(a) -> int:
    a &= 0xFF
    return (((a << 1) ^ 0x8D) & 0xFF) if (a & 0x80) else ((a << 1) & 0xFF)

其中0x8D替代了标准的0x1B作为约简多项式系数。

MixColumn矩阵非标准

MixColumn使用的扩散矩阵为:

[[4, 1, 6, 2],
 [2, 4, 1, 6],
 [6, 2, 4, 1],
 [1, 6, 2, 4]]

该矩阵在GF(2^8)下可逆,确保了解密可行性。

加密与解密功能互换

调试发现,原本应执行加密的地方实际调用了“解密”逻辑,反之亦然。这相当于将整个加解密流程倒置。

CBC模式 + PKCS#7填充

使用PBKDF2-HMAC-SHA256从passwordsalt生成48字节密钥材料,前28字节作为AES-160密钥,后20字节作为CBC模式的初始IV。

时间戳校验与RSA签名

解密后JSON需包含license_codesign字段。前者必须与初始生成的UUID一致,后者需通过RSA-4096签名验证。

程序内置一个1024字节的大整数n(模数)和d(私钥指数),用于签名验证。调试发现,sign字段为UUID(去-后转为字节)使用公钥e=65537加密所得。

提取出的关键参数如下:

n = int.from_bytes(bytes.fromhex(
    "B75815AF28D17CCE2ECA787C6004EC6F1A51AC14B5A7AF746C7B3DD1354517C3CF38B35BE17E64BAB76A47FEA92396DDA947CC268EE0DACC097911912AB64B4C572D7518003C8B24D9DF5E950D44FE9613805ABDC47EA1F693EB6B04D56A124522126770E9FC771B185EC44F308D57FAA6D3C67A585A5E1672F5F89FADB9B073E913166A99D5A3896BDC6430B1C4A5AECDA7FEFA15AEB41A37E76D61698FF36A2F1B2B0A0CBC1AA0AD068C8ECF9388558B0257335EADC4831397917CE1C9B5E2B033C6935F1B57A06BC830B3D03CC8C8D758A38CD8D85583435B3594A7599F1B692B9FBF0E98B388E6A96D20D6245EF5503F79693552987CCAEC2C86D481A45EEA1C573D33FA15109962E5C8D2A02A6923FB375C6B1F05FC4CDCCC17055AADBA17085FC8C22563DA4FFD05FEBD07A485B5B28C3203890FDABDDD6693C40FCECE05D4FEA9EE46F1447416B7FF4D914A6A5787917637977A3330659D8191CD102093F15ED4D2444E60A4950AE51EBF616721F8785F5656130CFABE2174DBB9F9E5121F8F10670EC0538465B283A02C187989E06C07E3BD3792C5E5E7C49752ADCCFA573DF668C90AB67C61CFC5E46D5CC387D51078D27ADBD8E3AE3CDA5C7A00E741B60E8DD1AD61D0F7633FC2E9F30A3BAC74897001B1AEB1B7C27A0C25A75620B8FD1374084F86186C56AF651FBEEE0DFEDA25FC110EFCD8BBB0C05222326BBF"
), "big")

e = 65537

完整EXP构造

综合上述分析,编写完整利用脚本如下:

import hashlib
from base64 import b64encode
import zstandard as zstd

def zstd_compress(data):
    return zstd.compress(data)

def xtime(a) -> int:
    a &= 0xFF
    return (((a << 1) ^ 0x8D) & 0xFF) if (a & 0x80) else ((a << 1) & 0xFF)

def gf_mul(a, b) -> int:
    a &= 0xFF
    b &= 0xFF
    res = 0
    for _ in range(8):
        if b & 1:
            res ^= a
        a = xtime(a)
        b >>= 1
    return res & 0xFF

AES_SBOX = bytes.fromhex(
    "637C699066329A0E6441CBA99FFAD5AA6524F777371D83EB981A2A7DBD2502EEE"
    "5E7455029C4ECA7CCF05C4D1396A2099EFF5AA1C76FE9150C1BC5975614A5B620"
    "D62111700D7F4E4652354BA4C9011E310F2F17FCDB7430DE481C950653D36718F"
    "D2D1F7A8D8775B426E071A3825807D4BADAA8B5D99CCFF960D81200798904C2B8"
    "3C614276DF6CEA495462E8B3F50BF1287ED2CD23F28E80F836E3D722DDF34A2E5"
    "510C0B15943AC683FBB6DAFCAC638B973AEDCBC9DC3D14CFEA63B92E42B5BFB2C"
    "F6C1B25D8FEF78915F9472ED4088B7443427E16A0586C8938A7B8451E63D990A3"
    "3BF39038C086B3E8519CEB08BABA0E247BE4F5E9B57AD6E81163AD0F4"
)

INV_SBOX = bytearray(256)
for i, v in enumerate(AES_SBOX):
    INV_SBOX[v] = i
INV_SBOX = bytes(INV_SBOX)

def rijndael_rounds(Nb, Nk) -> int:
    return max(Nb, Nk) + 6

def rot_word(w) -> int:
    return ((w << 8) & 0xFFFFFFFF) | ((w >> 24) & 0xFF)

def sub_word(w, sbox) -> int:
    return (
        (sbox[(w >> 24) & 0xFF] << 24) |
        (sbox[(w >> 16) & 0xFF] << 16) |
        (sbox[(w >> 8)  & 0xFF] << 8)  |
        (sbox[(w >> 0)  & 0xFF] << 0)
    ) & 0xFFFFFFFF

def rcon(i) -> int:
    c = 1
    for _ in range(i - 1):
        c = xtime(c)
    return (c << 24) & 0xFFFFFFFF

def key_expansion(key, Nb, Nk, sbox):
    if len(key) != 4 * Nk:
        raise ValueError(f"key length must be {4*Nk} bytes for Nk={Nk}, got {len(key)}")
    Nr = rijndael_rounds(Nb, Nk)
    W_words = Nb * (Nr + 1)
    w = [0] * W_words
    for i in range(Nk):
        w[i] = int.from_bytes(key[4*i:4*i+4], "big")
    for i in range(Nk, W_words):
        temp = w[i - 1]
        if i % Nk == 0:
            temp = sub_word(rot_word(temp), sbox) ^ rcon(i // Nk)
        elif Nk > 6 and (i % Nk) == 4:
            temp = sub_word(temp, sbox)
        w[i] = (w[i - Nk] ^ temp) & 0xFFFFFFFF
    return w

def bytes_to_state(block, Nb):
    if len(block) != 4 * Nb:
        raise ValueError("bad block size")
    s = [[0]*Nb for _ in range(4)]
    for c in range(Nb):
        for r in range(4):
            s[r][c] = block[4*c + r]
    return s

def state_to_bytes(s, Nb):
    out = bytearray(4*Nb)
    for c in range(Nb):
        for r in range(4):
            out[4*c + r] = s[r][c] & 0xFF
    return bytes(out)

def add_round_key(s, round_w, round_idx, Nb):
    base = round_idx * Nb
    for c in range(Nb):
        w = round_w[base + c]
        s[0][c] ^= (w >> 24) & 0xFF
        s[1][c] ^= (w >> 16) & 0xFF
        s[2][c] ^= (w >> 8)  & 0xFF
        s[3][c] ^= (w >> 0)  & 0xFF

def sub_bytes(s, sbox, Nb):
    for r in range(4):
        for c in range(Nb):
            s[r][c] = sbox[s[r][c]]

def shift_rows(s, Nb):
    for r in range(1, 4):
        k = r % Nb
        row = s[r]
        s[r] = row[-k:] + row[:-k]

def inv_shift_rows(s, Nb):
    for r in range(1, 4):
        k = r % Nb
        row = s[r]
        s[r] = row[k:] + row[:k]

MIX_MAT = [
    [4, 1, 6, 2],
    [2, 4, 1, 6],
    [6, 2, 4, 1],
    [1, 6, 2, 4],
]

def gf_mat_inv_4x4(mat):
    A = [[mat[r][c] & 0xFF for c in range(4)] + [1 if c == r else 0 for c in range(4)] for r in range(4)]
    def gf_inv(x) -> int:
        x &= 0xFF
        if x == 0:
            raise ZeroDivisionError("no inverse for 0")
        for y in range(1, 256):
            if gf_mul(x, y) == 1:
                return y
        raise ZeroDivisionError("no inverse found (should not happen)")
    for col in range(4):
        pivot = None
        for r in range(col, 4):
            if A[r][col] != 0:
                pivot = r
                break
        if pivot is None:
            raise ValueError("matrix not invertible")
        if pivot != col:
            A[col], A[pivot] = A[pivot], A[col]
        inv_p = gf_inv(A[col][col])
        for j in range(8):
            A[col][j] = gf_mul(A[col][j], inv_p)
        for r in range(4):
            if r == col:
                continue
            factor = A[r][col]
            if factor == 0:
                continue
            for j in range(8):
                A[r][j] ^= gf_mul(factor, A[col][j])
    inv = [[A[r][4 + c] & 0xFF for c in range(4)] for r in range(4)]
    return inv

INV_MIX_MAT = gf_mat_inv_4x4(MIX_MAT)

def mix_single_column(col):
    a0, a1, a2, a3 = [x & 0xFF for x in col]
    out = []
    for r in range(4):
        out.append(
            gf_mul(MIX_MAT[r][0], a0) ^
            gf_mul(MIX_MAT[r][1], a1) ^
            gf_mul(MIX_MAT[r][2], a2) ^
            gf_mul(MIX_MAT[r][3], a3)
        )
    return [x & 0xFF for x in out]

def inv_mix_single_column(col):
    a0, a1, a2, a3 = [x & 0xFF for x in col]
    out = []
    for r in range(4):
        out.append(
            gf_mul(INV_MIX_MAT[r][0], a0) ^
            gf_mul(INV_MIX_MAT[r][1], a1) ^
            gf_mul(INV_MIX_MAT[r][2], a2) ^
            gf_mul(INV_MIX_MAT[r][3], a3)
        )
    return [x & 0xFF for x in out]

def mix_columns(s, Nb):
    for c in range(Nb):
        col = [s[r][c] for r in range(4)]
        mc = mix_single_column(col)
        for r in range(4):
            s[r][c] = mc[r]

def inv_mix_columns(s, Nb):
    for c in range(Nb):
        col = [s[r][c] for r in range(4)]
        mc = inv_mix_single_column(col)
        for r in range(4):
            s[r][c] = mc[r]

BLOCK_SIZE = 20

def pkcs7_pad(data, block_size=BLOCK_SIZE):
    pad_len = block_size - (len(data) % block_size)
    if pad_len == 0:
        pad_len = block_size
    return data + bytes([pad_len]) * pad_len

def pkcs7_unpad(padded, block_size=BLOCK_SIZE):
    if not padded or (len(padded) % block_size) != 0:
        raise ValueError("bad padded length")
    pad_len = padded[-1]
    if pad_len < 1 or pad_len > block_size:
        raise ValueError("bad padding")
    if padded[-pad_len:] != bytes([pad_len]) * pad_len:
        raise ValueError("bad padding bytes")
    return padded[:-pad_len]

def derive_key_and_mask(password_b, salt, ITER):
    enc_data_48 = hashlib.pbkdf2_hmac("sha256", password_b, salt, ITER, dklen=48)
    key28 = enc_data_48[:28]
    mask20 = enc_data_48[28:]
    return key28, mask20

def xor_bytes(a, b):
    return bytes(x ^ y for x, y in zip(a, b))

def encrypt(data, password_b, salt, ITER):
    key28, iv = derive_key_and_mask(password_b, salt, ITER)
    data_p = pkcs7_pad(data, BLOCK_SIZE)
    out = bytearray()
    prev_block = iv
    for i in range(0, len(data_p), BLOCK_SIZE):
        plaintext_block = data_p[i:i + BLOCK_SIZE]
        mixed = xor_bytes(plaintext_block, prev_block)
        ciphertext_block = rijndael160_decrypt_block(mixed, key28, Nb=5, Nk=7)
        out += ciphertext_block
        prev_block = ciphertext_block
    return bytes(out)

def decrypt(ciphertext, password_b, salt, ITER):
    if len(ciphertext) % BLOCK_SIZE != 0:
        raise ValueError("密文长度必须是 20 字节的倍数")
    key28, iv = derive_key_and_mask(password_b, salt, ITER)
    out = bytearray()
    prev_block = iv
    for i in range(0, len(ciphertext), BLOCK_SIZE):
        ciphertext_block = ciphertext[i:i + BLOCK_SIZE]
        decrypted_core = rijndael160_encrypt_block(ciphertext_block, key28, Nb=5, Nk=7)
        plaintext_block = xor_bytes(decrypted_core, prev_block)
        out += plaintext_block
        prev_block = ciphertext_block
    return pkcs7_unpad(bytes(out), BLOCK_SIZE)

def encode_varint(x: int) -> bytes:
    if x < 0:
        raise ValueError("varint encoder expects non-negative int")
    out = bytearray()
    while True:
        b = x & 0x7F
        x >>= 7
        if x:
            out.append(b | 0x80)
        else:
            out.append(b)
            break
    return bytes(out)

def encode_key(field_number: int, wire_type: int) -> bytes:
    return encode_varint((field_number << 3) | wire_type)

def field_length_delimited(field_number: int, data) -> bytes:
    if isinstance(data, str):
        data = data.encode("utf-8")
    return encode_key(field_number, 2) + encode_varint(len(data)) + data

MASK128 = (1 << 128) - 1
R = 0xE1000000000000000000000000000000

def aes_ecb_encrypt_block(key: bytes, block16: bytes) -> bytes:
    if len(block16) != 16:
        raise ValueError("block16 must be 16 bytes")
    if len(key) not in (16, 24, 32):
        raise ValueError("key must be 16/24/32 bytes")
    from Crypto.Cipher import AES
    return AES.new(key, AES.MODE_ECB).encrypt(block16)

def inc32(counter16: bytes) -> bytes:
    if len(counter16) != 16:
        raise ValueError("counter16 must be 16 bytes")
    prefix = counter16[:12]
    c = int.from_bytes(counter16[12:], "big")
    c = (c + 1) & 0xFFFFFFFF
    return prefix + c.to_bytes(4, "big")

def gf128_mul_gcm(x: int, y: int) -> int:
    z = 0
    v = x
    for i in range(128):
        if (y >> (127 - i)) & 1:
            z ^= v
        if v & 1:
            v = (v >> 1) ^ R
        else:
            v >>= 1
    return z & MASK128

def ghash(H: bytes, aad: bytes, c: bytes) -> bytes:
    if len(H) != 16:
        raise ValueError("H must be 16 bytes")
    H_int = int.from_bytes(H, "big")
    X = 0
    def iter_blocks(data: bytes):
        for i in range(0, len(data), 16):
            blk = data[i:i+16]
            if len(blk) < 16:
                blk = blk + b"\x00" * (16 - len(blk))
            yield blk
    for blk in iter_blocks(aad):
        X = gf128_mul_gcm(X ^ int.from_bytes(blk, "big"), H_int)
    for blk in iter_blocks(c):
        X = gf128_mul_gcm(X ^ int.from_bytes(blk, "big"), H_int)
    len_block = (len(aad) * 8).to_bytes(8, "big") + (len(c) * 8).to_bytes(8, "big")
    X = gf128_mul_gcm(X ^ int.from_bytes(len_block, "big"), H_int)
    return X.to_bytes(16, "big")

def gcm_ctr_crypt(key: bytes, J0: bytes, data: bytes) -> bytes:
    if len(J0) != 16:
        raise ValueError("J0 must be 16 bytes")
    out = bytearray()
    counter = inc32(J0)
    for off in range(0, len(data), 16):
        block = data[off:off+16]
        ks = aes_ecb_encrypt_block(key, counter)
        out.extend(bytes(b ^ k for b, k in zip(block, ks[:len(block)])))
        counter = inc32(counter)
    return bytes(out)

def gcm_encrypt_and_tag(key: bytes, nonce12: bytes, plaintext: bytes, aad: bytes = b""):
    if len(nonce12) != 12:
        raise ValueError("nonce must be 12 bytes")
    J0 = nonce12 + b"\x00\x00\x00\x01"
    H = aes_ecb_encrypt_block(key, b"\x00" * 16)
    ciphertext = gcm_ctr_crypt(key, J0, plaintext)
    S = aes_ecb_encrypt_block(key, J0)
    g = ghash(H, aad, ciphertext)
    tag = bytes(x ^ y for x, y in zip(S, g))
    return ciphertext, tag

def build_packet(nonce12: bytes, ciphertext: bytes, tag16: bytes) -> bytes:
    if len(nonce12) != 12:
        raise ValueError("nonce must be 12 bytes")
    if len(tag16) != 16:
        raise ValueError("tag must be 16 bytes")
    body = nonce12 + ciphertext + tag16
    length = len(body)
    return length.to_bytes(4, "little") + body

def make_packet_from_nonce_plain(key: bytes, nonce12: bytes, plaintext: bytes, aad: bytes = b""):
    ciphertext, tag16 = gcm_encrypt_and_tag(key, nonce12, plaintext, aad=aad)
    pkt = build_packet(nonce12, ciphertext, tag16)
    return pkt

n = int.from_bytes(bytes.fromhex(
    "BF6B322252C0B0BBD8FC0E11FC25DAFE0DEEBE1F65AF566C18864F087413FDB82056A7250C7AC2B7B1AEB101708974AC3B0AF3E9C23F63F7D061ADD18D0EB641E7007A5CDA3CAEE3D8DB7AD27810D587C35C6DE4C5CF617CB60AC968F63D57FACCAD5297C4E7E5C59237BDE3076CE08979182CA083B2658453C00E67108F1F12E5F9B9DB7421BEFA0C1356565F78F8216761BF1EE50A95A4604E44D2D45EF1932010CD91819D6530337A9737769187576A4A914DFFB7167444F146EEA9FED405CECE0FC49366DDBDDA0F8903328CB2B585A407BDFE05FD4FDA6325C2C85F0817BAAD5A0517CCDC4CFC051F6B5C37FB23692AA0D2C8E562991015FA333D571CEA5EA481D4862CECCA7C98523569793F50F55E24D6206DA9E688B3980EBF9F2B691B9F59A794355B438355D8D88CA358D7C8C83CD0B330C86BA0571B5F93C633B0E2B5C9E17C91971383C4AD5E3357028B558893CF8E8C06ADA01ABC0C0A2B1B2F6AF38F69616DE7371AB4AE15FAFEA7CDAEA5C4B13064DC6B89A3D5996A1613E973B0B9AD9FF8F572165E5A587AC6D3A6FA578D304FC45E181B77FCE97067122245126AD5046BEB93F6A17EC4BD5A801396FE440D955EDFD9248B3C0018752D574C4BB62A91117909CCDAE08E26CC47A9DD9623A9FE476AB7BA647EE15BB338CFC3174535D13D7B6C74AFA7B514AC511A6FEC04607C78CA2ECE7CD128AF1558B7"
), "big")

d = int.from_bytes(bytes.fromhex(
    "32C892BD6E6CF6B66F83B78BE7F4771C0DC0382A8644B54DEA57BFA20381C63F523D0B0D16397F6D52B380FC5BC9EBED41A0CF434628A131FED3DB548BF2CA41C3B269C4369600E42C0556997E07214F6A721C29A49D3744E9DB04C25709C14CA57E9A39EFA082621F3FB09E09BB45FAD2E8A9F64FDA457A8CE9982899C90EBA69CF0E12FDC572304E81D6D7056F478D3D2B3E9448B9BD27A5F13DEB1D32AF2E944440F58888A46EDC497AD2D91F14E4092C0D4EBF37E8BA220C4D00469377D6AE9E16AAD55C6619D73F65DF364B03A28AF910A0C442FC8871ECF9F8AA4624147F8F3C21BBC5BAF0A5B00A3CE67367AA665D4BDB8036F3289E8EE6192FFDEB8A800A60196CF0C73E75EA5B397057451ADFD512AEE8F1E2C04F959B74270DD7A2F434A3C378C50734570CFFF1880C3D0879A4378AB2E06E2A51C4021205F8CC8D5A24878DB2457984F0BDA467D617CBC92AE65D154C1C4A98740007F4DB588DEBABD1579EA1E5162E3A7B716FB2ABC99274A8E252C2DF32AD674C85219B55213BF4C7621E5E7D583F04F69A68391F1F93FC472425900C9B51DECBDE9B2F742753AF51B9F1C146119D628D379A427D92C8F860D4D662DAFB0E9AF766B0FE580E3325E90F364F8C747095D4A259803139C3937B58EF20E601E8B78B70C2093A4D7335847EF18E6CFD8694BD6928350D66F3809B573C73A00889411C0B97C1204F6986"
), "big")

e = 65537

def gen_packet(license_code):
    password_b = b"mypassword"
    salt = bytes.fromhex("1020304050")
    ITER = 1000
    TARGET_HEX = "".join(license_code.split("-"))
    calculated_sig = pow(int.from_bytes(bytes.fromhex(TARGET_HEX), "big"), e, n)
    raw_data = b'{"license_code":"' + license_code.encode() + b'","sign":"' + calculated_sig.to_bytes((calculated_sig.bit_length() + 7) // 8, 'big').hex().encode() + b'"}'
    ct = encrypt(raw_data, password_b, salt, ITER)
    protobuf_msg = b"".join([
        field_length_delimited(1, ct),
        field_length_delimited(2, password_b),
        field_length_delimited(3, salt),
        field_length_delimited(4, hashlib.sha256(raw_data).digest()),
    ])
    b64 = b64encode(protobuf_msg)
    buffer = zstd_compress(b64)
    key = bytes.fromhex("f6778d8728d8f17ce8c5c81f45c3d5fd869ca851b7575be540776f4f26c1140d")
    nonce12 = b"b" * 12
    packet = make_packet_from_nonce_plain(key, nonce12, buffer, aad=b"")
    return packet

远程交互脚本使用pwntools实现自动化:

from pwn import *
import subprocess
from solve import *

context.log_level = 'debug'

host = '223.6.249.127'
port = 11240

io = remote(host, port)

log.info("正在解析挑战信息...")
io.recvuntil(b"plz run this command to get solve result")

cmd = ""
while True:
    line = io.recvline().decode().strip()
    if "hashcash" in line:
        cmd = line
        break

log.info(f"提取到的命令: {cmd}")

try:
    solution = subprocess.check_output(cmd, shell=True).strip()
    log.success(f"计算结果: {solution.decode()}")
    io.sendline(solution)
except Exception as e:
    log.error(f"本地执行失败: {e}")
    io.close()
    exit()

try:
    io.recvuntil(b"license code:")
    code = io.recvline().strip()
    log.success(f"收到 License Code: {code.decode()}")
    try:
        payload = gen_packet(code.decode())
        log.info(f"正在发送长度为 {len(payload)} 的字节流...")
        io.send(payload)
    except ValueError:
        log.error("输入的不是有效的十六进制字符串!")
except EOFError:
    log.error("服务器在验证后断开连接。")

io.interactive()

总结

本次逆向完整复现了从协议分析、加密算法识别、魔改点定位到最终EXP构造的全过程。关键技术点包括:

  • 多层嵌套协议解析(AES-GCM → zstd → Base64 → Protobuf)
  • 魔改AES-160的S-Box与MixColumn分析
  • RSA签名逻辑的逆向与利用
  • 动态ELF加载行为的理解

对于从事逆向工程和CTF竞赛的安全研究人员而言,此类题目提供了宝贵的实战经验。

参考资料

[1] 阿里CTF2026-license, 微信公众号:mp.weixin.qq.com/s/tL-QO8GQSxX71nTcGz10AA

版权声明:本文由 云栈社区 整理发布,版权归原作者所有。




上一篇:Claude Code 接入Kaggle Competition Suite:打造终端内的全自动建模工作流
下一篇:ESP32-C5开源复刻Moji 2.0智能AI桌面机器人:低至75元的AI语音助手方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-2 04:21 , Processed in 0.394938 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表