Android小记

本文最后更新于 2025年2月12日 凌晨

最近 SWDD 师傅(Android 神!)给了几道简单的 Android 题目,我拿来做了做,感觉 Android 真有意思吧hhhhhh可惜我不会呜呜呜。做题期间在 ida 的深色和浅色模式下反复横跳,最终选择了深色(果然一开始干正事什么都变得有趣了起来)

APK 反编译-level1

jeb 反编译

1
flag{Your_are_go0d_at_Uncompile_Android!}

Native 层反编译-level2

Native层反编译,那么我们得去把 so 层提取出来,用 apktool

1
apktool d apk名 -o "需要生成的文件夹"

找到 \lib\x86_64\libsummertrain.so 文件丢入 ida 就出来

1
flag{Now_You_Know_Native_uncompile}

静态注册-level3

提取 .so 文件丢入 ida,找到 Java_com_swdd_summertrain_MainActivity_Check 函数

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
__int64 __fastcall Java_com_swdd_summertrain_MainActivity_Check(__int64 a1, __int64 a2, __int64 a3)
{
...
v27 = v3;
v5 = 0LL;
v6 = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0LL);
if ( v6 )
{
v7 = v6;
v8 = strlen(v6);
v9 = (char *)operator new[](v8 + 1);
if ( !v8 )
goto LABEL_11;
if ( v8 < 8 || v9 < &v7[v8] && v7 < &v9[v8] )
{
v10 = 0LL;
LABEL_7:
v11 = v8 + ~v10;
v12 = v8 & 3;
if ( (v8 & 3) != 0 )
{
do
{
v9[v10] = v7[v10] ^ 0x33;
++v10;
--v12;
}
while ( v12 );
}
if ( v11 >= 3 )
{
do
{
v9[v10] = v7[v10] ^ 0x33;
v9[v10 + 1] = v7[v10 + 1] ^ 0x33;
v9[v10 + 2] = v7[v10 + 2] ^ 0x33;
v9[v10 + 3] = v7[v10 + 3] ^ 0x33;
v10 += 4LL;
}
while ( v8 != v10 );
}
goto LABEL_11;
}
if ( v8 < 0x20 )
{
v10 = 0LL;
LABEL_22:
v26 = v10;
v10 = v8 & 0xFFFFFFFFFFFFFFF8LL;
do
{
*(_QWORD *)&v9[v26] = *(_QWORD *)&v7[v26] ^ 0x3333333333333333LL;
v26 += 8LL;
}
while ( v10 != v26 );
if ( v8 != v10 )
goto LABEL_7;
LABEL_11:
v9[v8] = 0;
v13 = memcmp(v9, "U_RTH}\\Dlj\\Flx]\\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\\]Ncovariant return thunk to ", v8) == 0;
v14 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL))(a1, "java/lang/Boolean");
v15 = v14;
v16 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
a1,
v14,
"<init>",
"(Z)V");
v5 = _JNIEnv::NewObject(a1, v15, v16, v13, v17, v18, v27);
(*(void (__fastcall **)(__int64, __int64, const char *))(*(_QWORD *)a1 + 1360LL))(a1, a3, v7);
operator delete[](v9);
return v5;
}
v10 = v8 & 0xFFFFFFFFFFFFFFE0LL;
v20 = (((v8 & 0xFFFFFFFFFFFFFFE0LL) - 32) >> 5) + 1;
if ( (v8 & 0xFFFFFFFFFFFFFFE0LL) == 32 )
{
v22 = 0LL;
if ( (v20 & 1) == 0 )
goto LABEL_20;
}
else
{
v21 = v20 & 0xFFFFFFFFFFFFFFFELL;
v22 = 0LL;
do
{
v23 = _mm_xor_ps(*(__m128 *)&v7[v22 + 16], (__m128)xmmword_CA80);
*(__m128 *)&v9[v22] = _mm_xor_ps(*(__m128 *)&v7[v22], (__m128)xmmword_CA80);
*(__m128 *)&v9[v22 + 16] = v23;
v24 = _mm_xor_ps(*(__m128 *)&v7[v22 + 48], (__m128)xmmword_CA80);
*(__m128 *)&v9[v22 + 32] = _mm_xor_ps(*(__m128 *)&v7[v22 + 32], (__m128)xmmword_CA80);
*(__m128 *)&v9[v22 + 48] = v24;
v22 += 64LL;
v21 -= 2LL;
}
while ( v21 );
if ( (v20 & 1) == 0 )
goto LABEL_20;
}
v25 = _mm_xor_ps(*(__m128 *)&v7[v22 + 16], (__m128)xmmword_CA80);
*(__m128 *)&v9[v22] = _mm_xor_ps(*(__m128 *)&v7[v22], (__m128)xmmword_CA80);
*(__m128 *)&v9[v22 + 16] = v25;
LABEL_20:
if ( v8 == v10 )
goto LABEL_11;
if ( (v8 & 0x18) == 0 )
goto LABEL_7;
goto LABEL_22;
}
return v5;
}

代码主要流程为:对字符串 v6 进行异或,分为几种情况:

  • 如果字符串长度小于 8,逐字节进行异或,每个字符都与 0x33 异或
  • 如果长度在 8 ~ 32 之间,按 8 字节为一组进行异或,异或值是 0x3333333333333333LL
  • 如果长度超过 32 字节,使用 SSE 指令_mm_xor_ps),每次处理 16 字节,用一个全局常量 xmmword_CA80 进行异或

解码后的字符串会与一个固定字符串 U_RTH}\\Dlj\\Flx]\\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\\]Ncovariant return thunk to 进行比较,相等则正确,exp 如下

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
#include "stdafx.h"
#include <iostream>
#include <cstring>
#include <immintrin.h>
#include <cstdint>


void decode(char* encoded, size_t length) {
if (length < 8) {
for (size_t i = 0; i < length; ++i) {
encoded[i] ^= 0x33;
}
} else if (length < 32) {
for (size_t i = 0; i < length; i += 8) {
uint64_t* block = reinterpret_cast<uint64_t*>(encoded + i);
*block ^= 0x3333333333333333LL;
}
} else {
for (size_t i = 0; i < length; ++i) {
encoded[i] ^= 0x33;
}
}
}

int main() {
const char* originalEncodedString = "U_RTH}\\Dlj\\Flx]\\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\\]Ncovariant return thunk to";
size_t length = strlen(originalEncodedString);

// 动态分配内存,防止栈溢出
char* encodedString = new char[length + 1];
strcpy(encodedString, originalEncodedString);

std::cout << "加密字符串: " << encodedString << std::endl;

decode(encodedString, length);

std::cout << "解密后的字符串: " << encodedString << std::endl;

// 释放动态分配的内存
delete[] encodedString;
return 0;
}

1
flag{Now_You_Know_Native_dynamic_registration}

动态注册-level4

动态注册的方法一般来说需要分析 JNI_OnLoad 函数,把 libsummertrain.so 丢进 ida分析

emmmmm,这里我似乎没分析出什么来,然后看了眼其他函数,发现了 sub_172E0 函数有东西

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
int __cdecl sub_110B0(int a1, int a2, int a3)
{
...
v3 = (const char *)(*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);
if ( v3 )
{
v4 = (unsigned int)v3;
v5 = strlen(v3);
v6 = (_BYTE *)operator new[](v5 + 1);
v7 = v5;
v8 = v6;
if ( !v7 )
goto LABEL_21;
v9 = 0;
v24 = v7;
if ( v7 < 8 || (unsigned int)v8 < v4 + v7 && v4 < (unsigned int)&v8[v7] )
goto LABEL_17;
v9 = 0;
if ( v7 < 0x20 )
{
LABEL_14:
v17 = v9;
v9 = v7 & 0xFFFFFFF8;
do
{
*(_QWORD *)&v8[v17] = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)(v4 + v17)), (__m128i)-1LL).m128i_u64[0];
v17 += 8;
}
while ( v9 != v17 );
if ( v7 != v9 )
goto LABEL_17;
LABEL_21:
v8[v7] = 0;
v25 = memcmp(v8, &unk_78FD, v7) == 0;
v20 = (*(int (__cdecl **)(int, const char *))(*(_DWORD *)a1 + 24))(a1, "java/lang/Boolean");
v21 = (*(int (__cdecl **)(int, int, const char *, const char *))(*(_DWORD *)a1 + 132))(a1, v20, "<init>", "(Z)V");
v22 = _JNIEnv::NewObject(a1, v20, v21, v25);
(*(void (__cdecl **)(int))(*(_DWORD *)a1 + 680))(a1);
operator delete[](v8);
return v22;
}
v9 = v7 & 0xFFFFFFE0;
v10 = (v7 & 0xFFFFFFE0) - 32;
v11 = (v10 >> 5) + 1;
if ( v10 )
{
v12 = v11 & 0xFFFFFFFE;
v13 = 0;
do
{
v14 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 16)), (__m128i)-1LL);
*(__m128i *)&v8[v13] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13)), (__m128i)-1LL);
*(__m128i *)&v8[v13 + 16] = v14;
v15 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 48)), (__m128i)-1LL);
*(__m128i *)&v8[v13 + 32] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 32)), (__m128i)-1LL);
*(__m128i *)&v8[v13 + 48] = v15;
v13 += 64;
v12 -= 2;
}
while ( v12 );
if ( (v11 & 1) == 0 )
goto LABEL_12;
}
else
{
v13 = 0;
if ( (v11 & 1) == 0 )
goto LABEL_12;
}
v16 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13 + 16)), (__m128i)-1LL);
*(__m128i *)&v8[v13] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)(v4 + v13)), (__m128i)-1LL);
*(__m128i *)&v8[v13 + 16] = v16;
LABEL_12:
v7 = v24;
if ( v24 == v9 )
goto LABEL_21;
if ( (v24 & 0x18) == 0 )
{
LABEL_17:
v18 = v7 + ~v9;
for ( i = v7 & 3; i; --i )
{
v8[v9] = ~*(_BYTE *)(v4 + v9);
++v9;
}
v7 = v24;
if ( v18 >= 3 )
{
do
{
v8[v9] = ~*(_BYTE *)(v4 + v9);
v8[v9 + 1] = ~*(_BYTE *)(v4 + v9 + 1);
v8[v9 + 2] = ~*(_BYTE *)(v4 + v9 + 2);
v8[v9 + 3] = ~*(_BYTE *)(v4 + v9 + 3);
v9 += 4;
}
while ( v24 != v9 );
}
goto LABEL_21;
}
goto LABEL_14;
}
return 0;
}

首先用 v5 存储调用 (*(_QWORD *)a1 + 1352LL) 所返回的字符串,v6 指向指向 v5,然后 v8来存储字符串

而且代码根据字符串长度进行不同的解密处理:

  • 长度小于 8 的情况,逐字节进行按位取反
  • 长度大于或等于 8 小于 32 的情况,利用 _mm_xor_si128 进行 8 字节为单位的批量解密
  • 长度大于或等于 32 的情况,一次处理 64 字节
    • **_mm_xor_si128**:通过向量化操作,将 16 字节数据与 -1 进行异或,相当于每个字节都执行 ~ 操作(按位取反)

代码中有一句 v12 = memcmp(v8, &unk_DCF0, v7) == 0,主要作用是通过 memcmp 函数比较两个内存区域的内容是否相等,那么 v5 中的字符串就存储在 unk_DCF0 中,提取出来

1
0x99, 0x93, 0x9E, 0x98, 0x84, 0xB1, 0x90, 0x88, 0xA0, 0xA6, 0x90, 0x8A, 0xA0, 0xB4, 0x91, 0x90, 0x88, 0xA0, 0xB1, 0x9E, 0x8B, 0x96, 0x89, 0x9A, 0xA0, 0x8D, 0x9A, 0x9E, 0x93, 0xA0, 0x9B, 0x86, 0x91, 0x9E, 0x92, 0x96, 0x9C, 0xA0, 0x8D, 0x9A, 0x98, 0x96, 0x8C, 0x8B, 0x8D, 0x9E, 0x8B, 0x96, 0x90, 0x91, 0x82

exp

1
2
3
4
5
6
7
8
9
10
11
12
ida_chars = [
0x99, 0x93, 0x9E, 0x98, 0x84, 0xB1, 0x90, 0x88, 0xA0, 0xA6,
0x90, 0x8A, 0xA0, 0xB4, 0x91, 0x90, 0x88, 0xA0, 0xB1, 0x9E,
0x8B, 0x96, 0x89, 0x9A, 0xA0, 0x8D, 0x9A, 0x9E, 0x93, 0xA0,
0x9B, 0x86, 0x91, 0x9E, 0x92, 0x96, 0x9C, 0xA0, 0x8D, 0x9A,
0x98, 0x96, 0x8C, 0x8B, 0x8D, 0x9E, 0x8B, 0x96, 0x90, 0x91,
0x82
]

decrypted_string = ''.join(chr(~byte & 0xFF) for byte in ida_chars)
print(decrypted_string)

1
flag{Now_You_Know_Native_real_dynamic_registration}

GAME-level5

Jeb 打开发现了关于 Unity 的东西,猜测这个程序估计是用 Unity 写的

\assets\bin\Data\Managed\ 下的 Assembly-CSharp.dll 文件丢进 dnSpy 看看,在 GameWindow 找到 flag

1
flag{justaeasyunitygame}

GAME-level6

第一次见这个东西,还挺新奇hhhhh

发现有一个 Mono 文件夹

Unity 使用 Mono 方式打出来的 apk,如果没有加密我们可以直接从包内拿到 Assembly-CSharp.dll,可以直接使用 dnSpy.exe 对其进行反编译。
如果使用 IL2CPP 方式出包,则没有 Assembly-CSharp.dll,有一个 IL2CppDumper 工具,https://github.com/Perfare/Il2CppDumper,通过它,我们可以逆向得到 Assembly-CSharp.dll

创建一个 inputoutput,将 libil2cpp.soglobal-metadata.dat 拷贝到 input 目录中,

可以创建一个批处理文件到 input

1
..\Il2CppDumper.exe libil2cpp.so global-metadata.dat ..\output

环境 .NET 需要用 6.0.0 版本

然后打开这个 .dll 文件发现有一个 FlagText,但是是没有内容的

打开 libil2cpp.so ,加载 IL2CppDumper 文件夹里的 ida_py3.py 文件把 dump 出来的 script.json 加载进去,FlagText 偏移值为 0x40,在 ida 里面跳转,可以看到有 Flags 字样,但是还是什么都没有

查看 LoadLevel 函数,加载到了 UnityEngine_Transform__SetParent_8767024 函数,但是我 ida 后面加载不出来了,唉唉唉

Init_Array-level7

看源码,调用了 nativeCheck,因此就应该去分析 .so 文件

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
package com.swdd.summertrain;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.swdd.summertrain.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;

static {
System.loadLibrary("summertrain");
}

public native Boolean Check(String arg1) {
}

public void onClick(View V) {
if(this.Check(((TextView)this.findViewById(id.input)).getText().toString()).booleanValue()) {
Toast.makeText(this, "Good!", 0).show();
return;
}

Toast.makeText(this, "Try Again", 0).show();
}

@Override // androidx.fragment.app.FragmentActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding activityMainBinding0 = ActivityMainBinding.inflate(this.getLayoutInflater());
this.binding = activityMainBinding0;
this.setContentView(activityMainBinding0.getRoot());
}
}

查看 Java_com_swdd_summertrain_MainActivity_Check 函数

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
__int64 __fastcall Java_com_swdd_summertrain_MainActivity_Check(__int64 a1, __int64 a2, __int64 a3)
{
...
v3 = a3;
v4 = 0LL;
v5 = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0LL);
if ( !v5 )
return v4;
v6 = v5;
v26 = v3;
v7 = strlen(v5);
v8 = (char *)operator new[](v7 + 1);
if ( !v7 )
goto LABEL_11;
if ( v7 < 8 || v8 < &v6[v7] && v6 < &v8[v7] )
{
v9 = 0LL;
LABEL_7:
v10 = v7 + ~v9;
v11 = v7 & 3;
if ( (v7 & 3) != 0 )
{
do
{
v8[v9] = ~v6[v9];
++v9;
--v11;
}
while ( v11 );
}
if ( v10 >= 3 )
{
do
{
v8[v9] = ~v6[v9];
v8[v9 + 1] = ~v6[v9 + 1];
v8[v9 + 2] = ~v6[v9 + 2];
v8[v9 + 3] = ~v6[v9 + 3];
v9 += 4LL;
}
while ( v7 != v9 );
}
goto LABEL_11;
}
if ( v7 >= 0x20 )
{
v9 = v7 & 0xFFFFFFFFFFFFFFE0LL;
v19 = (((v7 & 0xFFFFFFFFFFFFFFE0LL) - 32) >> 5) + 1;
if ( (v7 & 0xFFFFFFFFFFFFFFE0LL) == 32 )
{
v21 = 0LL;
if ( (v19 & 1) == 0 )
goto LABEL_22;
}
else
{
v20 = v19 & 0xFFFFFFFFFFFFFFFELL;
v21 = 0LL;
do
{
v22 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 16]), (__m128i)-1LL);
*(__m128i *)&v8[v21] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21]), (__m128i)-1LL);
*(__m128i *)&v8[v21 + 16] = v22;
v23 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 48]), (__m128i)-1LL);
*(__m128i *)&v8[v21 + 32] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 32]), (__m128i)-1LL);
*(__m128i *)&v8[v21 + 48] = v23;
v21 += 64LL;
v20 -= 2LL;
}
while ( v20 );
if ( (v19 & 1) == 0 )
goto LABEL_22;
}
v24 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21 + 16]), (__m128i)-1LL);
*(__m128i *)&v8[v21] = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v6[v21]), (__m128i)-1LL);
*(__m128i *)&v8[v21 + 16] = v24;
LABEL_22:
if ( v7 == v9 )
goto LABEL_11;
if ( (v7 & 0x18) == 0 )
goto LABEL_7;
goto LABEL_24;
}
v9 = 0LL;
LABEL_24:
v25 = v9;
v9 = v7 & 0xFFFFFFFFFFFFFFF8LL;
do
{
*(_QWORD *)&v8[v25] = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)&v6[v25]), (__m128i)-1LL).m128i_u64[0];
v25 += 8LL;
}
while ( v9 != v25 );
if ( v7 != v9 )
goto LABEL_7;
LABEL_11:
v8[v7] = 0;
v12 = 0;
if ( v7 == (int)__strlen_chk(&storedBytes, 256LL) )
v12 = memcmp(v8, &storedBytes, v7) == 0;
v13 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL))(a1, "java/lang/Boolean");
v14 = v13;
v15 = (*(__int64 (__fastcall **)(__int64, __int64, const char *, const char *))(*(_QWORD *)a1 + 264LL))(
a1,
v13,
"<init>",
"(Z)V");
v4 = _JNIEnv::NewObject(a1, v14, v15, v12, v16, v17, v26);
(*(void (__fastcall **)(__int64, __int64, const char *))(*(_QWORD *)a1 + 1360LL))(a1, v27, v6);
operator delete[](v8);
return v4;
}

大概流程和 “动态注册” 这个题差不多,对传入字符串按位取反,最后存储在 storedBytes 中,把密文提取出来,解出来居然是假的,what can i say.jpg

然后看了下 Shangwendada 的《对init_array段调用的方法进行Hook》这篇文章,首先我们在 segments 窗口中看到各个段的偏移,找到了 .init_array

找到了 sub_17360 函数

1
2
3
4
5
__int64 sub_17360()
{
memcpy(&storedBytes, &unk_CA70, 0x100uLL);
return __android_log_print(4LL, "GenFunction", "Gen function called.");
}

原来 storedBytes 保存了从 unk_CA70 复制的数据,那 unk_CA70 应该就是正确的密文

exp 也是和之前那个题一样,换一下密文就行

1
2
3
4
5
6
7
ida_chars = [
0x99, 0x93, 0x9E, 0x98, 0x84, 0xBA, 0x9E, 0x8C, 0x86, 0xB6,
0x91, 0x96, 0x8B, 0xBE, 0x8D, 0x8D, 0x9E, 0x86, 0x82
]

decrypted_string = ''.join(chr(~byte & 0xFF) for byte in ida_chars)
print(decrypted_string)
1
flag{EasyInitArray}

Frida 端口检测-level8

先反编译一下,在 native 层调用了 Check 函数

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
package com.swdd.summertrain;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.swdd.summertrain.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;

static {
System.loadLibrary("summertrain");
}

public native Boolean Check(String arg1) {
}

public void onClick(View view0) {
String s = ((TextView)this.findViewById(id.input)).getText().toString();
if(this.Check(s).booleanValue()) {
Toast.makeText(this, "Good! Your flag is flag{" + s + "}", 0).show();
return;
}

Toast.makeText(this, "Try Again", 0).show();
}

@Override // androidx.fragment.app.FragmentActivity
protected void onCreate(Bundle bundle0) {
super.onCreate(bundle0);
ActivityMainBinding activityMainBinding0 = ActivityMainBinding.inflate(this.getLayoutInflater());
this.binding = activityMainBinding0;
this.setContentView(activityMainBinding0.getRoot());
}
}


没啥有用的信息,直接运行一下程序,正常是可以运行的,在启动了 frida 后程序停止运行了

这个时候就可以查看 .so 文件了,在 segments 窗口中看各个段的偏移,找到了 .init_array 然后进入sub_F40 函数

下面的一大堆什么玩意儿我也看不懂qwq,但是前面有一个 check_ports 函数

大概内容是通过 connect 检测连接 127.0.0.1 的两个端口检测是否被连接

1
2
*&addr.sa_family = 0xA2690002;       
*addr.sa_data = 0x8A5D;

这里有一个网络字节转换的知识点(emmm计网的东西还没学),网络字节序是大端模式(Big-Endian),而主机字节序可能是小端模式(Little-Endian),关于这个大小端(死去的回忆突然开始攻击我),简单说一下

  • 大端字节序(Big Endian):最高有效位存于最低内存地址处,最低有效位存于最高内存处
  • 小端字节序(Little Endian):最高有效位存于最高内存地址,最低有效位存于最低内存处

所以我们可以根据信息得出端口

1
2
0xA2690002	->	0xA269	->	0x69A2	->	27042
0x8A5D -> 0x5D8A -> 23946

23946 端口是 IDA android_server 的默认端口,27042 是 Frida 默认端口

因此我们解决办法就是开启 frida 时把端口改了,不然会被检测到

1
./frida-server -l 0.0.0.0:9997

然后我打开程序的时候老是跳出这个弹窗,烦死了

可以使用这个命令来关掉(菜鸡,还不知道有什么其他方法)

1
adb shell am clear-debug-app

写一个脚本 hook.js

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
function hook() {
var targetAddress = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(targetAddress, {
onEnter: function (args) {
try {
var input = Memory.readUtf8String(args[0]);
if (input.includes("aaa")) {
console.log("FLAG: " + Memory.readUtf8String(args[1]));
}
} catch (e) {
console.error("Error: " + e);
}
}
});
}

function main() {
Java.perform(function () {
hook();
var MainActivity = Java.use("com.swdd.summertrain.MainActivity");
var MainActivityInstance = MainActivity.$new();
MainActivityInstance.Check("aaa");
});
}
setImmediate(main);

这里需要先把端口转发一下,把模拟器的 9997 端口转发到本地的 9997,因为通过 adb forward 把它的端口映射到了本地,hook 时需要写一个 127.0.0.1:9997 (其实还可以用本机的 IP,但是有时候可能会比较麻烦,模拟器有时候无法直接通过 IP 访问设备上的一些服务)连接到这个端口

1
2
3
./frida-server 0.0.0.0:9997
adb forward tcp:9997 tcp:9997
frida -H 127.0.0.1:9997 -f com.swdd.summertrain -l hook.js

1
flag{53cd3f37664bd01357182ca13bc2f9b6}

Frida检测之Maps-level9

Java 层没什么东西,同样是看 Native 层,这里有一个检测 maps 的。

/proc/self/maps 是一个特殊的文件,它包含了当前进程的内存映射信息。当你打开这个文件时,它会显示一个列表,其中包含了进程中每个内存区域的详细信息。这些信息通常包括:

  • 起始地址(Start Address)
  • 结束地址(End Address)
  • 权限(如可读、可写、可执行)
  • 共享/私有标志(Shared or Private)
  • 关联的文件或设备(如果内存区域是文件映射的)
  • 内存区域的偏移量
  • 内存区域的类型(如匿名映射、文件映射、设备映射等)

当注入frida后,在maps文件中就会存在 frida-agent-64.sofrida-agent-32.so 等文件。

(上面这段专业术语 copy 的 52 的正己师傅的hhhhh)

这个函数流程就是打开 proc/self/maps ,然后检测是否有这些内存特征,如果有就直接终止程序

  • frida:检测 Frida 注入的 Agent 或 Server
  • gadget/agent:检测 Frida Gadget 或调试工具组件
  • /data/local/tmp/:检测临时目录下的注入文件(如 Frida Server)
  • -64.so/-32.so:检测特定架构的动态库(如 Frida 的 64/32 位库)

hook 脚本,重定向一个 maps

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
// 定义一个函数,用于重定向并修改maps文件内容,以隐藏特定的库和路径信息
function mapsRedirect() {
// 定义伪造的maps文件路径
var FakeMaps = "/data/data/com.swdd.summertrain/maps";
// 获取libc.so库中'open'函数的地址
const openPtr = Module.getExportByName('libc.so', 'open');
// 根据地址创建一个新的NativeFunction对象,表示原生的'open'函数
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
// 查找并获取libc.so库中'read'函数的地址
var readPtr = Module.findExportByName("libc.so", "read");
// 创建新的NativeFunction对象表示原生的'read'函数
var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
// 分配512字节的内存空间,用于临时存储从maps文件读取的内容
var MapsBuffer = Memory.alloc(512);
// 创建一个伪造的maps文件,用于写入修改后的内容,模式为"w"(写入)
var MapsFile = new File(FakeMaps, "w");
// 使用Interceptor替换原有的'open'函数,注入自定义逻辑
Interceptor.replace(openPtr, new NativeCallback(function(pathname, flag) {
// 调用原始的'open'函数,并获取文件描述符(FD)
var FD = open(pathname, flag);
// 读取并打印尝试打开的文件路径
var ch = pathname.readCString();
if (ch.indexOf("/proc/") >= 0 && ch.indexOf("maps") >= 0) {
console.log("open : ", pathname.readCString());
// 循环读取maps内容,并写入伪造的maps文件中,同时进行字符串替换以隐藏特定信息
while (parseInt(read(FD, MapsBuffer, 512)) !== 0) {
var MBuffer = MapsBuffer.readCString();
MBuffer = MBuffer.replaceAll("/data/local/tmp/re.frida.server/frida-agent-64.so", "1");
MBuffer = MBuffer.replaceAll("re.frida.server", "1");
MBuffer = MBuffer.replaceAll("frida", "1");
MBuffer = MBuffer.replaceAll("agent", "1");
MBuffer = MBuffer.replaceAll("-32.so", "1");
MBuffer = MBuffer.replaceAll("-64.so", "1");
MBuffer = MBuffer.replaceAll("gadget", "1");
MBuffer = MBuffer.replaceAll("/data/local/tmp", "/1");
// 将修改后的内容写入伪造的maps文件
MapsFile.write(MBuffer);
}
// 为返回伪造maps文件的打开操作,分配UTF8编码的文件名字符串
var filename = Memory.allocUtf8String(FakeMaps);
// 返回打开伪造maps文件的文件描述符
return open(filename, flag);
}
// 如果不是目标maps文件,则直接返回原open调用的结果
return FD;
}, 'int', ['pointer', 'int']));
}

function hook() {
mapsRedirect();
var targetAddress=Module.findExportByName("libc.so","strcmp");
Interceptor.attach(targetAddress,{
onEnter:function (args){
var input =Memory.readUtf8String(args[0]);
if (input.includes("aaa")){
console.log("FLAG:"+Memory.readUtf8String(args[1]));
}
},onLeave:function (retval){

}
})
var MainActivity=Java.use("com.swdd.summertrain.MainActivity");
var MainActivityInstance=MainActivity.$new();
MainActivityInstance.Check("aaa");

}
function main(){
Java.perform(function (){
hook();
})
}
setImmediate(main);

运行得出 flag

1
flag{f5d02d7eede3a75ee6e6cc0a9673c76f}

Frida 检测之 inlineHook-level10

native 层的 check 函数

读取磁盘中 libc.so 文件 signal 函数的原始 8 字节数,获取内存中 signal 函数的实际代码比较二者是否一致,如果不一致程序就会退出,所以我们要模拟 fread 读取了预期数据,伪造返回值

获取 hook 前面的 8 个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let bytes_count = 8
let address = Module.getExportByName("libc.so","signal")
//读取 signal 函数

let before = ptr(address)
console.log("")
console.log(" before hook: ")
console.log(hexdump(before, {
offset: 0,
length: bytes_count,
header: true,
ansi: true
}));

得出

1
e9 7f f7 97 f4 83 e4 f0

hook 脚本

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
function hook_memcmp_addr(){
//hook反调试
var memcmp_addr = Module.findExportByName("libc.so", "fread");
if (memcmp_addr !== null) {
console.log("fread address: ", memcmp_addr);
Interceptor.attach(memcmp_addr, {
onEnter: function (args) {
this.buffer = args[0]; // 保存 buffer 参数
this.size = args[1]; // 保存 size 参数
this.count = args[2]; // 保存 count 参数
this.stream = args[3]; // 保存 FILE* 参数
},
onLeave: function (retval) {
// 这里可以修改 buffer 的内容,假设我们知道何时 fread 被用于敏感操作
console.log(this.count.toInt32());
if (this.count.toInt32() == 8) {
// 模拟 fread 读取了预期数据,伪造返回值
Memory.writeByteArray(this.buffer, [0xe9, 0x7f, 0xf7, 0x97, 0xf4, 0x83, 0xe4, 0xf0]);
retval.replace(8); // 填充前8字节
console.log(hexdump(this.buffer));
}
}
});
} else {
console.log("Error: memcmp function not found in libc.so");
}
}

function hook() {
hook_memcmp_addr();
var targetAddress=Module.findExportByName("libc.so","strcmp");
Interceptor.attach(targetAddress,{
onEnter:function (args){
var input =Memory.readUtf8String(args[0]);
if (input.includes("aaaaaaa")){
console.log("FLAG:"+Memory.readUtf8String(args[1]));
}
},onLeave:function (retval){


}
})
var MainActivity=Java.use("com.swdd.summertrain.MainActivity");
var MainActivityInstance=MainActivity.$new();
MainActivityInstance.Check("aaaaaaa");

}
function main(){
Java.perform(function (){
hook();

})
}

setImmediate(main);

得出 flag

1
flag{a8490cd255d3a0a982fac16130183b76}

Android小记
http://example.com/2025/02/11/Android小记/
作者
butt3rf1y
发布于
2025年2月11日
许可协议