基本介绍
逆向这部分和密码学挂钩,并且涉及Windows、Linux、Android 平台的多种编程技术,要求利用常用工具对源代码及二进制文件进行逆
向分析,掌握Android移动应用APK文件的逆向分析,掌握加解密、内核编程、算法、反调试和代码混淆技术。
要求:
- 熟悉如操作系统,汇编语言,加解密等相关知识
- 具有丰富的多种高级语言的编程经验
- 熟悉多种编译器的编译原理
- 较强的程序理解和逆向分析能力
常见加密算法
base64 加密
此内容在之前的4. Crypto入门中已有说明
TEA 算法
TEA 算法每一次可以操作64bit,采用128bit作为key,算法采用迭代的形式,推荐的迭代轮数是64轮,最少32轮。

RC4 算法
同样在之前的Crypto中已有提到
代码混淆
花指令
花指令(junk code)是一种专门用来迷惑反编译器的指令片段,这些指令片段不会影响程序的原有功能,但会使得反汇编器的结果出现偏差,从而使破解者分析失败。比较经典的花指令技巧有利用jmp、call、ret指令改变执行流,从而使得反汇编器解析出与运行时不相符的错误代码。
Self-Modifield Code
控制流平坦化
movofuscator
| 混淆技术 |
原理简述 |
逆向对抗策略 |
| 花指令 (Junk Code) |
插入无意义指令,或利用 jmp /call /ret 改变执行流,使反编译器错误解析代码。 |
手动修复执行流,识别并跳过 / 删除花指令。 |
| Self-Modified Code |
代码在运行时动态修改自身的指令或数据。 |
在修改发生后,重新进行动态分析和反汇编。 |
| 控制流平坦化 (Control Flow Flattening) |
将所有条件判断和循环结构转化为一个集中的主调度循环(switch/case )。 |
还原控制流图(CFG),利用工具辅助去混淆。 |
| Movfuscator |
仅使用 mov 指令实现所有逻辑运算和控制流。 |
专注于数据流分析,而非指令语义。 |
迷宫问题
这个算是逆向里面比较典型的一种题目类型,迷宫问题有以下特点:
- 在内存中布置一张“地图”
- 将用户输入限制在少数几个字符范围内.
- 一般只有一个迷宫入口和一个迷宫出口
布置的地图可以由可显字符(比如#和*)组合而成(这非常明显,查看字符串基本就知道这是个迷宫题了.),也可以单纯用不可显的十六进制值进行表示.可以将地图直接组成一条非常长的字符串,或是一行一行分开布置.如果是一行一行分开布置的话,因为迷宫一般都会比较大,所以用于按行(注意,布置并非按顺序布置,每行都对应一个具体的行号,你需要确定行号才能还原迷宫地图)布置迷宫的函数会明显重复多次。
而被限制的字符通常会是一些方便记忆的组合(不是也没办法),比如w/s/a/d,h/j/k/l,I/r/u/d这样的类似组合.当然各个键具体的操作需要经过分析判断(像那种只用一条字符串表示迷宫的,就可以用t键表示向右移动12个字符这样).对于二维的地图,一般作者都会设置一个X坐标和一个Y坐标用于保存当前位置,我们也可以根据这个特点来入手分析。
一般情况下,迷宫是只有1个入口和1个出口,像入口在最左上角(0,0)位置,而出口在最右下角(max_X,max_Y)处.但也有可能是出口在迷宫的正中心,用一个Y字符表示等等.解答迷宫题的条件也是需要根据具体情况判断的。
当然迷宫的走法可能不止1条,也有情况是有多条走法,但是要求某一个走法比如说代价最小.那么这就可以变相为一个算法问题。
程序脱壳
保护壳类型有许多,简单的压缩壳可以归类为如下几种:
直接将程序代码全部解压到内存中再继续执行代码。
- unpack -> execute -> unpack -> execute
解压部分代码,再边解压边执行。
- unpack -> [decoder | encoded code] ->decode ->execode
代码有过编码,在解压后再运行函数将真正的程序代码解压执行。
对于脱壳也有相关的方法,比如单步调试法,ESP定律等等。
对于逆向和pwn,前期可以先去学习ida、Lgdb、pwntools、jadx-gui这些工具的基本使用,慢慢学习对程序进行分析、调试,pwn的话是需要控制程序执行流,逆向则需要掌握一些常见的加密算法特征以及解密的方法。
此外,就是对于Python脚本的编写,以及对于C、C++伪代码、汇编代码的审计与分析能力。
例题
helloworld(ida反编译)
下载题目所给文件,使用IDA打开。
可以直接看到代码中的flag的值。
flag{7ujm8ikhy6}
base(base64换表)
使用IDA打开,先按F5对main函数进行反编译,可以看到里面有一个变量名为a5mc58bphliax7j的字符串,双击后看到值为5Mc58bPHLiAx7J8ocJIlaVUxaJvMcoYMaoPMaOfg15c475tscHfM/8==,按ESC返回,继续分析main函数,可以看到其中使用sub_401570这个函数对字符串进行了处理,双击查看:
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
|
__int64 __fastcall sub_401570(const char *a1, _QWORD *a2, int *a3)
{
int v6; // r15d
int v7; // r12d
int v8; // r13d
__int64 v9; // r14
_BYTE *v10; // rax
_BYTE *v11; // r9
__int64 v12; // r8
char v13; // cl
char v14; // r11
char v15; // r10
__int64 result; // rax
v6 = strlen(a1);
v7 = v6 % 3;
if ( v6 % 3 )
{
v8 = 4 * (v6 / 3) + 4;
v9 = v8;
v10 = malloc(v8 + 1i64);
v10[v8] = 0;
if ( v6 <= 0 )
goto LABEL_5;
}
else
{
v8 = 4 * (v6 / 3);
v9 = v8;
v10 = malloc(v8 + 1i64);
v10[v8] = 0;
if ( v6 <= 0 )
goto LABEL_8;
}
v11 = v10;
v12 = 0i64;
do
{
v11 += 4;
v13 = a1[v12];
*(v11 - 4) = aQvejafhmuyjbac[v13 >> 2];
v14 = a1[v12 + 1];
*(v11 - 3) = aQvejafhmuyjbac[(v14 >> 4) | (16 * v13) & 0x30];
v15 = a1[v12 + 2];
v12 += 3i64;
*(v11 - 2) = aQvejafhmuyjbac[(v15 >> 6) | (4 * v14) & 0x3C];
*(v11 - 1) = aQvejafhmuyjbac[v15 & 0x3F];
}
while ( v6 > (int)v12 );
LABEL_5:
if ( v7 == 1 )
{
v10[v9 - 2] = 61;
v10[v9 - 1] = 61;
}
else if ( v7 == 2 )
{
v10[v9 - 1] = 61;
}
LABEL_8:
*a2 = v10;
result = 0i64;
*a3 = v8;
return result;
}
|
可以看到里面有提到aQvejafhmuyjbac这个数组,双击查看,值为qvEJAfHmUYjBac+u8Ph5n9Od17FrICL/X0gVtM4Qk6T2z3wNSsyoebilxWKGZpRD将其作为base64表解密得到flag:NSSCTF{a8d4347722800e72e34e1aba3fe914ae}
pyc(pyc反编译)
pyc文件反编译的方法:
使用在线网站
python反编译
在线Python pyc文件编译与反编译
使用工具 pycdc.exe
1
|
pycdc.exe input.pyc -o output.py
|
使用 uncompyle 库
1
2
|
pip install uncompyle
uncompyle test.pyc > test.py
|
题解:
在在线网站中打开pyc文件,在代码中看到custom_table的值为自定义码表,使用cyberchef解码即可
CnHongKe{SpAcia1_ba3e_64}
o.0(简单替换)
使用IDA打开exe文件,同样使用F5对main函数进行反编译,看到里面调用了一个mian_0函数,我们双击跟进,可以看到有如下的语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
v3 = &v7;
for ( i = 82i64; i; --i )
{
*(_DWORD *)v3 = -858993460;
v3 += 4;
}
for ( j = 0; ; ++j )
{
v10 = j;
if ( j > j_strlen(Str2) )
break;
if ( Str2[j] == 111 )
Str2[j] = 48;
}
sub_1400111D1("input the flag:");
sub_14001128F("%20s", Str1);
v5 = j_strlen(Str2);
if ( !strncmp(Str1, Str2, v5) )
sub_1400111D1("this is the right flag!\n");
else
sub_1400111D1("wrong flag\n");
return 0;
}
|
这里着重关注Str2这个变量,所以我们双击跟进,发现它的值是{hello_world},回到刚才的代码处,继续分析下面的代码,看到第12~13行应该是对字符串做了一个替换,不过这里写的都是ASCII码,我们选中这两个数字,按R,查看其对应的字符。发现是将字符o都替换成0,由此可以确定flag为:
flag{hell0_w0rld}
xor(异或)
此处指的是按位异或,即把字符先转为8位二进制,然后对应位分别进行异或。
异或性质:
A ^ B ^ B = A
演示:
1
2
3
4
5
6
7
8
|
A = 100
B = 67
C = A ^ B # 第一次异或
print(C)
A_restored = C ^ B # 第二次异或
print(A_restored)
|
题解:
使用IDA打开,同样使用F5对main函数进行反编译,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int __fastcall 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;
}
|
可以看到程序首先判断输入的字符串长度是否等于33,接着使用异或(后一位与前一位)进行了加密,最终将加密后的内容与global这个变量进行比较,所以我们先双击global跟进一下。
1
|
_global dq offset aFKWOXZUPFVMDGH
|
可以看到global的数据指向了变量aFKWOXZUPFVMDGH,继续双击跟进。
这个变量的数据有好几个,我们使用shift+E,选择string literal提取值:
1
|
f\nk\fw&O.@\x11x\rZ;U\x11p\x19F\x1Fv\"M#D\x0Eg\x06h\x0FG2O
|
在Python中,字节串(bytes)是一种特殊的数据类型,它由一系列字节组成。每个字节都是一个整数,取值范围从 0 到 255。字节串 byte_data是由原始字节值构成的,二字节值本身就是整数美因茨直接转为列表时,你会看到每个字节被表示为它的 **整数值 **,即其 ASCII 值(或字节值)。
1
2
3
|
enc = b'f\nk\fw&O.@\x11x\rZ;U\x11p\x19F\x1Fv\"M#D\x0Eg\x06h\x0FG2O'
list = list(enc)
print(list)
|
再次基础上我们可以扩展出解题脚本:
1
2
3
4
5
6
7
8
|
enc = b'f\nk\fw&O.@\x11x\rZ;U\x11p\x19F\x1Fv\"M#D\x0Eg\x06h\x0FG2O'
list = list(enc)
# print(list)
flag = 'f'
for i in range(1,len(list)):
m = list[i]^list[i-1]
flag += chr(m)
print(flag)
|
<font style="color:rgba(0, 0, 0, 0.88);">flag{QianQiuWanDai_YiTongJiangHu}</font>
RC4
使用IDA打开,同样使用F5对main函数进行反编译,对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
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
|
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-44Ch]
char v5; // [esp+0h] [ebp-44Ch]
char v6; // [esp+0h] [ebp-44Ch]
char v7; // [esp+0h] [ebp-44Ch]
char v8; // [esp+0h] [ebp-44Ch]
char v9; // [esp+0h] [ebp-44Ch]
_DWORD v10[3]; // [esp+194h] [ebp-2B8h] BYREF
int v11; // [esp+1A0h] [ebp-2ACh]
FILE *v12; // [esp+1ACh] [ebp-2A0h]
FILE *v13; // [esp+1B8h] [ebp-294h]
char v14[264]; // [esp+1C4h] [ebp-288h] BYREF
char v15[264]; // [esp+2CCh] [ebp-180h] BYREF
char Str1[60]; // [esp+3D4h] [ebp-78h] BYREF
char Str[56]; // [esp+410h] [ebp-3Ch] BYREF
__CheckForDebuggerJustMyCode(&unk_40B027);
memset(Str, 0, 50);
memset(Str1, 0, 50);
memset(v15, 0, 0x100u);
memset(v14, 0, 0x100u);
v11 = 1;
do
{
sub_401037("**************************我的Flag出了什么问题??**************************\n", v4);
sub_401037(asc_406BFC, v5);
sub_401037("**************************那有没有恢复的方法呢??**************************\n", v6);
sub_401037(asc_406CA8, v7);
sub_401037("做出你的选择:\n", v8);
sub_401037("1.充钱\n2.退出\n", v9);
sub_401073("%d", (char)v10);
if ( v10[0] == 1 )
{
v13 = fopen("flag.txt", "r");
if ( !v13 )
{
sub_401037("打开源文件失败!\n", v4);
getchar();
exit(0);
}
v12 = fopen("enflag.txt", "w");
if ( !v12 )
{
sub_401037("找不到加密文件!\n", v4);
getchar();
exit(0);
}
sub_401037("\n请输入您的密钥:", v4);
sub_401073("%s", (char)Str);
sub_401069(Str, Str1);
sub_401028(Str, v15, v14, v13, v12);
}
else if ( v10[0] == 2 )
{
v11 = 0;
}
else
{
sub_401037("\n操作不合法!\n\n", v4);
}
}
while ( v11 );
return 0;
}
|
分析代码,可以看到程序需要同时有flag.txt和enflag.txt两个文件存在才能正常运行,接下来sub_401069函数对输入的字符串Str进行了处理,得到Str1,我们跟进这个函数看看做了什么:里面套娃了函数sub_401A70,继续跟进。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
char __cdecl sub_401A70(char *Str, char *Str1)
{
char v3; // [esp+0h] [ebp-E4h]
signed int i; // [esp+D0h] [ebp-14h]
signed int v5; // [esp+DCh] [ebp-8h]
__CheckForDebuggerJustMyCode(&unk_40B027);
v5 = strlen(Str);
for ( i = 0; i < v5; ++i )
Str1[i] += Str[i] ^ 0x1F;
if ( !strcmp(Str1, "DH~mqqvqxB^||zll@Jq~jkwpmvez{") )
sub_401037("充值成功.\n", v3);
else
sub_401037("Error!\n", v3);
return *Str1;
}
|
Str为我们输入的内容,函数将 Str 中的每个字符与 0x1F 异或,结果存储在 Str1 中,
之后将 Str1 与字符串 DH~mqqvqxB^||zll@Jq~jkwpmvez{进行比较,如果两个字符串完全相同则返回充值成功。
根据异或特性,逆向求解密钥:
1
2
3
4
5
|
str1 = "DH~mqqvqxB^||zll@Jq~jkwpmvez{"
str = ''
for i in str1:
str += chr(0x1f^ord(i))
print(str)
|
跑出来结果是[Warnning]Access_Unauthorized,可能是个密钥,但是不是flag,
继续跟进下一个函数sub_401028(套娃sub_4014E0):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int __cdecl sub_4014E0(char *Str, int a2, int a3, FILE *Stream, FILE *a5)
{
char v6; // [esp+0h] [ebp-D8h]
size_t v7; // [esp+D0h] [ebp-8h]
__CheckForDebuggerJustMyCode(&unk_40B027);
v7 = strlen(Str);
sub_4010F0(a2, Str, v7);
sub_4010C8(a3);
sub_40116D(a3, a2);
sub_4010EB(a3, Stream, a5);
fclose(Stream);
fclose(a5);
return sub_401037(aE, v6);
|
里面有4个函数,直接扔给AI分析:
| 伪代码函数名 |
对应 RC4 步骤 |
作用描述(C/C++ 伪代码实现) |
sub_401780(a1) |
S-box 初始化(Initialization) |
将 256 字节的目标数组 a1 (S-box S)初始化为恒等排列:S[i]=i。 |
sub_401800(a1, a2, a3) |
密钥扩展(Key Expansion) |
将密钥 a2 (长度 a3 )扩展成 256 字节的临时数组 K。如果密钥短于 256 字节,则循环重复填充。 |
sub_4018E0(a1, a2) |
密钥调度算法(KSA) |
使用扩展后的密钥 K(a2 )对 S-box S(a1 )进行 256 轮置乱,使其进入高度随机状态。 |
sub_4015E0(a1, Stream, a3) |
伪随机生成与加解密(PRGA) |
循环生成密钥流 Ki,并将其与输入文件(Stream )的字节异或(Ci=Pi⊕Ki),结果写入输出文件(a3 )。 |
函数 sub_4014E0 将上述所有 RC4 组件整合在一起,实现了对文件内容进行 RC4 加密或解密的功能。
使用cyberchef进行解密,填入上面的密钥[Warnning]Access_Unauthorized作为Passphrase,选择文件enflag.txt作为input,得到flag:
flag{RC4&->ENc0d3F1le}
faze(动态调试exe)
IDA 快捷键 A:将数据转换位置字符串
其他常用的快捷键:
- F5:一键反编译
- R:将数字转换为ASCII码对应值
- shift+F12:可以打开string窗口,一键找出所有的字符串
- shift+E:快速提取数据
题解
使用IDA打开,同样使用F5对main函数进行反编译,看到第42行左右有对输入的字符串做一个比较判断:
1
2
3
4
|
if ( (unsigned __int8)std::operator==<char>(argc, argv, v12, v11) )
v8 = std::operator<<<std::char_traits<char>>(argc, argv, "Correct!", refptr__ZSt4cout);
else
v8 = std::operator<<<std::char_traits<char>>(argc, argv, "Incorrect!", refptr__ZSt4cout);
|
但是直接跟进参数v12和v11里面都是空的,应该是程序把用来比较的字符串存在其他地方了。
我们点击if那行左面的蓝色小点给程序下一个断点,然后在上面的调试器中选择 **Local Windows debugger **,点击绿色三角开始调试,如图:

在弹出的程序窗口中随便输入一串字符回车,虽然程序不会有任何回弹,但是用来判断的字符串很可能已经解密并且存在内存中了,我们再次F5对main函数进行反编译,找到那行if语句,分别对v12和v11进行跟进,发现flag应该在v12中,选择flag的第一个字符,按A转为字符串即可。
ISCC{(,O?SX=_y6k]}
SP26(upx脱壳)
UPX (Ultimate Packer for eXecutables)是一种免费、开源、流行的可执行文件压缩器(或称为 壳),你可以在GitHub上下载它:GitHub - upx/upx: UPX - the Ultimate Packer for eXecutables。
UPX 的核心目的是 缩小文件大小,不过它的压缩和自解压机制使其同时具有了混淆代码和保护程序的作用。
如果要学习手动脱壳,可以去学习一下 ESP 定律,这里使用工具一键脱壳:
1
2
3
4
5
|
1. 加壳
upx.exe -o <output_file> <input_file>
2. 脱壳
upx.exe -d <input_file>
|
补充: IDA快捷键D循环切换各种数据定义大小:db→dw→dd→dq→do,然后循环。
题解
如果直接用IDA打开会看到左边只有start这一个函数,这就是upx加壳的特征。
使用upx脱壳后,再用IDA打开,像上题一样下断点动调if函数,跟进字符串参数v13后多次按D切换数据定义大小即可看到flag
ISCC{r8X@u$P0#q^m}
maze(花指令与迷宫题)
花指令实质就是一串垃圾指令,它与程序本身的功能无关,并不影响程序本身的逻辑。
在软件保护中,花指令被作为一种手段来增加静态分析的难度。
花指令也可以被用在病毒或木马上,通过加入花指令改变程序的特征码,躲避杀软的扫描,从而达到免杀的目的。
题解
- 修复程序
使用IDA打开发现代码中有报红,原因是 call 了一个奇怪的地址,IDA无法分析,F5也无法反编译:
1
2
3
4
5
6
|
.text:00401028 xor eax, ecx
.text:0040102A cmp eax, ecx
.text:0040102C jnz short near ptr loc_40102E+1
.text:0040102E
.text:0040102E loc_40102E: ; CODE XREF: .text:0040102C↑j
.text:0040102E call near ptr 0EC85D78Bh
|
cmp eax, ecx会比较 eax 和 ecx 寄存器的值。
如果这两个值不相等,cmp指令会设置条件码中的 ZF(零标志)为 0,表示为不相等。
接下来,jnz 会检查 ZF 标志,如果为0(即不相等),程序就会跳转到 loc_40102E+1,也就是 call 指令的位置。
如果相等,则继续执行下一条指令。
观察程序停在的地方,call 指令用于调用一个函数,调用了一个远程函数地址 0EC85D78B,并且是near
类型的调用,意味着它是在当前段内进行的跳转。由于地址看起来非常奇怪,这是
一个不存在的地址,就会导致 IDA 无法正常反汇编出原始代码。
我们需要将它们 Nop(汇编中的空指令)掉(也就是使用 Nop 指令将它们改为空)
首先将 jnz 跳转指令 nop 掉:右键相应行,选择 NOP
接下来我们要按D把call命令所在行转成字节序列,对里面的数据逐个nop,直到程序能正常被反编译位置。
幸好这里的第一个数据 0E8h就是所谓的花指令,把它nop掉就可以了。
在工具栏中依次选择 Edit-Patch program-Apply patches to ,打包修改后的程序。

- 破解迷宫
用IDA打开新的exe,按F5反编译:
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
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [esp+10h] [ebp-14h]
char v5[16]; // [esp+14h] [ebp-10h] BYREF
sub_401140(aGoThroughTheMa);
scanf("%14s", v5);
for ( i = 0; i <= 13; ++i )
{
switch ( v5[i] )
{
case 'a':
--dword_408078;
break;
case 'd':
++dword_408078;
break;
case 's':
--dword_40807C;
break;
case 'w':
++dword_40807C;
break;
default:
continue;
}
}
if ( dword_408078 == 5 && dword_40807C == -4 )
{
sub_401140(aCongratulation);
sub_401140(aHereIsTheFlagF);
}
else
{
sub_401140(aTryAgain);
}
return 0;
}
|
注意到里面switch语句中的wasd,说明这是一道迷宫题。
shift+F12打开string窗口,在里面看到了一个有很多星号的字符串,这是迷宫的地图。
我们写脚本把它正确打印出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
def main():
maze = "*******+********* ****** **** ******* **F****** ******************" # 迷宫数据:+为起点,F为终点
sid = maze.index("+") # 查找起点 '+' 的索引
eid = maze.index("F") # 查找终点 'F' 的索引
ex, ey = -4, 5 # 用于计算迷宫尺寸的参数
c = (eid - ey) // (-ex) # 计算迷宫的列数
r = len(maze) // c # 计算迷宫的行数
for i in range(r):
print(maze[c * i: c * (i + 1)]) # 按列切割并打印迷宫
if __name__ == "__main__":
main()
"""运行结果是下面的:
*******+**
******* **
**** **
** *****
** **F****
** ****
**********
"""
|
答案应该是ssaaasaassdddw,运行程序,输入答案得到flag:
flag{ssaaasaassdddw}
安卓逆向(jadx-gui使用)
jadx是一款开源的常用安卓逆向工具,你可以在GitHub上下载它:Releases · skylot/jadx
mobile-签到1
在导航-文本搜索中直接检索关键词
1
2
3
4
5
6
7
|
flag
flag1
flag{
ZmxhZ
ctf
ctf{
等等
|
本题的关键字是ctf{
flag{Andr01d_1s_so00oo_e@sy_t0_cr4ck!!!}
mobile-签到2
进入源代码-work.pangbai.ezmydroid(因为原题名叫pangbai)
在AESECBUtils中看到使用的加密逻辑是AES的ECB模式
在FirstFragment-binding中看到了密文cTz2pDhl8fRMfkkJXfqs2t8JBsqLkvQZDLYpWjEtkLE=和密钥1145141919810000

在cyberchef中先对base64解密,再AES-ECB解密:

得到flag:flag{@_g00d_st@r7_f0r_ANDROID}