第一篇文章里面介绍了Android是基础的CS架构,客户端和服务端架构 。安卓为什么要这么设计呢?当时问了GTP,他给出的回答是稳定性。如果服务端和客户端在一个进程内,客户端崩溃了,服务端也会一起崩溃,导致整个系统不稳定 。
这些API可以直接操作Android系统 ,安卓本身通过各种各样的Manager去提供对应的Api去获取和修改 。比如PackageManager,ActivityManager等,这些Manager里面都会持有一个代理人 。当我们去调用这个Manager里面的一些Api的时候,一些简单的Api他会尝试去自己在本进程Native或者Java去实现,如果一些复杂的字段,比如查询系统的一些信息,或者调用一些系统关键函数,这种时候他会去调用“IPC代理人 ”,这个IPC代理人就是像服务端通讯的关键 。他相当于是向服务端的传话得人 ,代理设计模式 。对不同的Manager提供不一样的功能 ,而他传的话就是对应的IPC协议 。这个协议如何传递的,就是通过底层的共享内存Binder去实现的 。
也就是说这个方法底层调用的是Binder的驱动,最终会去native层写入,剩下的就是开始运行服务端的逻辑了。把数据写入到transact方法的参数3里面。然后程序返回,下面是这个方法的原型 。
这块还有的大厂更恶心,他不走transact方法,因为transact方法底层走的就是Binder,可以直接在Native层调用的Binder 驱动,实现了transact 这个方法 。然后进行IPC通讯,直接不走Java层 。
在之前第二篇设备指纹里面介绍了获取Android Id的五种方式,第五种方式因为当时没时间也没对高版本兼容,所以一直没发 ,这块抽空对照不同Android完善一下 。
直接构建IPC协议和服务端进行通讯 ,这块targetSdkVersion 必须升级到32以上,因为getAttributionSource这个玩意32版本以上好像才有。
Reflective access to CALL_TRANSACTION will throw an exception when targeting API 33 and above
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
public String getAndroidId5(Context context) {
try
{
/
/
Acquire the ContentProvider
Class<?> activityThreadClass
=
Class.forName(
"android.app.ActivityThread"
);
Method currentActivityThreadMethod
=
activityThreadClass.getMethod(
"currentActivityThread"
);
Object
currentActivityThread
=
currentActivityThreadMethod.invoke(null);
Method acquireProviderMethod
=
activityThreadClass.getMethod(
"acquireProvider"
, Context.
class
, String.
class
,
int
.
class
, boolean.
class
);
Object
provider
=
acquireProviderMethod.invoke(currentActivityThread, context,
"settings"
,
0
, true);
/
/
Get the Binder
Class<?> iContentProviderClass
=
Class.forName(
"android.content.IContentProvider"
);
Field mRemoteField
=
provider.getClass().getDeclaredField(
"mRemote"
);
mRemoteField.setAccessible(true);
IBinder binder
=
(IBinder) mRemoteField.get(provider);
/
/
Create the Parcel
for
the arguments
Parcel data
=
Parcel.obtain();
data.writeInterfaceToken(
"android.content.IContentProvider"
);
if
(android.os.Build.VERSION.SDK_INT
>
=
android.os.Build.VERSION_CODES.S) {
context.getAttributionSource().writeToParcel(data,
0
);
data.writeString(
"settings"
);
/
/
authority
data.writeString(
"GET_secure"
);
/
/
method
data.writeString(
"android_id"
);
/
/
stringArg
data.writeBundle(Bundle.EMPTY);
}
else
if
(android.os.Build.VERSION.SDK_INT
=
=
android.os.Build.VERSION_CODES.R) {
/
/
android
11
data.writeString(context.getPackageName());
data.writeString(null);
/
/
featureId
data.writeString(
"settings"
);
/
/
authority
data.writeString(
"GET_secure"
);
/
/
method
data.writeString(
"android_id"
);
/
/
stringArg
data.writeBundle(Bundle.EMPTY);
}
else
if
(android.os.Build.VERSION.SDK_INT
=
=
android.os.Build.VERSION_CODES.Q) {
/
/
android
10
data.writeString(context.getPackageName());
data.writeString(
"settings"
);
/
/
authority
data.writeString(
"GET_secure"
);
/
/
method
data.writeString(
"android_id"
);
/
/
stringArg
data.writeBundle(Bundle.EMPTY);
}
else
{
data.writeString(context.getPackageName());
data.writeString(
"GET_secure"
);
/
/
method
data.writeString(
"android_id"
);
/
/
stringArg
data.writeBundle(Bundle.EMPTY);
}
Parcel reply
=
Parcel.obtain();
binder.transact((
int
) iContentProviderClass.getDeclaredField(
"CALL_TRANSACTION"
).get(null), data, reply,
0
);
reply.readException();
Bundle bundle
=
reply.readBundle();
reply.recycle();
data.recycle();
return
bundle.getString(
"value"
);
} catch (Exception e) {
e.printStackTrace();
return
null;
}
}
很有可能被IO重定向,导致得到的签名是错误的,所以我们可以让三方进程去加载当前apk文件,通过共享内存的方式,然后当前进程对apk文件maps里面的内存签名进行解析即可 。这块需要双进程通讯 。
我一般分析的SO文件的时候直接对jni交互进行监听,配合以前自己写的一套jnitrace,在保存的调用栈里面,看他如果调用了Parcel.obtain() 初始化或者 这种writeLong ()写入数据的方法,基本就可以确认他是IPC获取的一些字段,具体看他写入的内容是什么,或者看他写入的token是什么,比如上面的获取签名的token就是"android.content.pm.IPackageManager" ,即可知道他想做什么字段的获取。
发现就拿android id来说,他最终读取的文件路径是/data/system/users/0/settings_ssaid.xml ,这个目录下,/data/system/users/0/我发现这里面全是各种注册表和各种配置信息 。,我这边尝试改了一下里面的android id 。然后直接手机重启 ,我发现我之前自己写的Hunter获取的设备指纹android id竟然变了 。
后来我把这些文件都拷贝出来,把里面熟悉的值都随机了一份,通过magisk 插件系统文件替换的方式,对文件/data/system/users/0/进行替换 ,真没想到以前被封的设备解封了。而且不需要回复出厂设置,只需要软重启一下就行 。
现在基本大厂想要在回复出厂设置保持设备指纹不变基本不可能 。这套方案我测试过一段时间,现阶段基本大厂从客户端角度基本没办法对抗 。只能靠一些服务端指纹去做检测 。(我课程里面会更详细的去讲这套方案的落地实现 ,包括途中踩得一些坑。)
指向自己的文件,因为这个Maps是不断变化的,所以需要在svc openat这块进行拦截生成 一份新的。然后指向到这份新的文件,在新的maps里面他会对里面的item路径进行反转,转换成正常的目录,而不是包含沙箱的目录 。导致获取的数据被欺骗 。
这块读文件偏移完全可以不读取Maps ,而是读取proc/self/maps_files 对这个文件进行opendir ,对每个文件进行遍历,然后再路径拼接,通过readlinkat去反查路径 即可 。
所以我们可以自己实现一份 ,因为native注册底层本质上是给artmethod里面的fnptr进行赋值,最终调用artmethod里面的RegisterNative方法,所以我们可以不直接调用Jni直接走 artmethod里面的注册方法。具体实现如下,因为artmethod里面的注册方法每个版本的实现都不一样 ,所以这块需要根据不同版本进行case分发 。
调用的话很简单直接尝试调用我们自己实现的方法,如果失败了则调用系统的api ,这样可以有效防止jni被hook实现,jni RegisterNative 函数被监听 。
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/
/
/
/
Created by Zhenxi on
2022
/
8
/
22.
/
/
static void
*
art_method_register
=
nullptr;
static void
*
class_linker_
=
nullptr;
size_t OffsetOfJavaVm(
bool
has_small_irt,
int
SDK_INT) {
if
(has_small_irt) {
switch (SDK_INT) {
case ANDROID_T:
case ANDROID_SL:
case ANDROID_S:
return
sizeof(void
*
)
=
=
8
?
624
:
300
;
case ANDROID_R:
case ANDROID_Q:
return
sizeof(void
*
)
=
=
8
?
528
:
304
;
default:
LOGE(
"OffsetOfJavaVM Unexpected android version %d"
, SDK_INT);
abort();
}
}
else
{
switch (SDK_INT) {
case ANDROID_T:
case ANDROID_SL:
case ANDROID_S:
return
sizeof(void
*
)
=
=
8
?
520
:
300
;
case ANDROID_R:
case ANDROID_Q:
return
sizeof(void
*
)
=
=
8
?
496
:
288
;
default:
LOGE(
"OffsetOfJavaVM Unexpected android version %d"
, SDK_INT);
abort();
}
}
}
template<typename T>
int
findOffset(void
*
start, size_t
len
, size_t step, T value) {
if
(nullptr
=
=
start) {
return
-
1
;
}
for
(
int
i
=
0
; i <
=
len
; i
+
=
step) {
T current_value
=
*
reinterpret_cast<T
*
>((size_t) start
+
i);
if
(value
=
=
current_value) {
return
i;
}
}
return
-
1
;
}
/
*
*
*
根据runtime获取class_linker
*
https:
/
/
github.com
/
magician8520
/
BlackBox
/
blob
/
99f26925aa303fd0a71543e3713ef3fc57a08e81
/
Bcore
/
pine
-
core
/
src
/
main
/
cpp
/
android.h
*
/
void
*
getClassLinker() {
if
(class_linker_ !
=
nullptr) {
return
class_linker_;
}
int
SDK_INT
=
get_sdk_level();
/
/
If SmallIrtAllocator symbols can be found, then the ROM has merged commit
"Initially allocate smaller local IRT"
/
/
This commit added a pointer member between `class_linker_`
and
`java_vm_`. Need to calibrate offset here.
/
/
https:
/
/
android.googlesource.com
/
platform
/
art
/
+
/
4dcac3629ea5925e47b522073f3c49420e998911
/
/
https:
/
/
github.com
/
crdroidandroid
/
android_art
/
commit
/
aa7999027fa830d0419c9518ab56ceb7fcf6f7f1
bool
has_smaller_irt
=
getSymCompat(getlibArtPath(),
"_ZN3art17SmallIrtAllocator10DeallocateEPNS_8IrtEntryE"
) !
=
nullptr;
size_t jvm_offset
=
OffsetOfJavaVm(has_smaller_irt, SDK_INT);
auto runtime_instance_
=
*
reinterpret_cast<void
*
*
>
(getSymCompat(getlibArtPath(),
"_ZN3art7Runtime9instance_E"
));
auto val
=
jvm_offset
? reinterpret_cast<std::unique_ptr<JavaVM>
*
>(
reinterpret_cast<uintptr_t>(runtime_instance_)
+
jvm_offset)
-
>get()
: nullptr;
if
(val
=
=
getVm()) {
LOGD(
"JavaVM offset matches the default offset"
);
}
else
{
LOGW(
"JavaVM offset mismatches the default offset, try search the memory of Runtime"
);
int
offset
=
findOffset(runtime_instance_,
1024
,
4
, getVm());
if
(offset
=
=
-
1
) {
LOGE(
"Failed to find java vm from Runtime"
);
return
nullptr;
}
jvm_offset
=
offset;
LOGW(
"Found JavaVM in Runtime at %zu"
, jvm_offset);
}
const size_t kDifference
=
has_smaller_irt
? sizeof(std::unique_ptr<void>)
+
sizeof(void
*
)
*
3
: SDK_INT
=
=
ANDROID_Q
? sizeof(void
*
)
*
2
: sizeof(std::unique_ptr<void>)
+
sizeof(void
*
)
*
2
;
class_linker_
=
*
reinterpret_cast<void
*
*
>(reinterpret_cast<uintptr_t>(runtime_instance_)
+
jvm_offset
-
kDifference);
return
class_linker_;
}
bool
call_MethodRegister(JNIEnv
*
env, void
*
art_method, void
*
native_method) {
if
(art_method_register
=
=
nullptr) {
if
(get_sdk_level() < ANDROID_S) {
/
/
android
11
art_method_register
=
getSymCompat(getlibArtPath(),
"_ZN3art9ArtMethod14RegisterNativeEPKv"
);
if
(art_method_register
=
=
nullptr) {
art_method_register
=
getSymCompat(getlibArtPath(),
"_ZN3art9ArtMethod14RegisterNativeEPKvb"
);
}
}
else
{
/
/
12
以上还是在libart里面,但是在linker里面实现,符号名称存在变化
art_method_register
=
getSymCompat(getlibArtPath(),
"_ZN3art11ClassLinker14RegisterNativeEPNS_6ThreadEPNS_9ArtMethodEPKv"
);
}
if
(art_method_register
=
=
nullptr) {
LOG(ERROR) <<
"register native method get art_method_register = null "
;
return
false;
}
}
if
(get_sdk_level() >
=
ANDROID_S) {
/
/
12
以上
/
/
const void
*
RegisterNative(Thread
*
self
, ArtMethod
*
method, const void
*
native_method)
auto call
=
reinterpret_cast<void
*
(
*
)(void
*
, void
*
, void
*
,
void
*
)>(art_method_register);
/
/
get
self
thread
void
*
self
=
getSymCompat(getlibArtPath(),
"_ZN3art6Thread14CurrentFromGdbEv"
);
if
(
self
=
=
nullptr) {
LOG(ERROR) <<
"register native method get CurrentFromGdb = null "
;
return
false;
}
/
/
手动计算一下linker实例地址
void
*
classLinker
=
getClassLinker();
if
(classLinker
=
=
nullptr) {
LOG(ERROR) <<
"register native method get getClassLinker = null "
;
return
false;
}
call(classLinker,
self
, art_method, native_method);
/
/
LOG(ERROR) <<
"register native method get getClassLinker success! "
;
}
else
if
(get_sdk_level() >
=
ANDROID_R) {
auto call
=
reinterpret_cast<void
*
(
*
)(void
*
, void
*
)>(art_method_register);
call(art_method, native_method);
}
else
{
auto call
=
reinterpret_cast<void
*
(
*
)(void
*
, void
*
,
bool
)>(art_method_register);
call(art_method, native_method, true);
}
return
true;
}
inline static
bool
IsIndexId(jmethodID mid) {
return
((reinterpret_cast<uintptr_t>(mid)
%
2
) !
=
0
);
}
static jfieldID field_art_method
=
nullptr;
bool
RegisterNativeMethod(JNIEnv
*
env,
jclass clazz,
const JNINativeMethod
*
methods,
size_t nMethods) {
if
(env
=
=
nullptr) {
LOG(ERROR) <<
"register native method JNIEnv = null "
;
return
false;
}
void
*
arm_method
=
nullptr;
for
(
int
i
=
0
; i < nMethods; i
+
+
) {
jmethodID methodId
=
env
-
>GetMethodID(clazz, methods[i].name, methods[i].signature);
if
(methodId
=
=
nullptr) {
/
/
maybe static
env
-
>ExceptionClear();
methodId
=
env
-
>GetStaticMethodID(clazz, methods[i].name, methods[i].signature);
if
(methodId
=
=
nullptr) {
LOG(ERROR) <<
"register native method get orig method == null "
<< methods[i].signature;
env
-
>ExceptionClear();
return
false;
}
}
if
(get_sdk_level() >
=
ANDROID_R) {
if
(field_art_method
=
=
nullptr) {
jclass pClazz
=
env
-
>FindClass(
"java/lang/reflect/Executable"
);
field_art_method
=
env
-
>GetFieldID(pClazz,
"artMethod"
,
"J"
);
}
if
(field_art_method
=
=
nullptr) {
LOG(ERROR) <<
"register native method get artMethod == null "
;
return
false;
}
if
(IsIndexId(methodId)) {
jobject method
=
env
-
>ToReflectedMethod(clazz, methodId, true);
arm_method
=
reinterpret_cast<void
*
>(env
-
>GetLongField(method, field_art_method));
/
/
LOG(ERROR) <<
"arm_method "
<<arm_method ;
}
}
else
{
arm_method
=
methodId;
}
if
(arm_method
=
=
nullptr) {
LOG(ERROR) <<
"register native method art method == null "
;
return
false;
}
if
(!call_MethodRegister(env, arm_method, methods[i].fnPtr)) {
LOG(ERROR) <<
"register native method fail "
<<
methods[i].name <<
" "
<< methods[i].signature;
return
false;
}
/
/
LOG(INFO) <<
"register native method success "
<< methods[i].name <<
" "
/
/
<< methods[i].signature;
}
return
true;
}
可以给老鼠一些假大米(“脏数据”)去定位,有很多老鼠不知道自己的大米有问题,正在吃的时候就被猫抓到了,或者对老鼠的搬运速度进行限制(“请求速度”),或者当发现某个老鼠带着包裹进来粮仓的时候都进行限制(“策略”)
这时候猫就需要去检查都有哪些可以装数据的办法(”定制策略“)去分析老鼠的行为,看看不同的老鼠都在都在做什么。当然每次定制的策略都不一样,老鼠也不知道,只有猫知道 ,所以老鼠一直处在明,而猫在暗 。
ok 经过上面的例子总结和反思,我们发现一个问题,如果在不Root的情况下,注入方法主要两种,重打包或者把Apk放到沙箱里面 。并且在不修改系统文件,那么我应该如何修改“气味”呢?
决定猫和老鼠明暗关系位置的关系本质上是 “气味”主导因素 。这个“设备风险标签”是这场游戏中决定胜败的主要因素 ,如果“设备风险标签” 是没问题,也配合一些多开软件,云手机等控制多只老鼠即可 。
这里面的标签分为很多种 。每个子项又分为很多小项,不同的标签颜色不同或者说不同价值的标签对不同猫咪的反应程度也不一样 。比如重打包这种标签,在一些高度敏感的场景,会直接被猫进行封号。
第三项现在So层基本大厂都差不多,都是各种混淆配合控制流 ,但是Java层防护做的不够 ,java层其实可以参考我之前19年搞的Java控制流混淆 。https://bbs.kanxue.com/thread-255514.htm ,可以直接废掉Jadx反编译软件 。
很多小白基本都是遇到一个指纹,咦,发现自己没有修改 。赶紧去Hook修改一下。去打个补丁 ,在我看来这是一种很Low的办法 。绕来绕去人家采集一个字段有N种手段,很容易导致遗漏,特别是一些大厂基本采集一个字段都是N种获取方式,就比如第二篇文章里面的磁盘大小,或者Android id的获取五种方式 。
你打了一个补丁补上去,在其他地方设备指纹或者环境风险又泄漏了,最后代码写的破破烂烂 。所以在边界值修改对应的值是最完美的方案 。先把架子搭好了,后面发现什么直接在边界值处的callback进行修改即可 。
先说IPC,IPC的话很简单,我在上面也说了可以动态代理,也可以直接去用Hook框架Hook binder里面的交互方法 。当发现触发指定的IPC协议的时候,直接模拟服务端往里面写入即可。
这块还有个细节点,为了防止程序直接通过cache获取,因为有的字段初始化以后可能被保存到cache里面 ,如果不存在的话再通过ipc去获取。Apk在启动一瞬间就进行了初始化,cache会被保存 。很多IPC代理人会这么设计 ,所以需要清理掉cache ,这个cache可以是Parcel的cache也可以是IPC代理人里面的cache 。比如Parcel里面的mCreators 或者sPairedCreators 都需要清空 。如果是IPC代理人的话也可以看代码看具体实现,看看是否包含cache,有的话清掉即可 。
如果想做自动化的方式,怎么把自己模拟的更像一个真实的“老鼠” ,比如可以在自动化点击的记录一些人手操作的路径 ,而不是单纯地去点击 。在点击过程中添加一些随机路径 ,这些都是很不错的对抗手段 。