安卓逆向 刷题笔记(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/
  4. 解密后发现得到了 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)会使程序永远不跳转,从而绕过时间检查 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
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文件,修改后缀,解压 解压

  1. 在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

  1. 用这个密钥打开 Encryto.db ,浏览数据可以看到GN0ZntIM2xsMF9Eb19ZMHVfTG92M19UZW5jM250IX0=,base64解码, >Tctf{H3ll0_Do_Y0u_Lov3_Tenc3nt!}

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