安卓逆向 刷题笔记(1)
攻防世界 Mobile
ez_dex
- 这道题在网上看了好多 wp 基本都是手撕加密,但这题的 so 层加密挺不好分析的,而用frida-hook可以直接跳过解密,找到已经解密好的 dex 文件。
- 用 jadx 打开,在 AndroidManifest.xml
中找到关键信息:
android:hasCode="false",声明 APK 中不包含 Java/Kotlin 字节码(.dex 文件),表示所有逻辑都在 Native 层;当android:value="native"时,系统会默认加载 libnative.so,此类应用的入口是 Native 层的android_main()函数(而非 Java 的 Activity.onCreate()
- 提取出 so 文件,用 ida
分析,直接搜索找到
android_main()函数,首先注意到经过加密的filename和name,写一个脚本进行字符串解密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
37import struct
filename_dwords=[-1651995345,-2003974520,-1966700387,
-2000190330,-2071422265,-947092071, -1920499569,
-1936879484,-2138061167,-962950011, -1702328950,
-946172774, -376337267, ]
#13*4=52
filename_bytes=bytearray()
for dword in filename_dwords:
filename_bytes.extend(struct.pack('<i',dword))
#stuck.pack('<i',dword) 将每个整数打包为小端序的四字节二进制数据
#extend()将这些字节添加到字节数组中
filename_bytes.append(0)
#在字节数组末尾添加一个空字节,作为字符串的终止符
for i in range(1,53):
filename_bytes[i]^=0xE9
filename=filename_bytes.split(b'\0')[0].decode('utf-8')
name_dwords=[-1651995194,-2003974520,-1966700387,-2000190330,
-2071422265,-947092071 ,-1920499569,-1936879484,2138061167,
-962950011 ,-1853059706]
#11*4+1+1=46
name_bytes=bytearray()
for dword in name_dwords:
name_bytes.extend(struct.pack('<i',dword))
v42=-5690
name_bytes.extend(struct.pack('<h',v42))
name_bytes.append(0)
name_bytes[0]=0x2f
for i in range(1,47):
name_bytes[i]^=0xE9
name=name_bytes.split(b'\0')[0].decode('utf-8')
print("filename: ",filename)
print("name: ",name)
# filename: /data/data/com.a.sample.findmydex/files/classes.dex
# name: /data/data/com.a.sample.findmydex/files/odex/ - 解密后发现得到了 dex 文件存放的位置,但是继续往下看,找到关键部分
用frida hook直接拿到解密后的 dex 文件
因为摇晃次数 v10 还直接控制数据解密流程,所以不能像满足的时间条件一样给 nop 掉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
32var lib = Module.load("libnative.so");
var imports = lib.enumerateImports();
var removePtr;
for (let i = 0; i < imports.length; i++) {
if (imports[i].name.indexOf("remove") != -1) {
console.log(imports[i].name, imports[i].address);
removePtr = imports[i].address;
}
}
console.log(removePtr);
Interceptor.replace(removePtr, new NativeCallback(function (arg0) {
console.log("remove called");
console.log("arg0: " + arg0);
return 0;
}
, 'int', ['pointer']));
Interceptor.attach(removePtr, {
onEnter: function (args) {
console.log("remove called");
console.log("arg0: " + args[0]);
},
onLeave: function (retval) {
console.log("remove return value: " + retval);
}
})
var targetAddr = lib.base.add(0x2866);
Memory.protect(targetAddr, 0x10, 'rwx');
targetAddr.writeByteArray([0x00, 0xBF]);
var buf = targetAddr.readByteArray(100);
console.log(buf);

BGT(Branch if Greater Than) 是关键跳转,决定是否判定超时,NOP 掉
BGT(0x2866)会使程序永远不跳转,从而绕过时间检查 4.
有报错但 dex 文件是成功 dump 出来了的
但到这里也不能直接拿到因为没有权限,所以可以把它 pull 到普通目录比如
Download 里面后再存到本地,这样就能获取了 5. 在原 apk 中添加 dex
文件,加密就已经出来了
密钥:I
have a male fish and a female fish 获取密文: 1
2
3
4
5
6
7
8
9
10
11import base64
m = [-120, 77, -14, -38, 17, 5, -42, 44, -32, 109, 85, 31, 24, -91, -112, -83,
64, -83, -128, 84, 5, -94, -98, -30, 18, 70, -26, 71, 5, -99, -62, -58, 117, 29,
-44, 6, 112, -4, 81, 84, 9, 22, -51, 95, -34, 12, 47, 77]
data = []
for i in m:
data.append(i&0xFF)
#i&0xFF 操作将有符号整数转换为无符号字
print(base64.b64encode(bytes(data)))
#b'iE3y2hEF1izgbVUfGKWQrUCtgFQFop7iEkbmRwWdwsZ1HdQGcPxRVAkWzV/eDC9N'

qwb{TH3y_Io<e_EACh_OTh3r_FOrEUER}
easyjni
变种base64 算法逆向 1. jadx 打开,看 MainACtivity
,关键方法: a(String str)方法 1
2
3
4
5
6
7private boolean a(String str) {
try {
return ncheck(new a().a(str.getBytes()));
} catch (Exception e) {
return false;
}
}
然后调用 native 方法 ncheck() 进行验证
ncheck(String str)方法
1 | |
声明了一个本地方法,实现在 libnative.so 中
查看 a 函数 ,可以分析推测出这是一个变种 base64

- 分析 so 文件, ida
打开,找到
Java_com_a_easyjni_MainActivity_ncheck,
v5 = (const char *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);这一行的等效代码为const char *input = (*env)->GetStringUTFChars(env, jstr, NULL);,+ 676是 GetStringUTFChars 的偏移量第一个循环 : 把输入字符串的前16位和后16位对调
1
2
3
4
5
6
7
8
9if ( strlen(input) == 32 )
{
for ( i = 0; i != 16; ++i )
{
v7 = &v12[i];
v12[i] = input[i + 16];
v8 = input[i];
v7[16] = v8;
}第二个循环 : 把每一对字符前后对调
1
2
3
4
5
6
7
8
9v9 = 0;
do
{
v13 = v12[v9]; // v13 =v12[0]
v12[v9] = v12[v9 + 1]; //v12[0]=v12[1]
v12[v9 + 1] = v13; //v12[1]=v13
v9 += 2;
}
- 解题脚本 >flag{just_ANot#er_@p3}
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
41def decode(base64_str):
base64_charset = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN"
base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b', '')) for s in base64_str if
s != '=']
resp = bytearray()
nums = len(base64_bytes) // 4
remain = len(base64_bytes) % 4
integral_part = base64_bytes[0:4 * nums]
while integral_part:
tmp_unit = ''.join(integral_part[0:4])
tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
for i in tmp_unit:
resp.append(i)
integral_part = integral_part[4:]
if remain:
remain_part = ''.join(base64_bytes[nums * 4:])
tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
for i in tmp_unit:
resp.append(i)
return resp
if __name__ == '__main__':
flag_1 = 'MbT3sQgX039i3g==AQOoMQFPskB1Bsc7'
print('加密后的字符串:',flag_1)
# 前后16位调换位置
flag_2 = flag_1[len(flag_1)//2:] + flag_1[0:len(flag_1)//2]
print('前后16位调换位置:',flag_2)
flag_3 = ''
for i in range(len(flag_2)//2):
flag_3 += flag_2[i*2+1] + flag_2[i*2]
print('前后两位调换位置:',flag_3)
print('解密后:',decode(flag_3))
# 加密后的字符串: MbT3sQgX039i3g==AQOoMQFPskB1Bsc7
# 前后16位调换位置: AQOoMQFPskB1Bsc7MbT3sQgX039i3g==
# 前后两位调换位置: QAoOQMPFks1BsB7cbM3TQsXg30i9g3==
# 解密后: bytearray(b'flag{just_ANot#er_@p3}')
easyjava
算法逆向 1. jadx打开,分析
MainActivity,主要逻辑大概是先提取中间部分(去除flag{}),然后对传入的每个字符调用a()方法,而a()方法中是先调用b.a(str),再调用a.a(结果)
2. 看 a 类和 b
类,主逻辑分别是将传入的整数返回对应的映射字符,以及传入的字符返回对应的映射整数
3. 解密脚本 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
46cipherText = 'wigwrkaugala'
aArray = [21,4,24,25,20,5,15,9,17,6,13,3,18,12,10,19,0,22,2,11,23,1,8,7,14,16]
aString = 'abcdefghijklmnopqrstuvwxyz'
bArray = [17,23,7,22,1,16,6,9,21,0,15,5,10,18,2,24,4,11,3,14,19,12,20,13,8,25]
bString = 'abcdefghijklmnopqrstuvwxyz'
def changeBArrayandString():
global bString
global bArray
chArray = bArray[0]
chString = bString[0:1]
#切片 [0:1] 保留原始类型(bytes),而索引 [0] 返回的是整数值
for i in range(len(bArray) - 1):
bArray[i] = bArray[i + 1]
bArray[len(bArray) - 1] = chArray
bString = bString[1:]
bString += chString
#分别把bString和bArray中的每一个元素往前移动一位,首位元素放到末尾
def getBchar(ch):
#根据输入的整数ch获取对应的字符
v2 = bArray[ch]
#使用ch作为索引,获取bArray中的对应元素
arg = bString[v2]
#使用v2作为索引,获取bString中的对应元素
changeBArrayandString()
return arg
def getAint(ch):
#根据输入的字符ch获取对应的整数
global aString
global aArray
v1 = aString.index(ch)
#获取对应字符的索引
arg5 = aArray[v1]
return arg5
print('flag{',end='')
for k in cipherText:
#先得到密文的索引
v0 = getAint(k)
print(getBchar(v0),end='')
print('}')
android 2.0
算法逆向 1.
将apk文件放入jadx中,源代码里面找到MainActiviy,找到关键部分函数

- 这段代码的功能:
这段代码实现了一个简单的 Android 应用,它有一个按钮、一个文本框和一个显示结果的文本
用户在文本框(EditText)里输入一个字符串(可能是密码),然后点击按钮时,代码会调用一个名为 JNI.getResult(str) 的本地方法来判断密码是否正确
如果 JNI.getResult(str) 返回 0,界面会显示 “Wrong”
如果 JNI.getResult(str) 返回 1,界面会显示 “Great”
点进JNI
关键部分是 JNI.getResult(str):
JNI.getResult(str) 是一个 本地方法,也就是说,它的实现并不在 Java 代码里,而是在 C/C++ 代码中,并且通过 JNI(Java Native Interface)被调用
这意味着需要分析那个本地的.so文件,找出这个getResult
方法的实现 2.
修改后缀,将.apk改成.zip,解压后找到.so文件,把libNative.so用IDA打开,找到几个关键函数
3. -
分析First函数
还原:先异或0x80,再除以2
1
2
3
4
5
6
7
8
9def First():
enc="LN^dl"
dec=""
for i in range(0,4):
dec+=(chr(int((ord(enc[i])^0x80)/2)))
dec+='l'
print(dec)
First()
#fgorl
- 分析
Second函数
直接异或还原 1
2
3
4
5
6
7
8
9
10
11def second():
a5=[ord(' '),ord('5'),ord('-'),0x16,ord('a')]
a1=[ord('L'),ord('N'),ord('^'),ord('d'),ord('l')]
s1=""
for i in range(0,4):
x=a5[i]^a1[i]
s1+=chr(x)
s1+='a'
print(s1)
second()
#l{sra
Third同理
如果v6=a5就执行异或操作,所以v6=a5
1
2
3
4
5
6
7
8
9
10
11
12 def third():
a5 = [ord(' '), ord('5'), ord('-'), 0x16, ord('a')]
dec = [ord('A'), ord('F'), ord('B'), ord('o'), ord('}')]
enc = ""
for i in range(0, 4):
x = a5[i] ^ dec[i]
enc += chr(x)
enc += '}'
print(enc)
third()
#asoy}
- 由
Init函数还原flag - Init函数是将输入的长度为15的字符串,每三个为一组,得到三组字符串(即刚刚解出的三组数据)
1 | |
flag{sosorryla}
基础android
用misc方法解题了 1.
将apk文件放入010editor中,发现文件头是50 4B 03
04是zip文件,修改后缀,解压 
- 在assets文件夹下发现
time_2.zip,放入010editor,文件头是FF D8 FF,jpg文件,修改后缀,打开后是一张图片
flag{08067-wlecome}
APK逆向
- 打开apk文件,用
jadx打开,找到关键MainActiivty, 注意到edit_sn和edit_userName,找到edit_userName = "Tenshine", - 在
CheckSN中看到代码逻辑
md5加密,转为hex字符,for循环注意i+=2 - 解题脚本
1
2
3
4
5
6
7
8
9
10import hashlib
str=b"Tenshine"
str1=hashlib.md5(str).hexdigest()
flag=""
for i in range(len(str1)):
if(i%2==0):
flag+=str1[i]
print(flag)
#bc72f242a6af3857a
flag为
bc72f242a6af3857a
APK逆向-2
- 用 jadx 打开后发现
AndroidManifest反编译失败,改 apk 后缀为 zip ,解压后把AndroidManifest放进 010editor ,发现有两处错误
第一处 : Chunk Type :
4 bytes,始终为 0x001c0001
第二处 : Unkown : 4 bytes,固定值,0x00000000 - 修改之后

- 把修改后的文件重新压缩,再改后缀 zip 为 apk
,重新放进jadx中打开可以看到
AndroidManifest可以被成功反编译了,flag即出 >8d6efd232c63b7d2
app1
- 用
jadx打开,MainAvtivity里面发现有个versionCode和versionName,在BuildConfig里面看到关键信息
- 将apk在模拟器上运行,会发现随便输入会弹出“再接再厉,加油~”,不输入会弹出“”年轻人不要耍小聪明噢”,结合代码梳理出逻辑:
先检查 inputString 是否满足 versionCode.charAt(i) ^ versionName 规则
再检查长度是否相等
两者同时满足,显示 “恭喜开启闯关之门!”
1 | |
flag为
W3l_T0_GAM3_0ne
app3
- 下载附件后是一个 .ab
文件,这是安卓备份格式的文件,它分为加密和未加密两种类型,.ab 文件的前 24
个字节是类似文件头的东西,如果是加密的,在前 24 个字节中会有 AES-256
的标志,如果未加密,则在前 24 个字节中会有 none 的标志;对于没有加密的ab
文件,可以用以下指令解压
java -jar abp.jar unpack app3.ab app3.jar解压后可以看到里面有一个 base.apk,还有两个数据库文件,用DB Browser打开发现是加密的,需要去找密钥 - 把app3.jar 直接用 jadx 打开,找到MainActivity,注意到在
OnCreate之后调用了函数a
1
2
3
4
5
6
7
8
9
10
11private void a() {
SQLiteDatabase.loadLibs(this);
this.b = new a(this, "Demo.db", null, 1);
ContentValues contentValues = new ContentValues();
contentValues.put("name", "Stranger");
contentValues.put("password", (Integer) 123456);
com.example.yaphetshan.tencentwelcome.a.a aVar = new com.example.yaphetshan.tencentwelcome.a.a();
String a = aVar.a(contentValues.getAsString("name"), contentValues.getAsString("password"));
this.a = this.b.getWritableDatabase(aVar.a(a + aVar.b(a, contentValues.getAsString("password"))).substring(0, 7));
this.a.insert("TencentMicrMsg", null, contentValues);
}
- 加载SQLite数据库库文件
- 创建一个名为a的SQLiteOpenHelper子类实例
- 建ContentValues对象,用于存储要插入数据库的键值对,向contentValues添加键值对 name:Stranger,password:123456
- 实例化了一个com.example.yaphetshan.tencentwelcome.a.a类
- 调用aVar的a方法,传入name和password,返回一个字符串结果。点进去注意到这一块代码:取传入的两个字符串的前四位作为返回值,即得到”Stra”和”1234”
- 先调用aVar.b方法处理 a 和 password,然后与 a 拼接,再用aVar.a方法处理拼接后的字符串,然后取前七个字符作为数据库的密钥
获取密钥的脚本如下: 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
77import java.security.MessageDigest;
import java.util.*;
public class b {
public b() {
super();
}
public static void main(String[] args)
{
String varV2 = "Stra1234";
String varV1B = a(varV2);
String varKey = varV2 + varV1B + "yaphetshan";
System.out.print("KEY = ");
System.out.print(b(varKey).substring(0,7));
}
public static final String a(String arg9) {
String v0_2;
int v0 = 0;
char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] v1 = arg9.getBytes();
MessageDigest v3 = MessageDigest.getInstance("MD5");
v3.update(v1);
byte[] v3_1 = v3.digest();
int v4 = v3_1.length;
char[] v5 = new char[v4 * 2];
int v1_1 = 0;
while(v0 < v4) {
int v6 = v3_1[v0];
int v7 = v1_1 + 1;
v5[v1_1] = v2[v6 >>> 4 & 15];
v1_1 = v7 + 1;
v5[v7] = v2[v6 & 15];
++v0;
}
v0_2 = new String(v5);
}
catch(Exception v0_1) {
v0_2 = null;
}
return v0_2;
}
public static final String b(String arg9) {
String v0_2;
int v0 = 0;
char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] v1 = arg9.getBytes();
MessageDigest v3 = MessageDigest.getInstance("SHA-1");
v3.update(v1);
byte[] v3_1 = v3.digest();
int v4 = v3_1.length;
char[] v5 = new char[v4 * 2];
int v1_1 = 0;
while(v0 < v4) {
int v6 = v3_1[v0];
int v7 = v1_1 + 1;
v5[v1_1] = v2[v6 >>> 4 & 15];
v1_1 = v7 + 1;
v5[v7] = v2[v6 & 15];
++v0;
}
v0_2 = new String(v5);
}
catch(Exception v0_1) {
v0_2 = null;
}
return v0_2;
}
}
- 用这个密钥打开 Encryto.db
,浏览数据可以看到
GN0ZntIM2xsMF9Eb19ZMHVfTG92M19UZW5jM250IX0=,base64解码, >Tctf{H3ll0_Do_Y0u_Lov3_Tenc3nt!}