攻防世界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)后,程序将当前的 i 和 j 分别存入两个全局变量。
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+82↑o
.data:0000000000202020 ; sub_A92+76↑o ...
.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,

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

最后走迷宫就可。
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;
}
程序的验证逻辑非常清晰:
- 输入:用户输入
Flag。 - 第一层处理:经过两个未知的函数
sub_140001120和sub_140001240处理。 - 第二层处理:将处理后的结果与
0x22进行异或 (XOR)。 - 比较:将最终结果与全局数组
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)%256和j=(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+E5↑o
.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}