首页
社区
课程
招聘
记录学习Protobuf第二弹!从二次开发到动静态逆向分析APP
2023-9-2 22:39 3827

记录学习Protobuf第二弹!从二次开发到动静态逆向分析APP

2023-9-2 22:39
3827

利用现成Dem0二次开发,逆向分析protobuf交互数据

文章仅供思路参考,请勿用于非法用途!

书接上文,上一篇文章搭建了基本开发环境,并利用大佬Demo完成数据交互。这一次,将会在大佬开源的demo基础上进行简简单单的二次开发(原本计划从头到尾开发Dem0,无奈开发知识太欠缺了,光配环境就卡了好久~),增加请求以及返回数据的多样性,并利用最近所学到的知识实现动静态分析自己所写的APP。

目标:

  1. Android Studio上做一些基础的配置来为二次开发做好基础。
  2. 在大佬demo基础上进行二次开发,增加多个请求数据及返回响应字段。
  3. 浅浅尝试阅读一下大佬demo开发源码&build proto 文件编译后生成的新的文件(Message字段、Grpc类文件等)。
  4. 中间人抓包拦截并保存交互数据,分析二进制文件。以及使用protoc 解码二进制文件尝鲜。
  5. 尝试动静态分析APP,根据已有开发知识一起寻找蛛丝马迹来完成逆向分析协议。
  6. todo:最后的最后,尝试对此项目升级为HTTPS。(PS: 没忍住,在第二步就完成了升级)

前面做了大量了铺垫,从搭建环境到二次开发再到最后打包APP进行动静态分析。这一路就像是打怪升级,遇到了一个又一个麻烦,不过最让我欣慰的是最后的动静态分析算是弥补了上一篇文章的盲点,了解了Java代码如何将数据写入二进制流以及将它hook拿出来分析。当然了这篇文章也有遗憾,那就让遗憾在下一篇文章中解决吧!

Android Studio基础配置

后来啊,踩的坑多了,才知道,原来android开发环境不是取决于Android Studio的版本,而是取决于所创建项目Gradle版本以及Gradle Plugins 版本。

这一步骤是为了实现在本机环境下搭建一个可以正常开发的环境,因上篇文章采用的是老版本的AS~

1. 下载并导入项目到Android Studio中

  • 本次开发的AS版本为 Android Studio Flamingo 2022.2.1 Patch2​​​ 版本不重要~

    图1 - Android Studio 版本

  • 插个题外话,Android Studio可以实现多版本共存,具体操作如下:

    • 先在历史下载页面中挑选出想要下载的版本,点击就可看出下面的对话框。

      image

    • 直接点击对应的ZIP包即可完成下载,下载完成之后解压运行bin目录下 androidstudio.exe 就可以直接使用~

    • 倘若点击之后,浏览器无法下载,可以尝试复制下载链接至迅雷即可完成下载~

  • 下载大佬项目,并导入Android Studio中,立马就会报错~

    • Unable to make field private static final java.util.Map java.lang.ProcessEnvironment.theCaseInsensitiveEnvironment accessible: module java.base does not "opens java.lang" to unnamed module @1d8d30f7
  • 没关系问题可以解决!依次点击 File -> Project Structure。 在Project选项卡中设置 Android Gradle Plugin Version 为3.3.0 (更多Gradle&GradlePluginsd对比)

  • 在SDK Location 选项卡中 点击 Gradle Settings 选择JDK11(也可尝试低版本JDK)

  • 最后点击 强制同步 按钮即可完成导入项目!

    image

2. 为Android Studio安装插件 & 利用插件来生成编译文件

  1. 依次点击 File -> Settings -> Plugins -> 搜索 GenProtobuf​​和 Protocol Buffers​​,安装完成之后,记得点击Apply。

    image
    前者GenProtobuf提供了一种快速根据proto文件生成编译后的文件,作用类似与上节所利用protoc工具生成的xxxx_pb2 和 xxxx_pb2_grpc 两个py文件,利用生成的两个文件来实现客户端与服务端交互功能。后者Protocol Buffers为一种proto文件语法高亮提示的功能。

  2. 在Android开发中,一般会在main目录下,新建名为proto的文件夹来存放.proto文件。如成功安装了GenProtobuf​​插件,会在工具栏Tools -> Configure GenProtobuf 选项。下图为配置界面

    image

  3. 关键的一步来了! 在proto文件下,找到helloworld.proto 文件,然后右击 -> quick gen protobuf rules

    image
    image

  4. 此番操作,会在com.xuexiang.protobudfdemo 目录下生成必要的文件。细心的你发现了为何没有生成类似与xxx_pb2_grpc类似的Grpc文件呢? 原因是因为上述 此插件执行等价的命令 上图中第二行​ 它就没有加上生成java_grpc 的代码选项。

  5. 可能这一步会很琐碎麻烦,目前还不知道有何快捷方式可以方便的方式可以快速完成!

    • 在helloworld.proto目录下,进入到上一篇文章中的虚拟环境中,执行如下命令

    • python -m grpc_tools.protoc -I./ --grpc-java_out=D:\Project_Code\Learning_Python_Code\LearningProtobufDem0\Protobuf-gRPC-Android-master\app\src\main\java helloworld.proto​​ 当然你可能会报下面错误,问题当然可以解决!

      image

    • 莫慌~如果没印象下载过此文件,可点击此处下载。重新执行此命令 并加上 --plugin=protoc-gen-grpc-java=文件路径​​选项。也可省事直接将下载好的文件加入PATH中。

    • ​未加入系统环境PATH中:python -m grpc_tools.protoc -I./ --plugin=protoc-gen-grpc-java=D:\T00ls\protoc-23.4-win64\bin\protoc-gen-grpc-java.exe --grpc-java_out=D:\Project_Code\Learning_Python_Code\LearningProtobufDem0\Protobuf-gRPC-Android-master\app\src\main\java helloworld.proto​​

    • 已加入系统环境PATH中:​python -m grpc_tools.protoc -I./ --grpc_java_out=D:\Project_Code\Learning_Python_Code\LearningProtobufDem0\Protobuf-gRPC-Android-master\app\src\main\java helloworld.proto​​

  6. 此时此刻,确实是会在目录下生成一些Grpc文件。但如果不出意外~还是会出意外....

    image

  7. 百思不得其解,我把新生成的这些文件(生成的中间类文件)全部删除,项目代码居然可以直接跑~ ?它代码运行到手机时自己会编译一份字段class文件以及Grpc 类文件,做这些的目的可能就是为了方便开发吧 开发完成后直接把这些文件删除!欸~ 那实际开发就没这些代码也能欢快的跑给逆向也没啥帮助。还是对开发流程基本知识不够了解~

  8. 由于此次proto代码文件未做修改,使用上次的代码也是可以完成 android <--> Python服务端的通信。

    imageimage​​

利用大佬demo简单二次开发 & 尝试对HTTP升级为HTTPS

1. 对已有的HTTP升级为HTTPS?

代码参考链接GitHub 基与python的GRPC SSL/TLS加密及Token鉴权 - Nolinked - 博客园 (cnblogs.com) openssl生成证书server.key server.crt

  1. 手动生成一份crt证书文件

    • OpenSSL安装教程

    • openssl genrsa -out server.key 2048

      image

    • openssl req -new -key server.key -out server.csr​ & openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

      image

    • 经过上述操作报下面这样的错误~ 原因呢也很简单,那就是没有正确的设置IP到证书里面去,导致无法连接到服务器!

      • details = "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:50051: Peer name 127.0.0.1 is not in peer certificate"
        debug_error_string = "UNKNOWN:failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:50051: Peer name 127.0.0.1 is not in peer certificate {created_time:"2023-08-28T02:44:43.8965393+00:00", grpc_status:14}"
    • 经过一番查阅之后,我们需要新创建一个配置文件,在配置文件内加上我们的IP地址,然后重新利用openssl进行生成包含IP的证书文件

      1. 新创建一个名为 san.cnf 的文件如下。可以在 [alt_names] 下 按照类似的格式来增加IP地址。127.0.0.1地址是作为测试使用,而192.168.1.111是本地IP 为后续Android通信做铺垫。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        [req]
        distinguished_name = req_distinguished_name
        req_extensions = v3_req
        prompt = no
         
        [req_distinguished_name]
        CN = 127.0.0.1
         
        [v3_req]
        subjectAltName = @alt_names
         
        [alt_names]
        IP.1 = 127.0.0.1
        IP.2 = 192.168.1.111
      2. 运行以下命令生成私钥和 CSR 文件:

        1
        openssl req -new -nodes -out server.csr -keyout server.key -config san.cnf
      3. 运行以下命令生成自签名证书:

        1
        openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt -extensions v3_req -extfile san.cnf
      4. 运行以下命令查看是否包含IP地址

        1
        openssl x509 -in server.crt -text -noout
      5. image

  2. 对Python实现的服务端进行升级HTTPS

    • 服务端升级如下

      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
      from datetime import datetime
       
      import grpc
      import helloworld_pb2 as pb2
      import helloworld_pb2_grpc as pb2_grpc
      from concurrent import futures
       
      class Greeter(pb2_grpc.GreeterServicer):
          def SayHello(self, request, context):
              name = request.name
              sendtime = datetime.now().strftime('%Y-%m-%d-%H:%M:%S')
              result = f"My Name is {name}!!! send time is {sendtime} "
              print(f"服务器接收到客户端参数 -> name = {name}")
              print(f"服务器响应时间: {sendtime}")
              return pb2.HelloReply(message = result)
       
       
      def run():
          with open('certificate/server.key', 'rb') as f:
              private_key = f.read()
          with open('certificate/server.crt', 'rb') as f:
              certificate_chain = f.read()
       
          server_credentials = grpc.ssl_server_credentials(
              ((private_key, certificate_chain,),))
          grpc_server = grpc.server(
              futures.ThreadPoolExecutor(max_workers=4),
          )
          pb2_grpc.add_GreeterServicer_to_server(Greeter(),grpc_server)
          ip_port = "127.0.0.1:50051"
          # ip_port = "0.0.0.0:50051"
          # HTTP的写法
          # grpc_server.add_insecure_port(ip_port)
          # HTTPS的写法
          grpc_server.add_secure_port(ip_port,server_credentials)
          grpc_server.start()
          print(f"service start at {ip_port}")
          grpc_server.wait_for_termination()
       
      if __name__ == "__main__":
          run()
    • 客户端升级如下

      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
      from datetime import datetime
       
      import grpc
      import helloworld_pb2 as pb2
      import helloworld_pb2_grpc as pb2_grpc
      from concurrent import futures
       
      def run():
          with open('certificate/server.crt', 'rb') as f:
              trusted_certs = f.read()
       
          credentials = grpc.ssl_channel_credentials(root_certificates=trusted_certs)
          # HTTP 写法
          # conn = grpc.insecure_channel("127.0.0.1:50051")
          # HTTPS 写法
          conn = grpc.secure_channel("127.0.0.1:50051",credentials)
          client = pb2_grpc.GreeterStub(channel=conn)
          name = "xiaozhu0513"
          response = client.SayHello(pb2.HelloRequest(
              name = name
          ))
          print(f"客户端发送 {name} -> 服务器")
          print("客户端接收到服务器返回信息 ",response.message)
       
       
      if __name__ == "__main__":
          run()
    • 最终实现效果如下

    • image

  3. 增加一行代码让Android实现HTTP升级为HTTPS

    • 大佬demoGithub 中 readme.md 文件中提出构建HTTPS很简单 真 · 一行代码
    1. copy server.crt 文件到 res/raw 目录下

      image

    2. 取消newSSLChannel 注释, 并在此之前增加 InputStream openRawResource = mActivity.getApplicationContext().getResources().openRawResource(R.raw.server);​​

      image

  4. 运行APP到手机上~ 最终效果如下,顺利完成了由HTTP 升级到HTTPS! imageimageimage

2. 在大佬demo上进行二次开发

目的:对HTTP 及 HTTPS 进行二次开发,增加多个请求及响应数据字段,并完成相应的android开发以及Python服务端开发!

  1. 增加编辑框,使原来界面更改为登录界面。编写新的proto文件并编译生成Grpc类文件。

    • 本着尽量不修改代码为前提,

      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
      // Copyright 2015 The gRPC Authors
      //
      // Licensed under the Apache License, Version 2.0 (the "License");
      // you may not use this file except in compliance with the License.
      // You may obtain a copy of the License at
      //
      //     http://www.apache.org/licenses/LICENSE-2.0
      //
      // Unless required by applicable law or agreed to in writing, software
      // distributed under the License is distributed on an "AS IS" BASIS,
      // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      // See the License for the specific language governing permissions and
      // limitations under the License.
      syntax = "proto3";
       
      option java_multiple_files = true;
      option java_package = "com.xuexiang.protobufdemo";
      option java_outer_classname = "HelloWorldProto";
      option objc_class_prefix = "HLW";
       
      package helloworld;
       
      // The greeting service definition.
      service Greeter {
        // Sends a greeting
        rpc SayHello (HelloRequest) returns (HelloReply) {}
      }
       
      // The request message containing the user's name.
      message HelloRequest {
        string name = 1;
        // 开始增加请求字段
        string password = 2;
        // 将设备信息设置为Map映射,换个口味来存放数据
        MapDevicesInfo mapDevicesInfo = 3;
      }
       
      // The response message containing the greetings
      message HelloReply {
        string message = 1;
        // userInfo 后端返回  JSON格式数据
        string userInfo = 2;
      }
      message MapDevicesInfo {
        map<string, string> mapstr = 1;
      }
    • 修改完helloworld.proto 之后,点击Build -> Make Project 会默认生成一些文件。可以顺利开始下一步了~

      image

  2. 修改HTTPS 以及HTTP 发送数据流程

    • 这里就不阐述二次开发的流程了 直接给出关键HTTPS核心代码,HTTP的代码也一模一样,因为二者使用的是同一份proto文件!
    • 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
      // 添加设备信息
      MapDevicesInfo.Builder builder = MapDevicesInfo.newBuilder();
      builder.putMapstr("手机厂商", getPhoneBrand);
      builder.putMapstr("手机型号", getPhoneModel);
      builder.putMapstr("手机系统版本号", getVersionRelease);
      builder.putMapstr("手机设备名", getDeviceName);
      builder.putMapstr("手机主板名", deviceBoard);
      builder.putMapstr("手机厂商名", getDeviceManufacturer);
      MapDevicesInfo mapDevicesInfo = builder.build();
      //开始网络请求
      //构建通道 HTTP的
      final ManagedChannel channel = gRPCChannelUtils.newChannel(host, port);
      //构建服务api代理
      mStub = GreeterGrpc.newStub(channel);
      //构建请求实体
      // 对于逆向分析来讲 此处message 是关键点
      HelloRequest request = HelloRequest.newBuilder()
              .setName(urName)
              .setPass(uPassword)
              .setMapDevicesInfo(mapDevicesInfo)
              .build();
      //进行请求
      mStub.sayHello(request, new SimpleStreamObserver<HelloReply>() {
          @SuppressLint("SetTextI18n")
          @Override
          protected void onSuccess(HelloReply value) {
              // value 就是返回的数据对象,这里简单粗暴一点~
              tvGrpcResponse.setText(value.getMessage() + value.getUserInfo());
              btnSend.setEnabled(true);
          }
       
          @MainThread
          @Override
          public void onError(Throwable t) {
              super.onError(t);
              tvGrpcResponse.setText(Log.getStackTraceString(t));
              btnSend.setEnabled(true);
          }
       
          @Override
          public void onCompleted() {
              super.onCompleted();
              gRPCChannelUtils.shutdown(channel);
          }
      });
  3. 完成Python服务端的更新

    • 先利用最新的proto文件来生成pb2 以及 pb2_grpc 的代码文件

      • 再次进入虚拟环境并执行 python -m grpc_tools.protoc -I./ --python_out=./ --grpc_python_out=./ helloworld.proto​ 来生成文件。

        image

    • 照猫画虎编写Python 服务端代码

      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
      import json
      from datetime import datetime
      import jwt
      import grpc
      import helloworld_pb2 as pb2
      import helloworld_pb2_grpc as pb2_grpc
      from concurrent import futures
       
      # 定义一个密钥,用于加密和解密token
      secret_key = 'ProtobufDem0'
      class Greeter(pb2_grpc.GreeterServicer):
          def SayHello(self, request, context):
              name,_pass, mapDevicesInfo = request.name, request.password, request.mapDevicesInfo
              print(f"服务器接收到客户端参数 -> name = {name}")
              # print(f"服务器接收到客户端参数 -> mapDevicesInfo = {mapDevicesInfo}")
              sendtime = datetime.now().strftime('%Y-%m-%d-%H:%M:%S')
              userInfo = {}
              userToken = {
                  "userName": name,
                  "userPassword": _pass
              }
              print(userToken)
              usertoken = jwt.encode(userToken, secret_key, algorithm='HS256')
              userInfo["success"] = "ok"
              userInfo["userID"] = 10089
              userInfo["serverTime"] = sendtime
              userInfo["userToken"] = usertoken
              userinfo = json.dumps(userInfo)
              print(f"服务器响应时间: {sendtime}")
              return pb2.HelloReply(message = "OK",userInfo=userinfo)
       
       
      def run():
          with open('certificate/server.key', 'rb') as f:
              private_key = f.read()
          with open('certificate/server.crt', 'rb') as f:
              certificate_chain = f.read()
       
          server_credentials = grpc.ssl_server_credentials(
              ((private_key, certificate_chain,),))
          grpc_server = grpc.server(
              futures.ThreadPoolExecutor(max_workers=4),
          )
          pb2_grpc.add_GreeterServicer_to_server(Greeter(),grpc_server)
          # ip_port = "127.0.0.1:50051"
          ip_port = "0.0.0.0:50051"
          # HTTP的写法
          # grpc_server.add_insecure_port(ip_port)
          # HTTPS的写法
          grpc_server.add_secure_port(ip_port,server_credentials)
          grpc_server.start()
          print(f"service start at {ip_port}")
          grpc_server.wait_for_termination()
       
      if __name__ == "__main__":
          run()
  4. 最终效果演示

    imageimage​​

尝试理解一下Android开发中间编译生成文件的关系

文章进行到这里,已经不会在继续开发了,开始转头闷向逆向分析~ 尝试将proto文件删除,生成relese包进行分析 ,不可以删除,删除会报错滴~

  1. 对比一下源码 以及 jadx反编译之后的代码区别~

    • 刚脱入Jadx中,就看到了服务器ca证书凭证。

      image

    • release包和debugg包都生成了中间代码文件~ 也就是Grpc类文件

  2. 分析release包特点,因作者项目默认开启了混淆,只有release包才会混淆而debug包还不会被混淆!

  3. debug包分析,未被混淆,易上手~

    • 分析 Grpc类特点

      • 打开Grpc文件后,发现这个类导入了许多io.grpc 的包。按理来讲,即使开启了混淆,这些包都不会改变名字,这就算是导入库,若更改了名字,import 可能就会报错。所以此处像是一个特征码一样

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        import com.google.common.util.concurrent.ListenableFuture;
        import io.grpc.BindableService;
        import io.grpc.CallOptions;
        import io.grpc.Channel;
        import io.grpc.MethodDescriptor;
        import io.grpc.ServerServiceDefinition;
        import io.grpc.ServiceDescriptor;
        import io.grpc.protobuf.lite.ProtoLiteUtils;
        import io.grpc.stub.AbstractStub;
        import io.grpc.stub.ClientCalls;
        import io.grpc.stub.ServerCalls;
        import io.grpc.stub.StreamObserver;
        import io.grpc.stub.annotations.RpcMethod;
    • 分析Message字段特点

      • 分析HelloReply 字段特点,这以上一下对比,Grpc类 实现了一些客户端的方法,使用了io.grpc的包。而字段则是疯狂使用了com.google.protobuf 这样的包,且这样的包被混淆的几率也不大!

        1
        2
        3
        4
        5
        6
        7
        8
        9
        import com.google.protobuf.ByteString;
        import com.google.protobuf.CodedInputStream;
        import com.google.protobuf.CodedOutputStream;
        import com.google.protobuf.ExtensionRegistryLite;
        import com.google.protobuf.GeneratedMessageLite;
        import com.google.protobuf.InvalidProtocolBufferException;
        import com.google.protobuf.Parser;
        import java.io.IOException;
        import java.io.InputStream;
      • 我们在proto文件中,定义的消息体 message,中的字段顺序,和编译后生成的类文件顺序一致且存在这样的特点:

        • 上面proto文件中,HelloReply​ 消息体 定义了tag为1 的message 以及 tag为2的userInfo...
        • 在生成的同名类文件中,每个tag 都会生成5个与之关联的方法,分别是getMessage 、getMessageBytes、 setMessage、 clearMessage、 setMessageBytes 于此同时,我们定义的userInfo 也有类似的特点。
        • 与之 该类文件还有一个 WriteTo的方法,该方法直接就列出proto文件中消息体定义的tag~
        • 分析到此处时,发现这些文件都很类似,唯一不同的就是我们定义proto消息体中每个tag字段的类型,生成的文件会根据tag类型变化而变化。这个可能就是关键与重点。
  4. release包分析

    • 分析Grpc类特点

      • 虽然release包会混淆代码,但发现该Grpc也只是混淆了一些方法名,变量名。而关键的类名以及方法返回类型都没有被混淆~
    • release包和debug包大差不差,就是被混淆了。但幸运的是,还是会寻找到一些蛛丝马迹的!

  5. 大汇总一下Jadx静态分析结果:

    • 其一,无论是混淆前后,都会在Grpc类中寻找到一个返回值类型 为服务名+stub 的 一个方法,这个方法就是创建客户端的方法,也叫存根。运气好的话,可以直接定位到关键代码。

    • 其二,在proto文件中定义的消息体message,新生成的编译类文件中,利用号writeto方法可以加速还原proto文件。(PS:猜想,下一步做认证)

    • 其三, Grpc类特点 io.grpc.Channel;​ 消息体tag字段特征 com.google.protobuf.GeneratedMessageLite;​ 不同版本, 特征点可能会不同! writeTo(CodedOutputStream output)​ 也是一个特征点。name | password | mapDevicesInfo 以及对应的tag 正是proto文件中的信息!getSerializedSize() 也是类似的效果~

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public void writeTo(CodedOutputStream output) throws IOException {
              if (!this.name_.isEmpty()) {
                  output.writeString(1, getName());
              }
              if (!this.password_.isEmpty()) {
                  output.writeString(2, getPassword());
              }
              if (this.mapDevicesInfo_ != null) {
                  output.writeMessage(3, getMapDevicesInfo());
              }
          }

动静态分析APP,中间人抓包顺便保存为二进制文件,逆向分析protobuf

利用前面生成的debug包进行分析~

最终目标:编写frida脚本 拦截到通信IP, Port,通讯二进制数据逆向还原proto文件来实现模拟android客户端发送请求到Python服务器

  1. Jadx + Objection 实现动静态分析debug包

    • Jadx 静态分析

      1. 定位到最终发送HTTPS 请求位置, io.grpc.okhttp.OkHttpChannelBuilder.forAddress(String host, int port)​​​

      2. 静态定位到HTTPS 证书内容 com.xuexiang.protobufdemo.grpc.HttpsUtils.getSslSocketFactory(InputStream... certificates)​​​

      3. 开发中 HelloRequest request = HelloRequest.newBuilder().setName(urName).setPassword(uPassword).setMapDevicesInfo(mapDevicesInfo).mo960build();来设置待发送的数据信息,是否有统一的接口呢

        • 1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          public void writeTo(CodedOutputStream output) throws IOException {
                  if (!this.name_.isEmpty()) {
                      output.writeString(1, getName());
                  }
                  if (!this.password_.isEmpty()) {
                      output.writeString(2, getPassword());
                  }
                  if (this.mapDevicesInfo_ != null) {
                      output.writeMessage(3, getMapDevicesInfo());
                  }
              }
        • writeTo​​​ 这个方法作用 官方解释为 Serializes the message and writes it to output​​​. This does not flush or close the stream. 现在矛盾转换为 getName() getPassword() ... 类似方法返回值是什么以及对CodedOutputStream类型的变量 output又做了什
        • 第一个矛盾可以通过objection动态分析得到答案,第二个矛盾想要解决还是得看google源码,这个类对象在writeTo里面用到了writeString -> Write a string​​​ field, including tag, to the stream. 以及 writeMessage -> Write an embedded message field, including tag, to the stream. 两个方法
      4. Jadx静态分析总结:使用objection去查看 host,port 以及证书流。还有最重要的 那三个get方法!

    • Objection动态分析 启动!使用debug包来分析而不是使用release包,因为release包内代码做了混淆~

      1. HOST & Port 的分析:Objection 看看内存中是否存在io.grpc.okhttp.OkHttpChannelBuilder.forAddress​​​ 这个方法

        • 执行 android hooking search classes io.grpc.okhttp.OkHttpChannelBuilder​​​ 搜索这样的类名,内存中确实有这样的信息
        • image
        • 一把Hook上这个类 然后看看都有那些方法 执行 android hooking watch class io.grpc.okhttp.OkHttpChannelBuilder​​​ 然后手动触发一下虽然崩溃了但确实系统走了forAddress​​​这个方法
        • image
        • 直接hook这个返回 并且打印参数信息,android hooking watch class_method io.grpc.okhttp.OkHttpChannelBuilder.forAddress --dump-args --dump-backtrace --dump-return​​​,也确实打印出了ip 以及端口号信息等
        • image
      2. HTTPS 所涉及到证书的获取及分析:直接使用objection来hook相关类,执行 android hooking watch class_method com.xuexiang.protobufdemo.grpc.HttpsUtils.getSslSocketFactory --dump-args --dump-backtrace --dump-return​​​

        • image
        • 使用 objection来看看实参,先加载插件 plugin load /root/.objection/plugins/Wallbreaker​​​ ,搜索实例plugin wallbreaker objectsearch android.content.res.AssetManager$AssetInputStream​​​ 打印实例,似乎没发现一些端倪。(留给下一次吧!)
          图片描述
      3. Objection获取 HellpRequest中 三个get方法。com.xuexiang.protobufdemo.HelloRequest​​​

        • 执行 plugin wallbreaker objectsearch com.xuexiang.protobufdemo.HelloRequest 打印 内存中实例信息,检验过后 name 确实是输入的信息!

        image

        image

        • 使用 objection来查看内存中实例信息 plugin wallbreaker objectdump 0x21ca --fullname​​​

        • image

          image

        • 内存中搜寻实例 然后主动调用他们的get方法

          • 执行 android heap search instances com.xuexiang.protobufdemo.HelloRequest​​​ 查看内存中 实例信息

            image

          • 主动调用get方法,

            image

  2. Frida Hook 拦截通信数据。

    • 首先,我们在上一步进行了jadx+objection进行动静态分析app,并验证了一些猜想,现在借助Frida Hook来开发一个Hook 脚本。这一次,我们另辟蹊径,还记得上面那个writeTo方法吗? 这个方法目的将字段的二进制属性加入到一个 com.google.protobuf.CodedOutputStream​​类型 的对象中,通过调用这个类型的writeXXX方法 就可以得到tag以及对应的属性值!ok,原理有了,开始动手实践!

    • 再次确认目的,利用frida去hook这个类中部分write方法

      • 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
        (agent) Hooking com.google.protobuf.CodedOutputStream.write(byte)
        (agent) Hooking com.google.protobuf.CodedOutputStream.write(java.nio.ByteBuffer)
        (agent) Hooking com.google.protobuf.CodedOutputStream.write([B, int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeBool(int, boolean)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeBoolNoTag(boolean)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeByteArray(int, [B)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeByteArray(int, [B, int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeByteArrayNoTag([B)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeByteArrayNoTag([B, int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeByteBuffer(int, java.nio.ByteBuffer)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeBytes(int, com.google.protobuf.ByteString)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeBytesNoTag(com.google.protobuf.ByteString)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeDouble(int, double)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeDoubleNoTag(double)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeEnum(int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeEnumNoTag(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeFixed32(int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeFixed32NoTag(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeFixed64(int, long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeFixed64NoTag(long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeFloat(int, float)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeFloatNoTag(float)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeGroup(int, com.google.protobuf.MessageLite)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeGroupNoTag(com.google.protobuf.MessageLite)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeInt32(int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeInt32NoTag(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeInt64(int, long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeInt64NoTag(long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeLazy(java.nio.ByteBuffer)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeLazy([B, int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeMessage(int, com.google.protobuf.MessageLite)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeMessageNoTag(com.google.protobuf.MessageLite)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeMessageSetExtension(int, com.google.protobuf.MessageLite)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawByte(byte)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawByte(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawBytes(com.google.protobuf.ByteString)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawBytes(java.nio.ByteBuffer)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawBytes([B)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawBytes([B, int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawLittleEndian32(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawLittleEndian64(long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawMessageSetExtension(int, com.google.protobuf.ByteString)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawVarint32(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeRawVarint64(long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSFixed32(int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSFixed32NoTag(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSFixed64(int, long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSFixed64NoTag(long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSInt32(int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSInt32NoTag(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSInt64(int, long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeSInt64NoTag(long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeString(int, java.lang.String)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeStringNoTag(java.lang.String)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeTag(int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeUInt32(int, int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeUInt32NoTag(int)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeUInt64(int, long)
        (agent) Hooking com.google.protobuf.CodedOutputStream.writeUInt64NoTag(long)
      • 注意看:这个方法是一个接口需要去hook哪里去实现的 public abstract class CodedOutputStream​​

      • 这个writeTo 方法是 重写了 com.google.protobuf.MessageLite 下的 writeTo

      • 目标 接口 CodedOutputStream​​ 中 有一个名为 OutputStreamEncoder 实现方法,它实现了基本的write方法 去hook它

        • android hooking watch class com.google.protobuf.CodedOutputStream$OutputStreamEncoder
        • image
        • image
        • 确实是打印了部分的tag标签以及入参值
        • 有写入write那么必会有read,同样的方法利用Objection来搜索一些信息如下
        • image
      • 使用frida Hook 这个类com.google.protobuf.CodedOutputStream$OutputStreamEncoder 部分方法。

        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
        ```
        function main(){
            Java.perform(function(){
                console.log("Main Success!");
                // 1. 获取这个com.google.protobuf.CodedOutputStream
                var OutputStreamEncoder = Java.use("com.google.protobuf.CodedOutputStream$OutputStreamEncoder");
                console.log("内存中 OutputStreamEncoder 实例 =>",OutputStreamEncoder,"\n");
         
         
                // 无法通过writeTo 来获取有哪些信息
                OutputStreamEncoder.writeString.implementation = function(fieldNumber,value){
                    console.log("OutputStreamEncoder.writeString" +"Tag = "+fieldNumber + " value = " + value);
                    return this.writeString(fieldNumber,value);
                }
                OutputStreamEncoder.writeMessage.implementation = function(fieldNumber,value){
                    console.log("OutputStreamEncoder.writeMessage" +"Tag = "+fieldNumber + " value = " + JSON.stringify(value));
                    return this.writeMessage(fieldNumber,value);
                }
                OutputStreamEncoder["writeInt32"].implementation = function (fieldNumber, value) {
                    console.log('writeInt32 is called' + ', ' + 'fieldNumber: ' + fieldNumber + ', ' + 'value: ' + value);
                    var ret = this.writeInt32(fieldNumber, value);
                    console.log('writeInt32 ret value is ' + ret);
                    return ret;
                };
                OutputStreamEncoder["write"].overload('byte').implementation = function (value) {
                    console.log('write is called' + ', ' + 'value: ' + value);
                    var ret = this.write(value);
                    console.log('write ret value is ' + ret);
                    return ret;
                };
         
                var CodedInputStream = Java.use("com.google.protobuf.CodedInputStream");
                console.log("内存中 CodedInputStream 实例 =>",CodedInputStream,"\n");
                CodedInputStream.readStringRequireUtf8.implementation = function(){
                    var ret = this.readString();
                    console.log("CodedInputStream.readStringRequireUtf8 => " + ret);
                    return ret;
                }
         
                // CodedInputStream.readMessage.overload('com.google.protobuf.MessageLite$Builder', 'com.google.protobuf.ExtensionRegistryLite') .implementation = function(a,b){
                //     var ret = this.readMessage(a,b);
                //     console.log("CodedInputStream.readMessage => " + ret);
                //     return ret;
                // }
            })
        }
         
         
         
         
         
        setImmediate(main)
        ```
      • 确实使用Fridahook拦截到了一些message消息体的~

      • ​​image​​

  3. 尝试根据protobuf数据格式分析二进制数据(HTTP) 推荐阅读 官方解释

    • image

    • 以我们抓取到的HTTP请求包分析为例子

      • 将bin文件拖入到010Editer中分析如下,目前对这块数据掌握的不是很深,当前就简简单单的分析一下~

        imageimage

      • 从第六个字节开始分析数据,bin(0x0A)-> '0b1010' :(二进制)后三个字节表示类型,前面的字节表示tag值。tag为1 且类型为2 ,可变字长,因此0x0A 下一个字节表示的是长度!

      • int(0x09) -> 9:以为这从 61 开始 到 第一排结束 70 表示的都是tag为1 对应的value~

      • 第二排开始 bin(0x12) ->'0b10010' : tag为2 且类型也为2 的可变长数据,那么0x12 之后的0x08 就是 tag为2 的value的长度 也是到 70 结束

      • 0x70 的下一个 bin(0x1A) -> '0b11010' tag 为3 且类型也为2的可变长数据,1A过后的 A3 就是tag为3对应的数据长度 int(0XA3) -> 163

下集预告:使用Python第三方库(blackboxprotobuf)辅助自动还原proto文件 Python序列化反序列化官方Demo
因文件超过大小限制,百度网盘链接https://pan.baidu.com/s/18gK37XCimZy-0NO2cbBKDw
提取码:0513


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2023-9-3 12:21 被哇哈哈919编辑 ,原因: 添加原创关键字~
收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 19674
活跃值: (29330)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-9-4 09:16
2
2
感谢分享
雪    币: 983
活跃值: (1320)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_fssslkzs 2023-9-4 11:38
3
0
感谢分享ing
游客
登录 | 注册 方可回帖
返回