L3HCTF RE wp

image-20250715134457405.png

冠军!这次真的起飞了,正式打CTF一年零两个月终于拿下了第一个大赛的冠军

RE7道题出了道,不过有俩题都是队友给的思路我把解flag脚本搓出来了,其中TemporaPardox拿下1血,AWayOut2拿下2血,Good!

其他方向队友均很给力,每个方向都有血,其中Web甚至6道拿下4道1血,SU战队也是在包队带领下起飞一次了,希望以后愈战愈勇!

TemporalParadox

main开头有花指令,nop掉跳转jmp即可反编译。动态调试跑一轮就知道各个函数的功能

__int64 __fastcall sub_140001D05(__int64 a1, __int64 a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  sub_140002180(a1, a2);
  sub_14000A510(a1, a2, v2, (unsigned int)v53, v3, v4);
  v58 = get_time(a1, a2, v5, 0, v6, v7);
  if ( v58 > 1751990400 && v58 <= 1752052051 )
  {
    gen_query(a1, a2, v8, (unsigned int)v49, v9, v10);
    v57 = &v54;
    v12 = std::string::c_str(a1, a2, v11, v49);
    v15 = md5(a1, a2, v12, (unsigned int)v53, v13, v14, v41);
    sub_14000A820(a1, a2, v15, (unsigned int)v50, (unsigned int)&v54, v16, v42, v47);
    sub_14000A6E0(a1, a2, v17, (unsigned int)&v54, v18, v19, v43);
    v20 = std::operator<<<std::char_traits<char>>(a1, a2, "query: ", &std::cout);
    v21 = std::operator<<<char>(a1, a2, v49, v20);
    std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v21);
    v22 = std::operator<<<char>(a1, a2, v50, &std::cout);
    std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v22);
    std::string::~string(a1, a2, v23, v50);
    std::string::~string(a1, a2, v24, v49);
  }
  std::string::basic_string(a1, a2, v8, v52);
  v25 = std::operator<<<std::char_traits<char>>(a1, a2, "Please input the right query string I used:", &std::cout);
  std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v25);
  std::operator>><char>(a1, a2, v52, &std::cin);
  v56 = &v55;
  v27 = std::string::c_str(a1, a2, v26, v52);
  v30 = md5(a1, a2, v27, (unsigned int)v53, v28, v29, v41);
  sub_14000A820(a1, a2, v30, (unsigned int)v51, (unsigned int)&v55, v31, v44, v47);
  sub_14000A6E0(a1, a2, v32, (unsigned int)&v55, v33, v34, v45);
  if ( (unsigned __int8)sub_14000A8E0(
                          a1,
                          a2,
                          (unsigned int)"8a2fc1e9e2830c37f8a7f51572a640aa",
                          (unsigned int)v51,
                          v35,
                          v36,
                          v46,
                          v48) )
    v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Congratulations!", &std::cout);
  else
    v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Wrong!", &std::cout);
  std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v37);
  std::string::~string(a1, a2, v38, v51);
  std::string::~string(a1, a2, v39, v52);
  return 0LL;
}

可以看出我们需要得到正确的query并满足query md5加密后的值等于8a2fc1e9e2830c37f8a7f51572a640aa;if里是对时间的判断显然是告诉我们要爆破的话时间范围是(1751990400,1752052051)

进入gen_query可以看到各个参数的生成,可以看到两种query,一种是满足pow_like函数的判断则没有a、b、x、y参数,但多了cipher参数;get_rand是模拟生成随机数

__int64 __fastcall sub_140001963(
        __time64_t *a1,
        __int64 a2,
        int a3,
        __int64 a4,
        int a5,
        int a6,
        double a7,
        double a8,
        double a9,
        double a10)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  sub_140001518((_DWORD)a1, a2, a3, (unsigned int)v51, a5, a6);
  time = get_time(a1);
  sub_1400014B5((_DWORD)a1, a2, v10, time, v11, v12);
  v58 = 0;
  v57 = 0;
  v56 = 0;
  v55 = 0;
  for ( i = 0; i < (int)gen_rand(); ++i )
  {
    v58 = gen_rand();
    v57 = gen_rand();
    v56 = gen_rand();
    v55 = gen_rand();
  }
  v52 = gen_rand();
  std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::basic_stringstream(a1, a2, v13, v49);
  v14 = (double)dword_14000B0E0;
  v15 = (double)(int)(v58 | v56);
  v18 = v14 * pow_like(v15, 2.0, v15, a10, v16, v17, (double)dword_14000B0E0);
  v19 = (double)dword_14000B0E4;
  if ( v18 == pow_like((double)(int)(v57 | v55), 2.0, v15, (double)(int)(v57 | v55), v20, v21, v18) * v19 )
  {
    v22 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50);
    v23 = std::operator<<<char>(a1, a2, v51, v22);
    v24 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v23);
    v25 = std::ostream::operator<<(a1, a2, time, v24);
    v26 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v25);
    v27 = std::ostream::operator<<(a1, a2, v52, v26);
    v28 = std::operator<<<std::char_traits<char>>(a1, a2, "&cipher=", v27);
    v31 = sub_14000184D((_DWORD)a1, a2, time, v52, v29, v30, v48);
    std::ostream::operator<<(a1, a2, v31, v28);
  }
  else
  {
    v32 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50);
    v33 = std::operator<<<char>(a1, a2, v51, v32);
    v34 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v33);
    v35 = std::ostream::operator<<(a1, a2, time, v34);
    v36 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v35);
    v37 = std::ostream::operator<<(a1, a2, v52, v36);
    v38 = std::operator<<<std::char_traits<char>>(a1, a2, "&a=", v37);
    v39 = std::ostream::operator<<(a1, a2, v58, v38);
    v40 = std::operator<<<std::char_traits<char>>(a1, a2, "&b=", v39);
    v41 = std::ostream::operator<<(a1, a2, v57, v40);
    v42 = std::operator<<<std::char_traits<char>>(a1, a2, "&x=", v41);
    v43 = std::ostream::operator<<(a1, a2, v56, v42);
    v44 = std::operator<<<std::char_traits<char>>(a1, a2, "&y=", v43);
    std::ostream::operator<<(a1, a2, v55, v44);
  }
  std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::str(a1, a2, v49, a4);
  std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::~basic_stringstream(a1, a2, v45, v49);
  std::string::~string(a1, a2, v46, v51);
  return a4;
}
__int64 gen_rand()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  v1 = (((dword_14000B040 << 13) ^ (unsigned int)dword_14000B040) >> 17) ^ (dword_14000B040 << 13) ^ dword_14000B040;
  dword_14000B040 = (32 * v1) ^ v1;
  return dword_14000B040 & 0x7FFFFFFF;
}
__int64 __fastcall sub_1400014B5(_DWORD a1, _DWORD a2, _DWORD a3, unsigned int a4)
{
  __int64 result; // rax
  unsigned int v5; // [rsp+10h] [rbp+10h]

  v5 = a4;
  if ( !a4 )
    v5 = 1;
  result = v5;
  dword_14000B040 = v5;
  return result;
}

调试可以发现dword_14000B040初始值为get_time返回的time,此外salt值固定为tlkyeueq7fej8vtzitt26yl24kswrgm5,因此a、b、x、y实际上都和t相关

因此首先我写了个python脚本来爆破(c不擅长,部分函数如sub_14000184D直接让gemini分析生成模拟代码,但事后发现其实没用到)

import math
from hashlib import md5, sha1

def gen(dword):
    v1 = ((((dword << 13)&0xffffffff) ^ dword) >> 17) ^ ((dword << 13)&0xffffffff) ^ dword
    dword = (((32 * v1)&0xffffffff) ^ v1) &0xffffffff
    return dword, dword & 0x7FFFFFFF


S_BOX_TABLE_7FF65E2BC020 = [0x0000000E, 0x00000004, 0x0000000D, 0x00000001, 0x00000002, 0x0000000F, 0x0000000B, 0x00000008, 0x00000003, 0x0000000A, 0x00000006, 0x0000000C, 0x00000005, 0x00000009, 0x00000000, 0x00000007]

P_BOX_TABLE_7FF65E2BC0A0 = [0x00000001, 0x00000005, 0x00000009, 0x0000000D, 0x00000002, 0x00000006, 0x0000000A, 0x0000000E, 0x00000003, 0x00000007, 0x0000000B, 0x0000000F, 0x00000004, 0x00000008, 0x0000000C, 0x00000010]


def to_u32(n):
    """将一个数转换为32位无符号整数"""
    return n & 0xFFFFFFFF


def to_s32(n):
    """将一个数转换为32位有符号整数"""
    n = n & 0xFFFFFFFF
    if n & 0x80000000:
        return n - 0x100000000
    return n


def generate_salt(dword_array):
    """
    对应 C++ 函数 sub_7FF65E2B1518
    根据硬编码的 dword 数组生成一个32字符的 salt 字符串。
    """
    if not dword_array:
        raise ValueError("错误: dword_7FF65E2BB060 数组为空,请填写数据。")

    s = []
    for i in range(32):
        v9 = dword_array[i]
        v10 = 0

        # C++ int 是32位的,Python int 是无限精度的,需要模拟32位行为
        v9_s32 = to_s32(v9)

        if v9_s32 >= 0:
            v10 = v9_s32 / 3 + 48
        elif v9_s32 >= -728:
            # ~v9 在C++中是对32位整数按位取反
            v10 = ~v9_s32 & 0xFFFFFFFF
        else:
            # 这里的 sub_7FF65E2B31D0 / 1.0986... 被我们分析为 log3
            # math.log(x) 是 ln(x),math.log(3) 是 ln(3)
            # log3(x) = ln(x) / ln(3)
            try:
                log_val = math.log(-v9_s32) / math.log(3)
                v10 = log_val - 6.0 + 48.0
            except ValueError:
                # 如果 -v9_s32 <= 0,log会出错,这里设置一个默认值
                v10 = 48  # '0'

        # 将计算结果转换为字符
        s.append(chr(int(v10) & 0xFF))

    return "".join(s)


def s_box_transform(state, s_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B16C1 (S-盒替换)
    """
    if not s_box_table:
        raise ValueError("错误: S_BOX_TABLE_7FF65E2BC020 数组为空,请填写数据。")

    s = to_u32(state)
    for _ in range(4):
        # 提取高4位作为索引
        index = (s >> 12) & 0xF
        sbox_val = s_box_table[index]
        # (16 * s) 等价于 (s << 4)
        s = sbox_val | (s << 4)
    return to_u32(s)


def p_box_transform(state, p_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B1785 (P-盒置换)
    """
    if not p_box_table:
        raise ValueError("错误: P_BOX_TABLE_7FF65E2BC0A0 数组为空,请填写数据。")

    s = to_u32(state)
    new_state = 0
    for i in range(16):
        # 获取源比特的位置 (C数组是1-based, Python是0-based)
        source_bit_pos = p_box_table[i] - 1
        # 检查源比特是否为1
        if (s >> source_bit_pos) & 1:
            # 如果是1,则在目标位置i设置比特
            new_state |= (1 << i)
    return new_state


def round_function(state, s_box_table, p_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B17F7 (轮函数)
    """
    state = s_box_transform(state, s_box_table)
    state = p_box_transform(state, p_box_table)
    return state


def generate_round_key(key, round_num):
    """
    对应 C++ 函数 sub_7FF65E2B16A0 (轮密钥生成)
    """
    key_u32 = to_u32(key)
    shift_amount = 4 * (round_num - 1)
    # C++ 代码中 (unsigned int) >> 是逻辑右移
    shifted_key = key_u32 << shift_amount
    return to_u32(shifted_key) >> 16


def encrypt_token(timestamp, r_key, s_box_table, p_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B184D (加密主函数)
    """
    state = to_u32(timestamp)

    # 循环 3 轮
    for i in range(1, 4):
        round_key = generate_round_key(r_key, i)
        state ^= round_key
        state = round_function(state, s_box_table, p_box_table)

    # 循环后的第4步
    round_key_4 = generate_round_key(r_key, 4)
    state ^= round_key_4
    state = s_box_transform(state, s_box_table)

    # 最终返回前的第5步
    round_key_5 = generate_round_key(r_key, 5)
    final_state = state ^ round_key_5

    return to_u32(final_state)


for t in range(1751990400, 1752052052):
    dword = t
    dword, ret = gen(dword)
    cnt = ret
    i = 0
    while i < cnt:
        dword, ret = gen(dword)
        a = ret
        dword, ret = gen(dword)
        b = ret
        dword, ret = gen(dword)
        x = ret
        dword, ret = gen(dword)
        y = ret
        dword, ret = gen(dword)
        cnt = ret
        i+=1
    dword, ret = gen(dword)
    r = ret
    # pow(a | x, 2)
    val1 = math.pow(float(to_s32(a) | to_s32(x)), 2.0)
    # pow(b | y, 2)
    val2 = math.pow(float(to_s32(b) | to_s32(y)), 2.0)

    if math.isclose(0x61 * val1, 0xb * val2):
        cipher = encrypt_token(
            t, r,
            S_BOX_TABLE_7FF65E2BC020,
            P_BOX_TABLE_7FF65E2BC0A0
        )
        query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&cipher={cipher}"
    else:
        query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&a={a}&b={b}&x={x}&y={y}"
    print(t, query)
    if md5(query.encode()).hexdigest() == "8a2fc1e9e2830c37f8a7f51572a640aa":
        print(sha1(query.encode()).hexdigest())

但python爆破速度非常慢,直接让gemini转为c语言脚本

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
// 引入 OpenSSL 库头文件
#include <openssl/md5.h>
#include <openssl/sha.h>


// 伪随机数生成器,对应 python 的 gen 函数
// 使用指针来返回两个值
void gen(uint32_t* dword, uint32_t* ret) {
    uint32_t v1 = ((((*dword << 13) ^ *dword) >> 17) ^ ((*dword << 13) ^ *dword));
    *dword = (32 * v1) ^ v1;
    *ret = *dword & 0x7FFFFFFF;
}

// 辅助函数:将二进制哈希值转换为十六进制字符串
void bytes_to_hex(const unsigned char* bytes, char* hex_string, size_t len) {
    for (size_t i = 0; i < len; ++i) {
        sprintf(hex_string + (i * 2), "%02x", bytes[i]);
    }
    hex_string[len * 2] = '\0';
}


int main() {
    const char* target_md5 = "8a2fc1e9e2830c37f8a7f51572a640aa";

    for (uint32_t t = 1751990400; t < 1752052052; ++t) {
        uint32_t dword = t;
        uint32_t ret;

        // 初始 gen 调用
        gen(&dword, &ret);
        uint32_t cnt = ret;

        uint32_t a = 0, b = 0, x = 0, y = 0;
        int i = 0;
        while (i < cnt) {
            gen(&dword, &a);
            gen(&dword, &b);
            gen(&dword, &x);
            gen(&dword, &y);
            gen(&dword, &cnt);
            i++;
        }

        uint32_t r;
        gen(&dword, &r);
        // C中需要更大的缓冲区来格式化字符串
        char query[512];
        snprintf(query, sizeof(query), "salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t=%u&r=%u&a=%u&b=%u&x=%u&y=%u", t, r, a, b, x, y);
        printf("%u %s\n", t, query);
        fflush(stdout); // 强制刷新输出缓冲区,确保立即看到打印

        // 计算 MD5
        unsigned char md5_result[MD5_DIGEST_LENGTH];
        MD5((unsigned char*)query, strlen(query), md5_result);

        char md5_hex[MD5_DIGEST_LENGTH * 2 + 1];
        bytes_to_hex(md5_result, md5_hex, MD5_DIGEST_LENGTH);
      
        // 比较 MD5
        if (strcmp(md5_hex, target_md5) == 0) {
            printf("Found MD5 match!\n");
            // 计算并打印 SHA1
            unsigned char sha1_result[SHA_DIGEST_LENGTH];
            SHA1((unsigned char*)query, strlen(query), sha1_result);
          
            char sha1_hex[SHA_DIGEST_LENGTH * 2 + 1];
            bytes_to_hex(sha1_result, sha1_hex, SHA_DIGEST_LENGTH);
          
            printf("SHA1: %s\n", sha1_hex);
            break; // 找到结果,退出循环
        }
    }

    return 0;
} 

gcc ./paradox_solve.c -o solve -lssl -lcrypto -lm

d6b6c078-3e16-4928-b82d-338792bcb5c0.png

得到正确query的sha1结果

题目很简单后面就爆了

终焉之门

动调发现出现了代码字符串,后续分析代码发现有处地方做了异或解密

__int64 sub_140091CC0()
{
  sub_140001450(byte_140093A60, 2318LL);
  return sub_140001450(byte_140093380, 1760LL);
}
__int64 __fastcall sub_140001450(_BYTE *a1, __int64 a2)
{
  unsigned __int64 v3; // rcx
  __int64 result; // rax

  if ( a2 != 1 )
  {
    *a1 ^= 0x56u;
    v3 = 1LL;
    if ( a2 != 2 )
    {
      do
      {
        result = (unsigned __int8)aVm0xd1ntuxlwa1[v3 % 0x1CC];
        a1[v3++] ^= result;
      }
      while ( v3 != a2 - 1 );
    }
  }
  return result;
}

提取出来解密得到vm代码

#version 430 core

layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(std430, binding = 0) buffer OpCodes  { int opcodes[]; };
layout(std430, binding = 2) buffer CoConsts { int co_consts[]; };
layout(std430, binding = 3) buffer Cipher   { int cipher[16]; };
layout(std430, binding = 4) buffer Stack    { int stack_data[256]; };
layout(std430, binding = 5) buffer Out      { int verdict;         };

const int MaxInstructionCount = 1000;

void main()
{
    if (gl_GlobalInvocationID.x > 0) return;

    uint ip = 0u;
    int sp = 0;
    verdict = -233;

    while (ip < uint(MaxInstructionCount))
    {
        int opcode = opcodes[int(ip)];
        int arg    = opcodes[int(ip)+1];

        switch (opcode)
        {
            case 2:
                stack_data[sp++] = co_consts[arg];
                break;
            case 7:
            {
                int b = stack_data[--sp];
                int a = stack_data[--sp];
                stack_data[sp++] = a + b;
                break;
            }
            case 8:
            {
                int a = stack_data[--sp];
                int b = stack_data[--sp];
                stack_data[sp++] = a - b;
                break;
            }
            case 14:
            {
                int b = stack_data[--sp];
                int a = stack_data[--sp];
                stack_data[sp++] = a ^ b;
                break;
            }

            case 15:
            {
                int b = stack_data[--sp];
                int a = stack_data[--sp];
                stack_data[sp++] = int(a == b);
                break;
            }

            case 16:
            {
                bool ok = true;
                for (int i = 0; i < 16; i++)
                {
                    if (stack_data[i] != (cipher[i] - 20))
                    { 
                        ok = false; 
                        break; 
                    }
                }
                verdict = ok ? 1 : -1;
                return;
            }

            case 18:
            {
                int c = stack_data[--sp];
                if (c == 0) ip = uint(arg);
                break;
            }

            default:
                verdict = 500;
                return;
        }

        ip+=2;
    }
    verdict = 501;
}

需要去寻找opcode等参数,来到main发现开头出现很多数组初始化,且大小正好对应vm函数的传参

__int64 sub_140091CF0()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  v0 = 0;
  sub_14008E370();
  sub_140023480(8256LL);
  sub_14001F730(1280LL, 800LL, "Password Checker");
  sub_140021100(&v26, 0LL, byte_140093380);
  v1 = _mm_loadu_si128(&v26);
  v2 = sub_14000E700(byte_140093A60, 37305LL);
  v20 = sub_14000EEE0(v2);
  v21 = sub_14000EFF0(672LL, &opcodes, 35050LL);
  v3 = sub_14000EFF0(128LL, &co_consts, 35050LL);
  v22 = sub_14000EFF0(64LL, &cipher, 35050LL);
  v23 = sub_14000EFF0(1024LL, &stack_data, 35050LL);
  v4 = sub_14000EFF0(4LL, &verdict, 35050LL);
  v33[0] = 0LL;
  v24 = v4;
  *(_QWORD *)Str = 0LL;
  v29 = 0LL;
  v30 = 0LL;
  v31 = 0LL;
  memset(v32, 0, sizeof(v32));
  *(_QWORD *)((char *)v33 + 5) = 0LL;
  sub_1400231A0(60LL);
  while ( !(unsigned __int8)sub_14001CAC0() )
  {
    v5 = sub_140025A40();
    if ( v5 > 0 && v0 <= 99 )
    {
      v6 = v0 + 1;
      do
      {
        Str[v6 - 1] = v5;
        v0 = v6;
        v5 = sub_140025A40();
        v7 = (int)v6++ <= 99;
      }
      while ( v7 && v5 > 0 );
    }
    v8 = sub_1400258E0(259LL);
    if ( v0 > 0 && v8 )
      Str[--v0] = 0;
    if ( (unsigned __int8)sub_1400258E0(257LL)
      && strlen(Str) == 40
      && !strncmp(Str, "L3HCTF{", 7uLL)
      && HIBYTE(v32[0]) == '}' )
    {
      v25 = v0;
      v12 = &Str[7];
      v13 = 0;
      do
      {
        v17 = *v12;
        v18 = v12[1];
        if ( v17 > 96 )
          v14 = v17 - 87;
        else
          v14 = v17 - 48;
        v19 = 16 * v14;
        v15 = v13;
        v16 = v18 - 48;
        if ( v18 >= 97 )
          v16 = v18 - 87;
        v12 += 2;
        v13 += 4;
        v27 = v16 + v19;
        sub_14000F0B0(v3, &v27, 4LL, v15);
      }
      while ( (char *)v32 + 7 != v12 );
      v0 = v25;
      sub_14000C100(v20);
      sub_14000F180(v21, 0LL);
      sub_14000F180(v3, 2LL);
      sub_14000F180(v22, 3LL);
      sub_14000F180(v23, 4LL);
      sub_14000F180(v24, 5LL);
      sub_14000EFE0(1LL, 1LL, 1LL);
      sub_14000F140(v24, &verdict, 4LL, 0LL);
      sub_14000C110();
    }
    sub_14001FC90();
    v26 = v1;
    sub_140020650(&v26);
    v9 = sub_14001E170();
    v26 = v1;
    *(float *)&v9 = v9;
    v27 = LODWORD(v9);
    v10 = sub_140021440(&v26, "time");
    v26 = v1;
    sub_140021460(&v26, v10, &v27, 0LL);
    sub_14003B9D0(0, 0, 1280, 800, -1);
    sub_140020690();
    sub_14004DA20((unsigned int)Str, 100, 200, 40, -16777216);
    if ( verdict == 1 )
      sub_14004DA20((unsigned int)"success", 100, 300, 40, -13863680);
    else
      sub_14004DA20((unsigned int)"wrong password", 100, 300, 40, -13162010);
    sub_14004DA20((unsigned int)"Type password and press [Enter] to check!", 100, 100, 20, -8224126);
    sub_14004DA20((unsigned int)"Press [Backspace] to delete characters.", 100, 130, 20, -8224126);
    sub_140025CE0();
  }
  sub_14001FAA0();
  return 0LL;
}

可以看到要求flag格式,且内容为16进制字符串32位;此外可以看到v3即co_consts存储了16个十六进制字节

可以模拟vm跑一遍流程了

KNOWN_CIPHER = [243, 130, 6, 509, 336, 56, 178, 222, 346, 407, 156, 471, 110, 40, 326, 151]

class VM:

    def __init__(self):
        self.opcodes = [0x00000002, 0x00000000, 0x00000002, 0x00000001, 0x00000002, 0x00000000, 0x0000000E, 0x00000000, 0x00000002, 0x00000010, 0x00000008, 0x00000000, 0x00000002, 0x00000002, 0x00000002, 0x00000001, 0x0000000E, 0x00000000, 0x00000002, 0x00000011, 0x00000008, 0x00000000, 0x00000002, 0x00000003, 0x00000002, 0x00000002, 0x0000000E, 0x00000000, 0x00000002, 0x00000012, 0x00000007, 0x00000000, 0x00000002, 0x00000004, 0x00000002, 0x00000003, 0x0000000E, 0x00000000, 0x00000002, 0x00000013, 0x00000007, 0x00000000, 0x00000002, 0x00000005, 0x00000002, 0x00000004, 0x0000000E, 0x00000000, 0x00000002, 0x00000014, 0x00000008, 0x00000000, 0x00000002, 0x00000006, 0x00000002, 0x00000005, 0x0000000E, 0x00000000, 0x00000002, 0x00000015, 0x00000007, 0x00000000, 0x00000002, 0x00000007, 0x00000002, 0x00000006, 0x0000000E, 0x00000000, 0x00000002, 0x00000016, 0x00000007, 0x00000000, 0x00000002, 0x00000008, 0x00000002, 0x00000007, 0x0000000E, 0x00000000, 0x00000002, 0x00000017, 0x00000007, 0x00000000, 0x00000002, 0x00000009, 0x00000002, 0x00000008, 0x0000000E, 0x00000000, 0x00000002, 0x00000018, 0x00000007, 0x00000000, 0x00000002, 0x0000000A, 0x00000002, 0x00000009, 0x0000000E, 0x00000000, 0x00000002, 0x00000019, 0x00000007, 0x00000000, 0x00000002, 0x0000000B, 0x00000002, 0x0000000A, 0x0000000E, 0x00000000, 0x00000002, 0x0000001A, 0x00000007, 0x00000000, 0x00000002, 0x0000000C, 0x00000002, 0x0000000B, 0x0000000E, 0x00000000, 0x00000002, 0x0000001B, 0x00000008, 0x00000000, 0x00000002, 0x0000000D, 0x00000002, 0x0000000C, 0x0000000E, 0x00000000, 0x00000002, 0x0000001C, 0x00000008, 0x00000000, 0x00000002, 0x0000000E, 0x00000002, 0x0000000D, 0x0000000E, 0x00000000, 0x00000002, 0x0000001D, 0x00000007, 0x00000000, 0x00000002, 0x0000000F, 0x00000002, 0x0000000E, 0x0000000E, 0x00000000, 0x00000002, 0x0000001E, 0x00000008, 0x00000000, 0x00000010, 0x00000000, 0x00000002, 0x00000010, 0x00000002, 0x00000011, 0x0000000F, 0x00000000, 0x00000012, 0x00000054, 0x00000002, 0x0000001F, 0x00000001, 0x00000000, 0x00000003, 0x00000001]
        self.co_consts = [0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x000000B0, 0x000000C8, 0x000000FA, 0x00000086, 0x0000006E, 0x0000008F, 0x000000AF, 0x000000BF, 0x000000C9, 0x00000064, 0x000000D7, 0x000000C3, 0x000000E3, 0x000000EF, 0x00000087, 0x00000000]
        self.co_consts[:16] = list(range(16))
        self.stack_data = [0] * 256
        self.ip = 0
        self.sp = 0
        self.max_instructions = 1000

    def run(self):
        while self.ip < len(self.opcodes) and self.ip < self.max_instructions * 2:
            opcode = self.opcodes[self.ip]
            arg = self.opcodes[self.ip + 1]
            print(opcode, end=" ")
            if opcode == 2:  # LOAD_CONST
                self.stack_data[self.sp] = self.co_consts[arg]
                self.sp += 1
                print(f"push {hex(self.co_consts[arg])}({arg})")
            elif opcode == 7:  # BINARY_ADD
                b = self.stack_data[self.sp - 1]
                a = self.stack_data[self.sp - 2]
                self.sp -= 1
                self.stack_data[self.sp - 1] = a + b
                print(f"add {b}, {a}")
            elif opcode == 8:  # BINARY_SUBTRACT
                b = self.stack_data[self.sp - 1]
                a = self.stack_data[self.sp - 2]
                self.sp -= 1
                self.stack_data[self.sp - 1] = b - a
                print(f"sub {b}, {a}")
            elif opcode == 14:  # BINARY_XOR
                b = self.stack_data[self.sp - 1]
                a = self.stack_data[self.sp - 2]
                self.sp -= 1
                self.stack_data[self.sp - 1] = a ^ b
                print(f"xor {b}, {a}")
            elif opcode == 15:  # COMPARE_OP (==)
                b = self.stack_data[self.sp - 1]
                a = self.stack_data[self.sp - 2]
                self.sp -= 1
                self.stack_data[self.sp - 1] = 1 if a == b else 0
                print(f"cmp {b}, {a}")
            elif opcode == 18:  # POP_JUMP_IF_FALSE
                c = self.stack_data[self.sp - 1]
                self.sp -= 1
                if c == 0:
                    self.ip = arg
                    continue
                print("pop")
            else:
                print("cmp")
                return
            self.ip += 2


if __name__ == "__main__":
    vm = VM()
    input = [val for val in vm.co_consts[:16]]
    print(input)
    vm.run()
    correct_cipher = [val for val in vm.stack_data[:16]]
    print(correct_cipher)
    print(f"CIPHER (16个整数): {[i-20 for i in KNOWN_CIPHER]}")

分析下日志,可以看到每次先异或下一个字节,然后结果一个常数加或减异或结果

2 push 0x0(0)
2 push 0x1(1)
2 push 0x0(0)
14 xor 0, 1
2 push 0xb0(16)
8 sub 176, 1
2 push 0x2(2)
2 push 0x1(1)
14 xor 1, 2
2 push 0xc8(17)
8 sub 200, 3
2 push 0x3(3)
2 push 0x2(2)
14 xor 2, 3
2 push 0xfa(18)
7 add 250, 1

可以逆回去

cipher = [0xF3, 0x82, 0x06, 0x1FD, 0x150, 0x38, 0xB2, 0xDE, 0x15A, 0x197, 0x9C, 0x1D7, 0x6E, 0x28, 0x146,
          0x97]
CoConsts = [0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF, 0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00]
opcodes = [0x00000002, 0x00000000, 0x00000002, 0x00000001, 0x00000002, 0x00000000, 0x0000000E, 0x00000000,
                0x00000002, 0x00000010, 0x00000008, 0x00000000, 0x00000002, 0x00000002, 0x00000002, 0x00000001,
                0x0000000E, 0x00000000, 0x00000002, 0x00000011, 0x00000008, 0x00000000, 0x00000002, 0x00000003,
                0x00000002, 0x00000002, 0x0000000E, 0x00000000, 0x00000002, 0x00000012, 0x00000007, 0x00000000,
                0x00000002, 0x00000004, 0x00000002, 0x00000003, 0x0000000E, 0x00000000, 0x00000002, 0x00000013,
                0x00000007, 0x00000000, 0x00000002, 0x00000005, 0x00000002, 0x00000004, 0x0000000E, 0x00000000,
                0x00000002, 0x00000014, 0x00000008, 0x00000000, 0x00000002, 0x00000006, 0x00000002, 0x00000005,
                0x0000000E, 0x00000000, 0x00000002, 0x00000015, 0x00000007, 0x00000000, 0x00000002, 0x00000007,
                0x00000002, 0x00000006, 0x0000000E, 0x00000000, 0x00000002, 0x00000016, 0x00000007, 0x00000000,
                0x00000002, 0x00000008, 0x00000002, 0x00000007, 0x0000000E, 0x00000000, 0x00000002, 0x00000017,
                0x00000007, 0x00000000, 0x00000002, 0x00000009, 0x00000002, 0x00000008, 0x0000000E, 0x00000000,
                0x00000002, 0x00000018, 0x00000007, 0x00000000, 0x00000002, 0x0000000A, 0x00000002, 0x00000009,
                0x0000000E, 0x00000000, 0x00000002, 0x00000019, 0x00000007, 0x00000000, 0x00000002, 0x0000000B,
                0x00000002, 0x0000000A, 0x0000000E, 0x00000000, 0x00000002, 0x0000001A, 0x00000007, 0x00000000,
                0x00000002, 0x0000000C, 0x00000002, 0x0000000B, 0x0000000E, 0x00000000, 0x00000002, 0x0000001B,
                0x00000008, 0x00000000, 0x00000002, 0x0000000D, 0x00000002, 0x0000000C, 0x0000000E, 0x00000000,
                0x00000002, 0x0000001C, 0x00000008, 0x00000000, 0x00000002, 0x0000000E, 0x00000002, 0x0000000D,
                0x0000000E, 0x00000000, 0x00000002, 0x0000001D, 0x00000007, 0x00000000, 0x00000002, 0x0000000F,
                0x00000002, 0x0000000E, 0x0000000E, 0x00000000, 0x00000002, 0x0000001E, 0x00000008, 0x00000000,
                0x00000010, 0x00000000, 0x00000002, 0x00000010, 0x00000002, 0x00000011, 0x0000000F, 0x00000000,
                0x00000012, 0x00000054, 0x00000002, 0x0000001F, 0x00000001, 0x00000000, 0x00000003, 0x00000001]
op_pattern = [i for i in opcodes[10:160:10]]
print(op_pattern)
targets = [c - 20 for c in cipher]
inputs = [0] * 16

inputs[0] = targets[0]

for i in range(1, 16):
    prev_input = inputs[i - 1]
    target = targets[i]
    const = CoConsts[i - 1]
    op_type = op_pattern[i - 1]

    if op_type == 7:  # ADD
        intermediate = (target - const)&0xff
    elif op_type == 8:  # SUB
        intermediate = (const - target)&0xff

    inputs[i] = intermediate ^ prev_input

print(bytes(inputs).hex())
# df9d4ba41258574ccb7155b9d01f5c58

obfuscate

ida反编译错误,main有爆红,不用管直接分析其他函数,分别找到如下函数

首先是3处有效反调试

  1. ptrace

    __int64 sub_7E20()
    {
      __int64 result; // rax
    
      result = ptrace(PTRACE_TRACEME, 0LL, 0LL);
      if ( result == -1 )
        _exit(1);
      return result;
    }
  2. 读取文件Tracerid值(动调跟进解密后的字符串可以看到Tracerid),可以发现读取了"/proc/self/status"并寻找"TracerPid:"

    __int64 sub_55555555BE50()
    {
      __int64 result; // rax
      int v1; // [rsp+0h] [rbp-10h] BYREF
      __int64 v2; // [rsp+5h] [rbp-Bh] BYREF
      __int16 v3; // [rsp+Dh] [rbp-3h]
      char v4; // [rsp+Fh] [rbp-1h]
    
      v2 = 0xFBF31E8058E31D9LL;
      v3 = 31209;
      v4 = -17;
      v1 = 1726956429;
      sub_55555555C1D0((__int64)&v2, (__int64)&v1, 11LL, 4LL);
      result = sub_55555555BEC0((char *)&v2);
      if ( (int)result > 0 )
        _exit(1);
      return result;
    }
    __int64 __fastcall sub_55555555BEC0(char *a1)
    {
      char s[140]; // [rsp+10h] [rbp-D0h] BYREF
      unsigned int v3; // [rsp+9Ch] [rbp-44h] BYREF
      FILE *stream; // [rsp+A0h] [rbp-40h]
      char *needle; // [rsp+A8h] [rbp-38h]
      int v7; // [rsp+B4h] [rbp-2Ch] BYREF
      char filename[18]; // [rsp+BAh] [rbp-26h] BYREF
      int v9; // [rsp+CCh] [rbp-14h] BYREF
      char modes[2]; // [rsp+D2h] [rbp-Eh] BYREF
      int v11; // [rsp+D4h] [rbp-Ch] BYREF
      int v12; // [rsp+DAh] [rbp-6h] BYREF
      __int16 v13; // [rsp+DEh] [rbp-2h]
    
      v12 = 1053795514;
      v13 = -10245;
      v11 = 468703135;
      sub_55555555C1D0((__int64)&v12, (__int64)&v11, 6LL, 4LL);
      *(_WORD *)modes = 29201;
      v9 = 233665123;
      sub_55555555C1D0((__int64)modes, (__int64)&v9, 2LL, 4LL);
      qmemcpy(filename, "uU \\9\n!V6C}@.D&F)%", sizeof(filename));
      v7 = 861021530;
      sub_55555555C1D0((__int64)filename, (__int64)&v7, 18LL, 4LL);
      needle = a1;
      stream = fopen(filename, modes);
      if ( stream )
      {
        v3 = -1;
        while ( fgets(s, 128, stream) && (!(unsigned int)__isoc99_sscanf(s, &v12, s, &v3) || !strstr(s, needle)) )    // "TracerPid:"
          ;
        fclose(stream);
        return v3;
      }
      else
      {
        return (unsigned int)-1;
      }
    }
  3. getpid

    unsigned __int64 sub_8030()
    {
      unsigned __int64 v0; // rax
      unsigned __int64 v1; // rax
      unsigned __int64 result; // rax
      __int64 v3; // [rsp+8h] [rbp-8h]
    
      v0 = __rdtsc();
      v3 = v0;
      getpid();
      v1 = __rdtsc();
      result = v1 - v3;
      if ( result > 0x186A0 )
        _exit(1);
      return result;
    }

后来发现还有个不怎么常见的

const char **sub_55555555C070()
{
  const char **result; // rax
  const char **i; // [rsp+8h] [rbp-28h]
  _QWORD v2[4]; // [rsp+10h] [rbp-20h] BYREF

  v2[0] = "LD_PRELOAD";
  v2[1] = "gdb";
  v2[2] = "TERM=linux";
  v2[3] = qword_55555555EDD8;
  for ( i = (const char **)v2; ; ++i )
  {
    result = i;
    if ( !*i )
      break;
    if ( getenv(*i) )
      _exit(1);
  }
  return result;
}

通过检测LD_PRELOAD和gdb等环境变量来反调试

一个个force jmp或者nop后patch即可动调

比较函数如下

int __fastcall sub_6180(__int64 a1)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  *(_QWORD *)format = 0x61C477DB26D672BDLL;
  v11 = 0x41BD3F9C2FD86CACLL;
  v9 = 1102520059;
  xor_dec(format, &v9, 16LL, 4LL);
  memcpy(dest, &unk_9031, sizeof(dest));
  v7 = 1350490027;
  xor_dec(dest, &v7, 38LL, 4LL);
  memcpy(src, &cmp, 0x21uLL);
  v5 = 1189641421;
  xor_dec(src, &v5, 33LL, 4LL);
  v4 = a1;
  memcpy(v3, src, 0x21uLL);
  for ( i = 0; i < 32; ++i )
  {
    if ( *(unsigned __int8 *)(v4 + i) != (unsigned __int8)v3[i] )
    {
      printf(format);
      exit(1);
    }
  }
  return printf(dest);
}

以及两个混淆比较厉害的加密函数(分析可知是rc5加密),可以借助ida9自带的goomba插件解除部分混淆,右键

De-obfuscate即可,但是得到的函数依然存在很多逻辑混淆

比如恒真恒假跳转

if ( unk_B1C8 < 10 && unk_B1C8 >= 10 )    // 恒假
    goto LABEL_26;
if ( unk_B218 >= 10 || unk_B218 < 10 )    // 恒真
      break;

减1

*v37 - 1067854539 + 1067854538    // 等价于*v37 - 1

异或

*v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48    // 等价于*v24^0x51BF6B48

偶数次异或值不变

(*v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48) ^ (*v23 & 0xAE4094B7 | ~*v23 & 0x51BF6B48)    // 等价于*v24^*v23

v9 ^ v8 | v9 & v8    // 等价于v9|v8

最后可以得到两份干净简洁的伪代码交给gemini分析下

__int64 __fastcall sub_555555555250(__int64 a1, __int64 a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  v41 = a1;
  v42 = a2;
        v2 = HIDWORD(v42);
        v32 = &v27 - 2;
        v33 = &v27 - 2;
        v34 = &v27 - 2;
        v35 = &v27 - 2;
        v36 = &v27 - 2;
        v37 = &v27 - 2;
        v38 = &v27 - 2;
        v39 = &v27 - 2;
        v40 = &v27 - 2;
        *(&v27 - 2) = v41;
        HIDWORD(v27) = v2;
        LODWORD(v27) = 0;
LABEL_3:
  v31 = *v37 < 4u;
  if ( v31 )
  {
    v3 = v38;
    *(v34 + *v37) = 0;
    *v3 = 0;
    while ( 1 )
    {
      if ( *v38 >= 4u )
      {
        ++*v37;
        goto LABEL_3;
      }
      *(v34 + *v37) = *(*v33 + (*v38 + 4 * *v37)) + (*(v34 + *v37) << 8);
      ++*v38;
    }
  }
  v4 = v37;
  **v32 = 0xB7E15163;
  *v4 = 1;
  while ( 1 )
  {
    if ( *v37 >= 0x1Au )
      break;
    *(*v32 + 4LL * *v37) = *(*v32 + 4LL * (*v37 - 1)) - 0x61C88647;
    ++*v37;
  }
  v7 = v35;
  v8 = v36;
  v9 = v37;
  *v38 = 0;
  *v9 = 0;
  *v8 = 0;
  *v7 = 0;
  *v39 = 0;
  while ( 2 )
  {
    if ( *v39 < 78 )
    {
      v10 = v38;
      v11 = v36;
      v12 = v34;
      v13 = v35;
      v15 = ((*v35 + *(*v32 + 4LL * *v37) + *v36) >> 29) | ((*v35 + *(*v32 + 4LL * *v37) + *v36) << 3);
      *(*v32 + 4LL * *v37) = v15;
      *v13 = v15;
      v18 = v37;
      v19 = ((*v11 + *v35 + *(v12 + *v10)) >> ((*v11 + *v35) + 32)) | ((*v13 + *(v12 + *v10) + *v11) << (*v13 + *v11));
      *(v12 + *v10) = v19;
      *v11 = v19;
      v20 = v38;
      *v18 = (*v18 + 1) % 0x1A;
      *v20 = (*v20 + 1) & 3;
      ++*v39;
    }
    break;
  }
  result = 1;
  return result;
}
int *__fastcall sub_555555555E80(_DWORD *a1, __int64 a2, _DWORD *a3)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  v26 = a1;
  v27 = a2;
  v28 = a3;
    v3 = v28;
    v22 = &v21 - 2;
    v23 = (&v21 - 2);
    v24 = &v21 - 2;
    v25 = &v21 - 2;
    *(&v21 - 2) = v26;
    v21 = v3;
    LODWORD(v21) = **(&v21 - 2) + *v3;
    *(&v21 - 4) = (*(&v21 - 2))[1] + v21[1];
    *(&v21 - 4) = 1;
  while ( *v25 <= 0xCu )
  {
      *v23 = *(*v21 + 4LL * (2 * *v25)) + (((*v24 ^ *v23) << *v24) | ((*v24 ^ *v23) >> (32 - *v24)));
      *v24 = (((*v23 ^ *v24) >> (32 - *v23)) | ((*v23 ^ *v24) << *v23)) + *(*v21 + 4LL * (2 * *v25 + 1));
      *v23 = *v24 ^ *v23;
    ++*v25;
  }
  v11 = v22;
  v12 = v24;
  **v22 = *v23;
  result = *v11;
  result[1] = *v12;
  return result;
}

分析可知是RC5的密钥扩展和加密函数,其中加密函数地方做了魔改,对在轮加密种A多异或了B,解密脚本如下

还有个很坑的点卡了很久,密钥是"cleWtemoH3Lo!FTC",而不是WelcometoL3HCTF!,正好是后者小端存储形式的字符串(需要动调去密钥扩展里看从内存里读取的到底是什么值)

import struct


class RC5:
    def __init__(self, key: bytes):
        self.w = 32  # 字长(比特)
        self.r = 12  # 轮数
        self.b = len(key)  # 密钥长度
        self.t = 2 * (self.r + 1)  # 密钥表大小
        self.mod = 1 << self.w  # 模数
        self.S = self._expand_key(key)

    def _expand_key(self, key: bytes) -> list:
        # 初始化常量
        P, Q = 0xB7E15163, 0x61C88647

        # 初始化密钥表
        S = [P]
        for i in range(1, self.t):
            S.append((S[i - 1] - Q) % self.mod)

        # 将密钥转换为字列表
        c = max(len(key) // 4, 1)
        L = [0] * c
        for i in range(len(key)):
            idx = i // 4
            shift = 8 * (i % 4)
            L[idx] = (L[idx] + (key[i] << shift)) % self.mod

        # 混合密钥
        i = j = 0
        A = B = 0
        for _ in range(3 * max(self.t, c)):
            A = S[i] = self.rotl((S[i] + A + B) % self.mod, 3)
            B = L[j] = self.rotl((L[j] + A + B) % self.mod, (A + B) % self.w)
            i = (i + 1) % self.t
            j = (j + 1) % c

        return S

    def rotl(self, x: int, n: int) -> int:
        n %= self.w
        return ((x << n) | (x >> (self.w - n))) % self.mod

    def rotr(self, x: int, n: int) -> int:
        n %= self.w
        return ((x >> n) | (x << (self.w - n))) % self.mod

    def decrypt_block(self, data: bytes) -> bytes:
        # 解析输入块
        A = struct.unpack('<I', data[:4])[0]
        B = struct.unpack('<I', data[4:8])[0]

        # 解密过程
        for i in range(self.r, 0, -1):
            A = A ^ B
            B = self.rotr((B - self.S[2 * i + 1]) % self.mod, A) ^ A
            A = self.rotr((A - self.S[2 * i]) % self.mod, B) ^ B

        B = (B - self.S[1]) % self.mod
        A = (A - self.S[0]) % self.mod

        # 打包输出
        return struct.pack('<II', A, B)

    def encrypt_block(self, data: bytes) -> bytes:
        """加密一个64位数据块"""
        A = struct.unpack('<I', data[:4])[0]
        B = struct.unpack('<I', data[4:8])[0]

        # 初始白化
        A = (A + self.S[0]) % self.mod
        B = (B + self.S[1]) % self.mod

        # 轮函数
        for i in range(1, self.r + 1):
            A = (self.rotl((A ^ B), B) + self.S[2 * i]) % self.mod
            B = (self.rotl((B ^ A), A) + self.S[2 * i + 1]) % self.mod
            A ^= B

        return struct.pack('<II', A, B)

    def encrypt(self, plaintext: bytes) -> bytes:
        """加密任意长度数据"""
        # 分块加密
        blocks = [plaintext[i:i+8] for i in range(0, len(plaintext), 8)]
        ciphertext = b''
        for block in blocks:
            ciphertext += self.encrypt_block(block)
        return ciphertext

    def decrypt(self, ciphertext: bytes) -> bytes:
        # 处理填充(示例使用PKCS#7)
        blocks = [ciphertext[i:i + 8] for i in range(0, len(ciphertext), 8)]
        plaintext = b''
        for block in blocks:
            plaintext += self.decrypt_block(block)
        return plaintext


if __name__ == "__main__":
    key = b"cleWtemoH3Lo!FTC"
    rc5 = RC5(key)
    plainttext = b"flag{11111222222333333333333334}"
    ciphertext = rc5.encrypt(plainttext)
    print(ciphertext.hex())
    ciphertext = bytes([0x1B, 0xBB, 0xA1, 0xF2, 0xE9, 0x7C, 0x87, 0x21, 0x8A, 0x37, 0xFD, 0x0A, 0x94, 0x1A, 0x81, 0xBC, 0x40, 0x1E, 0xE3, 0xAA, 0x73, 0x2E, 0xD8, 0x3F, 0x84, 0xB8, 0x71, 0x42, 0xCC, 0x35, 0x8B, 0x39])

    plaintext = rc5.decrypt(ciphertext)
    print(f"Decrypted: {plaintext}")

easyvm

调试可以发现是类tea加密,8字节一组变化

最开始做复杂了一点点去分析VM里每个指令的作用并试图模拟,最后才想到直接ida下条件断点在重要运算指令上即可,把两个操作数打印下就知道每一步计算都做了什么

找到vm计算指令的位置(tea中主要为add、sub、xor、shl、shr),简单写下idapython脚本,以此来模拟trace获得加密log

0d036838-7eaf-4210-9666-00f20abc8cff.png

import idc, idaapi
op1_val = idc.get_reg_value("EDX")
op2_val = idc.get_reg_value("ECX") & 0xFF
result_val = idc.get_reg_value("EDX")
print(f"shl {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val<<op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EDX")
op2_val = idc.get_reg_value("ECX") & 0xFF
print(f"shr {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val>>op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EAX")
rbp_val = idc.get_reg_value("RBP")
mem_addr = rbp_val + 0x4C
op2_val = idc.get_wide_dword(mem_addr)
print(f"xor {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val^op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EAX")
rbp_val = idc.get_reg_value("RBP")
mem_addr = rbp_val + 0x1C
op2_val = idc.get_wide_dword(mem_addr)
print(f"sub {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val-op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EAX")
op2_val = idc.get_reg_value("EDX")
print(f"add {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val+op2_val)&0xffffffff)}")  

log取一轮加密来分析

shl 0x32323232, 0x3 = 0x91919190
add 0xa56babcd, 0x91919190 = 0x36fd3d5d
add 0x0, 0x32323232 = 0x32323232
add 0x0, 0x32323232 = 0x32323232
xor 0x32323232, 0x36fd3d5d = 0x4cf0f6f
shr 0x32323232, 0x4 = 0x3232323
add 0xffffffff, 0x3232323 = 0x3232322
xor 0x4cf0f6f, 0x3232322 = 0x7ec2c4d
add 0x31313131, 0x7ec2c4d = 0x391d5d7e
add 0x11223344, 0x0 = 0x11223344
shl 0x391d5d7e, 0x2 = 0xe47575f8
add 0xffffffff, 0xe47575f8 = 0xe47575f7
add 0x11223344, 0x391d5d7e = 0x4a3f90c2
add 0xabcdef01, 0x4a3f90c2 = 0xf60d7fc3
xor 0xf60d7fc3, 0xe47575f7 = 0x12780a34
shr 0x391d5d7e, 0x5 = 0x1c8eaeb
add 0xa56babcd, 0x1c8eaeb = 0xa73496b8
xor 0x12780a34, 0xa73496b8 = 0xb54c9c8c
add 0x32323232, 0xb54c9c8c = 0xe77ecebe
sub 0x40, 0x1 = 0x3f

可以看到三个密钥0xa56babcd、0xffffffff、0xabcdef01以及delta=0x11223344

往后分析看所有轮加密完total做了什么,可以发现下一组加密8字节用的total值是上一组结束后的total值

add 0x11223344, 0x488cd100 = 0x59af0444    // 上一组total
shl 0x6bc23e4e, 0x2 = 0xaf08f938
add 0xffffffff, 0xaf08f938 = 0xaf08f937
add 0x59af0444, 0x6bc23e4e = 0xc5714292
add 0xabcdef01, 0xc5714292 = 0x713f3193
xor 0x713f3193, 0xaf08f937 = 0xde37c8a4
shr 0x6bc23e4e, 0x5 = 0x35e11f2
add 0xa56babcd, 0x35e11f2 = 0xa8c9bdbf
xor 0xde37c8a4, 0xa8c9bdbf = 0x76fe751b
add 0x34343434, 0x76fe751b = 0xab32a94f
sub 0x40, 0x1 = 0x3f
shl 0xab32a94f, 0x3 = 0x59954a78
add 0xa56babcd, 0x59954a78 = 0xff00f645
add 0x59af0444, 0xab32a94f = 0x4e1ad93
add 0x0, 0x4e1ad93 = 0x4e1ad93
xor 0x4e1ad93, 0xff00f645 = 0xfbe15bd6
shr 0xab32a94f, 0x4 = 0xab32a94
add 0xffffffff, 0xab32a94 = 0xab32a93
xor 0xfbe15bd6, 0xab32a93 = 0xf1527145
add 0x6bc23e4e, 0xf1527145 = 0x5d14af93
add 0x11223344, 0x59af0444 = 0x6ad13788    // 新一组total

搞懂加密逻辑直接开逆

from ctypes import c_uint32


def tea_encrypt(r, v, key, delta):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    total = c_uint32(0)
    for i in range(r):
        v0.value += ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1])
        total.value += delta
        v1.value += ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0])
    return v0.value, v1.value


def tea_decrypt(r, v, key, delta, id):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    total = c_uint32(delta * r * (id//2+1))
    for i in range(r):
        v1.value -= ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0])
        total.value -= delta
        v0.value -= ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1])
    return v0.value, v1.value

v = [2272944806, 1784017395, 2920892487, 2984657895, 2840586369, 2613617290, 3301943967, 4053798049]
k = [0xa56babcd, 0xffffffff, 0xabcdef01]
delta = 0x11223344
for i in range(0, len(v), 2):
    v[i:i+2] = tea_decrypt(64, v[i:i+2], k, delta, i)
print(list(map(hex, v)))
v = "".join([int.to_bytes(v[i], byteorder='little', length=4).decode() for i in range(len(v))])
print(v)

AWayOut2

这个题感觉现出的,一股纯纯为难人的味道

我整体的思路很取巧,就是侧信道爆破,大部分人应该没想到

最开始看了眼很明显代码根本读不懂,动调还会跑飞,一开始目标就挺明确,就是侧信道爆破。借助的是pintool,在以往好多比较难分析逻辑的题发挥了一定作用。因此这次比赛我也是毫不犹豫直接上来爆破分析,爆破第一位字母发现hjk指令数和其他异常,后续逐渐发现hjkl为输入范围

a 419744191

k 419730508
h 419730512
j 419730514
l 419744180 # x

la 640207084
lk 640193401
ll 640207073 # x
lj 640193407

lla 860727831
llk 860714148
lll 860714144
llj 860727830 # x

可以发现输入正确的指令数会和输入不是hjkl指令数相差较小,而其他的指令数绝对值差都大于1000,因此可以逐位进行爆破,使用DFS遍历所有可能

由于输入后返回结果时间较长,不使用多线程爆破时间太长了,因此我写了份基本单线程脚本

import os
import collections
import time
from pwn import *

# --- 配置 ---
context.log_level = 'error'
PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"]
# 字符集顺序现在变得更重要。将常见字符放在前面可能会提高效率。
CHARSET = "{hjkl"
FLAG_LENGTH = 118
ANOMALY_THRESHOLD = 9000  # 显著差异的阈值,可以微调


# --- 辅助函数 (与之前相同) ---

def get_instruction_count(test_flag):
    """
    运行 PIN 工具并获取给定 flag 的指令数。
    """
    try:
        p = process(PIN_COMMAND)
        p.recvuntil(b'try:\n', timeout=5)
        p.sendline(test_flag.encode())
        output = p.recvall(timeout=20)
        p.close()

        lines = output.strip().splitlines()
        if lines and lines[-1].isdigit():
            return int(lines[-1])
        else:
            return -1
    except Exception as e:
        return -1


# --- 新的主逻辑 (激进 DFS) ---

def solve_aggressive_dfs(known_prefix=""):
    """
    使用激进的深度优先搜索策略。
    一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。
    """
    # 基本情况:如果长度达到目标,说明成功了
    if len(known_prefix) == FLAG_LENGTH:
        print("\n" + "=" * 60)
        print(f"[*] 成功!找到完整 Flag: {known_prefix}")
        print("=" * 60)
        return True

    current_pos = len(known_prefix)
    print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...")

    # 用于存储当前层级已知的最小指令数
    min_count_for_this_level = 1000000

    total_chars = len(CHARSET)
    for i, char in enumerate(CHARSET):
        test_flag = known_prefix + char
        progress_text = f"    -> 进度: {i + 1}/{total_chars} | 测试: '{test_flag}'"
        print(progress_text, end=' ')

        count = get_instruction_count(test_flag)
        print(count)
        if count == -1:
            continue  # 跳过执行失败的尝试
        if i == 0:
            tmp = count
            continue
        # 核心逻辑:检查当前字符的指令数是否比“已知最小”还要显著降低
        # ANOMALY_THRESHOLD 是你说的 "10000左右"
        if abs(tmp - count) > ANOMALY_THRESHOLD:
            # continue
            print(f"\n    [*] 发现显著更优字符 '{char}'...")
            print(f"    [*] 立即深入 DFS 搜索 '{test_flag}'...")

            # 立即递归,不再测试当前层级的其他字符
            if solve_aggressive_dfs(test_flag):
                return True  # 如果这条路成功了,直接返回 True
            else:
                # 如果深入后发现是死胡同,打印回溯信息并继续在当前层级搜索
                print(f"\n[!] 路径 '{test_flag}' 是死胡同, 回溯到第 {current_pos + 1} 位, 继续搜索...")
                # 即使是死胡同,这个 count 也是一个新的有效最小值,需要更新
                min_count_for_this_level = count

        # 如果不是显著降低,就只更新当前层级的最小指令数
        elif count < min_count_for_this_level:
            min_count_for_this_level = count

    # 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的
    print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...")
    return False


# --- 程序入口 ---
if __name__ == "__main__":
    print("=" * 60)
    print("开始使用“激进”DFS 策略进行全长度 Flag 爆破")
    print("=" * 60)

    # 假设 'h' 仍然是正确的第一个字符
    if not solve_aggressive_dfs(""):
        # 如果不确定第一个字符,使用下面这行:
        # if not solve_aggressive_dfs(""):
        print("\n" + "=" * 60)
        print("[!] 未能找到完整的 Flag。")
        print("=" * 60)

上面的是最基础的脚本,后面多线程发现了更多bug

  • 要限制方向,不能跑反方向,比如你之前向右走了你下一步不能再向左了,中午跑的时候还没发现,下午才发现结果里不停的jkjkjk
  • timeout加得大大的,越往后越慢,出现了好几次获取指令数为-1,直接把我dfs搞乱了,跑了一下午才发现出错了,赶紧把跑出来的路径打印出来,果然出现了一些很不合理的路径如下,还好前面的都没问题不至于再从头开始跑

288ea616-82c6-470a-9528-e9670fd33236

  • threshold指令差值应该看绝对值,最开始爆破时候没发现l方向完指令数增大,导致做差出现负数,所以一直没有l方向
  • 要有一个错误基准,因此设置了一个{输入,后续输入4个方向和输入{指令数进行比较,绝对值差在1000以内是可以走的方向

爆破脚本如下

#!/usr/bin/env python3
import os
from pwn import *
from concurrent.futures import ThreadPoolExecutor, as_completed

PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"]
CHARSET = "jkhl{"
FLAG_LENGTH = 118
MAX_THREADS = 16

def get_instruction_count(test_flag):
    """
    运行 PIN 工具并获取给定 flag 的指令数。
    """
    try:
        p = process(PIN_COMMAND)
        p.recvline(timeout=200)
        p.sendline(test_flag.encode())
        output = p.recvall(timeout=200)
        p.close()
      
        lines = output.strip().splitlines()
        if lines and lines[-1].isdigit():
            return int(lines[-1])
        else:
            return -1
    except Exception as e:
        return -1

# --- 新的主逻辑 (激进 DFS) ---

def solve_aggressive_dfs(known_prefix=""):
    """
    使用激进的深度优先搜索策略。
    一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。
    """
    # 基本情况:如果长度达到目标,说明成功了
    if len(known_prefix) == FLAG_LENGTH:
        print("\n" + "="*60)
        print(f"[*] 成功!找到完整 Flag: {known_prefix}")
        print("="*60)
        return True

    current_pos = len(known_prefix)
    print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...")

    # 1. 获取基准字符 '{' 的指令数 (此步骤仍然串行)
    print(f"    -> 获取基准指令数 (字符: '{{')...", end='')
    ref_flag = known_prefix + "{"
    ref_count = get_instruction_count(ref_flag)
    print(f" 指令数: {ref_count}")
  
    # 2. 准备所有要并行测试的任务
    if known_prefix:
        if known_prefix[-1] == "l":
            chars_to_test = ["l", "j", "k"]
        elif known_prefix[-1] == "h":
            chars_to_test = ["h", "j", "k"]
        elif known_prefix[-1] == "j":
            chars_to_test = ["l", "j", "h"]
        elif known_prefix[-1] == "k":
            chars_to_test = ["l", "h", "k"]
    else:   
        chars_to_test = ["h", "l", "j", "k"]
    flags_to_test = [known_prefix + char for char in chars_to_test]
  
    candidates = []

    # 3. 使用线程池并行爆破所有字符
    with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
        # executor.map 会将 get_instruction_count 函数应用到 flags_to_test 中的每一项
        # 它会按顺序返回结果,这非常方便
        all_counts = executor.map(get_instruction_count, flags_to_test)

        # 4. 收集并处理结果
        print("    -> 所有并行任务已完成,正在分析结果...")
        for char, count in zip(chars_to_test, all_counts):
            if count != -1:
                diff = abs(ref_count - count)
                print(f"{known_prefix+char} (指令数: {count}, 差值: {diff})")
                if diff < 1000:
                    candidates.append((char, count))

    # 对所有候选路径并行发起 DFS 递归
    if candidates:
        with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
            # 提交所有递归任务
            futures = [executor.submit(solve_aggressive_dfs, known_prefix + char) for char, _ in candidates]
            # 等待第一个成功返回的结果
            for future in as_completed(futures):
                if future.result(): # 如果某个子任务返回 True
                    return True # 立刻将成功信号向上传递

    # 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的
    print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...")
    return False

# --- 程序入口 ---
if __name__ == "__main__":
    context.log_level = 'error'

    print("="*60)
    print("开始使用“激进”DFS 策略进行全长度 Flag 爆破")
    print("="*60)
  
    if not solve_aggressive_dfs("lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjl"):
        print("\n" + "="*60)
        print("[!] 未能找到完整的 Flag。")
        print("="*60)

跑到最后基本就看出来路径没问题了,

d177101f-3d17-4d53-9632-214263c460a8

正确路径为lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjljjllljjhhjjljjlj

md5后即为flag

应该有更好的思路,但已经没心思研究这个史题了

最后修改:2025 年 07 月 28 日
如果觉得我的文章对你有用,请随意赞赏