吾爱破解2022春节——Windows中级题

拿到程序的第一时间运行一下,熟悉一下程序的流程。这个程序的流程是要我们输入UID和key。用PE工具查一下壳,发现有UPX壳,用ESP定律即可脱壳。验证程序是否脱壳成功,可载入OD看是否能查询到字符串,或载入IDA查看是否有函数或是否可以反编译出伪代码。脱壳后还要保证程序与未脱壳程序执行的流程要一致。

IDA静态分析

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
...
SetConsoleTitleA("【2022春节】解题领红包之三");
sub_403930((int)dword_41DDD0, "Input your UID: ");
sub_402800(&v27);
if ( v27 > 2000000 ) // v27 == UID,不能大于2000000
{
v3 = sub_403930((int)dword_41DDD0, "Invalid UID, please input again.");
sub_402660(10);
v4 = 0;
...
}
sub_403930((int)dword_41DDD0, "Input your Key: ");
sub_403BC0(&dword_41DE60, &v29); // v29 == key
v8 = sub_401100(v27); // v8 = UID % 25
v9 = sub_401080(v27); // v9 = map[UID % 12]
v25 = sub_401110(v9); // 与v9有关,即与map[UID % 12]有关
v24 = v8; // v24 = v8 = UID % 25
v28 = &v20;
sub_402460((int)&v20, (int)&v29); // v20 = v29 = key
if ( sub_401520(v20, v21, v22, v23, v24, v25) == 1 )// 与key, UID % 12和UID % 25有关,关键函数
{
v10 = sub_403930((int)dword_41DDD0, "Success");
sub_402660(10);
v11 = 0;
...
}
...
1
2
3
4
int __cdecl sub_401100(signed int a1)
{
return a1 % 25;
}
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
int __cdecl sub_401080(signed int a1)
{
int v2; // [sp+0h] [bp-30h]@1
int v3; // [sp+4h] [bp-2Ch]@1
int v4; // [sp+8h] [bp-28h]@1
int v5; // [sp+Ch] [bp-24h]@1
int v6; // [sp+10h] [bp-20h]@1
int v7; // [sp+14h] [bp-1Ch]@1
int v8; // [sp+18h] [bp-18h]@1
int v9; // [sp+1Ch] [bp-14h]@1
int v10; // [sp+20h] [bp-10h]@1
int v11; // [sp+24h] [bp-Ch]@1
int v12; // [sp+28h] [bp-8h]@1
int v13; // [sp+2Ch] [bp-4h]@1

v2 = 1;
v3 = 3;
v4 = 5;
v5 = 7;
v6 = 9;
v7 = 11;
v8 = 15;
v9 = 17;
v10 = 19;
v11 = 21;
v12 = 23;
v13 = 25;
return *(&v2 + a1 % 12);
}
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
char __cdecl sub_401520(char a1, int a2, int a3, int a4, signed int a5, int a6)
{
...
v19 = v18;
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(0);
sub_4034E0("flag", strlen("flag")); // flag初始化
v23 = v18;
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(0);
sub_4034E0(&dword_41A0F8, strlen((const char *)&dword_41A0F8));
...
sub_402BE0(&dword_41A0B0, strlen((const char *)&dword_41A0B0));// flag初始化后:v19[]=flag{Happy_New_Year_52Pojie_2022}
v17 = a5; // v17 = UID % 25
v16 = a6; // v16 = map[UID % 12]
v51 = &v12;
sub_402460((int)&v12, (int)&a1); // v12 = a1 = key
sub_4011B0((int)&v50, v12, v13, v14, v15, v16, v17);// 这个关键函数的返回值存到v50[],也就是说我们输入的key要经过转换才能变成v19[]
LOBYTE(v52) = 29;
v8 = sub_403ED0((int)&v50, (int)&v19); // v8 == 0,字符串比较函数,比较v19[]与v50[]
LOBYTE(v52) = 28;
v17 = 1;
if ( v8 ) // v8 == 0
{
...
return = 0; // 返回0是错的,可以不看
}
else
{
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(v17);
LOBYTE(v52) = 27;
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(1);
LOBYTE(v52) = 26;
...
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(1);
LOBYTE(v52) = 0;
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(1);
v52 = -1;
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(1);
result = 1;
}
return result;
}

由于sub_4011B0里面的算法看伪代码太过复杂,想直观看到key变换成什么,可以用OllyDbg动态调试,直接看比较函数sub_403ED0出来的与原key比较。

OD动态调试

程序说的UID其实是吾爱论坛的UID,我看大牛们wp时百度了好久UID在哪里查看…

载入OD,输入自己的UID和随便的key,比如hhhhhhhhhhhh。输入完后去到sub_401520进去,再去到sub_403ED0下断运行至此处。

1
2
3
4
5
6
7
00401CB2    E8 F9F4FFFF     call dumped_.004011B0
00401CB7 83C4 1C add esp,0x1C ;403ED0参数从这里开始
00401CBA 8D4C24 10 lea ecx,dword ptr ss:[esp+0x10]
00401CBE C68424 EC010000>mov byte ptr ss:[esp+0x1EC],0x1D
00401CC6 51 push ecx ;第二个参数入栈v19[]=flag{Happy_New_Year_52Pojie_2022}的首地址
00401CC7 8D8C24 D4010000 lea ecx,dword ptr ss:[esp+0x1D4];第一个参数的首地址在ecx中(v50[])
00401CCE E8 FD210000 call dumped_.00403ED0

为了验证入栈的是否是v19,可查看堆栈窗口。

1
2
3
4
5
6
7
0012FD44   0012FD58	;首地址为0012FD58
0012FD48 006B5B28
0012FD4C 00000017
0012FD50 0012FFC0
0012FD54 FFFFFFFF
0012FD58 003200FF ;但这个并不是v19,再下一个地址才是
0012FD5C 00392009 ASCII "flag{Happy_New_Year_52Pojie_2022}"

再找ecx->数据窗口中跟随,同理,第二个地址存放的是比较的数据。

1
0012FF18  00 00 00 00 99 20 39 00 0C 00 00 00 1F 00 00 00  ....?9........

继续数据窗口跟随,我们输入的“h”已经全变为“q”了。说明很有可能是单表替换中的移位密码,位数为9。

1
00392099  71 71 71 71 71 71 71 71 71 71 71 71 00 00 00 00  qqqqqqqqqqqq....

再试多几次验证一下,发现不是移位密码!

happynewyear->qxiizktbztxg,只是普通的单表替换。

a b c d e f g h i j k l m n o p q r s t
x w v u t s r q p o n m l k j i h g f e
u v w x y z A B C D E F G H I J K L M N
d c b a z y X W V U T S R Q P O N M L K
O P Q R S T U V W X Y Z 0 1 2 3 4 { _
J I H G F E D C B A Z Y 0 1 2 3 4 { _

所以flag{Happy_New_Year_52Pojie_2022}逆过来就是smxr{Qxiiz_Ktb_Ztxg_52Ijopt_2022}

注意,这个只是在我UID为1787123的情况下的替换表,UID不同替换表也不同。我看wp说这其实是仿射密码,也就是替换表与静态分析map[UID % 12]UID % 25其实是有关系的。

会的还是太少了,仿射密码的加解密原理自行百度,这里不想写了,最后附上大牛的注册机:

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
#include <stdio.h>
#include <string.h>
char const flag[] = "flag{Happy_New_Year_52Pojie_2022}";
int map[] = {1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25};
int main()
{
int uid;
while (scanf("%d", &uid) != EOF)
{
int A = map[uid % 12];
int B = uid % 25;
char buf[sizeof(flag)];
strcpy(buf, flag);
char *p = buf;
while (*p)
{
if ('a' <= *p && *p <= 'z')
{
*p = ((*p - 'a') * A + B) % 26 + 'a';
}
else if ('A' <= *p && *p <= 'Z')
{
*p = ((*p - 'A') * A + B) % 26 + 'A';
}
++p;
}
printf("%s\n", buf);
}
return 0;
}