攻防世界 难度5 津门杯 GoodRE

zhugeshi 发布于 2025-04-09 100 次阅读


攻防世界 津门杯 GoodRE

1.点开,64位,无壳,无混淆

2.查看基本函数逻辑

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 *i64_v3_input; // r12
  int *v4; // r14
  __int64 v5; // rbx
  int v6_Input; // eax
  int *v7; // rbp
  char *v8; // r13
  int v10[18]; // [rsp+10h] [rbp-3F8h] BYREF
  char v11[72]; // [rsp+58h] [rbp-3B0h] BYREF
  char v12[72]; // [rsp+A0h] [rbp-368h] BYREF
  char v13[72]; // [rsp+E8h] [rbp-320h] BYREF
  int v14[72]; // [rsp+130h] [rbp-2D8h] BYREF
  char v15[288]; // [rsp+250h] [rbp-1B8h] BYREF
  __int64 v16_input[8]; // [rsp+370h] [rbp-98h] BYREF
  __int64 v17[11]; // [rsp+3B0h] [rbp-58h] BYREF

  v17[3] = __readfsqword(0x28u);
  memset(v16_input, 0, sizeof(v16_input));
  v17[0] = 0LL;
  v17[1] = 0LL;
  std::operator<<<std::char_traits<char>>(&std::cout, ">> ", a3);
  std::operator>><char,std::char_traits<char>>(&std::cin, v16_input);
  if ( (unsigned int)strlen((const char *)v16_input) == 64 )
  {
    i64_v3_input = v16_input;
    v4 = (int *)&unk_561CC045A020;
    v5 = 0LL;
    do
    {
      v6_Input = CheckInput(i64_v3_input, 8);
      v7 = v10;
      AnalyzeBlock((__int64)&v10[v5], v6_Input);
      AnalyzeBlock((__int64)&v14[v5], 17);
      v8 = v15;
      AnalyzeBlock((__int64)&v15[v5 * 4], *v4);
      ++i64_v3_input;
      ++v4;
      v5 += 9LL;
    }
    while ( i64_v3_input != v17 );
    sub_561CC0456B30(v10, v14);
    sub_561CC0456B30((int *)v11, v14);
    sub_561CC0456B30((int *)v12, v14);
    sub_561CC0456B30((int *)v13, v14);
    while ( !(unsigned int)sub_561CC0456ADC(v7, v8) )
    {
      v7 += 9;
      v8 += 36;
      if ( v7 == v14 )
      {
        __printf_chk(1LL, "flag{%s}\n", (const char *)v16_input);
        return 0LL;
      }
    }
    puts("error");
  }
  else
  {
    puts("error");
  }
  return 0LL;
}

首先会检查输入的长度

if ( (unsigned int)strlen((const char *)v16_input) == 64 )

接着我们继续跟踪,会发现一个函数对我们的输入进行处理,我们点进去分析一下

v6_Input = CheckInput(i64_v3_input, 8);

//*************************************
//**********下面是跟踪代码**************
//*************************************

__int64 __fastcall sub_558EE6283A3B(_BYTE *a1_Input, int a2)
{
  unsigned __int64 v2_LenOfStrplus1; // kr08_8
  int v3_LenOfStr; // r10d
  _BYTE *v4_Input; // r9
  __int64 result; // rax
  __int64 i; // rdx

  v2_LenOfStrplus1 = strlen(a0123456789abcd) + 1;
  if ( a2 <= 0 )
    return 0LL;
  v3_LenOfStr = v2_LenOfStrplus1 - 1;
  v4_Input = a1_Input;
  LODWORD(result) = 0;
  while ( 1 )
  {
    if ( v3_LenOfStr <= 0 )
      return 0LL;
    for ( i = 0LL; a0123456789abcd[i] != *v4_Input; ++i )
    {
      if ( i == (_DWORD)v2_LenOfStrplus1 - 2 )
        return 0LL;
    }
    if ( v3_LenOfStr <= (int)i )
      break;
    result = (unsigned int)(i + (v2_LenOfStrplus1 - 1) * result);
    if ( ++v4_Input == &a1_Input[a2 - 1 + 1] )
      return result;
  }
  return 0LL;
}

化简一下函数的逻辑,大概是这样的

__int64 convert_custom_base_string(_BYTE *input, int length) //length == 8
{
    const char *alphabet = "0123456789ABCDEF";  // 自定义进制字符集
    int base = strlen(alphabet);             // 基数
    __int64 result = 0;

    if (length <= 0)
        return 0;

    for (int i = 0; i < length; ++i) {
        char ch = input[i];
        int value = -1;

        // 找出字符在 alphabet 中的位置
        for (int j = 0; j < base; ++j) {
            if (alphabet[j] == ch) {
                value = j; //value为ch在表中的索引
                break;
            }
        }

        // 非法字符
        if (value == -1)
            return 0;

        // 防止当前字符的值大于等于进制
        if (value >= base)
            return 0;

        result = result * base + value;
    }

    return result;
}

动态调试一下就可以发现函数会检查我们的输入是否在字符串中,并且最后会返回原来的输入,所以我们重命名后就不用管他了。

接下来我们又看到一个函数,继续跟踪

v7 = v10;
AnalyzeBlock((__int64)&v10[v5], v6_Input);
AnalyzeBlock((__int64)&v14[v5], 17);
v8 = v15;
AnalyzeBlock((__int64)&v15[v5 * 4], *v4);

//*************************************
//**********下面是跟踪代码**************
//*************************************

__int64 __fastcall sub_558EE6283408(__int64 a1, int a2)
{
  __int64 v2; // rax
  unsigned int v3; // edx

  *(_BYTE *)(a1 + 8) = 0;
  *(_DWORD *)(a1 + 4) = a2;
  v2 = 4LL;
  while ( 1 )
  {
    v3 = v2 - 1;
    if ( *(_BYTE *)(a1 + v2 + 3) )
      break;
    if ( !--v2 )
      goto LABEL_4;
  }
  v3 = v2;
LABEL_4:
  *(_DWORD *)a1 = v3;
  return v3;
}

一下是化简的代码
这里假设了一个结构体,以便于理解代码。

这个是结构体
struct Block {
    int value0;      // +0
    int inputValue;  // +4
    char bytes[4];   // +8 ... 11(从偏移 +7 到 +4 向下检查)
};

__int64 analyze_block(__int64 ptr, int input) //ptr指向结构体
{
    // Set byte at offset +8 to 0
    *(_BYTE *)(ptr + 8) = 0;

    // Save input value to offset +4
    *(_DWORD *)(ptr + 4) = input;

    // Start checking from the highest offset
    int i = 4;
    while (i > 0) {
        if (*(_BYTE *)(ptr + i + 3))  // Check bytes at offsets +7, +6, +5, +4
            break;
        --i;
    }

    // Save result to offset +0
    *(_DWORD *)ptr = i;
    return i;
}

可以看到a2是我们的输入,这个函数把我们的输入存入结构体的inputValue字段.(然后从最高位开始检查,如果发现有字节不为0,则停止,并返回结果。暂时不知道有啥用)

那么继续往下跟就可以猜测是加密代码了,因为出现的都是同一个函数,而且内部逻辑很复杂。

这里我重命名了一些函数

unsigned __int64 __fastcall sub_561CC0456B30(int *a1, int *a2)
{
  int v2; // r14d
  int v0[12]; // [rsp+20h] [rbp-338h] BYREF
  int v1[12]; // [rsp+50h] [rbp-308h] BYREF
  int v6[12]; // [rsp+80h] [rbp-2D8h] BYREF
  int v7[12]; // [rsp+B0h] [rbp-2A8h] BYREF
  int v8[12]; // [rsp+E0h] [rbp-278h] BYREF
  _DWORD v9[12]; // [rsp+110h] [rbp-248h] BYREF
  int v10[12]; // [rsp+140h] [rbp-218h] BYREF
  int v11[12]; // [rsp+170h] [rbp-1E8h] BYREF
  int v12[12]; // [rsp+1A0h] [rbp-1B8h] BYREF
  int v13[12]; // [rsp+1D0h] [rbp-188h] BYREF
  int v14[12]; // [rsp+200h] [rbp-158h] BYREF
  int v15[12]; // [rsp+230h] [rbp-128h] BYREF
  int v16[12]; // [rsp+260h] [rbp-F8h] BYREF
  int v17[12]; // [rsp+290h] [rbp-C8h] BYREF
  int v18[12]; // [rsp+2C0h] [rbp-98h] BYREF
  int v19[10]; // [rsp+2F0h] [rbp-68h] BYREF
  unsigned __int64 v20; // [rsp+318h] [rbp-40h]

  v20 = __readfsqword(0x28u);
  memcpy_my(v0, a1);
  memcpy_my(v1, a1 + 9);
  AnalyzeBlock((__int64)v6, 0);
  AnalyzeBlock((__int64)v7, 0x830A5376);
  AnalyzeBlock((__int64)v8, 0x1D3D2ACF);
  Xor(v9, v8, v7);
  memcpy_my(v10, a2);                           // sub_557EA15F0A13: 类似于 memcpy,带长度限制,复制数据块。
  memcpy_my(v11, a2 + 9);
  memcpy_my(v12, a2 + 18);
  memcpy_my(v13, a2 + 27);
  v2 = 32;
  do
  {
    sub_561CC04562E9(v6, v6, v9);
    sub_561CC0456767((__int64)v14, v1, 4);
    sub_561CC04562E9(v14, v14, v10);
    sub_561CC04562E9(v15, v1, v6);
    ShiftLeft((__int64)v16, v1, 5);
    sub_561CC04562E9(v16, v16, v11);
    Xor(v14, v14, v15);
    Xor(v14, v14, v16);
    sub_561CC04562E9(v0, v0, v14);
    sub_561CC0456767((__int64)v17, v0, 4);
    sub_561CC04562E9(v17, v17, v12);
    sub_561CC04562E9(v18, v0, v6);
    ShiftLeft((__int64)v19, v0, 5);
    sub_561CC04562E9(v19, v19, v13);
    Xor(v17, v17, v18);
    Xor(v17, v17, v19);
    sub_561CC04562E9(v1, v1, v17);
    --v2;
  }
  while ( v2 );
  memcpy_my(a1, v0);
  memcpy_my(a1 + 9, v1);
  return __readfsqword(0x28u) ^ v20;
}

这时候我们可以借助ai帮助分析一下每个函数的作用,然后综合一下。因为出现了好多重复的函数,所以猜测是把一些步骤用函数封装了,好让我们看不出来。

stateL = a1[0..8];
stateR = a1[9..17];

key0 = a2[0..8];
key1 = a2[9..17];
key2 = a2[18..26];
key3 = a2[27..35];

for (int i = 0; i < 32; i++) {
    mix(v6, v6, const_block); // v6 更新

    temp1 = RightCompress(stateR, 4);
    temp1 = temp1 + key0;

    temp2 = Add(stateR, v6);
    temp3 = LeftCompress(stateR, 5);
    temp3 = temp3 + key1;

    mixed1 = XOR(temp1, temp2);
    mixed1 = XOR(mixed1, temp3);

    stateL = Add(stateL, mixed1);

    temp4 = RightCompress(stateL, 4);
    temp4 = temp4 + key2;

    temp5 = Add(stateL, v6);
    temp6 = LeftCompress(stateL, 5);
    temp6 = temp6 + key3;

    mixed2 = XOR(temp4, temp5);
    mixed2 = XOR(mixed2, temp6);

    stateR = Add(stateR, mixed2);
}

// 结果回写
a1[0..8] = stateL;
a1[9..17] = stateR;

假如你是密码领域大神,可能你能直接看出来这个是tea加密算法,然后你就可以直接用这个算法解密了。(但是我没这么厉害,所以就寄了,看了WP才明白hh)

但是说实话还是挺明显的。
比如32次循环,两个int,4个key。这些都是关键。

3.解密

回主函数看一下这个

__printf_chk(1LL, "flag{%s}\n", (const char *)v16_input);

一路跟踪回去发现__v4 = (int *)&unk_561CC045A020;__

点进去看看

.data:0000561CC045A020 unk_561CC045A020 db  3Bh ; ;            ; DATA XREF: main+EF↑o
.data:0000561CC045A021                 db  1Ah
.data:0000561CC045A022                 db 0AEh
.data:0000561CC045A023                 db  79h ; y
.data:0000561CC045A024                 db 0D3h
.data:0000561CC045A025                 db  80h ; €
.data:0000561CC045A026                 db  60h ; `
.data:0000561CC045A027                 db  59h ; Y
.data:0000561CC045A028                 db  80h ; €
.data:0000561CC045A029                 db  3Eh ; >
.data:0000561CC045A02A                 db 0E0h
.data:0000561CC045A02B                 db  80h ; €
.data:0000561CC045A02C                 db  73h ; s
.data:0000561CC045A02D                 db  8Dh
.data:0000561CC045A02E                 db  6Ch ; l
.data:0000561CC045A02F                 db  84h
.data:0000561CC045A030                 db 0F7h
.data:0000561CC045A031                 db  1Ch
.data:0000561CC045A032                 db 0A0h
.data:0000561CC045A033                 db  21h ; !
.data:0000561CC045A034                 db  32h ; 2
.data:0000561CC045A035                 db 0CAh
.data:0000561CC045A036                 db 0CAh
.data:0000561CC045A037                 db 0C7h
.data:0000561CC045A038                 db  14h
.data:0000561CC045A039                 db 0ACh
.data:0000561CC045A03A                 db 0F9h
.data:0000561CC045A03B                 db  45h ; E
.data:0000561CC045A03C                 db  2Fh ; /
.data:0000561CC045A03D                 db 0F2h
.data:0000561CC045A03E                 db 0F5h
.data:0000561CC045A03F                 db 0C5h

一眼顶真的加密后的数据

现在还缺少密钥和delta,所以还不能解密。

回到加密代码我们动调分析

AnalyzeBlock((__int64)v7, 0x830A5376);
AnalyzeBlock((__int64)v8, 0x1D3D2ACF);
Xor(v9, v8, v7);

推测这里代码为

v7 = 0x830A5376
v8 = 0x1D3D2ACF
v9 = v8 ^ v7 //v9 == 0x9E3779B9

显然这是经典的delta的值

在继续动调得到4个key都是0x11,就能写解密代码了,反正看了wp,就白嫖了别的大佬的代码。

https://blog.csdn.net/yjh_fnu_ltn/article/details/139221612

#include <debugapi.h>
#include <stdio.h>
int main()
{
    unsigned int key[4] = {0x11, 0x11, 0x11, 0x11};                                                                             // 密钥
    unsigned int value[8] = {0x79AE1A3B, 0x596080D3, 0x80E03E80, 0x846C8D73, 0x21A01CF7, 0x0C7CACA32, 0x45F9AC14, 0x0C5F5F22F}; // 密文

    int dalte = 0x9e3779b9;
    int i = 0, j = 0, h = 0;
    int wheel = 32; // 轮数
    unsigned int sum = 0;

    // 逆算法
    for (i = 0; i < 8; i++, i++)
    {
        sum = (dalte * (wheel));
        // 每轮加密
        // for (j = 0; j < 4; ++j)
        {
            for (h = 0; h < wheel; ++h)
            {
                value[i + 1] -= (key[3] + (value[i] >> 5)) ^ (sum + value[i]) ^ (key[2] + 16 * value[i]);
                value[i] -= (key[1] + (value[i + 1] >> 5)) ^ (sum + value[i + 1]) ^ (key[0] + 16 * value[i + 1]);
                sum -= dalte;
            }
        }
    }
    for (i = 0; i < 8; i++)
    {
        printf("%X", value[i]);
    }
    return 0;
}
//7DEA3F6D3B3D6C0C620864ADD2FA2AE1A61F2736F0060DA0B97E8356D017CE59
超级大菜鸡!!!
最后更新于 2025-04-09