noycorp @noycorpDASCTF 2024 [最后一战|寒夜破晓,冬至终章]RE-re刻板印象

无壳32位文件,直接定位到主函数处发现一个简单的异或处理:

int __cdecl sub_401740(int a1)
{
  int result; // eax
  int i; // [esp+58h] [ebp-4h]

  for ( i = 0; i < 48; ++i )
  {
    *(_BYTE *)(i + a1) ^= byte_42F090[i % 26];
    result = i + 1;
  }
  return result;
}

并且也给出了比对的密文,因此直接先解密:

enc = [0x18, 0x09, 0x1C, 0x14, 0x37, 0x1D, 0x16, 0x2D, 0x3C, 0x05, 
       0x16, 0x3E, 0x02, 0x03, 0x10, 0x2C, 0x0E, 0x31, 0x39, 0x15, 
       0x04, 0x3A, 0x39, 0x03, 0x0D, 0x13, 0x2B, 0x3E, 0x06, 0x08, 
       0x37, 0x00, 0x17, 0x0B, 0x00, 0x1D, 0x1C, 0x00, 0x16, 0x06, 
       0x07, 0x17, 0x30, 0x03, 0x30, 0x06, 0x0A, 0x71]
s = "Laughter_is_poison_to_fear"
for i in range(48):
    enc[i] ^= ord(s[i % len(s)])
    print(chr(enc[i]), end="")
This_is_clearly_a_fake_flag_so_try_to_find_more.

假的flag,说明有其他处理。回到sub_401740处,看一看它的汇编代码:

此处修改了堆栈的返回值导致IDA在分析伪代码的时候把这里当作函数结尾了,后面的汇编代码没有被分析,直接将loc_401795整块nop掉,重新定义一下函数就能识别到下面的代码了:

_DWORD *__cdecl sub_401740(int a1)
{
  _DWORD *result; // eax
  int m; // [esp+4h] [ebp-54h]
  unsigned int k; // [esp+1Ch] [ebp-3Ch]
  unsigned int v4; // [esp+20h] [ebp-38h]
  unsigned int v5; // [esp+24h] [ebp-34h]
  unsigned int v6; // [esp+28h] [ebp-30h]
  int j; // [esp+2Ch] [ebp-2Ch]
  _DWORD v8[6]; // [esp+38h] [ebp-20h] BYREF
  int v9; // [esp+50h] [ebp-8h]
  int i; // [esp+54h] [ebp-4h]

  for ( i = 0; i < 48; ++i )
    *(_BYTE *)(i + a1) ^= byte_42F090[i % 26];
  v9 = a1;
  strcpy((char *)v8, "{you_find_it_!?}");
  result = v8;
  for ( j = 0; j < 12; j += 2 )
  {
    v6 = *(_DWORD *)(v9 + 4 * j);
    v5 = *(_DWORD *)(v9 + 4 * j + 4);
    v4 = 0;
    for ( k = 0; k < 0x20; ++k )
    {
      v6 += (v4 + v8[v4 & 3]) ^ (v5 + ((16 * v5) ^ (v5 >> 5)));
      v4 -= 1640531527;
      v5 += (v4 + v8[(v4 >> 11) & 3]) ^ (v6 + ((16 * v6) ^ (v6 >> 5)));
    }
    *(_DWORD *)(v9 + 4 * j) = v6;
    result = (_DWORD *)j;
    *(_DWORD *)(v9 + 4 * j + 4) = v5;
  }
  for ( m = 0; m < 48; ++m )
  {
    *(_BYTE *)(m + a1) ^= byte_42F060[m];
    result = (_DWORD *)(m + 1);
  }
  return result;
}

看起来是一个XTEA加密和异或处理,对应地编写解密脚本:

def bytes_to_uint32_list(byte_list): 
    """将字节列表转换为 uint32_t 列表"""
    while len(byte_list) % 4 != 0:
        byte_list.append(0x00)
    uint32_list = []
    for i in range(0, len(byte_list), 4):
        uint32_val = (byte_list[i+3] << 24) | (byte_list[i+2] << 16) | (byte_list[i+1] << 8) | byte_list[i]
        uint32_list.append(uint32_val)
    return uint32_list

def uint32_list_to_bytes(uint32_list):
    """将 uint32_t 列表转换回字节列表"""
    byte_list = []
    for num in uint32_list:
        byte_list.extend([num & 0xFF, (num >> 8) & 0xFF, (num >> 16) & 0xFF, (num >> 24) & 0xFF])
    return byte_list

def xtea_decrypt(v, key):
    delta = 0x61C88647
    for i in range(0,12,2):
        v0 = v[i]
        v1 = v[i+1]
        sum = 0 - delta * 32
        for j in range(32):
            v1 -= (sum + key[(sum >> 11) & 3]) ^ (v0 + ((16 * v0) ^ (v0 >> 5)))
            v1 = (v1 & 0xFFFFFFFF)
            sum +=delta
            v0 -= (sum + key[sum & 3]) ^ (v1 + ((16 * v1) ^ (v1 >> 5)))
            v0 =  (v0 & 0xFFFFFFFF)
        v[i] = v0
        v[i+1] = v1
    return v
    
enc = [0x18, 0x09, 0x1C, 0x14, 0x37, 0x1D, 0x16, 0x2D, 0x3C, 0x05, 
       0x16, 0x3E, 0x02, 0x03, 0x10, 0x2C, 0x0E, 0x31, 0x39, 0x15, 
       0x04, 0x3A, 0x39, 0x03, 0x0D, 0x13, 0x2B, 0x3E, 0x06, 0x08, 
       0x37, 0x00, 0x17, 0x0B, 0x00, 0x1D, 0x1C, 0x00, 0x16, 0x06, 
       0x07, 0x17, 0x30, 0x03, 0x30, 0x06, 0x0A, 0x71]
s = "Laughter_is_poison_to_fear"
xor_list = [0xDA, 0x30, 0x23, 0xE3, 0xDC, 0x39, 0x82, 0x60, 0xA5, 0x44, 
            0x68, 0xC2, 0x43, 0x7A, 0xBB, 0xE4, 0x50, 0xE1, 0x02, 0xC2, 
            0x81, 0x59, 0xEA, 0x1E, 0xC6, 0x8B, 0x71, 0x38, 0x27, 0x83, 
            0x94, 0xD8, 0xF4, 0x8D, 0x1A, 0x2A, 0x56, 0x8A, 0x4A, 0xD4, 
            0x54, 0xDC, 0x24, 0x3F, 0xB9, 0xED, 0x7B, 0x9A]

for i in range(48):
    enc[i] = enc[i] ^ xor_list[i]

key = [ord(i) for i in "{you_find_it_!?}"]
key = bytes_to_uint32_list(key)

n_enc = bytes_to_uint32_list(enc)
n_enc = xtea_decrypt(n_enc, key)
enc = uint32_list_to_bytes(n_enc)

for i in range(len(enc)):
    enc[i] ^= ord(s[i % 26])
    print(chr(enc[i]), end="")

这个流程包括了异或→XTEA→再异或,两次异或的列表都比较简单。看XTEA加密,原文是以8字节为一组送入,即两边各传入4字节,而密文列表每个是1字节,也就是要将密文列表中每4个为一组,每次送入两组进行加密。直接手动去制作新列表也可以,但为了方便就定义函数来进行,同时要注意采用小端序,另外加密用的key也要先用同样的方式处理再送入(原16字节转换为4字节一组)。在具体的加密过程中,由于脚本是在python环境中运行,每次v1、v0的值更改后为了保证仍在32位无符号整数的范围内还要和0xFFFFFFFF进行与操作,这样才符合原本溢出取低32位的情况。最后解密完成后为了方便输出也要再将得到的32位无符号整型列表转换为字节列表,运行的结果为:

fakeflag_plz_Try_more_hard_to_find_the_true_flag

仍然是个假flag,事已至此就不得不动调了,先到达设想中的加密函数sub_40126C处下断点然后开始,注意输入的测试字符串字符长度要等于48,直接用假flag就好。接下来单步进入去看流程,在汇编界面中可以发现跳转到sub_401740后还有一个奇怪的跳转:

先将这里转化为数据再分析指令可以发现跳转到了sub_41F00:

接着进去看sub_41F00,很明显的是SMC的结构:

int __cdecl sub_41F000(int a1)
{
  int v2[5]; // [esp+4h] [ebp-14h] BYREF

  memset(v2, 204, sizeof(v2));
  v2[4] = (int)malloc(0x4359u);
  sub_401190((void *)v2[4]);
  sub_40119A();
  sub_40126C(a1);
  VirtualProtect((LPVOID)v2[4], 0x4358u, 0x40u, (PDWORD)&v2[2]);
  v2[0] = v2[4] + 1;
  return ((int (__cdecl *)(int))(v2[4] + 1))(a1);
}

根据内容可知a1即用户输入有先经过sub_40126C也就是上文的加密函数进行加密,而新的加密是在VirtualProtect函数之后进行,怀疑就是return处,因此就在return处下断点,直接运行到这里查看汇编:

经过测试可以确定是在这个call指令处跳转,继续单步去看会发现一段很奇怪的指令:

暂且没看懂这是在干什么,就继续往下,但是之后几乎都是类似的指令段:

一路运行,可以发现这些指令段的结构非常相似,并且这些段落全部运行完毕之后就进入密文的比对了,因此这里面必定是存在加密的操作。多次来回查看这些段落,可以确定它们大部分的指令都是一致的:

可以看到存在明显差异的只有一处,也就是在popa和pusha之间的这一条指令,那么不妨猜测,只有这个指令是真正发挥作用的,其他的指令都只是为了控制像这样的不同指令段之间的跳转行为。为了验证我们的想法,可以先手动把明显存在差异的这个指令记录下来:

mov ebp,esp
sub esp,78
push esi
push edi
mov dword ptr ss:[ebp-14],C
mov eax,dword ptr ss:[ebp+8]
mov dword ptr ss:[ebp-10],eax
mov byte ptr ss:[ebp-48],7B
mov byte ptr ss:[ebp-47],57
mov byte ptr ss:[ebp-46],68
mov byte ptr ss:[ebp-45],61
mov byte ptr ss:[ebp-44],74
mov byte ptr ss:[ebp-43],5F
...............

这样一看果然就像是正常的指令段了,但是尝试了一下可以知道进行了相当,相当多这样的跳转,全部手动记录的话简直不当人,所以就考虑用脚本来抽出那些符合特征的指令进行分析。

这里re领域大神pRism师傅给出了一种方法,直接编写ida python脚本实现在ida中自动调试并且将特征指令写入文件。得到的文件可以直接通过ida打开编译成伪代码,相对的运行需要较长的时间:

from idc import *
import idaapi
import idautils
import ida_dbg

def check_address(target_ea):
    while True:
        ida_dbg.wait_for_next_event(ida_dbg.WFNE_SUSP,-1)
        current_ea = ida_dbg.get_reg_val("EIP")
        if current_ea == target_ea:
            print("finished")
            break
        else:
            handle_instruction(current_ea)
            ida_dbg.step_into()
            

def handle_instruction(ea):
    instr = idc.GetDisasm(ea)
    
    if "jmp     ebx" in instr:
        count = 0
        ea_temp = ea
        has_popa = False
        prev_instr = ""
        while count < 11:
            ea_temp = idc.prev_head(ea_temp)
            if ea_temp == idc.BADADDR:
                break
            prev_instr = idc.GetDisasm(ea_temp)
            count += 1
        if "popa" not in prev_instr:
            machine_code = ida_bytes.get_bytes(ea_temp, idc.get_item_size(ea_temp))
            with open("C:\\工作区\\box\\ctf\\dasctf\\re\\trace_output.bin", "ab") as f:
                f.write(machine_code)
            print(prev_instr)

check_address(0x0041F082)

得到的文件就有:

可以说是非常的强大,而我自己所采用的方法则是用到x32dbg的跟踪功能(因为不会用ida),根据先前的测试可以知道程序是在0x41F07F地址处的call指令去到加密指令块的,于是就在该地址下断点运行:

到达这里之后,在跟踪选项中选择跟踪进入,中断条件设置为EIP == 0x41F82也就是下面运行完所有加密指令后回来的地址,并且勾选记录跟踪,需要注意的是最大跟踪次数要设置的足够大不然有可能无法记录到完整的加密操作,这里就随意设置了个两百万:

之后等他跟踪完毕即可,左下角可以看到跟踪了有104601步,在跟踪界面中可以看到每一步的指令操作,之后右击复制选择导出表,就能将这些信息保存到csv表中:

稍微整理一下,只保留需要的指令列:

其中标注的就是符合特征的目标指令,接下来就编写一个脚本,将这些目标指令提取出来:

import pandas as pd

path = "c:\\Users\\Procyon_Nan\\Desktop\\data.csv"
df = pd.read_csv(path)

# 定义特征指令
feature_start = "popad "
feature_end = "pushad "

# 创建新表用于保存目标指令
out = pd.DataFrame()

# 遍历所有指令,如果上下存在特征指令就进行保存
for index in range(1, len(df["command"])):
    if (
        df["command"][index - 1] == feature_start
        and df["command"][index + 1] == feature_end
    ):
        out = out._append(df.loc[index], ignore_index=True)
out.to_csv("c:\\Users\\Procyon_Nan\\Desktop\\out.csv")

在定义特征指令时要注意csv表中的指令结尾是带有一个空格的,不然就无法匹配到了。之后得到的新表里就只剩下目标指令了:

剩下的就是阅读汇编了,当然这里可以直接将这些汇编指令丢给AI去分析,众所周知机器肯定是比我更懂机器语言的,分析结果也算基本正确:

不过也只是“基本”正确,通过它的分析我们可以确定是xxtea加密和一个异或,但其中的key、循环轮数和delta值还是需要人工去验证的。自然用AI多少是有点作弊,那么就单纯地去看汇编:

mov byte ptr ss:[ebp-48],7B     // {
mov byte ptr ss:[ebp-47],57     // W
mov byte ptr ss:[ebp-46],68     // h
mov byte ptr ss:[ebp-45],61     // a
mov byte ptr ss:[ebp-44],74     // t
mov byte ptr ss:[ebp-43],5F     // _
mov byte ptr ss:[ebp-42],69     // i
mov byte ptr ss:[ebp-41],73     // s
mov byte ptr ss:[ebp-40],5F     // _
mov byte ptr ss:[ebp-3F],74     // t
mov byte ptr ss:[ebp-3E],68     // h
mov byte ptr ss:[ebp-3D],69     // i
mov byte ptr ss:[ebp-3C],73     // s
mov byte ptr ss:[ebp-3B],5F     // _
mov byte ptr ss:[ebp-3A],3F     // ?
mov byte ptr ss:[ebp-39],7D     // }

开头这里定义了一个数组,明显像是key的东西。再往下,可以发现有个惹眼的指令:

add edx,11451419

臭啊,很臭啊,这么臭的值显然是人为取的。并且在表中搜索可以发现它重复出现了多次,每次出现都是隔了499条指令:

中间的这499条指令大概就是循环,仔细阅读可以发现有明显的位移操作:

add edx,11451419
mov dword ptr ss:[ebp-18],edx
mov eax,dword ptr ss:[ebp-18]
shr eax,2
and eax,3
mov dword ptr ss:[ebp-24],eax
mov dword ptr ss:[ebp-4],0
sub edx,1
cmp dword ptr ss:[ebp-4],edx
mov eax,dword ptr ss:[ebp-4]
mov ecx,dword ptr ss:[ebp-10]
mov edx,dword ptr ds:[ecx+eax*4+4]
mov dword ptr ss:[ebp-C],edx
mov eax,dword ptr ss:[ebp-8]
shr eax,5
mov ecx,dword ptr ss:[ebp-C]
shl ecx,2
xor eax,ecx
mov edx,dword ptr ss:[ebp-C]
shr edx,3
mov ecx,dword ptr ss:[ebp-8]
shl ecx,4
xor edx,ecx
add eax,edx

那么问题来了,什么样的加密需要用到一个长度为16字节的key,一个特定的值,并且加密中有右移5,左移2,右移3,左移4和异或操作呢?好难猜啊.jpg

到这里基本就可以确定是xxtea了,但就这么去写解密得到的结果还是错误的,还有猫腻。再去读汇编看看会发现在xxtea加密的结尾处还有一个长度为48的表:

mov byte ptr ss:[ebp-78],8F
mov byte ptr ss:[ebp-77],6C
mov byte ptr ss:[ebp-76],A6
mov byte ptr ss:[ebp-75],3F
mov byte ptr ss:[ebp-74],94
mov byte ptr ss:[ebp-73],3D
mov byte ptr ss:[ebp-72],F5
mov byte ptr ss:[ebp-71],D9
mov byte ptr ss:[ebp-70],36
mov byte ptr ss:[ebp-6F],66
mov byte ptr ss:[ebp-6E],51
mov byte ptr ss:[ebp-6D],D7
mov byte ptr ss:[ebp-6C],66
mov byte ptr ss:[ebp-6B],2F
mov byte ptr ss:[ebp-6A],B3
mov byte ptr ss:[ebp-69],8F
mov byte ptr ss:[ebp-68],C0
mov byte ptr ss:[ebp-67],61
mov byte ptr ss:[ebp-66],9E
mov byte ptr ss:[ebp-65],CE
mov byte ptr ss:[ebp-64],E9
mov byte ptr ss:[ebp-63],D7
mov byte ptr ss:[ebp-62],E1
mov byte ptr ss:[ebp-61],BF
mov byte ptr ss:[ebp-60],13
mov byte ptr ss:[ebp-5F],14
mov byte ptr ss:[ebp-5E],16
mov byte ptr ss:[ebp-5D],14
mov byte ptr ss:[ebp-5C],C2
mov byte ptr ss:[ebp-5B],E7
mov byte ptr ss:[ebp-5A],C3
mov byte ptr ss:[ebp-59],3A
mov byte ptr ss:[ebp-58],7F
mov byte ptr ss:[ebp-57],94
mov byte ptr ss:[ebp-56],A1
mov byte ptr ss:[ebp-55],E7
mov byte ptr ss:[ebp-54],24
mov byte ptr ss:[ebp-53],E
mov byte ptr ss:[ebp-52],A7
mov byte ptr ss:[ebp-51],5C
mov byte ptr ss:[ebp-50],D3
mov byte ptr ss:[ebp-4F],77
mov byte ptr ss:[ebp-4E],FE
mov byte ptr ss:[ebp-4D],4F
mov byte ptr ss:[ebp-4C],11
mov byte ptr ss:[ebp-4B],DC
mov byte ptr ss:[ebp-4A],69
mov byte ptr ss:[ebp-49],23

往下则是非常一致的异或操作,显然就是与xxtea加密后的结果再次异或。到此为止所有汇编指令的信息就都得到了,综合上先前得到假flag的流程,整个程序的加密流程就是: xor1 → xtea → xor2 → xxtea → xor3

整理已知的信息去写完整的解密脚本则有:

def bytes_to_uint32_list(byte_list): 
    """将字节列表转换为 uint32_t 列表"""
    while len(byte_list) % 4 != 0:
        byte_list.append(0x00)
    uint32_list = []
    for i in range(0, len(byte_list), 4):
        uint32_val = (byte_list[i+3] << 24) | (byte_list[i+2] << 16) | (byte_list[i+1] << 8) | byte_list[i]
        uint32_list.append(uint32_val)
    return uint32_list

def uint32_list_to_bytes(uint32_list):
    """将 uint32_t 列表转换回字节列表"""
    byte_list = []
    for num in uint32_list:
        byte_list.extend([num & 0xFF, (num >> 8) & 0xFF, (num >> 16) & 0xFF, (num >> 24) & 0xFF])
    return byte_list

def xxtea_decrypt(v, key):
    delta = 0x11451419
    n = len(v)
    rounds = 6 + 52 // n
    x = (rounds * delta) & 0xffffffff
    y = v[0]
    for i in range(rounds):
        e = (x >> 2) & 3
        for p in range(n - 1, 0, -1):
            z = v[p - 1]
            v[p] = (v[p] - ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((y ^ x) + (key[(p & 3) ^ e] ^ z)))) & 0xffffffff
            y = v[p]
        p -= 1
        z = v[n - 1]
        v[0] = (v[0] - ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((y ^ x) + (key[(p & 3) ^ e] ^ z)))) & 0xffffffff
        y = v[0]
        x = (x - delta) & 0xffffffff
    return v

def xtea_decrypt(v, key):
    delta = 0x61C88647
    for i in range(0,12,2):
        v0 = v[i]
        v1 = v[i+1]
        sum = 0 - delta * 32
        for j in range(32):
            v1 -= (sum + key[(sum >> 11) & 3]) ^ (v0 + ((16 * v0) ^ (v0 >> 5)))
            v1 = (v1 & 0xFFFFFFFF)
            sum += delta
            v0 -= (sum + key[sum & 3]) ^ (v1 + ((16 * v1) ^ (v1 >> 5)))
            v0 =  (v0 & 0xFFFFFFFF)
        v[i] = v0
        v[i+1] = v1
    return v

# step1
enc = [0x18, 0x09, 0x1C, 0x14, 0x37, 0x1D, 0x16, 0x2D, 0x3C, 0x05, 
       0x16, 0x3E, 0x02, 0x03, 0x10, 0x2C, 0x0E, 0x31, 0x39, 0x15, 
       0x04, 0x3A, 0x39, 0x03, 0x0D, 0x13, 0x2B, 0x3E, 0x06, 0x08, 
       0x37, 0x00, 0x17, 0x0B, 0x00, 0x1D, 0x1C, 0x00, 0x16, 0x06, 
       0x07, 0x17, 0x30, 0x03, 0x30, 0x06, 0x0A, 0x71]
final_xor_table = [0x8f, 0x6c, 0xa6, 0x3f, 0x94, 0x3d, 0xf5, 0xd9,
                   0x36, 0x66, 0x51, 0xd7, 0x66, 0x2f, 0xb3, 0x8f,
                   0xc0, 0x61, 0x9e, 0xce, 0xe9, 0xd7, 0xe1, 0xbf,
                   0x13, 0x14, 0x16, 0x14, 0xc2, 0xe7, 0xc3, 0x3a,
                   0x7f, 0x94, 0xa1, 0xe7, 0x24, 0x0e, 0xa7, 0x5c,
                   0xd3, 0x77, 0xfe, 0x4f, 0x11, 0xdc, 0x69, 0x23]
for i in range(48):
    enc[i] ^= final_xor_table[i]

# step2
enc = bytes_to_uint32_list(enc)
key = [ord(i) for i in "{What_is_this_?}"]
key = bytes_to_uint32_list(key)
enc = xxtea_decrypt(enc, key)
enc = uint32_list_to_bytes(enc)

# step3
xor_table = [0xDA, 0x30, 0x23, 0xE3, 0xDC, 0x39, 0x82, 0x60, 0xA5, 0x44, 
            0x68, 0xC2, 0x43, 0x7A, 0xBB, 0xE4, 0x50, 0xE1, 0x02, 0xC2, 
            0x81, 0x59, 0xEA, 0x1E, 0xC6, 0x8B, 0x71, 0x38, 0x27, 0x83, 
            0x94, 0xD8, 0xF4, 0x8D, 0x1A, 0x2A, 0x56, 0x8A, 0x4A, 0xD4, 
            0x54, 0xDC, 0x24, 0x3F, 0xB9, 0xED, 0x7B, 0x9A]
for i in range(48):
    enc[i] ^= xor_table[i]
    
# step4
enc = bytes_to_uint32_list(enc)
key = [ord(i) for i in "{you_find_it_!?}"]
key = bytes_to_uint32_list(key)
enc = xtea_decrypt(enc, key)
enc = uint32_list_to_bytes(enc)

# step5
first_str = [ord(i) for i in "Laughter_is_poison_to_fear"]
for i in range(48):
    enc[i] ^= first_str[i % 26]
    print(chr(enc[i] % 128), end="")

拼尽全力终于解出答案。

flag:DASCTF{You_come_to_me_better_than_all_the_good.}
0 reply, 2 reactionsAn article posted on 3/5/2025, 1:33:43 PM
Solar Network Post Web PreviewTo get full view of this post, open it on Solian