Flutter 逆向

[NewStar 2024 Week5] Ohn_flutter!!!

java 层看到密文

使用 blutter 恢复 libapp.so 的符号
先克隆 blutter 项目

1
git clone https://github.com/worawit/blutter --depth=1
1
2
D:\aaa\flutter\blutter>python -m pip install pyelftools
D:\aaa\flutter\blutter>python blutter.py ..\ns24\ .\output

ns24 目录中存放着已经提取出来的 so 文件,在 blutter 目录下找到刚刚新生成的 output 文件夹
找到里面的 ida_script\addNames.py,在 ida 8.3 中(ida 9.0 的 api 改变了,使用这个脚本会有报错,不能完全恢复)用这个脚本恢复 so 文件的符号,由于我 8.3 的 ida 没法反编译 arm,所以把把这个保存成 i64,然后选择 bn 中的 Analysis -> Import Debug Info from External File 导入 i64 文件,即可在 bn 中看到还原了函数名的 so 文件

搜索 ohn_flutter 查找关键函数

用 blutter 里面的 blutter_frida.js 脚本 hook 函数的参数

ohn_flutter$drink_drink::_fixkey_2fea14

得到 key 111,104,110,95,102,108,117,116,116,101,114,107,107,107,107,107

hook ohn_flutter$doi_::jumppp_2fe3c8的参数也能得到 key 为 ohn_flutterkkkkk

点进 ohn_flutter$doi_::jumppp_2fe3c8 -> ohn_flutter$drink_drink::ens_2fe410 -> ohn_flutter$drink_drink::encrypt_2fe460

里面有不少有用的函数如最开始 hook 过的 ohn_flutter$drink_drink::_fixkey_2fea14

hook ohn_flutter$drink_drink::_encryptUint32List_2fe5ec

1
2
3
4
5
6
_Uint32List@730017de19 = [
1601071215,
1953852518,
1802659188,
1802201963
]

仔细看这个函数,发现了很多 xxtea 的特征
DELTA = 0x9E3779B9

rounds = 6 + 52 / n

函数的开头可以看到定义 x2 = 0x34(52)

MX = ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z))

然后往前交叉引用
hook dart_convert_Codec::encode_2d5178

1
2
_Uint8List@7300131939 = [0,44,116,172,168,123,57,249]
_Uint8List@730013b8e9 = [115,125,141,220,54,187,239,62,180,24,73,101,254,58,44,214]

点进这个函数,看到有个 base64 dart_convert_Base64Encoder::convert_409c50

到这里这条线的函数差不多就看完了
再回到开始的地方

1
ohn_flutter$EditView_MyEditTextState::_anon_closure_2d4f08 -> encrypt$encrypt_Encrypted::ctor_fromUtf8_2fe1c4 -> dart_convert_Utf8Encoder::convert_40b1a8

hook dart_convert_Utf8Encoder::convert_40b1a8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String@730078e5b9 = "{\"method\":\"TextInput.setClient\",\"args\":[1,{\"inputType\":{\"name\":\"TextInputType.text\",\"signed\":null,\"decimal\":null},\"readOnly\":false,\"obscureText\":false,\"autocorrect\":true,\"smartDashesType\":\"1\",\"smartQuotesType\":\"1\",\"enableSuggestions\":true,\"enableInteractiveSelection\":true,\"actionLabel\":null,\"inputAction\":\"TextInputAction.done\",\"textCapitalization\":\"TextCapitalization.none\",\"keyboardAppearance\":\"Brightness.light\",\"enableIMEPersonalizedLearning\":true,\"contentCommitMimeTypes\":[],\"autofill\":{\"uniqueIdentifier\":\"EditableText-28050040\",\"hints\":[],\"editingValue\":{\"text\":\"\",\"selectionBase\":0,\"selectionExtent\":0,\"selectionAffinity\":\"TextAffinity.downstream\",\"selectionIsDirectional\":false,\"composingBase\":-1,\"composingExtent\":-1},\"hintText\":\"Fill flag here and Press enter...\"},\"enableDeltaModel\":false}]}"        
String@7300790499 = "{\"method\":\"TextInput.setEditableSizeAndTransform\",\"args\":{\"width\":372.72727272727275,\"height\":24.0,\"transform\":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,10.0,105.63636363636364,0.0,1.0]}}"
String@7300791709 = "{\"method\":\"TextInput.setMarkedTextRect\",\"args\":{\"width\":2.0,\"height\":24.0,\"x\":0.18181818181818166,\"y\":0.18181818181818699}}"
String@7300792309 = "{\"method\":\"TextInput.setStyle\",\"args\":{\"fontFamily\":\"Roboto\",\"fontSize\":16.0,\"fontWeightIndex\":3,\"textAlignIndex\":4,\"textDirectionIndex\":1}}"
String@7300792f59 = "{\"method\":\"TextInput.setEditingState\",\"args\":{\"text\":\"\",\"selectionBase\":0,\"selectionExtent\":0,\"selectionAffinity\":\"TextAffinity.downstream\",\"selectionIsDirectional\":false,\"composingBase\":-1,\"composingExtent\":-1}}"
String@73007939c9 = "{\"method\":\"TextInput.show\",\"args\":null}"
String@73007942a9 = "{\"method\":\"TextInput.requestAutofill\",\"args\":null}"
String@73007a9c69 = "{\"method\":\"TextInput.setCaretRect\",\"args\":{\"width\":2.0,\"height\":24.0,\"x\":0.18181818181818166,\"y\":0.18181818181818699}}"
String@73007f7c19 = "{\"method\":\"TextInput.show\",\"args\":null}"
String@73007f8fd9 = "[null]"
String@73001063e9 = "{\"method\":\"TextInput.setCaretRect\",\"args\":{\"width\":2.0,\"height\":24.0,\"x\":36.909090909090914,\"y\":0.18181818181818699}}"
String@7300110ab9 = "{\"method\":\"TextInput.clearClient\",\"args\":null}"
String@7300111619 = "[null]"
String@7300111b89 = "{\"method\":\"TextInput.hide\",\"args\":null}"
String@73007f70a9 = "aaaa"
String@73001124c9 = "ohn_flutterkkkkk"
String@73000fd5d1 = "12345678901234561234567890123456"
String@7300113d99 = "1234567890123456"
String@7300113c49 = "mpxRIDtEnuE="

发现一个 AES

1
ohn_flutter$EditView_MyEditTextState::_anon_closure_2d4f08 -> encrypt$encrypt_AES::ctor_2d5474

hook encrypt$encrypt_AES::ctor_2d5474

1
2
3
4
5
Key@73001121a9 = {
"parent!Encrypted": {
"off_8!_Uint8List@7300112299": [49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54]
}
}
1
ohn_flutter$EditView_MyEditTextState::_anon_closure_2d4f08 -> encrypt$encrypt_Encrypter::encrypt_2d51f0

hook encrypt$encrypt_Encrypter::encrypt_2d51f0

1
2
3
4
5
6
7
IV@73001146b9 = {
"parent!Encrypted": {
"off_8!_Uint8List@7300114769": [
49,50,51,52,53,54,55,56,57,48,49,50,51,52,53,54
]
}
}

hook ohn_flutter$EditView_MyEditTextState::check_2d50c0

1
String@730011edd9 = "c32N3Da77z60GEll/jos1g=="

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
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
import base64
from Crypto.Cipher import AES

def d2b(dword):
return b''.join(i.to_bytes(4,'little') for i in dword)

def b2d(byte_array):
return [int.from_bytes(byte_array[i:i+4], 'little',signed=False) for i in range(0,len(byte_array),4)]

def xxtea_enc(v,key):
n = len(v)
if n < 1:
return v

delta = 0x9E3779B9
rounds = 6 + 52 // n
sum = 0
z = v[n - 1]

for _ in range(rounds):
sum = (sum + delta) & 0xFFFFFFFF
e = (sum >> 2) & 3

for p in range(n):
y = v[(p + 1) % n]
v[p] = (v[p] + (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))) & 0xFFFFFFFF
z = v[p]
return v

def xxtea_dec(v,key):
n = len(v)
if n < 1 :
return v

delta = 0x9E3779B9
rounds = 6 + 52 // n
sum = (rounds * delta) & 0xFFFFFFFF
y = v[0]

for _ in range(rounds):
e = (sum >> 2) & 3

for p in range(n - 1, -1, -1):
z = v[p-1] if p > 0 else v[n-1]
v[p] = (v[p] - (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))) & 0xFFFFFFFF
y = v[p]
sum = (sum - delta) & 0xFFFFFFFF

return v

def pkcs7_unpad(data):
return data[:-data[-1]]

if __name__ == "__main__":
cipher = b"/oIHOyDg6s6yqVd26AnYJ6u2YjPcMhawTe93+AJPAUiwGZM4KWvXjsib1tcnZHSnglaaVpbcOaTtNoMCr5od2A=="
data = base64.b64decode(cipher)

# if len(data) % 4 != 0:
# padding = 4 - (len(data) % 4)
# data += b'\x00' * padding

aes_key = b"12345678901234561234567890123456"
aes_iv = b"1234567890123456"

aes = AES.new(aes_key,AES.MODE_CBC,aes_iv)
aes_plain = pkcs7_unpad(aes.decrypt(data))
print(f"aes_plain: {aes_plain}")


xxtea_key = "ohn_flutterkkkkk"
xxtea_cipher = base64.b64decode(aes_plain)
print(f"xxtea_cipher: {xxtea_cipher}")
v = b2d(xxtea_cipher)
key = b2d(xxtea_key.encode())

dec = xxtea_dec(v,key)
result = d2b(dec)

print(f"decrypt: {(result)}")

flag 为 flag{U_@r4_F1u774r_r4_m@ster}

[WMCTF 2025] Want2BecomeMagicalGirl

参考学习的博客是 Pangbai 师傅的:https://pangbai.work/CTF/Reverse/Want2BecomeMagicalGirl/

上次在 windows 上构建的 blutter 似了,这次用 linux 构建
blutter 项目地址 :https://github.com/worawit/blutter

1
2
3
4
5
6
git clone https://github.com/worawit/blutter --depth=1
cd blutter
sudo apt install python3-pyelftools python3-requests git cmake ninja-build \
build-essential pkg-config libicu-dev libcapstone-dev

python3 blutter.py path/to/app/lib/arm64-v8a out_dir

使用 blutter 生成的 out_dir 目录下的 ida_script/addNames.py 脚本在 ida 中恢复符号

关注 asm 文件,用 ai 分析 magical_girl 里面的 dart 文件,把它还原为可读的代码

我的还原结果如下:
aes_crypt_null_safe.dart

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
// Placeholder shim to match calls from EditView.dart.
// If you want to run this outside the decompiled runtime, replace with a real AES implementation
// and keep the same method signatures.

import 'dart:typed_data';

class AesCrypt {
late Uint8List _key;
late Uint8List _iv;

void aesSetKeys(Uint8List key, Uint8List iv) {
_key = Uint8List.fromList(key);
_iv = Uint8List.fromList(iv);
}

void aesSetMode() {
// no-op in the shim. In original app, likely sets CBC/ECB.
}

Uint8List aesEncrypt(Uint8List plain) {
// This shim does NOT do real crypto. It only echoes input for readability/testing.
// Replace with real AES to match the app.
return Uint8List.fromList(plain);
}
}

在原始的 dart 代码中搜索 aesSetKeys 和 aesSetMode, 找到函数地址并进行 hook 操作
使用 blutter 生成的 frida 脚本 blutter_frida.js

注意脚本里的这部分内容

1
2
3
4
5
6
7
8
9
10
11
12
13
function onLibappLoaded() {
// xxx("remove this line and correct the hook value");
const fn_addr = 0x2915f4;

Interceptor.attach(libapp.add(fn_addr), {
onEnter: function () {
init(this.context);
let objPtr = getArg(this.context, 0);
const [tptr, cls, values] = getTaggedObjectValue(objPtr);
console.log(`${cls.name}@${tptr.toString().slice(2)} =`, JSON.stringify(values, null, 2));
}
});
}

其中的 let objPtr = getArg(this.context, 0); 的第二个参数 0 表示要被打印的参数索引

hook aesSetKeys 的第一个参数 key 和第二个参数 iv

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
[*] Spawned and attached to package 'work.pangbai.magic.magical_girl' (pid 19508)
[*] Script loaded.
[*] Resumed spawned pid 19508
[*] Running. Press Ctrl+C to stop.
setEnabled called with argument: false
Forcing button to be enabled.
_Uint8List@77006ddf29 = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
]

[*] Spawned and attached to package 'work.pangbai.magic.magical_girl' (pid 20635)
[*] Script loaded.
[*] Resumed spawned pid 20635
[*] Running. Press Ctrl+C to stop.
setEnabled called with argument: false
Forcing button to be enabled.
_Uint8List@770071f7f9 = [
122,
37,
197,
36,
198,
51,
76,
48,
243,
98,
175,
172,
63,
35,
3,
213
]

EditView.dart

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:native_add/native_add.dart' show getKey, getSym; // 来自反汇编
import 'aes_crypt_null_safe.dart' show AesCrypt; // 使用项目内 AES 实现
import 'null_sub0.dart' as bridge; // MethodChannel 发送

class MyEditText extends StatefulWidget {
const MyEditText({super.key});
@override
State<MyEditText> createState() => MyEditTextState();
}

class MyEditTextState extends State<MyEditText> {
final _controller = TextEditingController();
String _result = "";

// 反汇编里展示的大段文本
static const _lyrics = "魔法で最低な人を消そう\nLet's use magic to erase the worst people\n用魔法将恶人全部抹消\n\n"
"はぁ正直もうやめたい(はぁ)\nSigh... Honestly I want to quit sigh\n唉 说真的已经累了(唉)\n\n"
"魔法少女をやめたい(はぁ)\nI want to stop being a magical girl sigh\n不想再当魔法少女了(唉)\n\n"
"自分ごと消えちゃってさようなら\nMaybe I should disappear myself - goodbye\n连同自己也一起抹除 永别了";

@override
Widget build(BuildContext context) {
final appBar = AppBar(
title: const Text("Want2BecomeMagicalGirl ! ! !"),
backgroundColor: const Color(0xFFFFB6C1),
);

final input = TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: "Fill Magical Spell Here and Press enter...",
filled: true,
),
textAlign: TextAlign.start,
onSubmitted: (spell) async {
// 提交后立即清输入框
_controller.text = "";
await _checkAndSend(spell);
},
);

final title = Text(
_result.isEmpty ? "" : _result,
style: const TextStyle(
color: Colors.pink, fontSize: 16, fontWeight: FontWeight.w600,
),
);

return Scaffold(
appBar: appBar,
body: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
input,
title,
const Expanded(
child: Text(_lyrics),
),
],
),
),
);
}

// 注意:补位方式不是标准 PKCS#7。反汇编逻辑是:
// pad = 16 - (len & 0x0F); 若 pad == 0 则 pad = 16;
// 然后生成长度为 pad 的字符数组,每个元素的“码点”是 (pad << 1),
// 把它们拼成字符串再附加到原串上。
String _padToBlockSize(String s) {
final len = s.length;
var pad = 16 - (len & 0x0F);
if (pad == 0) pad = 16;
final codePoint = (pad << 1); // 2,4,...,32
final padStr = String.fromCharCodes(List.filled(pad, codePoint));
return s + padStr;
}

static String _uint8ListToHex(Uint8List data) {
// 反汇编是 MappedIterable + join,这里直写可读实现
const hex = "0123456789abcdef";
final b = StringBuffer();
for (final v in data) {
b.write(hex[v >> 4]);
b.write(hex[v & 0xF]);
}
return b.toString();
}

Future<void> _checkAndSend(String spell) async {
// 原代码里存在一个 getSym() 的分支与反调试/校验,保留但只做存在性判断。
final sym = getSym(); // ByteBuffer
final symBytes = sym.asUint8List(); // 使用 _ByteBuffer::asUint8List
// 反汇编判断: 某偏移+7 位置字节是否等于 0xD6(214);我们用兜底安全判断。
final looksOk = symBytes.length > 7 && symBytes[7] == 0xD6;

// 获取密钥(native 返回的 Iterable<int>)
final keyInts = List<int>.from(getKey());
final key = Uint8List.fromList(keyInts);

// 固定 IV:偶数 2,4,6,...,32,然后取前 16 个字节(反汇编里有组 32 -> 取 16)
final iv = Uint8List.fromList(List<int>.generate(16, (i) => (i + 1) * 2));

// 配置 AES
final aes = AesCrypt();
aes.aesSetKeys(key, iv); // 符合反汇编中 aesSetKeys(key, iv)
aes.aesSetMode(); // 反汇编里单独调用,无参,采用库默认模式

// 字符串补位(按该 App 的非常规规则),UTF-8 编码后加密
final padded = _padToBlockSize(spell);
final plain = utf8.encode(padded);
final cipher = aes.aesEncrypt(Uint8List.fromList(plain));

// 一段反调试:取 getSym() 的某 16 位值与 0xD6 异或判断,不满足就 pop()
// 这里只保留结构,不主动退出 App,避免影响调试:
if (looksOk) {
// 原代码里 true 路径会可能调用 SystemNavigator.pop();
// SystemNavigator.pop();
}

// 转 hex 发送到原生(反编译显示:invokeMethod(methodName = hex, arguments = null))
final hex = _uint8ListToHex(cipher);
final resp = await bridge.send(hex);

// 对比返回字符串
const okSig = "8sAFX45zT7uc0vSUyFNNly1h/d5zTt89tV3kcVr5P5n7lRKPyYtxg31zYNB2lPV0c5nf/x2/IK94XV9Ufs9XfaDG5IXxMlZy+Z2nE+ZZRFBSpMoKzQXfUq2TSjJJfQxV";
final msg = (resp == okSig)
? "You have become a magical girl ! !"
: "This spell has no magic power";

setState(() => _result = msg);
}
}

这里可以直接看到密文
8sAFX45zT7uc0vSUyFNNly1h/d5zTt89tV3kcVr5P5n7lRKPyYtxg31zYNB2lPV0c5nf/x2/IK94XV9Ufs9XfaDG5IXxMlZy+Z2nE+ZZRFBSpMoKzQXfUq2TSjJJfQxV

main.dart

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
import 'package:flutter/material.dart';
import 'EditView.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
// 主题来自反汇编的配色,做了适度精简
final primary = const Color(0xFFFFB6C1); // 4294948545
final accent = const Color(0xFFFF69B4); // 4294928820

return MaterialApp(
title: '可爱编辑器',
theme: ThemeData(
useMaterial3: true,
primaryColor: primary,
appBarTheme: AppBarTheme(
backgroundColor: primary,
titleTextStyle: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(),
cardTheme: const CardTheme(),
buttonTheme: const ButtonThemeData(),
colorScheme: ColorScheme.fromSeed(seedColor: accent),
),
home: const MyEditText(),
);
}
}

hook 120s 的弹窗,把按钮设置为可点击 true

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function() {
// 获取 android.widget.Button 类
const Button = Java.use("android.widget.Button");
// Hook setEnabled(boolean) 方法
Button.setEnabled.implementation = function(enabled) {
// 打印传入的原始参数值
console.log("setEnabled called with argument: " + enabled);

console.log("Forcing button to be enabled.");

this.setEnabled.call(this, true);
};
});

__int64 __fastcall magical_girl_aes_crypt_null_safe__Aes::aesEncryptBlock_28d5bc(__int64 a1) 里面发现 AES 是魔改了的,S 盒换了,列混淆和轮密钥加交换了顺序


Flutter 逆向
http://example.com/2025/09/23/flutter/
作者
Eleven
发布于
2025年9月23日
许可协议