BUUCTF 刷题笔记——Reverse 1
BUUCTF 刷题笔记——Reverse 1
easyre
第一道题,题目提示非常简单的逆向并提供一个 zip 压缩包,下载本地解压后是一个 exe 可执行文件。尝试用 IDA 反编译,发现 flag 出来了。
感谢善待新人
reverse1
依然给了一个压缩文件,解压后依然是一个 exe 可执行文件,再次尝试用 IDA 反编译,这次没有一眼看到 flag 了,甚至连主函数都没有。于是 Shift + F12 找找特别的字符串,发现了 this is the right flag!:
找到使用该字符串的位置,发现是如下 sub_1400118C0() 函数:
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__int64 sub_1400118C0()
{
char *v0; // rdi
__int64 i; // rcx
size_t v2; // rax
char v4[36]; // [rsp+0h] [rbp-20h] BYREF
int j; // [rsp+24h] [rbp+4h]
char Str1[224]; // [rsp+48h] [rbp+28h] BYREF
__int64 v7; // [rsp+128h] [rbp+108h]
v0 = v4;
for ( i = 82i64; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
for ( j = 0; ; ++j )
{
v7 = j;
if ( j > j_strlen(Str2) )
break;
if ( Str2[j] == 111 )
Str2[j] = 48;
}
sub_1400111D1("input the flag:");
sub_14001128F("%20s", Str1);
v2 = j_strlen(Str2);
if ( !strncmp(Str1, Str2, v2) )
sub_1400111D1("this is the right flag!\n");
else
sub_1400111D1("wrong flag\n");
sub_14001113B(v4, &unk_140019D00);
return 0i64;
}这段程序就是读取用户输入数据,并于内部字符串 Str2 作比较,比较正确说明是正确 flag。值得注意的是用于比较的内部字符串在参与比较前作了以下操作:
1
2if ( Str2[j] == 111 )
Str2[j] = 48;将 ASCII 码值为 111 的字符替换为码值为 48 的字符,即将 o 替换为 0。
在 IDA 中可以查看字符串 Str2 的值为 {hello_world},替换后即为 {hell0_w0rld}。根据 BUU 的提示,加上 flag 前缀即可提交。
有人不看提示直接提交喜提错误我不说是谁。
reverse2
本题的文件没有后缀名,不过因为惯性还是直接 IDA 反编译,本次含有主函数了,并且有 flag 出没:
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
29int __cdecl main(int argc, const char **argv, const char **envp)
{
int stat_loc; // [rsp+4h] [rbp-3Ch] BYREF
int i; // [rsp+8h] [rbp-38h]
__pid_t pid; // [rsp+Ch] [rbp-34h]
char s2[24]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v8; // [rsp+28h] [rbp-18h]
v8 = __readfsqword(0x28u);
pid = fork();
if ( pid )
{
waitpid(pid, &stat_loc, 0);
}
else
{
for ( i = 0; i <= strlen(&flag); ++i )
{
if ( *(&flag + i) == 105 || *(&flag + i) == 114 )
*(&flag + i) = 49;
}
}
printf("input the flag:");
__isoc99_scanf("%20s", s2);
if ( !strcmp(&flag, s2) )
return puts("this is the right flag!");
else
return puts("wrong flag!");
}与前一关类似,也是通过判断 ASCII 码值来做一些替换,不过本题是将字符 i 与字符 r 都替换为字符 1。查询内部字符串 flag 如下,由于程序是通过首字符地址来访问的 C 类型字符串,反编译时会分隔开来,因此完整字符串为 {hacking_for_fun}。
替换后为 {hack1ng_fo1_fun},因此 flag 为 flag{hack1ng_fo1_fun}。
这边每次都要自己组 flag,老是忘记。
内涵的软件
本题给的文件是一个 32 位的可执行文件,因此使用 32 位版的 IDA 打开,主函数仅仅调用了一个 main_0() 函数而已,因此查看该函数:
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
31int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4[4]; // [esp+4Ch] [ebp-Ch] BYREF
const char *v5; // [esp+50h] [ebp-8h]
int v6; // [esp+54h] [ebp-4h]
v6 = 5;
v5 = "DBAPP{49d3c93df25caad81232130f3d2ebfad}";
while ( v6 >= 0 )
{
printf(&byte_4250EC, v6);
sub_40100A();
--v6;
}
printf(asc_425088);
v4[0] = 1;
scanf("%c", v4);
if ( v4[0] == 89 )
{
printf(aOd);
return sub_40100A();
}
else
{
if ( v4[0] == 78 )
printf(&byte_425034);
else
printf(&byte_42501C);
return sub_40100A();
}
}虽然挺长一段代码,但实测开头的那个长得像 flag 的局部变量 v5 里面便是 flag,不过将 DBAPP 换成 flag 即可。
新年快乐
本题文件为 32 位可执行文件,但是在 IDA 中打开后却发现仅有两个函数,代码也奇奇怪怪找不到啥关键字,而且程序大部分数据所在的段名都含有此前没见过的 UPX。
基本可以确定,碰上个人第一次接触的加壳程序了,即类似压缩文件不过解压过程在执行时在内存中自动完成,因此程序可正常执行但是却无法反编译出多少有效信息,加壳主要用于压缩与加密。不过还好只是入门级的 UPX 压缩壳,可以去 他们官网 下载加壳程序,使用 -d 参数即可完成脱壳:
1
upx -d [文件路径]
看到如下界面即脱壳成功,此时文件就只是一个普通的可执行文件了。当然脱壳的方法有很多,这里暂时不作过多考究,
本小白还是慢慢来 。脱壳之后即可在 IDA 反编译出原程序了,主函数如下,有众多关键字 flag 出没:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int __cdecl main(int argc, const char **argv, const char **envp)
{
char Str2[14]; // [esp+12h] [ebp-3Ah] BYREF
char Str1[44]; // [esp+20h] [ebp-2Ch] BYREF
__main();
strcpy(Str2, "HappyNewYear!");
memset(Str1, 0, 32);
printf("please input the true flag:");
scanf("%s", Str1);
if ( !strncmp(Str1, Str2, strlen(Str2)) )
return puts("this is true flag!");
else
return puts("wrong!");
}程序依然是老流程,读取数据并与内部字符串 HappyNewYear! 作比较,不过这次没对内部字符串做任何修改,因此组合后的 flag{HappyNewYear!} 便是 flag。
xor
下载文件,解压发现含有 _MACOSX,因此应该是 MAC 来的文件,使用 Exeinfo PE 工具小查一下,确定是 64 位的 MAC 可执行程序,并且没有加壳。
直接 IDA 反编译,发现主函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+2Ch] [rbp-124h]
char __b[264]; // [rsp+40h] [rbp-110h] BYREF
memset(__b, 0, 0x100uLL);
printf("Input your flag:\n");
get_line(__b, 256LL);
if ( strlen(__b) != 33 )
goto LABEL_7;
for ( i = 1; i < 33; ++i )
__b[i] ^= __b[i - 1];
if ( !strncmp(__b, global, 0x21uLL) )
printf("Success");
else
LABEL_7:
printf("Failed");
return 0;
}虽然源码很容易就得到了,不过相比之前,本题对 flag 的处理更加有趣一些,首先读取用户输入,并且长度必须为 33,即 flag 会有 33 个字符。获取用户输入后程序会将输入字符串的后 32 位逐个与前一位作异或运算,计算后的结果与内部字符串相等才是 flag。也就是说 flag 经过一轮异或后会获得内部字符串,又由于对相同的数据异或两次数据就会复原,因此直接对内部字符串作一轮异或操作即可获得 flag。而内部字符串 global 如下,Shift + e 获取字符数组,取数值便于计算:
然后写个脚本就可以计算出 flag 了,脚本如下。值得注意的是脚本中的异或运算结果不应存入数组中,因为原计算是基于前字符已经运算完成的情况下进行的,因此复原过程中的每个数据都应保持原样。
1
2
3
4
5
6
7
8
9
10glb = [0x66,0x0A,0x6B,0x0C,0x77,0x26,0x4F,0x2E,
0x40,0x11,0x78,0x0D,0x5A,0x3B,0x55,0x11,
0x70,0x19,0x46,0x1F,0x76,0x22,0x4D,0x23,
0x44,0x0E,0x67,6,0x68,0x0F,0x47,0x32,0x4F,0]
s = chr(0x66)
for i in range(1,33):
s += chr(glb[i] ^ glb[i-1])
print(s)计算出的 flag 为 flag{QianQiuWanDai_YiTongJiangHu},
千秋万代,一统江湖 。
helloword
是个 apk 文件!没想到这么快就来到安卓了,直接丢进 IDA 里看看,不过反编译 apk 需要在打开时选择 APK Android Package 才行。
反编译出来相当多东西,没搞过安卓看到真的令人恐惧,直接 Shift + F12 查找字符串,字符串也是一大堆,所幸可以使用 Ctrl + F 搜索。结果直接就找到 flag,感谢饶命。
reverse3
再次回到普通的 exe 文件,使用 Exeinfo PE 打开查看一下先。是一个 32 位可执行文件,而且没加壳。
那好说,直接丢进 IDA 反编译,主函数依然仅仅调用了 main_0() 函数而已,因此直接查看该函数,有关键词 flag 出没:
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
35int __cdecl main_0(int argc, const char **argv, const char **envp)
{
size_t v3; // eax
const char *v4; // eax
size_t v5; // eax
char v7; // [esp+0h] [ebp-188h]
char v8; // [esp+0h] [ebp-188h]
signed int j; // [esp+DCh] [ebp-ACh]
int i; // [esp+E8h] [ebp-A0h]
signed int v11; // [esp+E8h] [ebp-A0h]
char Destination[108]; // [esp+F4h] [ebp-94h] BYREF
char Str[28]; // [esp+160h] [ebp-28h] BYREF
char v14[8]; // [esp+17Ch] [ebp-Ch] BYREF
for ( i = 0; i < 100; ++i )
{
if ( (unsigned int)i >= 0x64 )
j____report_rangecheckfailure();
Destination[i] = 0;
}
sub_41132F("please enter the flag:", v7);
sub_411375("%20s", (char)Str);
v3 = j_strlen(Str);
v4 = (const char *)sub_4110BE(Str, v3, v14);
strncpy(Destination, v4, 0x28u);
v11 = j_strlen(Destination);
for ( j = 0; j < v11; ++j )
Destination[j] += j;
v5 = j_strlen(Destination);
if ( !strncmp(Destination, Str2, v5) )
sub_41132F("rigth flag!\n", v8);
else
sub_41132F("wrong flag!\n", v8);
return 0;
}程序在读取用户输入后将输入数据丢进了 sub_4110BE() 函数做运算,然后把运算后数据放进一个 for 循环中逐个字符的码值加上索引,最终与内部字符串 Str2 相同则用户输入数据为 flag。这个 sub_4110BE() 函数令人在意,打开后发现其仅调用了一个 sub_411AB0() 函数,该函数内容实在有些复杂:
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71void *__cdecl sub_411AB0(char *a1, unsigned int a2, int *a3)
{
int v4; // [esp+D4h] [ebp-38h]
int v5; // [esp+D4h] [ebp-38h]
int v6; // [esp+D4h] [ebp-38h]
int v7; // [esp+D4h] [ebp-38h]
int i; // [esp+E0h] [ebp-2Ch]
unsigned int v9; // [esp+ECh] [ebp-20h]
int v10; // [esp+ECh] [ebp-20h]
int v11; // [esp+ECh] [ebp-20h]
void *v12; // [esp+F8h] [ebp-14h]
char *v13; // [esp+104h] [ebp-8h]
if ( !a1 || !a2 )
return 0;
v9 = a2 / 3;
if ( (int)(a2 / 3) % 3 )
++v9;
v10 = 4 * v9;
*a3 = v10;
v12 = malloc(v10 + 1);
if ( !v12 )
return 0;
j_memset(v12, 0, v10 + 1);
v13 = a1;
v11 = a2;
v4 = 0;
while ( v11 > 0 )
{
byte_41A144[2] = 0;
byte_41A144[1] = 0;
byte_41A144[0] = 0;
for ( i = 0; i < 3 && v11 >= 1; ++i )
{
byte_41A144[i] = *v13;
--v11;
++v13;
}
if ( !i )
break;
switch ( i )
{
case 1:
*((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
v5 = v4 + 1;
*((_BYTE *)v12 + v5) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
*((_BYTE *)v12 + ++v5) = aAbcdefghijklmn[64];
*((_BYTE *)v12 + ++v5) = aAbcdefghijklmn[64];
v4 = v5 + 1;
break;
case 2:
*((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
v6 = v4 + 1;
*((_BYTE *)v12 + v6) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
*((_BYTE *)v12 + ++v6) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
*((_BYTE *)v12 + ++v6) = aAbcdefghijklmn[64];
v4 = v6 + 1;
break;
case 3:
*((_BYTE *)v12 + v4) = aAbcdefghijklmn[(int)(unsigned __int8)byte_41A144[0] >> 2];
v7 = v4 + 1;
*((_BYTE *)v12 + v7) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | (16 * (byte_41A144[0] & 3))];
*((_BYTE *)v12 + ++v7) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | (4 * (byte_41A144[1] & 0xF))];
*((_BYTE *)v12 + ++v7) = aAbcdefghijklmn[byte_41A144[2] & 0x3F];
v4 = v7 + 1;
break;
}
}
*((_BYTE *)v12 + v4) = 0;
return v12;
}毕竟是伪代码,要直接在这里审计复杂算法还是太难了,这里从一个被反复使用的数组 aAbcdefghijklmn 入手,打开发现其内容为大小写字母、数字以及 +、/、= 三个符号,这是 base64 的字符表啊!那就先按 base64 算。
也就是说,将内部字符串 Str2 每一位码值减去索引后再进行 base64 解码结果即为 flag,其中内部字符串 Str2 为 e3nifIH9b_C@n@dH,因此可编写脚本如下:
1
2
3
4
5
6
7
8
9import base64
Str2 = "e3nifIH9b_C@n@dH"
flag = ""
for i in range(len(Str2)):
flag += chr(ord(Str2[i]) - i)
print(base64.b64decode(flag))执行结束后即可获得 b'{i_l0ve_you}',然而要提交到 BUU 平台的话就需要修改成 flag{i_l0ve_you} 才可通过。
某人花了三次机会才知道需要这样提交,痛啊!
不一样的flag
先验一下文件,本题文件为 32 位可执行文件,没有加壳,很好。
直接 IDA 反编译,主函数里就有关键词 flag 出没,代码略长,因此大概需要好好审计一下主函数了。
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
58
59
60
61
62
63
64
65
66
67
68int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
_BYTE v3[29]; // [esp+17h] [ebp-35h] BYREF
int v4; // [esp+34h] [ebp-18h]
int v5; // [esp+38h] [ebp-14h] BYREF
int i; // [esp+3Ch] [ebp-10h]
_BYTE v7[12]; // [esp+40h] [ebp-Ch] BYREF
__main();
v3[26] = 0;
*(_WORD *)&v3[27] = 0;
v4 = 0;
strcpy(v3, "*11110100001010000101111#");
while ( 1 )
{
puts("you can choose one action to execute");
puts("1 up");
puts("2 down");
puts("3 left");
printf("4 right\n:");
scanf("%d", &v5);
// 列出四个选项并等待用户输入
if ( v5 == 2 )
{
++*(_DWORD *)&v3[25];
// 用户选择 down 则 v3[25] 值减一
}
else if ( v5 > 2 )
{
if ( v5 == 3 )
{
--v4;
// 用户选择 left 则 v4 值减一
}
else
{
if ( v5 != 4 )
LABEL_13:
exit(1);
++v4;
// 用户选择 right 则 v4 值加一
}
}
else
{
if ( v5 != 1 )
goto LABEL_13;
--*(_DWORD *)&v3[25];
// 用户选择 up 则 v3[25] 值加一
}
for ( i = 0; i <= 1; ++i )
{
if ( *(int *)&v3[4 * i + 25] < 0 || *(int *)&v3[4 * i + 25] > 4 )
exit(1);
// v3[25] 取值范围为 [0,4]
}
if ( v7[5 * *(_DWORD *)&v3[25] - 41 + v4] == 49 )
exit(1);
// 指定位置码值不能等于 49,即字符 1
// 41 为 v7 到 v3 的偏移
if ( v7[5 * *(_DWORD *)&v3[25] - 41 + v4] == 35 )
{
puts("\nok, the order you enter is the flag!");
exit(0);
// 只有指定位置码值为 35 才算成功,即字符 #
}
}
}代码主要逻辑是让用户选择上下左右的一个方向,然后通过用户的选择来对指定值做加减一的操作,由于最终比较字符是将上下移动的值乘 5 后与左右移动的值相加,最后减去到 v3 字符串变量的偏移得到的,可以认为程序将 v3 字符串变量视为每行 5 个元素的矩阵。此外,其中上下移动值 v3[25] 限定区间为 [0,4],而 v3 字符串变量共含有 25 个元素,因此可进一步确定程序将该字符串视为五行五列的矩阵。又由于每一步移动后的值不能为 1 且移动到 # 时才算成功,因此从初始地址开始一步一步移动到终点的路线图大致如下:
只需输入按照上述路线移动的数字序列即为 flag,因此本题 flag 为 flag{222441144222}。
独立审计这一段代码可废了我好大劲。
SimpleRev
本题文件没有后缀名,丢进 Exeinfo PE 发现是 Linux 下的 64 位可执行文件,依旧没有加壳。
那还是直接丢进 IDA 中反编译,主函数中没有啥关键词出现,但是其调用了一个 Decry() 函数,点进去发现又是一段很长的代码,且包含一些关于 flag 的关键词。
看来又要慢慢审计代码救命。 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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85unsigned __int64 Decry()
{
char v1; // [rsp+Fh] [rbp-51h]
int v2; // [rsp+10h] [rbp-50h]
int v3; // [rsp+14h] [rbp-4Ch]
int i; // [rsp+18h] [rbp-48h]
int v5; // [rsp+1Ch] [rbp-44h]
char src[8]; // [rsp+20h] [rbp-40h] BYREF
__int64 v7; // [rsp+28h] [rbp-38h]
int v8; // [rsp+30h] [rbp-30h]
__int64 v9[2]; // [rsp+40h] [rbp-20h] BYREF
int v10; // [rsp+50h] [rbp-10h]
unsigned __int64 v11; // [rsp+58h] [rbp-8h]
v11 = __readfsqword(0x28u);
*(_QWORD *)src = 0x534C43444ELL;
v7 = 0LL;
v8 = 0;
v9[0] = 0x776F646168LL;
v9[1] = 0LL;
v10 = 0;
text = (char *)join(key3, v9);
// jion() 为自定义函数,连接 key3 与 v9
// v9 为整型数值,按小端序存储形式为 0x68 0x61 0x64 0x6F 0x77
// 因此 text 值为 killshadow
strcpy(key, key1);
// 将 key1 的值 "ADSFK" 赋给 key
strcat(key, src);
// 将 src 拼接在 key 之后
// src 同样为整形数据,按小端序存储形式为 0x4E 0x44 0x43 0x4C 0x53
// 拼接后的值为 ADSFKNDCLS
v2 = 0;
v3 = 0;
getchar();
v5 = strlen(key);
for ( i = 0; i < v5; ++i )
{
if ( key[v3 % v5] > 64 && key[v3 % v5] <= 90 )
key[i] = key[v3 % v5] + 32;
// 遍历 key 的每个字符,若为大写字母则改为小写字母
// 故 key 值为 adsfkndcls
++v3;
}
printf("Please input your flag:");
while ( 1 )
{
v1 = getchar();
if ( v1 == 10 )
break;
// 读取到换行则退出循环
if ( v1 == 32 )
{
++v2;
// 读取到空格则 v2 变量加一
}
else
{
if ( v1 <= 96 || v1 > 122 )
{
if ( v1 > 64 && v1 <= 90 )
{
str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
++v3;
// 大写字母与 key 中元素逐个操作,转成某个小写字母
}
}
else
{
str2[v2] = (v1 - 39 - key[v3 % v5] + 97) % 26 + 97;
++v3;
// 小写字母与 key 中元素逐个操作,转成某个小写字母
}
if ( !(v3 % v5) )
putchar(32);
// 循环使用了一次 key 之后打印一个空格
++v2;
}
}
if ( !strcmp(text, str2) )
// 处理过后的 str2 与 text 相同则输入值为 flag
puts("Congratulation!\n");
else
puts("Try again!\n");
return __readfsqword(0x28u) ^ v11;
}程序在与用户交互前会处理好内部数据,即后续用于作比较的内部字符串 killshadow 以及配合处理用户输入数据的内部密钥 adsfkndcls,由于这些数据均已知,因此我们只需做找出转换后符合条件的字符串即可。逆运算唯一的复杂之处在于每次都使用了取模运算,由于仅对大小写字母做处理,且模为 26,因此每一位符合条件的字符都应该有大小写各一位。
到这就可以直接写脚本了,直接逐个字母遍历过去,符合条件就是 flag 的重要组分。
1
2
3
4
5
6
7
8
9
10
11key = "adsfkndcls"
text = "killshadow"
flag = ""
for i in range(0, len(text)):
for j in range(65,91): # 仅取大写字母
# for j in range(97,123): # 仅取小写字母
if ord(text[i]) == (j - 39 - ord(key[i]) + 97) % 26 + 97:
flag += chr(j)
print(flag)计算出大写字母序列 KLDQCUDFZO 与小写字母序列 efxkwoxzti,虽然理论上大写版与小写版乃至他们交叉版本均符合条件,但是实测 BUU 仅接受 flag{KLDQCUDFZO}。
Java逆向解密
本题文件直接给了一个 class 文件,这我知道,拉进 idea 就可以反编译了。
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//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import java.util.ArrayList;
import java.util.Scanner;
public class Reverse {
public Reverse() {
}
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
System.out.println("Please input the flag :");
String str = s.next();
System.out.println("Your input is :");
System.out.println(str);
char[] stringArr = str.toCharArray();
Encrypt(stringArr);
}
public static void Encrypt(char[] arr) {
ArrayList<Integer> Resultlist = new ArrayList();
for(int i = 0; i < arr.length; ++i) {
int result = arr[i] + 64 ^ 32;
Resultlist.add(result);
}
int[] KEY = new int[]{180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65};
ArrayList<Integer> KEYList = new ArrayList();
for(int j = 0; j < KEY.length; ++j) {
KEYList.add(KEY[j]);
}
System.out.println("Result:");
if (Resultlist.equals(KEYList)) {
System.out.println("Congratulations!");
} else {
System.err.println("Error!");
}
}
}看多了 C 语言伪代码再看这种的就很舒适,程序逻辑非常简单,对输入字符串逐个进行加 64 后与 32 进行异或的操作,值得注意的是,加号的优先级是高于异或运算符的。当计算结果与内部的 KEY 数组内容一样,用户输入的数据即为 flag。
操作比较简单,直接写脚本进行逆操作:
1
2
3
4
5
6
7KEY = [180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65]
flag = ""
for i in range(len(KEY)):
flag += chr((KEY[i] ^ 32) - 64)
print(flag)运行之后便获得了 flag,提交 flag{This_is_the_flag_!} 即可。
[GXYCTF2019]luck_guy
本题文件为 Linux 系统中的 64 位可执行程序,没有加壳。
丢进 IDA 中反编译,主函数中多半是寒暄,值得注意的是其中的 patch_me() 函数:
1
2
3
4
5
6
7int __fastcall patch_me(int a1)
{
if ( a1 % 2 == 1 )
return puts("just finished");
else
return get_flag();
}该函数在判断输入为偶数时会调用 get_flag() 函数,这显然就是我们的目标函数了。该函数代码依旧很长,因此,又要慢慢审计了:
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
57unsigned __int64 get_flag()
{
unsigned int v0; // eax
int i; // [rsp+4h] [rbp-3Ch]
int j; // [rsp+8h] [rbp-38h]
__int64 s; // [rsp+10h] [rbp-30h] BYREF
char v5; // [rsp+18h] [rbp-28h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]
v6 = __readfsqword(0x28u);
v0 = time(0LL);
srand(v0);
// 以时间作为随机数种子
for ( i = 0; i <= 4; ++i )
{
switch ( rand() % 200 )
// rand() 以时间为种子,因此几乎无法预测随机序列,不过这不重要
{
case 1:
puts("OK, it's flag:");
memset(&s, 0, 0x28uLL);
// 赋 0 值,内存准备工作
strcat((char *)&s, f1);
// 将 f1 存入 &s 所指向的内存中
strcat((char *)&s, &f2);
// f2 的数据紧随 f1 存入指定内存
printf("%s", (const char *)&s);
break;
case 2:
printf("Solar not like you");
break;
case 3:
printf("Solar want a girlfriend");
break;
case 4:
s = 0x7F666F6067756369LL;
v5 = 0;
strcat(&f2, (const char *)&s);
// 为 f2 赋值
break;
case 5:
for ( j = 0; j <= 7; ++j )
{
if ( j % 2 == 1 )
*(&f2 + j) -= 2;
else
--*(&f2 + j);
}
// 对 f2 中的数据做处理
break;
default:
puts("emmm,you can't find flag 23333");
break;
}
}
return __readfsqword(0x28u) ^ v6;
}程序虽长,所幸逻辑简单,审计可知 flag 由 f1 与 f2 组成,f1 已知,为 GXY{do_not_,而 f1 则由后续操作完成赋值,且赋值后还需要另外的操作来完成数据处理。也就是说,上述代码中的 switch 语句中只有 1、 4、 5 是有效的,且必须按照 4、 5、 1 的顺序指向才可输出正确的 flag。由于程序执行由随机序列决定,且随机数取模高达 200,因此要靠程序自然输出基本不可能,但是现在毕竟是 Reverse,程序运不运行啥的都不重要,咱自己手工求出来即可。
由 case 4 可知 f2 被赋的值为整形数值 0x7F666F6067756369LL,小端存储的缘故,在内存中的顺序为:0x69、0x63、0x75、0x67、0x60、0x6F、0x66、0x7F,将这些数据按顺序进行 case 5 中的操作后与 f1 拼接即可获得 flag。还是写个脚本来做:
1
2
3
4
5
6
7
8
9
10
11f1 = 'GXY{do_not_'
f2 = [0x69, 0x63, 0x75, 0x67, 0x60, 0x6f, 0x66, 0x7f]
flag = ''
for i in range(8):
if i % 2 == 1:
flag += chr(f2[i] - 2)
else:
flag += chr(f2[i] - 1)
print(f1 + flag)运行后即可获得 GXY{do_not_hate_me},不过 BUU 格式问题,因此需要提交 flag{do_not_hate_me}
[BJDCTF2020]JustRE
本题文件为 32 位可执行程序,没有加壳。
丢进 IDA 反编译,主函数中有些复杂且并未发现什么关键词,因此直接 Shift + F12 查看字符串,发现一个类似 flag 形式的字符串:
点击进去发现该字符串被 DialogFunc() 函数引用,该函数内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24INT_PTR __stdcall DialogFunc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4)
{
CHAR String[100]; // [esp+0h] [ebp-64h] BYREF
if ( a2 != 272 )
{
if ( a2 != 273 )
return 0;
if ( (_WORD)a3 != 1 && (_WORD)a3 != 2 )
{
sprintf(String, &Format, ++dword_4099F0);
if ( dword_4099F0 == 19999 )
{
sprintf(String, " BJD{%d%d2069a45792d233ac}", 19999, 0);
SetWindowTextA(hWnd, String);
return 0;
}
SetWindowTextA(hWnd, String);
return 0;
}
EndDialog(hWnd, (unsigned __int16)a3);
}
return 1;
}大多为无关代码,仅关注关键字符串所在的 sprintf() 函数即可,可以发现字符串格式化输出,即占位符 %d 在输出时会被替换为其后所跟的整形数据。
因此格式化输出之后的 BJD{1999902069a45792d233ac} 即为 flag,当然 BUU 中需要提交 flag{1999902069a45792d233ac}。
值得注意的是,本题文件可双击打开,有个有趣的可视化界面。
刮开有奖
本题文件为 32 位可执行程序,没有加壳。
丢进 IDA 反编译,主函数仅调用了一个 DialogBoxParamA() 函数便退出了,该函数从对话框模板资源创建模式对话框。点进函数发现与其相关的函数都在 DialogFunc() 函数被调用:
而在 DialogFunc() 函数则存在 U g3t 1T! 这样的字符串存在,因此要获取 flag 就得从这个函数入手了。
又是一堆代码要审计。 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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75INT_PTR __stdcall DialogFunc(HWND hDlg, UINT a2, WPARAM a3, LPARAM a4)
{
const char *v4; // esi
const char *v5; // edi
int v7[2]; // [esp+8h] [ebp-20030h] BYREF
int v8; // [esp+10h] [ebp-20028h]
int v9; // [esp+14h] [ebp-20024h]
int v10; // [esp+18h] [ebp-20020h]
int v11; // [esp+1Ch] [ebp-2001Ch]
int v12; // [esp+20h] [ebp-20018h]
int v13; // [esp+24h] [ebp-20014h]
int v14; // [esp+28h] [ebp-20010h]
int v15; // [esp+2Ch] [ebp-2000Ch]
int v16; // [esp+30h] [ebp-20008h]
CHAR String[65536]; // [esp+34h] [ebp-20004h] BYREF
char v18[65536]; // [esp+10034h] [ebp-10004h] BYREF
if ( a2 == 272 )
return 1;
if ( a2 != 273 )
return 0;
if ( (_WORD)a3 == 1001 )
{
memset(String, 0, 0xFFFFu);
// 初始化 String 内存
GetDlgItemTextA(hDlg, 1000, String, 0xFFFF);
// 从对话框读取信息写入 String 指向的内存中
if ( strlen(String) == 8 )
// 当 String 长度为 8 才进入 if,否则退出,因此 flag 长度为 8
{
v7[0] = 90;
v7[1] = 74;
v8 = 83;
v9 = 69;
v10 = 67;
v11 = 97;
v12 = 78;
v13 = 72;
v14 = 51;
v15 = 110;
v16 = 103;
// 这些变量全部与 v7 在内存中连续,可认为同在 v7 数组中
sub_4010F0((int)v7, 0, 10);
memset(v18, 0, 0xFFFFu);
v18[0] = String[5];
v18[2] = String[7];
v18[1] = String[6];
// v18 初始化并特定位赋初值
v4 = (const char *)sub_401000(v18, strlen(v18));
// v18 处理后赋给 v4
memset(v18, 0, 0xFFFFu);
v18[1] = String[3];
v18[0] = String[2];
v18[2] = String[4];
// v18 再次初始化并特定位赋初值
v5 = (const char *)sub_401000(v18, strlen(v18));
// v18 再次处理后赋给 v5
if ( String[0] == v7[0] + 34
&& String[1] == v10
&& 4 * String[2] - 141 == 3 * v8
&& String[3] / 4 == 2 * (v13 / 9)
&& !strcmp(v4, "ak1w")
&& !strcmp(v5, "V1Ax") )
// 通过这么些个比较才行
{
MessageBoxA(hDlg, "U g3t 1T!", "@_@", 0);
}
}
return 0;
}
if ( (_WORD)a3 != 1 && (_WORD)a3 != 2 )
return 0;
EndDialog(hDlg, (unsigned __int16)a3);
return 1;
}程序在预处理后从对话框读取数据 String,首先限定其长度为 8 位,不符合则退出程序,因此可以判断 flag 长度为 8 位。长度符合的字符串 String 则会与函数的一些已知值的局部变量以及经过 sub_4010F0() 函数处理的 v7 变量做比较,还有特定位参与 sub_401000() 函数处理后与内部字符串常量的比较,而通过所有比较的字符串 String 即为 flag。因此要获取 flag,还有两个函数也需要好好审计审计,
救命啊!!! 那么就先审审 sub_4010F0() 函数。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
56int __cdecl sub_4010F0(int a1, int a2, int a3)
{
int result; // eax
int i; // esi
int v5; // ecx
int v6; // edx
result = a3;
// 初始化为数组末位,作为后续区间遍历的终点
for ( i = a2; i <= a3; a2 = i )
// 未到数组末位则继续循环
{
v5 = 4 * i;
v6 = *(_DWORD *)(4 * i + a1);
// 4 为整形数据所占用的空间,即 v6 赋值为区间起点处的值
if ( a2 < result && i < result )
// 未到区间终点则继续循环
{
do
{
if ( v6 > *(_DWORD *)(a1 + 4 * result) )
// v6 与区间内末尾作比较,大于末位才进入 if 语句
{
if ( i >= result )
break;
++i;
*(_DWORD *)(v5 + a1) = *(_DWORD *)(a1 + 4 * result);
// 末位数据存入 v6 数值原本的地址
if ( i >= result )
break;
while ( *(_DWORD *)(a1 + 4 * i) <= v6 )
{
if ( ++i >= result )
goto LABEL_13;
// 若 v6 值仍然不小于其原位后的一位,则继续递归调用直至结束
}
if ( i >= result )
break;
v5 = 4 * i;
*(_DWORD *)(a1 + 4 * result) = *(_DWORD *)(4 * i + a1);
// 若 v6 跟小了,则把比他大的放在区间末位
}
--result;
// 区间尾部前移,区间缩小
}
while ( i < result );
}
LABEL_13:
*(_DWORD *)(a1 + 4 * result) = v6;
// 若 v6 一直都大,则会一直存在于区间末位
sub_4010F0(a1, a2, i - 1);
result = a3;
++i;
}
return result;
}费了好大劲审计完后,发现大的数据总是会往后移,小的数据则前移,很显然这是一个升序的排序函数。
参观了网上好多题解发现大家都不会审计这个代码,因为修改一下直接运行就可以出结果了,谁是怨种我不说 。因此 v7 数组经过 sub_4010F0() 函数处理后为如下递增序列:1
2
351 67 69 72 74 78 83 90 97 103 110
对应字符序列为:
3 C E H J N S Z a g n最后审计一下 sub_401000() 函数,太长了…
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78_BYTE *__cdecl sub_401000(int a1, int a2)
{
int v2; // eax
int v3; // esi
size_t v4; // ebx
_BYTE *v5; // eax
_BYTE *v6; // edi
int v7; // eax
_BYTE *v8; // ebx
int v9; // edi
int v10; // edx
int v11; // edi
int v12; // eax
int i; // esi
_BYTE *result; // eax
_BYTE *v15; // [esp+Ch] [ebp-10h]
_BYTE *v16; // [esp+10h] [ebp-Ch]
int v17; // [esp+14h] [ebp-8h]
int v18; // [esp+18h] [ebp-4h]
v2 = a2 / 3;
v3 = 0;
if ( a2 % 3 > 0 )
++v2;
v4 = 4 * v2 + 1;
v5 = malloc(v4);
v6 = v5;
v15 = v5;
if ( !v5 )
exit(0);
memset(v5, 0, v4);
v7 = a2;
v8 = v6;
v16 = v6;
if ( a2 > 0 )
{
while ( 1 )
{
v9 = 0;
v10 = 0;
v18 = 0;
do
{
if ( v3 >= v7 )
break;
++v10;
v9 = *(unsigned __int8 *)(v3 + a1) | (v9 << 8);
++v3;
}
while ( v10 < 3 );
v11 = v9 << (8 * (3 - v10));
v12 = 0;
v17 = v3;
for ( i = 18; i > -6; i -= 6 )
{
if ( v10 >= v12 )
{
*((_BYTE *)&v18 + v12) = (v11 >> i) & 0x3F;
v8 = v16;
}
else
{
*((_BYTE *)&v18 + v12) = 64;
}
*v8++ = byte_407830[*((char *)&v18 + v12++)];
v16 = v8;
}
v3 = v17;
if ( v17 >= a2 )
break;
v7 = a2;
}
v6 = v15;
}
result = v6;
*v8 = 0;
return result;
}万不可死磕审计,太浪费时间了,注意到函数调用了一个字符数组 byte_407830,点开发现,这是老朋友 base64 字符表啊,应该又是 base64 加密计算而已,就不去老实审计了。
代码审完了,接下来照着最终判断条件逐个击破就行了。
1
2
3
4
5
6
7
8
9
10
11
12String[0] == v7[0] + 34
// v7[0]=51,故 String[0]='U'(码值为 85)
&& String[1] == v10
// v10='J',故 String[1]='J'
&& 4 * String[2] - 141 == 3 * v8
// v8=69,故 String[2]='W'(码值为 87)
&& String[3] / 4 == 2 * (v13 / 9)
// v13=90,故 String[3]='P'(码值为 80)
&& !strcmp(v4, "ak1w")
// ak1w 解密为 jMp,故 String[5,6,7]="jMp"
&& !strcmp(v5, "V1Ax")
// V1Ax 解密为 WP1,故 String[2,3,4]="WP1"综上,唯一符合条件的字符串 String 为 UJWP1jMp,因此提交 flag{UJWP1jMp} 即可。
简单注册器
本题文件为安卓的 apk 文件,由于 IDA 反编译处理着实有些看不懂,因此使用 JEB 进行反编译。在字符串一栏搜索到了关键字 flag{,双击即可在右侧看到调用该字符串的代码。
略微审计一下代码,发现 flag 仅由如下代码块生成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
char[] arr_c = "dd2940c04462b4dd7c450528835cca15".toCharArray();
arr_c[2] = (char)(arr_c[2] + arr_c[3] - 50);
arr_c[4] = (char)(arr_c[2] + arr_c[5] - 0x30);
arr_c[30] = (char)(arr_c[0x1F] + arr_c[9] - 0x30);
arr_c[14] = (char)(arr_c[27] + arr_c[28] - 97);
int i;
for(i = 0; i < 16; ++i) {
char a = arr_c[0x1F - i];
arr_c[0x1F - i] = arr_c[i];
arr_c[i] = a;
}
textview.setText("flag{" + String.valueOf(arr_c) + "}");
return;}虽说这么点代码不难审计,但是毕竟反编译代码十分完善,所以完全可以直接执行该代码块。将 textview.setText 换成 Java 的输出语句即可将 flag 输出,计算结果为 flag{59acc538825054c7de4b26440c0999dd}。
[GWCTF 2019]pyre
本题文件为 Python 编译后的二进制文件,直接丢进 在线工具 反编译一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 2.7
print "Welcome to Re World!"
print "Your input1 is your flag~"
l = len(input1)
for i in range(l):
num = ((input1[i] + i) % 128 + 128) % 128
code += num
for i in range(l - 1):
code[i] = code[i] ^ code[i + 1]
print code
code = [
"%1f", "%12", "%1d", "(", "0",
"4", "%01", "%06", "%14", "4",
",", "%1b", "U", "?", "o",
"6", "*", ":", "%01", "D",
";", "%", "%13",]程序读取输入,并且进行逐个取模、相加、异或操作后输出数据,而输出的数据在代码中已经给出,正如提示所言,现在求出用户输入即为 flag。
逆向编写一个脚本即可,脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14code = ["%1f", "%12", "%1d", "(", "0",
"4", "%01", "%06", "%14", "4",
",", "%1b", "U", "?", "o",
"6", "*", ":", "%01", "D",
";", "%", "%13"]
flag = ''
for i in range(len(code) - 2, -1, -1):
code[i] = chr(ord(code[i]) ^ ord(code[i + 1]))
for i in range(len(code)):
num = chr((ord(code[i]) - i) % 128)
flag += num
print(flag)执行之后即可输出 GWHT{Just_Re_1s_Ha66y!},直接提交 flag{Just_Re_1s_Ha66y!}。
总结
相比于此前 Web 与 PWN,这边的许多题目笔者都可以独立完成,大概也是因为刚入门吧,这个方向对新手还是蛮友好的。对于 Reverse 这边整体感觉就是,嗯对,逆向。不过,虽说整体毕竟顺利,但是动辄代码审计真的非常痛苦,还老是对伪代码审计。对于笔者这种正着都写不好代码的人来说,还要逆过来分析着实不易。
到这里 CTF 已经开辟了三个方向了,虽说都只是浅浅的了解了一下,但总归对自己的技术层次有了更深的了解,兴趣也被提上来了!