Featured image of post 攻防世界WP(持续更新)

攻防世界WP(持续更新)

目前只更新了部分re,博主还在做~

攻防世界WP

RE

Reversing-x64Elf-100

下载附件,得到一个以re结尾的文件,文件名太长,改为1.re。

印象中并没有以re为后缀的文件(很冷门),而且题目提示x64elf,且对于linux来说后缀没意义,因此首先file一下查看文件格式:

$ file 1.re
1.re: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=0f464824cc8ee321ef9a80a799c70b1b6aec8168, stripped

文件格式为elf,运行一下大概看看逻辑:

$ ./1.re
Enter the password: asd
Incorrect password!

程序让我们输入一个密码,因此使用ida查看反汇编后的程序,破解密码。

打开ida,查看main函数,按F5进入伪代码窗口:

int64 __fastcall main(int a1, char **a2, char **a3)
{
  char s[264]; // [rsp+0h] [rbp-110h] BYREF
  unsigned __int64 v5; // [rsp+108h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  printf("Enter the password: ");
  if ( !fgets(s, 255, stdin) )   //输入的密码存入字符串s
    return 0LL;
  if ( (unsigned int)sub_4006FD((__int64)s) )  //s被sub_4006FD调用
  {
    puts("Incorrect password!");
    return 1LL;
  }
  else
  {
    puts("Nice!"); //看样子需要我们让程序输出Nice,因此需要让if里的条件为假
    return 0LL;
  }
}

(分析在函数注释中)再查看sub_4006FD函数:

__int64 __fastcall sub_4006FD(__int64 a1) //a1就是字符数组s的首地址
{
  int i; // [rsp+14h] [rbp-24h]
  _QWORD v3[4]; // [rsp+18h] [rbp-20h]

  v3[0] = "Dufhbmf";
  v3[1] = "pG`imos";
  v3[2] = "ewUglpt";  
  for ( i = 0; i <= 11; ++i )  //12次循环,显然密码是12位的
  {
    if ( *(char *)(v3[i % 3] + 2 * (i / 3)) - *(char *)(i + a1) != 1 )
      return 1LL;
  }
  return 0LL;  //我们的目的是让函数返回0(main函数的分析)
}

要想让sub_4006FD函数返回0,那就要在12次循环中,保证每次*(char *)(v3[i % 3] + 2 * (i / 3)) - *(char *)(i + a1) != 1都为假,即: *(char *)(v3[i % 3] + 2 * (i / 3)) - *(char *)(i + a1) == 1。

也就是: *(char *)(i + a1) == *(char *)(v3[i % 3] + 2 * (i / 3)) - 1,即:s[i] = v[i%3] [2 * (i / 3)] - 1。 因此密码s就为: 当i从0到11时,v[i%3] [2 * (i / 3)] - 1的结果拼接起来。 使用python求解:

v3 = ["Dufhbmf", "pG`imos", "ewUglpt"]
flag = ""

for i in range(12) :
    #v3[row][col]
    row = i % 3
    col = 2 * (i // 3)

    flag += chr(ord(v3[row][col]) - 1)

print(flag)

得到flag:Code_Talkers

666

下载附件,把文件重命名为666,file查看类型:

$ file 666
666: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=eaed290e5801a19d53e8597ac1a0499ec8bef798, not stripped

文件为64位elf

拖入ida,F5查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[240]; // [rsp+0h] [rbp-1E0h] BYREF
  char v5[240]; // [rsp+F0h] [rbp-F0h] BYREF

  memset(s, 0, 0x1EuLL);
  printf("Please Input Key: ");
  __isoc99_scanf("%s", v5); //获取用户输入,存入v5
  encode(v5, s); // v5和s作为参数被encode调用
  if ( strlen(v5) == key ) // if条件需为真,即v5长度为key
  {
    if ( !strcmp(s, enflag) ) // if的条件需为真,即strcmp(s, enflag)为0,即s与enflag相同
      puts("You are Right"); //我们最终应该让程序执行到这里
    else
      puts("flag{This_1s_f4cker_flag}"); //假的flag
  }
  return 0;
}

目前比较不清楚的就是encode函数,key和enflag,分别双击key和enflag,得到:

.data:0000000000004080 key             dd 12h                  ; DATA XREF: encode+2D↑r

.data:0000000000004060 enflag          db 'izwhroz""w"v.K".Ni',0

因此key的值就为0x12,即key = 18;enflag = “izwhroz"“w"v.K”.Ni”,上面代码最后面的0其实就是字符串的标准结尾\0。

查看encode函数:

int __fastcall encode(const char *a1, __int64 a2) //a1为v5,a2为s首地址
{
  _BYTE v3[104]; // [rsp+10h] [rbp-70h]
  int v4; // [rsp+78h] [rbp-8h]
  int i; // [rsp+7Ch] [rbp-4h]

  i = 0;
  v4 = 0;
  if ( strlen(a1) != key )
    return puts("Your Length is Wrong"); //因此我们的输入长度应该为key,即18位
  for ( i = 0; i < key; i += 3 )   // i = 0,3,6,9,12,15
  {
    v3[i + 64] = key ^ (a1[i] + 6);
    v3[i + 33] = (a1[i + 1] - 6) ^ key;
    v3[i + 2] = a1[i + 2] ^ 6 ^ key;
    *(_BYTE *)(a2 + i) = v3[i + 64]; //a2[i] = key ^ (a1[i] + 6)
    *(_BYTE *)(a2 + i + 1LL) = v3[i + 33]; //a2[i+1] = (a1[i + 1] - 6) ^ key
    *(_BYTE *)(a2 + i + 2LL) = v3[i + 2]; //a2[i+2] = a1[i + 2] ^ 6 ^ key
  }
  return a2;
}

分析程序,我们得到: 当i分别取0,3,6,9,12,15时,应满足s[i] = key ^ (v5[i] + 6),s[i+1] = (v5[i + 1] - 6) ^ key,s[i+2] = v5[i + 2] ^ 6 ^ key。其中^表示异或运算,a ^ b = c => a = b ^ c,因此原式可变形为:

v5[i] = s[i] ^ key - 6, v5[i+1] = s[i+1] ^ key + 6 , v5[i+2] = s[i+2] ^ 6 ^ key。

而s与enflag相等,即s = “izwhroz"“w"v.K”.Ni” 使用python解密:

key = 18
enflag = 'izwhroz""w"v.K".Ni'
flag = ""

for i in range(0, 18, 3) :
    s0 = ord(enflag[i])
    s1 = ord(enflag[i+1])
    s2 = ord(enflag[i+2])

    flag += chr((s0 ^ key) - 6)
    flag += chr((s1 ^ key) + 6)
    flag += chr(s2 ^ 6 ^ key)

print(flag)

得到flag:unctf{b66_6b6_66b}

easyRE1

下载附件,得到两个文件easy-32和easy-64,file查看文件类型:

$ file easy*
easy-32: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9f675da777828773560c2d784a8bbdb10dfc68a6, not stripped
easy-64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=4fca13ce5e9b16fdd570ba44e1ca8b56a5211b3e, not stripped

分别是32位和64位的elf文件,先把32位程序拖入IDA查看:

在main函数直接得到flag:flag{db2f62a36a018bce28e46d976e3f9864} 呃呃呃,就这么简单…… 另一个64位的程序和这个32位的基本是一样的。

lucknum

file查看下载附件的类型:

$ file lucknum
lucknum: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=817b2b623ccc87cce3db8d07c9d68328f5cec26b, for GNU/Linux 3.2.0, not stripped

64位elf,拖入ida:

呃呃呃,直接得到flag:flag{c0ngr@tul@ti0n_f0r_luck_numb3r}

reverse_re3

file查看附件类型:

$ file main2
main2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=fbc35a1ef3ee0290f5fbef27cbce4537cc960ebc, stripped

64位elf程序,拖入ida查看:

main函数:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v4; // [rsp+4h] [rbp-Ch]

  sub_11B4(a1, a2, a3);  //初始化函数
  do
    v4 = sub_940();  //关键逻辑函数
  while ( v4 != 1 && v4 != -1 );
  return 0LL;
}

再查看sub_940():

__int64 sub_940()
{
  int v0; // eax
  int v2; // [rsp+8h] [rbp-218h]
  int v3; // [rsp+Ch] [rbp-214h]
  _BYTE v4[520]; // [rsp+10h] [rbp-210h] BYREF
  unsigned __int64 v5; // [rsp+218h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  v3 = 0;
  memset(v4, 0, 0x200uLL);
  _isoc99_scanf(&unk_1278, v4, v4);
  while ( 1 )
  {
    do
    {
      v2 = 0;
      sub_86C();
      v0 = (char)v4[v3];
      if ( v0 == 100 )
      {
        v2 = sub_E23();
      }
      else if ( v0 > 100 )
      {
        if ( v0 == 115 )
        {
          v2 = sub_C5A();
        }
        else if ( v0 == 119 )
        {
          v2 = sub_A92();
        }
      }
      else
      {
        if ( v0 == 27 )
          return 0xFFFFFFFFLL;
        if ( v0 == 97 )
          v2 = sub_FEC();
      }
      ++v3;
    }
    while ( v2 != 1 );
    if ( dword_202AB0 == 2 )
      break;
    ++dword_202AB0;
  }
  puts("success! the flag is flag{md5(your input)}");
  return 1LL;
}

我们应该让程序执行到puts(“success! the flag is flag{md5(your input)}")这一步,整体分析一下关键的逻辑部分,也就是从获取输出开始:

_isoc99_scanf(&unk_1278, v4, v4); //获取用户输入存储在v4,双击&unk_1278可以看到有一个%s,本质上就是一个占位符
while ( 1 )
  {
    do
    {
      v2 = 0;
      sub_86C();
      v0 = (char)v4[v3];
      if ( v0 == 100 )
      {
        v2 = sub_E23();
      }
      else if ( v0 > 100 )
      {
        if ( v0 == 115 )
        {
          v2 = sub_C5A();
        }
        else if ( v0 == 119 )
        {
          v2 = sub_A92();
        }
      }
      else
      {
        if ( v0 == 27 )
          return 0xFFFFFFFFLL;
        if ( v0 == 97 )
          v2 = sub_FEC();
      }
      ++v3;
    }
    while ( v2 != 1 );
    if ( dword_202AB0 == 2 )
      break;
    ++dword_202AB0;
  }
  puts("success! the flag is flag{md5(your input)}");
  return 1LL;

也就是:

_isoc99_scanf(&unk_1278, v4, v4);
while(1)
{
    do
    {

    }while();

    if(dword_202AB0 == 2)
        break;
    ++dword_202AB0;
}

puts(……);

我们要想得到flag,也就需要执行puts,那就就需要通过if里的break跳出循环,也就是需要dword_202AB0 == 2,也就是需要有三次v2 == 1,因为这样才能跳出dowhile触发++dword_202AB0;每一次执行do-while时v2都会赋值为0,我们需要让v2在do-while的内部逻辑里变为1,分析do-while:

do
    {
      v2 = 0;
      sub_86C();
      v0 = (char)v4[v3];  //在前面v3被初始化为0
      if ( v0 == 100 )
      {
        v2 = sub_E23();
      }
      else if ( v0 > 100 )
      {
        if ( v0 == 115 )
        {
          v2 = sub_C5A();
        }
        else if ( v0 == 119 )
        {
          v2 = sub_A92();
        }
      }
      else
      {
        if ( v0 == 27 )
          return 0xFFFFFFFFLL;
        if ( v0 == 97 )
          v2 = sub_FEC();
      }
      ++v3;
    }
    while ( v2 != 1 );

函数对v0进行if判断,查看一下各个数字对应的字符:

Python>chr(100)
'd'
Python>chr(115)
's'
Python>chr(119)
'w'
Python>chr(27)
'\x1b'
Python>chr(97)
'a'

那么有经验的话就能发现,这个题就是一个迷宫题,用户输入wasd进行移动,用户的输入存储在v4,v2代表通关标志,为1说明通关,dword_202AB0代表关卡数,在这里有三关。sub_86C()是用来定位玩家当前在地图上的坐标的,我们来分析一下:

unsigned __int64 sub_86C()
{
  int i; // [rsp+0h] [rbp-10h]
  int j; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  for ( i = 0; i <= 14; ++i )
  {
    for ( j = 0; j <= 14; ++j )
    {
      if ( dword_202020[225 * dword_202AB0 + 15 * i + j] == 3 )
      {
        dword_202AB4 = i;
        dword_202AB8 = j;
        break;
      }
    }
  }
  return __readfsqword(0x28u) ^ v3;
}

从两个for循环可以看出,这是一张 15 x 15 的二维地图,

if ( dword_202020[225 * dword_202AB0 + 15 * i + j] == 3 )

让我们拆解这个数组访问公式 Base[Offset]

  • dword_202020: 地图数据的基地址。这是需要去提取数据的地方。
  • dword_202AB0: 当前关卡数 。我们在上面分析过,它是 0, 1, 2。
  • 225: 意思是每一关的地图占用 225 个整数的大小。
  • 15 * i + j: 这是标准的二维转一维公式:行号 * 宽度 + 列号

程序根据当前是第几关(dword_202AB0),跳过相应大小的内存块,去读取当前的地图数据。

... == 3 //寻找的目标
  • 程序在遍历地图,寻找值等于 3 的格子。
  • 含义:在地图数据中,数字 3 代表玩家 (Player)
dword_202AB4 = i; // 保存行号 (Y坐标)
dword_202AB8 = j; // 保存列号 (X坐标)
break;

找到玩家(3)后,程序将当前的 ij 分别存入两个全局变量。

dword_202AB4: 全局变量 Player_Y

dword_202AB8: 全局变量 Player_X

那么另外当v0==119,115,97,100所执行的函数估计就是执行上下左右的移动了。

我们查看一下sub_E23(),也就是对应着字符d所执行的函数:

__int64 sub_E23()
{
  if ( dword_202AB8 != 14 )
  {
    if ( dword_202020[225 * dword_202AB0 + 1 + 15 * dword_202AB4 + dword_202AB8] == 1 )
    {
      dword_202020[225 * dword_202AB0 + 1 + 15 * dword_202AB4 + dword_202AB8] = 3;
      dword_202020[225 * dword_202AB0 + 15 * dword_202AB4 + dword_202AB8] = 1;
    }
    else if ( dword_202020[225 * dword_202AB0 + 1 + 15 * dword_202AB4 + dword_202AB8] == 4 )
    {
      return 1LL;
    }
  }
  return 0LL;
}

根据我们之前的分析把它转为伪代码:

// 伪代码:尝试向右移动
int Move_Right() {
    // 1. 边界检查:如果在最右边 (x == 14),就不能再往右走了
    if ( Player_X != 14 ) {

        // 计算目标格子的值:当前位置的右边一格
        int target_cell = Map[Level][Player_Y][Player_X + 1];

        // 2. 如果右边是 "路" (数字 1)
        if ( target_cell == 1 ) {
            // 移动玩家:
            // 把目标格子变成 3 (玩家)
            Map[Level][Player_Y][Player_X + 1] = 3; 

            // 把原来脚下的格子还原成 1 (路)
            Map[Level][Player_Y][Player_X] = 1;
        }

        // 3. 如果右边是 "终点" (数字 4)
        else if ( target_cell == 4 ) {
            return 1; // 返回 1,代表通关!
        }

        // 4. 如果是其他数字 (比如 0),那就是墙,什么都不做
    }
    return 0; // 没通关,继续游戏
}

估计其它三个函数也都是相同逻辑。

所以解题思路就是:

先从内存 dword_202020 处导出数据(共 3 块,每块 225 个 int),然后去走迷宫。

双击dword_202020,得到:、

data:0000000000202020 ; _DWORD dword_202020[675]
.data:0000000000202020 dword_202020    dd 5 dup(1), 0Ah dup(0), 5 dup(1), 0, 3, 2 dup(1), 6 dup(0)
.data:0000000000202020                                         ; DATA XREF: sub_86C+82o
.data:0000000000202020                                         ; sub_A92+76o ...
.data:0000000000202098                 dd 5 dup(1), 3 dup(0), 1, 6 dup(0), 5 dup(1), 3 dup(0)
.data:00000000002020F4                 dd 1, 6 dup(0), 5 dup(1), 3 dup(0), 5 dup(1), 2 dup(0)
.data:000000000020214C                 dd 5 dup(1), 7 dup(0), 1, 2 dup(0), 5 dup(1), 7 dup(0)
.data:00000000002021B8                 dd 1, 2 dup(0), 5 dup(1), 7 dup(0), 2 dup(1), 0, 5 dup(1)
.data:0000000000202214                 dd 8 dup(0), 1, 0, 5 dup(1), 8 dup(0), 4, 0, 4Dh dup(1)
.data:00000000002023AC                 dd 0Dh dup(0), 2 dup(1), 0, 3, 5 dup(1), 6 dup(0), 2 dup(1)
.data:0000000000202424                 dd 0, 2 dup(1), 3 dup(0), 1, 6 dup(0), 2 dup(1), 6 dup(0)
.data:0000000000202478                 dd 1, 6 dup(0), 2 dup(1), 0, 2 dup(1), 3 dup(0), 5 dup(1)
.data:00000000002024C8                 dd 2 dup(0), 2 dup(1), 0, 2 dup(1), 7 dup(0), 1, 2 dup(0)
.data:000000000020250C                 dd 2 dup(1), 0, 2 dup(1), 7 dup(0), 1, 2 dup(0), 2 dup(1)
.data:0000000000202550                 dd 0, 2 dup(1), 5 dup(0), 4 dup(1), 0, 2 dup(1), 0, 2 dup(1)
.data:0000000000202598                 dd 5 dup(0), 1, 2 dup(0), 1, 0, 2 dup(1), 0, 2 dup(1)
.data:00000000002025D4                 dd 5 dup(0), 1, 4 dup(0), 2 dup(1), 0, 6 dup(1), 0, 1
.data:0000000000202628                 dd 0, 2 dup(1), 0, 2 dup(1), 0, 0Bh dup(1), 0, 2 dup(1)
.data:000000000020267C                 dd 0Bh dup(0), 4, 0, 1Eh dup(1), 10h dup(0), 3, 2 dup(1)
.data:0000000000202774                 dd 0Eh dup(0), 1, 0, 3 dup(1), 0Ah dup(0), 3 dup(1), 0
.data:00000000002027F8                 dd 1, 0Bh dup(0), 1, 2 dup(0), 1, 8 dup(0), 2 dup(1), 0
.data:0000000000202864                 dd 1, 2 dup(0), 1, 9 dup(0), 3 dup(1), 2 dup(0), 1, 0Eh dup(0)
.data:00000000002028E8                 dd 1, 0Eh dup(0), 4 dup(1), 0Eh dup(0), 1, 0Eh dup(0)
.data:00000000002029A8                 dd 1, 0Eh dup(0), 1, 0Eh dup(0), 4 dup(1), 0Eh dup(0)
.data:0000000000202A68                 dd 1, 0Eh dup(0), 4, 0
.data:0000000000202A68 _data           ends
.data:0000000000202A68

从这个形式来看,我们之间的分析基本就是对的,编写脚本:

import hashlib
from collections import deque

# 1. ida中查看的原始数据 (Raw Data)
raw_data = """
.data:0000000000202020                 dd 5 dup(1), 0Ah dup(0), 5 dup(1), 0, 3, 2 dup(1), 6 dup(0)
.data:0000000000202098                 dd 5 dup(1), 3 dup(0), 1, 6 dup(0), 5 dup(1), 3 dup(0)
.data:00000000002020F4                 dd 1, 6 dup(0), 5 dup(1), 3 dup(0), 5 dup(1), 2 dup(0)
.data:000000000020214C                 dd 5 dup(1), 7 dup(0), 1, 2 dup(0), 5 dup(1), 7 dup(0)
.data:00000000002021B8                 dd 1, 2 dup(0), 5 dup(1), 7 dup(0), 2 dup(1), 0, 5 dup(1)
.data:0000000000202214                 dd 8 dup(0), 1, 0, 5 dup(1), 8 dup(0), 4, 0, 4Dh dup(1)
.data:00000000002023AC                 dd 0Dh dup(0), 2 dup(1), 0, 3, 5 dup(1), 6 dup(0), 2 dup(1)
.data:0000000000202424                 dd 0, 2 dup(1), 3 dup(0), 1, 6 dup(0), 2 dup(1), 6 dup(0)
.data:0000000000202478                 dd 1, 6 dup(0), 2 dup(1), 0, 2 dup(1), 3 dup(0), 5 dup(1)
.data:00000000002024C8                 dd 2 dup(0), 2 dup(1), 0, 2 dup(1), 7 dup(0), 1, 2 dup(0)
.data:000000000020250C                 dd 2 dup(1), 0, 2 dup(1), 7 dup(0), 1, 2 dup(0), 2 dup(1)
.data:0000000000202550                 dd 0, 2 dup(1), 5 dup(0), 4 dup(1), 0, 2 dup(1), 0, 2 dup(1)
.data:0000000000202598                 dd 5 dup(0), 1, 2 dup(0), 1, 0, 2 dup(1), 0, 2 dup(1)
.data:00000000002025D4                 dd 5 dup(0), 1, 4 dup(0), 2 dup(1), 0, 6 dup(1), 0, 1
.data:0000000000202628                 dd 0, 2 dup(1), 0, 2 dup(1), 0, 0Bh dup(1), 0, 2 dup(1)
.data:000000000020267C                 dd 0Bh dup(0), 4, 0, 1Eh dup(1), 10h dup(0), 3, 2 dup(1)
.data:0000000000202774                 dd 0Eh dup(0), 1, 0, 3 dup(1), 0Ah dup(0), 3 dup(1), 0
.data:00000000002027F8                 dd 1, 0Bh dup(0), 1, 2 dup(0), 1, 8 dup(0), 2 dup(1), 0
.data:0000000000202864                 dd 1, 2 dup(0), 1, 9 dup(0), 3 dup(1), 2 dup(0), 1, 0Eh dup(0)
.data:00000000002028E8                 dd 1, 0Eh dup(0), 4 dup(1), 0Eh dup(0), 1, 0Eh dup(0)
.data:00000000002029A8                 dd 1, 0Eh dup(0), 1, 0Eh dup(0), 4 dup(1), 0Eh dup(0)
.data:0000000000202A68                 dd 1, 0Eh dup(0), 4, 0
"""

def parse_ida_data(raw):
    data = []
    lines = raw.strip().split('\n')
    for line in lines:
        # 去掉前面的地址部分 (.data:xxxx dd )
        if 'dd' in line:
            content = line.split('dd')[1].strip()
            # 去掉可能的注释
            content = content.split(';')[0].strip()
            parts = content.split(',')
            for p in parts:
                p = p.strip()
                if not p: continue

                count = 1
                val = 0

                # 处理 "5 dup(1)" 这种情况
                if 'dup' in p:
                    d_parts = p.split('dup')
                    # 处理次数 (可能是 hex, 如 0Ah)
                    count_str = d_parts[0].strip()
                    if count_str.endswith('h'):
                        count = int(count_str[:-1], 16)
                    else:
                        count = int(count_str)

                    # 处理值
                    val_str = d_parts[1].replace('(', '').replace(')', '').strip()
                    if val_str.endswith('h'):
                        val = int(val_str[:-1], 16)
                    else:
                        val = int(val_str)
                else:
                    # 处理单个值
                    if p.endswith('h'):
                        val = int(p[:-1], 16)
                    else:
                        val = int(p)

                data.extend([val] * count)
    return data

# 2. 解析数据
full_map = parse_ida_data(raw_data)
print(f"[+] Total integers parsed: {len(full_map)}")

# 3. BFS 寻路函数
def bfs(level_data, level_idx):
    # 构建二维网格
    grid = []
    start = None
    end = None

    print(f"\n=== Level {level_idx + 1} Map ===")
    for r in range(15):
        row = level_data[r*15 : (r+1)*15]
        line_str = ""
        for c, val in enumerate(row):
            if val == 3:
                start = (r, c)
                line_str += "S" # Start
            elif val == 4:
                end = (r, c)
                line_str += "E" # End
            elif val == 1:
                line_str += "." # Road
            else:
                line_str += "#" # Wall
        grid.append(row)
        print(line_str)

    if not start or not end:
        print("[-] Error: Start or End point not found!")
        return ""

    queue = deque([ (start, "") ])
    visited = set()
    visited.add(start)

    # 移动方向: w(上), s(下), a(左), d(右)
    moves = {
        'w': (-1, 0),
        's': (1, 0),
        'a': (0, -1),
        'd': (0, 1)
    }

    while queue:
        (curr_r, curr_c), path = queue.popleft()

        if (curr_r, curr_c) == end:
            return path

        for move_char, (dr, dc) in moves.items():
            nr, nc = curr_r + dr, curr_c + dc

            if 0 <= nr < 15 and 0 <= nc < 15:
                val = grid[nr][nc]
                # 逻辑回顾: 1是路, 4是终点, 3也可以走(起点格子)
                # 0是墙
                if val != 0 and (nr, nc) not in visited:
                    visited.add((nr, nc))
                    queue.append(((nr, nc), path + move_char))

    return None

# 4. 主程序
flag_path = ""
# 每一关大小 15*15 = 225
chunk_size = 225

for i in range(3):
    start_idx = i * chunk_size
    level_data = full_map[start_idx : start_idx + chunk_size]

    # 确保数据长度足够
    if len(level_data) < chunk_size:
        break

    path = bfs(level_data, i)
    if path:
        print(f"[+] Level {i+1} Solved! Path: {path}")
        flag_path += path
    else:
        print(f"[-] Level {i+1} Failed to solve.")

# 5. 生成 Flag
print("\n" + "="*30)
print(f"Total Input: {flag_path}")
md5_hash = hashlib.md5(flag_path.encode()).hexdigest()
print(f"Final Flag: flag{{{md5_hash}}}")
print("="*30)

执行脚本,输出如下:

[+] Total integers parsed: 675

=== Level 1 Map ===
.....##########
.....#S..######
.....###.######
.....###.######
.....###.....##
.....#######.##
.....#######.##
.....#######..#
.....########.#
.....########E#
...............
...............
...............
...............
...............
[+] Level 1 Solved! Path: ddsssddddsssdss

=== Level 2 Map ===
..#############
..#S.....######
..#..###.######
..######.######
..#..###.....##
..#..#######.##
..#..#######.##
..#..#####....#
..#..#####.##.#
..#..#####.####
..#......#.#..#
..#...........#
..###########E#
...............
...............
[+] Level 2 Solved! Path: dddddsssddddsssaassssddds

=== Level 3 Map ===
###############
#S..###########
###.#...#######
###...#.#######
####.##.#######
#..#.##.#######
##...##.#######
#######.#######
#######....####
##########.####
##########.####
##########.####
##########....#
#############.#
#############E#
[+] Level 3 Solved! Path: ddssddwddssssssdddssssdddss

==============================
Total Input: ddsssddddsssdssdddddsssddddsssaassssdddsddssddwddssssssdddssssdddss
Final Flag: flag{aeea66fcac7fa80ed8f79f38ad5bb953}
==============================

得到flag:flag{aeea66fcac7fa80ed8f79f38ad5bb953}

当然也是可以不用写脚本的,手动输入去走迷宫也不难。

双击dword_202020,点击左上角edit,点击array,

image-20251121124235969

按上图设置,那么那些很难看的数据就转换成了规则的形式,每行15个数据:

image-20251121124412796

最后走迷宫就可。

1000Click

下载附件,得到一个exe程序,直接运行,它让我们点击一千次,我们可以使用CheatEngine(CE)对程序运行时的值进行修改,因为既然程序需要计数,内存里一定有一个变量存着“当前点击次数”。

打开CE,运行exe,在CE中选择这个进程,在右侧value中输入0,因为初始化是0次,点击FirstScan:

再在1000Click程序点击一次click,然后在CE把value改为1,点击NextScan,可以发现右侧范围缩小了,重复这个步骤,发现点击两次时,右侧就只有一个了,右键点击并修改它,改为999,最后在1000click程序中再点击一次,得到flag:flag{TIBntXVbdZ4Z9VRtoOQ2wRlvDNIjQ8Ra}

crypt

下载附件,得到crypt.exe程序,运行后随便输点内容,按enter后就退出了。

拖入ida查看,找到main函数,初始反编译代码变量混淆较多。通过分析上下文,我们可以识别出关键变量:

  • v10: 用户输入缓冲区 (Input)
  • Str: 看起来像密钥的字符串 (Key)
  • byte_14013B000: 最终比较的目标密文 (Target)

经过重命名和注释优化后的代码逻辑如下:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  // 变量声明
  char user_input[32]; // [rsp+30h] 存放用户输入的 Flag
  char key_str[128];   // [rsp+50h] 存放密钥
  void *ctx;           // [rsp+28h] 加密算法上下文句柄

  // 1. 初始化密钥字符串
  // "12345678abcdefghijklmnopqrspxyz" 看起来是 Key
  strcpy(key_str, "12345678abcdefghijklmnopqrspxyz");

  // 2. 内存初始化 (无关紧要的清空操作)
  memset(&key_str[32], 0, 0x60uLL);
  memset(user_input, 0, 0x17uLL);

  // 3. 获取用户输入
  sub_1400054D0("%s", user_input);

  // 4. 关键加密流程 (Algorithm Block)
  // 申请 0x408 (1032) 字节的内存,这通常是某种算法的状态空间
  ctx = malloc(0x408uLL);              

  // 疑似初始化函数:传入了 Context、Key 和 KeyLen
  v3 = strlen(key_str);
  sub_140001120(ctx, key_str, v3);     

  // 疑似加密函数:传入了 Context、Input 和 InputLen
  // 注意:此处直接修改了 user_input 的内容
  v4 = strlen(user_input);
  sub_140001240(ctx, user_input, v4);  

  // 5. 最终校验逻辑 (Check Loop)
  // 循环 22 次,暗示 Flag 长度为 22
  for ( i = 0; i < 22; ++i )
  {
    // 校验公式:(加密后的输入[i] ^ 0x22) == 硬编码数据[i]
    if ( ((unsigned __int8)user_input[i] ^ 0x22) != byte_14013B000[i] )
    {
      print("error"); // 验证失败
      return 0;
    }
  }

  print("nice job"); // 验证成功
  return 0;
}

程序的验证逻辑非常清晰:

  1. 输入:用户输入 Flag
  2. 第一层处理:经过两个未知的函数 sub_140001120sub_140001240 处理。
  3. 第二层处理:将处理后的结果与 0x22 进行异或 (XOR)。
  4. 比较:将最终结果与全局数组 byte_14013B000 比对。

解题的关键在于识别步骤 2 中的未知函数。虽然代码中没有符号信息,但我们可以通过特征识别来判定这是 RC4 算法

证据一:内存分配特征

代码中 malloc(0x408) 分配了 1032 字节

  • 标准 RC4 算法包含一个 S-Box(256个元素)和两个索引变量 i, j
  • 本题中,S-Box 显然使用了 int (4字节) 类型而非 char (1字节) 类型。
  • 计算验证:256 * 4 (S-Box) + 4 (i) + 4 (j) = 1024 + 8 = 1032 (0x408)
  • 结论:这是典型的 Int 型 RC4 上下文结构。

证据二:初始化函数 (KSA) 特征

进入 sub_140001120 分析:

  • 线性填充:存在 for (i=0; i<256; ++i) v9[i]=i;
  • 密钥打乱:存在 j = (j + S[i] + key[i%len]) % 256 的逻辑。
  • 交换:存在 swap(S[i], S[j]) 操作。
  • 结论:这完全符合 RC4 的 KSA (Key Scheduling Algorithm) 阶段。

证据三:加密函数 (PRGA) 特征

进入 sub_140001240 分析:

  • 循环次数为输入字符串的长度。
  • 内部包含 i=(i+1)%256j=(j+S[i])%256 的状态更新。
  • 核心异或操作:input[k] ^= S[(S[i]+S[j])%256]
  • 结论:这完全符合 RC4 的 PRGA (Pseudo-Random Generation Algorithm) 阶段。

外层加密公式:

Input[i] ^ 0x22 = Target[i]

其中:

  • Input 是你的输入。
  • 0x22 是固定的 Key(密钥)。
  • Target 是硬编码在程序里的数组 byte_14013B000

外层解密公式:

异或运算(XOR)有一个神奇的特性:如果 A ^ B = C,那么 A = C ^ B。

所以我们只需要把公式反过来:

Input[i] = Target[i] ^ 0x22

所以,接下来我们需要找到byte_14013B000的数据,双击,得到:

byte_14013B000  db 9Eh, 0E7h, 30h, 5Fh, 0A7h, 1, 0A6h, 53h, 59h, 1Bh, 0Ah
.data:000000014013B000                                         ; DATA XREF: main+E5o
.data:000000014013B00B                 db 20h, 0F1h, 73h, 0D1h, 0Eh, 0ABh, 9, 84h, 0Eh, 8Dh, 2Bh
.data:000000014013B016                 db 2 dup(0)

我们需要的是前22个数据:

0x9E, 0xE7, 0x30, 0x5F, 0xA7, 0x01, 0xA6, 0x53, 0x59, 0x1B, 0x0A, 0x20, 0xF1, 0x73, 0xD1, 0x0E, 0xAB, 0x09, 0x84, 0x0E, 0x8D, 0x2B

编写python脚本解密:

def rc4_decrypt(key, data):
    # RC4 初始化 (KSA)
    S = list(range(256))
    j = 0
    out = []

    # KSA
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]

    # RC4 生成密钥流并解密 (PRGA)
    i = j = 0
    for char in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        out.append(char ^ k)

    return ''.join([chr(c) for c in out])

# 1. 填入提取的 Hex 数据 (byte_14013B000)
target_data = [
    0x9E, 0xE7, 0x30, 0x5F, 0xA7, 0x01, 0xA6, 0x53, 0x59, 0x1B, 0x0A,
    0x20, 0xF1, 0x73, 0xD1, 0x0E, 0xAB, 0x09, 0x84, 0x0E, 0x8D, 0x2B
]

# 2. 准备密钥 (来自代码中的 Str)
key_str = "12345678abcdefghijklmnopqrspxyz"
key_bytes = [ord(c) for c in key_str]

# 3. 第一步:逆向 main 函数最后的异或校验
# 逻辑:v10_encrypted[i] ^ 0x22 == target_data[i]
# 所以:v10_encrypted[i] = target_data[i] ^ 0x22
ciphertext = []
print("[-] 正在还原 RC4 密文...")
for b in target_data:
    ciphertext.append(b ^ 0x22)

# 4. 第二步:RC4 解密
print("[-] 正在进行 RC4 解密...")
try:
    flag = rc4_decrypt(key_bytes, ciphertext)
    print("\n" + "="*30)
    print(f"Flag 成功解出: {flag}")
    print("="*30)
except Exception as e:
    print(f"解密出错: {e}")

运行脚本,得到flag:flag{nice_to_meet_you}

This blog has been running for
Published 11 posts · Total 48.46k words