yangyunhui

功不唐捐,玉汝于成

2025金盾信安杯-逆向题目lower

2025金盾信安杯 lower

2025金盾信安杯 逆向题目lower

目录


题目分析及截图

这道题代码不是很长,并且需要你动态调试+静态分析才能解出这道题。

那么我们依旧先查壳

可以看到我们不需要进行脱壳,并且这是一个64位elf文件

我们使用ida打开该文件

  1. Main 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    int __fastcall main(int argc, const char **argv, const char **envp)
    {
    __int64 v3; // rax
    __int64 v4; // rcx
    unsigned __int64 v5; // rdx
    unsigned int v6; // ebp
    int v7; // eax
    bool v8; // zf
    __int64 (__fastcall *v9)(); // rax
    __int64 (__fastcall *v10)(); // rdx
    _BYTE v12[80]; // [rsp+0h] [rbp-178h] BYREF
    char s[264]; // [rsp+50h] [rbp-128h] BYREF
    unsigned __int64 v14; // [rsp+158h] [rbp-20h]

    v14 = __readfsqword(0x28u);
    if ( fgets(s, 256, stdin) )
    {
    v3 = 0LL;
    v4 = -9218LL;
    while ( 1 )
    {
    v5 = (unsigned __int8)s[v3];
    v6 = v3;
    if ( (unsigned __int8)v5 <= 0xDu && !_bittest64(&v4, v5) )
    break;
    if ( ++v3 == 256 )
    {
    v6 = 256;
    break;
    }
    }
    v7 = derive_vm_seed();
    v8 = (((unsigned __int8)(((v6 ^ v7 ^ 0xCAFEBABE) >> 11) ^ ((v6 ^ v7 ^ 0xCAFEBABE) >> 5)) ^ (unsigned __int8)(v6 ^ v7) ^ 0xBE) & 1) == 0;
    v9 = check_len_alt_impl;
    if ( v8 )
    v9 = check_len_impl;
    v10 = run_and_verify_alt_impl;
    if ( v8 )
    v10 = run_and_verify_impl;
    g_check_ops = v9;
    off_5020 = (__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD))v10;
    if ( ((unsigned int (__fastcall *)(_QWORD, __int64, __int64 (__fastcall *)(), __int64 (__fastcall *)()))v9)(
    v6,
    256LL,
    v10,
    run_and_verify_impl)
    && (off_5018(v12, v6), (unsigned int)off_5020(s, v6, v12)) )
    {
    puts("yes");
    }
    else
    {
    puts("no");
    }
    }
    return 0;
    }

我们慢慢来分析这个函数

1
2
3
4
5
6
7
8
9
10
11
12
while ( 1 )
{
v5 = (unsigned __int8)s[v3];
v6 = v3;
if ( (unsigned __int8)v5 <= 0xDu && !_bittest64(&v4, v5) )
break;
if ( ++v3 == 256 )
{
v6 = 256;
break;
}
}

这段代码的意思就是看我们输入字符的长度

但是当我们输入的时候是有报错的,说明输入可能是有某些限制的

1
2
3
4
_BOOL8 __fastcall check_len_impl(int a1)
{
return a1 == 79;
}

在check_len_impl中我们可以看到return a1 == 79从这个函数名我们也可以大概猜到这个函数是用来检查输入的长度是否等于79的,我们可以再试一次长度是79的输入

这次是通过的(我只是为了验证输入79个字符是正确的,真正比赛不用这样,时间第一)

1
2
3
4
5
6
if ( ((unsigned int (__fastcall *)(_QWORD, __int64, __int64 (__fastcall *)(), __int64 (__fastcall *)()))v9)(
v6,
256LL,
v10,
run_and_verify_impl)
&& (off_5018(v12, v6), (unsigned int)off_5020(s, v6, v12)) )

这个if判断是非常重要的,就是验证你的输入长度是否等于79,加密后是否等于密文

我们跟进run_and_verify_impl函数看看

但是我们进去之后没有什么有用的信息,我们继续跟进,还是没有什么有用的函数,继续跟进,发现run_vm_inner函数

使用动态调试得到加密后的数组

具体调试步骤

1
2
3
gdb ./lower
b run_vm_inner
x/80xb $rdx

它就是我们flag加密后的进行比对的值,接下来我们需要去明白它的加密逻辑

这个在程序中看不到,嗯…比较烦人

我们跟进函数run_ir

其实大概率就是进行异或操作了,但是我不知道v13[1]是什么值,动调一下看看呢

动调步骤:

1
2
3
gdb ./lower
b run_ir
r

在STACK中,我们可以看到一行

04:0020│-148 0x7fffffffd6e8 ◂— 0x4000003001aa2000

因为是小端序(你可能想问为什么?)

在0x4000003001aa2000中,我们将每两个字符拆开

我们可以和v7 ^= v13[1];上下进行对比,我们倒着看这些数字(因为是小端序)

  • 0x20:这是XOR的操作码
  • 那么0xAA代表的就是异或的这个值

这个涉及操作码(Opcode),可以去看看

可能有更好的办法,但是我不太懂,欢迎来找我讨论

我们的exp如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data = [
0xcc, 0xc6, 0xcb, 0xcd, 0xd1, 0x9d, 0xcf, 0x9e,
0x92, 0xcb, 0x9c, 0xc9, 0xcb, 0x87, 0xce, 0xcc,
0xcb, 0x98, 0x87, 0x9b, 0x9b, 0xcc, 0x9a, 0x87,
0x93, 0xc9, 0x98, 0x98, 0x87, 0x99, 0x9a, 0x9a,
0x9f, 0x9a, 0x9f, 0x9d, 0x9f, 0x9c, 0x9b, 0x98,
0xce, 0x87, 0x92, 0xcf, 0x9c, 0xcb, 0x9e, 0x9a,
0x92, 0xc8, 0x87, 0xce, 0xcc, 0xcb, 0x98, 0x87,
0x9b, 0x9b, 0xcc, 0x9a, 0x87, 0x92, 0xc9, 0x9b,
0x99, 0x87, 0x99, 0x9a, 0x9a, 0x9f, 0x9a, 0x9f,
0x9d, 0x9f, 0x9c, 0x9b, 0x98, 0xce, 0xd7
]

flag = "".join([chr(b ^ 0xAA) for b in data])
print(f"解密出的字符串: {flag}")

结果如下:flag{7e48a6ca-dfa2-11f0-9c22-30050575612d-8e6a408b-dfa2-11f0-8c13-30050575612d}

相关知识点

操作码(Opcode):

  1. 什么是操作码??

    操作码 是指令中规定“执行什么操作”的部分。它像是一个开关,决定了CPU接下来要走那条电路逻辑

    • **它的本质:**只是一个数字。
    • 他的意义: 由程序中的if else或者switch分支赋予
    • 操作数(Operand): 紧跟在操作码后面的数据(比如我们异或操作的v13[1])
  2. 怎么在程序中找到操作码?

    找到一个自定义VM的操作码的映射关系,通常遵循 取指 -> 分发 -> 逻辑 的寻找路径。

    取指

    • 程序每三个字节取一次指令
    • 每组字节的第一个字节是操作码

    分发

    在代码中,作者使用了大量if-else分支

    • if (v14==17)
    • else if (v4 == 1)
    • else if (v4 == 32)
    • else if (v14 == 48)

    这些数字就是操作码

  3. 确定操作码的功能

    • 32(10进制),20(16进制),在代码中v7 ^= v13[1],说明这个操作码的含义就是异或(XOR)