[原创]聊聊大厂设备指纹其三&如何在风控对抗这场“猫鼠游戏”中转换角色
2023-6-16 14:38:8 Author: bbs.pediy.com(查看原文) 阅读量:28 收藏

第一篇文章里面介绍了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,有的话清掉即可 。

如果想做自动化的方式,怎么把自己模拟的更像一个真实的“老鼠” ,比如可以在自动化点击的记录一些人手操作的路径 ,而不是单纯地去点击 。在点击过程中添加一些随机路径 ,这些都是很不错的对抗手段 。


文章来源: https://bbs.pediy.com/thread-277637.htm
如有侵权请联系:admin#unsafe.sh