我如何在Android的安全性中发现复活节彩蛋而没有在Google找到工作

Google喜欢复活节彩蛋。 实际上,它是如此地爱他们,您几乎可以在他们的每个产品中找到它们。 Android复活节彩蛋的传统始于该操作系统的最早版本(我认为那里的每个人都知道当您进入常规设置并点击几次版本号时会发生什么)。

但是有时您会在最不可能的地方找到复活节彩蛋。 甚至有一个都市传说,有一天,程序员用Google搜索了“互斥锁”,但搜索结果并没有落到foo.bar上,而是解决了所有任务并找到了一份工作。

改建
图片

我也发生了同样的事情(除了没有幸福的结局)。 隐藏的消息绝对不可能存在,它反转了Java代码及其本机库,秘密VM,Google采访-所有这些都在下面。

机器人卫士


一个无聊的夜晚,我将手机出厂重置,然后再次进行设置。 首先,全新的Android安装要求我登录Google帐户。 我想知道:登录Android的过程如何工作? 夜晚突然变得不那么闷了。

我使用PortSwigger的Burp Suite来拦截和分析网络流量。 免费的社区版本足以满足我们的目的。 要查看https请求,我们首先需要在设备上安装PortSwigger的证书。 作为测试设备,我选择了拥有8年历史的Android 4.4三星银河S。 除此之外的任何更新,都可能会导致证书固定和填充问题。

老实说,Google API请求没有什么特别之处。 设备发出有关其自身的信息并获得令牌作为响应。唯一奇怪的步骤是对反滥用服务的POST请求。



发出请求后,在众多非常正常的参数中出现了一个有趣的参数,名为droidguard_result 。 这是一个很长的Base64字符串:



DroidGuard是Google的用于在实际设备中检测机器人和模拟器的机制。 例如,SafetyNet也使用DroidGuard的数据。 Google对于浏览器也有类似的东西-Botguard。

那是什么数据呢? 让我们找出答案。

协议缓冲区


是什么产生了该链接( www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI ),而Android内部是什么发出了此请求? 经过简短调查,结果发现该链接以这种确切的形式位于Google Play服务的混淆类之一内:

public bdd(Context var1, bdh var2) { this(var1, "https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI", var2); } 

正如我们在Burp中已经看到的那样,此链接上的POST请求具有Content-Type - application / x-protobuf (Google协议缓冲区,Google的二进制序列化协议)。 但是,它不是json-很难确切地知道发送了什么。

协议缓冲区的工作方式如下:

  • 首先,我们以特殊格式描述消息的结构并将其保存到.proto文件中;
  • 然后,我们编译.proto文件,然后protoc编译器会以选定的语言生成源代码(在Android的情况下为Java)。
  • 最后,我们在项目中使用生成的类。

我们有两种方式解码protobuf消息。 第一个是使用protobuf分析器,并尝试重新创建.proto文件的原始描述。 第二个是从Google Play服务中删除protoc生成的类,这是我决定要做的。

我们将获取与设备上安装的版本相同的Google Play Services的.apk文件(或者,如果设备已植根,则直接从那里获取文件)。 使用dex2jar,我们将.dex文件转换回.jar,并在选择的反编译器中打开。 我个人喜欢JetBrains的Fernflower。 它用作IntelliJ IDEA(或Android Studio)的插件,因此我们只需启动Android Studio并使用我们要分析的链接打开文件。 如果proguard不太努力,则可以将用于创建protobuf消息的反编译Java代码复制粘贴到您的项目中。

查看反编译的代码,我们看到Build。*常量在protobuf消息内发送。 (好的,这很难猜到)。

 ... var3.a("4.0.33 (910055-30)"); a(var3, "BOARD", Build.BOARD); a(var3, "BOOTLOADER", Build.BOOTLOADER); a(var3, "BRAND", Build.BRAND); a(var3, "CPU_ABI", Build.CPU_ABI); a(var3, "CPU_ABI2", Build.CPU_ABI2); a(var3, "DEVICE", Build.DEVICE); ... 

但不幸的是,在服务器的答复中,所有protobuf字段在经过混淆后都变成了字母汤。 但是我们可以使用错误处理程序发现其中的内容。 检查来自服务器的数据的方法如下:

 if (!var7.d()) { throw new bdf("byteCode"); } if (!var7.f()) { throw new bdf("vmUrl"); } if (!var7.h()) { throw new bdf("vmChecksum"); } if (!var7.j()) { throw new bdf("expiryTimeSecs"); } 

显然,这是在混淆之前调用字段的方式: byteCodevmUrlvmChecksumexpiryTimeSecs 。 这种命名方案已经给我们一些想法。

我们将来自Google Play服务的所有反编译类合并到一个测试项目中,对其进行重命名,生成测试Build。*命令并启动(模仿我们想要的任何设备)。 如果有人想自己做,这是指向我的GitHub的链接

如果请求正确,则服务器返回以下内容:
00:06:26.761 [main] INFO daresponse.AntiabuseResponse-字节码大小:34446
00:06:26.761 [main] INFO daresponse.AntiabuseResponse-vmChecksum:C15E93CCFD9EF178293A2334A1C9F9B08F115993
00:06:26.761 [main]信息daresponse.AntiabuseResponse-vmUrl: www.gstatic.com/droidguard/C15E93CCFD9EF178293A2334A1C9F9B08F115993
00:06:26.761 [main]信息daresponse.AntiabuseResponse-expiryTimeSecs:10

步骤1完成。 现在,让我们看看vmUrl链接背后隐藏的内容

秘密APK


链接直接将我们引到一个.apk文件,该文件以其自己的SHA-1哈希命名。 它很小-只有150KB。 这是完全合理的:如果20亿个Android设备中的每一个都下载了该设备,那么Google服务的流量为270TB。



作为Google Play服务的一部分的DroidGuardService类将文件下载到设备上,进行解压缩,提取.dex并通过反射使用com.google.ccc.abuse.droidguard.DroidGuard类。 如果出现错误,则DroidGuardService将从DroidGuard切换回Droidguasso。 但这完全是另一个故事。

本质上, DroidGuard类是围绕本机.so库的简单JNI包装器。 本地库的ABI与我们在protobuf请求中的CPU_ABI字段中发送的内容匹配:我们可以要求armeabi,x86甚至MIPS。

DroidGuardService服务本身不包含任何用于DroidGuard类的有趣逻辑。 它只是创建一个新的DroidGuard实例,从protobuf消息中DroidGuard发送byteCode ,调用一个公共方法,该方法返回一个字节数组。 然后将该数组发送到droidguard_result参数中的服务器。

为了大致了解DroidGuard内部的DroidGuard我们可以重复DroidGuard的逻辑(但由于我们已经拥有本机库,因此无需下载.apk)。 我们可以从秘密APK中提取一个.dex文件,将其转换为.jar,然后在我们的项目中使用。 唯一的问题是DroidGuard类如何加载本机库。 静态初始化块调用loadDroidGuardLibrary()方法:

 static { try { loadDroidGuardLibrary(); } catch (Exception ex) { throw new RuntimeException(ex); } } 

然后, loadDroidGuardLibrary()方法读取library.txt(位于.apk文件的根目录中),并通过System.load(String filename)调用以该名称加载该库。 对于我们来说不是很方便,因为我们需要以一种非常特定的方式来构建.apk,以便将library.txt和.so文件放入其根目录。 将.so文件保留在lib文件夹中,并通过System.loadLibrary(String libname)加载该文件会更加方便。

这并不难。 我们将对.dex文件使用smali / baksmali-汇编程序/反汇编程序。 使用它之后,classes.dex变成一堆.smali文件。 应该修改com.google.ccc.abuse.droidguard.DroidGuard类,以便静态初始化块调用System.loadLibrary("droidguard")方法,而不是loadDroidGuardLibrary() 。 Smali的语法非常简单,新的初始化块如下所示:

 .method static constructor <clinit>()V .locals 1 const-string v0, "droidguard" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void .end method 

然后,我们使用backsmali将其全部构建回.dex,然后将其转换为.jar。 最后,我们得到一个.jar文件,可以在我们的项目中使用-顺便说一下, 这里是

整个DroidGuard相关部分的长度为几个字符串。 最重要的部分是在解决反滥用服务之后,下载上一步中获得的字节数组,并将其交给DroidGuard构造函数:

 private fun runDroidguard() { var byteCode: ByteArray? = loadBytecode("bytecode.base64"); byteCode?.let { val droidguard = DroidGuard(applicationContext, "addAccount", it) val params = mapOf("dg_email" to "test@gmail.com", "dg_gmsCoreVersion" to "910055-30", "dg_package" to "com.google.android.gms", "dg_androidId" to UUID.randomUUID().toString()) droidguard.init() val result = droidguard.ss(params) droidguard.close() } } 

现在,我们可以使用Android Studio的探查器,查看DroidGuard工作期间发生的情况:



initNative()本机方法收集有关设备的数据,并调用Java方法hasSystemFeature(), getMemoryInfo(), getPackageInfo() ……虽然如此,但我仍然看不到任何可靠的逻辑。 好吧,剩下的就是反汇编.so文件。

libdroidguard.so


幸运的是,分析本机库并不比使用.dex和.jar文件困难。 我们需要一个类似于Hex-Rays IDA的应用程序,并且需要一些x86或ARM汇编代码的知识。 我选择了ARM,因为我有一个扎根的设备可以调试。 如果您没有,则可以使用x86库并使用仿真器进行调试。

类似于Hex-Rays IDA的应用将二进制文件反编译为类似于C代码的东西。 如果打开Java_com_google_ccc_abuse_droidguard_DroidGuard_ssNative方法,则会看到类似以下内容:

 __int64 __fastcall Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) ... v14 = (*(_DWORD *)v9 + 684))(v9, a5); v15 = (*(_DWORD *)v9 + 736))(v9, a5, 0); ... 

看起来不太有前途。 首先,我们需要采取一些初步步骤,将其转变为更有用的东西。 反编译器对JNI一无所知,因此我们安装了Android NDK并导入了jni.h文件。 众所周知,JNI方法的前两个参数是JNIEnv*jobject (this) 。 我们可以从DroidGuard的Java代码中找到其他参数的类型。 分配正确的类型后,无意义的偏移量将变成JNI方法调用:

 __int64 __fastcall Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative(_JNIEnv *env, jobject thiz, jobject context, jstring flow, jbyteArray byteCode, jobject runtimeApi, jobject extras, jint loggingFd, int runningInAppSide) { ... programLength = _env->functions->GetArrayLength)(_env, byteCode); programBytes = (jbyte *)_env->functions->GetByteArrayElements)(_env, byteCode, 0); ... 

如果我们有足够的耐心来追踪从反滥用服务器接收到的字节数组,我们将感到失望。 不幸的是,对于“这里发生了什么?”没有简单的答案。 它是纯净的,精炼的字节码,本机库是虚拟机。 一些AES加密散布在顶部,然后VM逐字节读取字节码,然后执行命令。 每个字节都是一个命令,后跟操作数。 命令不多,只有70条左右:读取int,读取字节,读取字符串,调用Java方法,将两个数字相乘,if-goto等。

唤醒新


我决定进一步研究该虚拟机的字节码结构。 调用还有另一个问题:有时(每两周一次)有一个新版本的本机库,其中的字节命令对被打乱了。 这并没有阻止我,我决定使用Java重新创建VM。

字节码的作用是完成所有例程以收集有关设备的信息。 例如,它加载一个带有方法名称的字符串,通过dlsym获取其地址并执行。 在Java版本的VM中,我仅重新创建了大约5种方法,并学会了解释反滥用服务字节码的前25个命令。 在第26条命令上,VM从字节码中读取另一个加密的字符串。 突然发现这不是另一种方法的名称。 远非如此。
虚拟机命令#26
方法调用vm-> vm_method_table [2 * 0x77]
方法vmMethod_readString
索引是0x9d
字符串长度是0x0066
(生成新密钥)
编码的字符串字节为EB 4E E6 DC 34 13 35 4A DD 55 B3 91 33 05 61 04 C0 54 FD 95 2F 18 72 04 C1 55 E1 92 28 11 66 04 DD 4F B3 94 33 04 35 0A C1 4E B2 DB 12 17 79 4F 92 55 FC DB 33 05 35 45 C6 01 F7 89 29 1F 71 43 C7 40 E1 9F 6B 1E 70 48 DE 4E B8 CD 75 44 23 14 85 14 A7 C2 7F 40 26 42 84 17 A2 BB 21 19 7A 43 DE 44 BD 98 29 1B
解码的字符串字节为59 6F 75 27 72 65 20 6E 6F 74 20 6A 75 73 74 20 72 75 6E 6E 69 6E 67 20 73 74 72 69 6E 67 73 20 6F 6E 20 6F 75 72 20 2E 73 6F 21 20 54 61 6C 6B 20 74 6F 20 75 73 20 61 74 20 64 72 6F 69 64 67 75 61 72 64 2D 68 65 6C 6C 6F 2B 36 33 32 36 30 37 35 34 39 39 36 33 66 36 36 31 40 67 6F 6F 67 6C 65 2E 63 6F 6D
解码后的字符串值为( 您不只是在.so上运行字符串,请与droidguard@google.com与我们联系
真奇怪 虚拟机以前从未与我交谈过。 我认为,如果您开始看到定向到您的秘密消息,您就会发疯。 为了确保我仍然保持理智,我通过虚拟机从反滥用服务运行了数百个不同的答案。 从字面上看,每25-30个命令在字节码中都会隐藏一条消息。 他们经常重复,但以下是一些独特的内容。 不过,我编辑了电子邮件地址:每封邮件都有一个不同的地址,例如“ droidguard+tag@google.com”,并且每个标签都是唯一的。
droidguard@google.com:请勿成为陌生人!
你进来了! 通过droidguard@google.com与我们联系
droidguard@google.com勇敢的旅行者的问候! 打个招呼!
找到这个容易吗? droidguard@google.com想知道
droidguard@google.com上的人们将非常感谢您的来信!
这是什么鬼话? 询问droidguard@google.com ...他们会知道的!
y! 想在这里见到你。 您是否已与droidguard@google.com通话?
您不仅在我们的.so上运行字符串! 通过droidguard@google.com与我们联系
我是被选中的人吗? 我以为是时候停止与DroidGuard纠缠并与Google交谈了,因为他们问我了。

您的电话对我们很重要


我在发现的电子邮件中告诉了我的发现。 为了使结果更令人印象深刻,我对分析过程进行了一些自动化。 关键是,字符串和字节数组存储在加密的字节码中。 VM使用编译器内联的常量对它们进行解码。 使用类似于Hex-Rays IDA的应用程序,您可以轻松提取它们。 但是随着每个新版本常量的变化,总是手动提取它们是很不方便的。

但是事实证明,Java解析本机库非常简单。 使用jelf (用于解析ELF文件的库),我们在二进制文件中找到Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative方法的偏移量,然后使用Capstone (具有针对多种语言(包括Java)的绑定的反汇编框架)获取汇编程序代码,并将其加载到常量中注册表。

最后,我得到了一个模拟整个DroidGuard流程的应用程序:向反滥用服务发出请求,下载.apk,解压缩,解析本机库,提取所需的常数,选择VM命令的映射并解释字节码。 我将其全部编译并发送给Google。 在此期间,我开始准备搬家,并在Glassdoor上搜索了Google的平均工资。 我决定不同意少于六位数的数字。

答案没多久。 DroidGuard团队成员的一封电子邮件仅显示:“您为什么还要这样做?”



“因为我可以”-我回答。 一位Google员工向我解释说DroidGuard应该可以保护Android免受黑客攻击(您不会说!),因此,将DroidGuard VM的源代码保留给我自己是明智的。 我们的谈话到此结束。

面试


一个月后,我收到了另一封电子邮件。 苏黎世的DroidGuard团队需要一名新员工。 我有兴趣加入吗? 当然可以!

没有进入Google的捷径。 我所能做的所有工作就是将我的简历转发给人事部门。 在那之后,我不得不经历通常的官僚作风和一系列采访。

关于Google采访的故事很多。 算法,奥运会任务和Google文档编程都不是我的事,所以我开始做准备。 我数十次阅读了Coursera的“算法”课程,解决了Hackerrank上的数百项任务,并学会了闭着眼睛绕过二维图。

两个月过去了。 说我觉得准备不足是一种轻描淡写。 Google文档成为我最喜欢的IDE。 我觉得我知道关于算法的所有知识。 当然,我知道自己的弱点,并意识到我可能不会通过在苏黎世进行的5次面试,但是免费进入程序员的迪斯尼乐园本身就是一种回报。 第一步是电话面试,以淘汰最弱的候选人,而不要浪费苏黎世开发人员在面对面的会议上的时间。 那天定了,电话响了...



……我立即不及格。 我很幸运-他们问了一个我以前在互联网上见过并且已经解决的问题。 这是关于序列化字符串数组。 我提供了在Base64中编码字符串并通过分隔符保存它们的方法。 面试官要求我开发Base64算法。 之后,采访变成了独白,采访中向我解释了Base64的工作原理,我试图记住Java中的位操作。

如果Google的任何人正在阅读此书
伙计们,如果你到了那里,你真是个天才! 说真的 我无法想象一个人如何清除他们摆在您面前的所有障碍。

通话3天后,我收到一封电子邮件,说他们不想再采访我。 这就是我与Google的交流结束的方式。

为什么在DroidGuard中有消息要求聊天,我仍然不知道。 可能只是为了统计数据。 我最初写信给的那个人告诉我,人们实际上在那儿写信,但是频率是变化的:有时他们一周会收到3封回复,有时是一年一次。

我相信有更简单的方法可以在Google接受采访。 毕竟,您可以问100,000个员工中的任何一个(诚然,虽然不是所有人都是开发人员)。 但这仍然是一个有趣的经历。

Source: https://habr.com/ru/post/zh-CN446790/


All Articles