安卓逆向 刷题笔记(1)

攻防世界 Mobile

ez_dex

  1. 这道题在网上看了好多 wp 基本都是手撕加密,但这题的 so 层加密挺不好分析的,而用frida-hook可以直接跳过解密,找到已经解密好的 dex 文件。
  2. 用 jadx 打开,在 AndroidManifest.xml 中找到关键信息:android:hasCode="false",声明 APK 中不包含 Java/Kotlin 字节码(.dex 文件),表示所有逻辑都在 Native 层;当android:value="native"时,系统会默认加载 libnative.so,此类应用的入口是 Native 层的android_main()函数(而非 Java 的 Activity.onCreate()
  3. 提取出 so 文件,用 ida 分析,直接搜索找到android_main()函数,首先注意到经过加密的filenamename,写一个脚本进行字符串解密
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
import 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/
  1. 解密后发现得到了 dex 文件存放的位置,但是继续往下看,找到关键部分

    用frida hook直接拿到解密后的 dex 文件
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
var 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);

因为摇晃次数 v10 还直接控制数据解密流程,所以不能像满足的时间条件一样给 nop 掉

BGT(Branch if Greater Than) 是关键跳转,决定是否判定超时,NOP 掉 BGT(0x2866)会使程序永远不跳转,从而绕过时间检查
3.
有报错但 dex 文件是成功 dump 出来了的

但到这里也不能直接拿到因为没有权限,所以可以把它 pull 到普通目录比如 Download 里面后再存到本地,这样就能获取了

4. 再原 apk 中添加 dex 文件,加密就已经出来了



密钥:I have a male fish and a female fish
获取密文:

1
2
3
4
5
6
7
8
9
10
11
import 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
7
private boolean a(String str) {
try {
return ncheck(new a().a(str.getBytes()));
} catch (Exception e) {
return false;
}
}

new a().a(str.getBytes()) 先对输入进行某种处理
然后调用 native 方法 ncheck() 进行验证
ncheck(String str)方法

1
private native boolean ncheck(String str);

声明了一个本地方法,实现在 libnative.so 中

查看 a 函数 ,可以分析推测出这是一个变种 base64

  1. 分析 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
9
if ( 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
9
v9 = 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;
}
  1. 解题脚本
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
def 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}')

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
46
cipherText = '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,找到关键部分函数
    mainactivity
  • 这段代码的功能:

这段代码实现了一个简单的 Android 应用,它有一个按钮、一个文本框和一个显示结果的文本。
用户在文本框(EditText)里输入一个字符串(可能是密码),
然后点击按钮时,代码会调用一个名为 JNI.getResult(str) 的本地方法来判断密码是否正确

如果 JNI.getResult(str) 返回 0,界面会显示 “Wrong”
如果 JNI.getResult(str) 返回 1,界面会显示 “Great”

点进JNI
mainactivity2

关键部分是 JNI.getResult(str):
JNI.getResult(str) 是一个 本地方法,也就是说,它的实现并不在 Java 代码里,而是在 C/C++ 代码中,并且通过 JNI(Java Native Interface)被调用。

这意味着需要分析那个本地的.so文件,找出这个getResult 方法的实现
2. 修改后缀,将.apk改成.zip,解压后找到.so文件,把libNative.soIDA打开,找到几个关键函数
.so
3.

  • 分析First函数
    First
    还原:先异或0x80,再除以2
1
2
3
4
5
6
7
8
9
def 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函数
    second
    直接异或还原
1
2
3
4
5
6
7
8
9
10
11
def 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同理
    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
2
3
4
5
6
7
8
9
10
def decode():
s1="fgorl"
s2="l{sra"
s3="asoy}"
flag=""
for i in range(0,5):
flag+=s1[i]+s2[i]+s3[i]
print(flag)
decode()
#flag{sosorryla}

flag{sosorryla}

基础android

用misc方法解题了

  1. 将apk文件放入010editor中,发现文件头是50 4B 03 04是zip文件,修改后缀,解压
    解压
  2. 在assets文件夹下发现time_2.zip,放入010editor,文件头是FF D8 FF,jpg文件,修改后缀,打开后是一张图片flag

    flag{08067-wlecome}

APK逆向

  1. 打开apk文件,用jadx打开,找到关键MainActiivty,
    注意到edit_snedit_userName,找到edit_userName = "Tenshine",
  2. CheckSN中看到代码逻辑
    checksn
    md5加密,转为hex字符,for循环注意i+=2
  3. 解题脚本
1
2
3
4
5
6
7
8
9
10
import 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

  1. 用 jadx 打开后发现AndroidManifest反编译失败,改 apk 后缀为 zip ,解压后把AndroidManifest放进 010editor ,发现有两处错误

    第一处 : Chunk Type : 4 bytes,始终为 0x001c0001
    第二处 : Unkown : 4 bytes,固定值,0x00000000
  2. 修改之后
  3. 把修改后的文件重新压缩,再改后缀 zip 为 apk ,重新放进jadx中打开可以看到AndroidManifest可以被成功反编译了,flag即出

    8d6efd232c63b7d2

app1

  1. jadx打开,MainAvtivity里面发现有个versionCodeversionName,在BuildConfig里面看到关键信息
    buildconfig
  2. 将apk在模拟器上运行,会发现随便输入会弹出“再接再厉,加油~”,不输入会弹出“”年轻人不要耍小聪明噢”,结合代码梳理出逻辑:

先检查 inputString 是否满足 versionCode.charAt(i) ^ versionName 规则
再检查长度是否相等
两者同时满足,显示 “恭喜开启闯关之门!”

1
2
3
4
5
6
7
versionName="X<cP[?PHNB<P?aj"
versionCode=15
flag=""
for i in range(len(versionName)):
flag+=chr(ord(versionName[i])^versionCode)
print(flag)
#W3l_T0_GAM3_0ne

flag为

W3l_T0_GAM3_0ne

app3

  1. 下载附件后是一个 .ab 文件,这是安卓备份格式的文件,它分为加密和未加密两种类型,.ab 文件的前 24 个字节是类似文件头的东西,如果是加密的,在前 24 个字节中会有 AES-256 的标志,如果未加密,则在前 24 个字节中会有 none 的标志;对于没有加密的ab 文件,可以用以下指令解压
    java -jar abp.jar unpack app3.ab app3.jar
    解压后可以看到里面有一个 base.apk,还有两个数据库文件,用DB Browser打开发现是加密的,需要去找密钥
  2. 把app3.jar 直接用 jadx 打开,找到MainActivity,注意到在 OnCreate之后调用了函数a
1
2
3
4
5
6
7
8
9
10
11
private 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
77
import 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;
}
}

运行得出 KEY = ae56f99
3. 用这个密钥打开 Encryto.db ,浏览数据可以看到GN0ZntIM2xsMF9Eb19ZMHVfTG92M19UZW5jM250IX0=,base64解码,

Tctf{H3ll0_Do_Y0u_Lov3_Tenc3nt!}


安卓逆向 刷题笔记(1)
http://example.com/2025/04/11/5/
作者
Eleven
发布于
2025年4月11日
许可协议