首页
论坛
课程
招聘
[原创]从分析一个赌球APP中入门安卓逆向、开发、协议分析
2021-11-13 17:23 27129

[原创]从分析一个赌球APP中入门安卓逆向、开发、协议分析

2021-11-13 17:23
27129

分析的APP样本如下,包含APK和抓到的流量
链接:https://pan.baidu.com/s/1f9cyEd6zFqVDPP4YTChLZA
提取码:v14r
--来自百度网盘超级会员V2的分享
后面分析要用到的工具:
Android studio(需要安装好模拟器)
WINRAR压缩包(提取apk文件中的.dex文件)
apktool(用WINRAR提取APK中的androidmanifest.xml文件时可能会导致乱码,所以要用它来提取)
d2j-dex2jar(将.dex文件转为.jar文件,后面会提到)
jd.gui(将.jar文件打开展示成java源代码,后面会提到)
Wireshark(用于分析流量)

APP开发的背景知识的介绍

APP开发遵循逻辑和视图分离的思想:
我们创建一个activity,android studio会自动生成其对应的xml文件。
注意,任何的activity都要在AndroidManifest.xml中定义。(一般androidstudio会自动完成)

视图

视图在xml中定义:可以直接可视化移动一个按钮进视图,也可以用代码编写。每个元素都会有一个id留给activity去调用。比如按钮A对应一个id,按钮B对应一个Id.
在xml中:
@id/id_name表示引用这个id。
@+id/button1 表示定义一个id。

逻辑

逻辑在activity中定义:activty要加载上面定义的视图,即布局,要调用setContentView(布局文件的id)。(项目添加的任何资源都会在R文件中生成一个资源id,这里布局文件的id即为对应xml文件的id)
如果要对布局进行一些操作,也是在activity中定义。比如说监听按钮的点击事件,在Java中要使用findviewByID()方法获取布局文件中定义的元素,然后再定义该元素的函数的内容,比如按钮元素的话就可以定义其setonclicklistener函数。而Kotlin不需要使用findviewbyID(),直接使用元素的名字就可以调用该元素了。

逻辑间如何跳转---intent

在activity中按钮的listen函数中定义

1
2
intent = Intet(this,另一个activity)
startactivity(intent)

即可实现跳转页面

隐式intent.

不指定跳转到哪个activity,而是指定跳转动作和类型,让系统来选择合适的activity.我们可以在AndroidManifest.xml中设置activity的可以相应的动作和类型、相应的协议类型(scheme)。
intent不仅可以打开activity,也可以打开网页。

1
2
3
intent = Intet(intent.action_view)
intent.data = uri.parse('www.baidu.com')
startactivity(intent)

甚至可以通过intent向下一个或者上一个页面传递数据。

常用UI控件---textview

在activity的xml中定义,显示文字。
width和height有三个可选值:
1.match_parent:和父布局大小一样(即和手机屏幕大小一样)
2.wrap_content:恰好包住里面内容。
3.固定值。
还有其他的属性可以选:是否居中,文字颜色,文字大小,。

APP逆向过程

目标:给一个APK反汇编出java源代码
流程:
1.先用压缩包提取classes.dex文件
图片描述
2.用dex2jar提取出jar文件
并将这个文件拷至dex2jar工具存放目录下。
图片描述
打开控制台,使用cd指令进入到dex2jar工具存放的目录下
进入到dex2jar目录下后,输入“d2j-dex2jar.bat classes.dex”指令运行
图片描述
执行完毕,查看dex2jar目录,会发现生成了classes.dex.dex2jar.jar文件
图片描述
3.将jar文件导入jd.gui看java源码
上一步中生成的classes.dex.dex2jar.jar文件,可以通过JD-GUI工具直接打开查看jar文件中的代码
图片描述

查找某个字符串在哪个页面出现的小trick

res/values/string.xml存了APK的字符串。
同目录下的public.xml有其对应的id。
查找当前目录下包含0x7f10002f的文件

1
2
3
findstr.exe /s /i "0x7f10002f" *.*
outdir\res\values\public.xml:    <public type="string" name="activity_alipay_real_name_hint" id="0x7f10002f" />
outdir\smali\com\happy\roulette\R$string.smali:.field public static final activity_alipay_real_name_hint:I = 0x7f10002f

赌球APP分析实战

定位第一个APP界面

我们用apktool解析出apk的文件夹如下:
(安装apktool前要先安装java1.8.关于apktool如何安装参考https://www.jianshu.com/p/b027856d55ac)
安装完并配置好环境变量后
用以下命令反编译输出到baz目录

1
apktool d xxx.apk -o baz

图片描述

 

从AndroidManifest.xml搜索android.intent.action.MAIN"定位到如下:

1
2
3
4
5
6
7
8
9
10
11
12
-<activity android:name="com.happy.roulette.activity.SplashActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" android:screenOrientation="portrait">
 
 
-<intent-filter>
 
<action android:name="android.intent.action.MAIN"/>
 
<category android:name="android.intent.category.LAUNCHER"/>
 
</intent-filter>
 
</activity>

所以第一个主页面是com.happy.roulette.activity.SplashActivity
以下便是第一个界面

 

图片描述

 

我们从com.happy.roulette.activity.SplashActivity.class的oncreate函数开始看(因为Oncreate函数是进入到一个新页面后要执行的第一个函数)。

1
2
3
4
5
6
7
8
protected void onCreate(@Nullable Bundle paramBundle) {
  super.onCreate(paramBundle);
  setContentView(2131492944);//加载定义好的布局
  TextView textView = (TextView)_$_findCachedViewById(R.id.tv_version_info); //设置文字
  Intrinsics.checkExpressionValueIsNotNull(textView, "tv_version_info");
  textView.setText("37_2.2.40"); //设置文字
  checkLocalHost();
}

我们继续看checkLocalHost()函数。
每次启动第一个界面都会检查一下host.

1
2
3
4
5
private final void checkLocalHost() {
  String str = HostManager.INSTANCE.loadHostUrl();//先取出url
  HostManager.INSTANCE.setNeedGetHost(true);
  checkAppMaintain(str, true); //然后验证url是否能连接
}

loadHostUrl函数会先取出url, 我们继续跟踪loadHostUrl

1
2
3
4
5
6
7
8
public final class HostManager {
  public final String loadHostUrl() {
    mSharedPreferencesManager = new SharedPreferencesManager(MyApplication.getAppContext());
    SharedPreferencesManager sharedPreferencesManager = mSharedPreferencesManager;
    if (sharedPreferencesManager == null)
      Intrinsics.throwUninitializedPropertyAccessException("mSharedPreferencesManager");
    return sharedPreferencesManager.get("key-host-url", "");
  }

发现使用了sharepreferences存储这个url在本地。
以下想在本地找到保存这个url的文件。
在模拟器上运行该APP,
打印出APP的package和当前页面的acitivy
以下为APP在主页面时运行命令得到的结果

1
2
C:\Users\Administrator>adb shell dumpsys window | findstr mCurrentFocus
  mCurrentFocus=Window{b007190 u0 com.cxinc.app.n9h/com.happy.roulette.activity.MainActivity}

以下为APP在登录页面时运行命令得到的结果

1
2
C:\Users\Administrator>adb shell dumpsys window | findstr mCurrentFocus
  mCurrentFocus=Window{ae65cbc u0 com.cxinc.app.n9h/com.happy.roulette.activity.login.LoginActivity}

通过 run-as 命令进入APP的文件目录下

1
2
3
4
5
6
C:\Users\Administrator>adb devices
List of devices attached
emulator-5554   device
C:\Users\Administrator>adb -s emulator-5554 shell
emulator64_x86_64:/ $ run-as com.cxinc.app.n9h
run-as: package not debuggable: com.cxinc.app.n9h

发现无法进入这个APP的目录,因为不是debug版本的APP。
所以后面会通过抓包来获取这个url。

 

我们继续看checkLocalHost函数,上面我们无法分析出loadHostUrl在本地哪个文件获取url.我们继续看后面的函数调用过程,通过loadHostUrl函数获取到url后,会调用checkAppMaintain来检查这个url。

1
2
3
private final void checkAppMaintain(String paramString, boolean paramBoolean) {
    this.mHostApi.checkUrl(paramString, new SplashActivity$checkAppMaintain$1(paramString, paramBoolean));
  }

一直跟踪下去
发现这里会请求这个url
发现会在这个url后拼接/api/checkAppWh.do,然后发送请求。

1
2
3
4
5
6
7
8
public void checkUrl(String paramString, BaseWebApi.ResultListener paramResultListener) {
  this.mAppUrl = paramString;
  StringBuilder stringBuilder = new StringBuilder();
  stringBuilder.append(this.mAppUrl);
  stringBuilder.append("/api/checkAppWh.do");
  StringRequest stringRequest = createStringRequest(0, stringBuilder.toString(), null, paramResultListener);
  getRequestQueue().add((Request)stringRequest);
}

tcpdump抓包

tcpdump是常用的一个抓包工具,linux或android环境下已经默认安装好。
当用android studio自带的模拟器启动APP后,在电脑终端输入adb shell进入模拟器终端:

1
C:\Users\Administrator>adb shell

进入模拟器终端后用tcpdump命令进行抓包,包保存在/sdcard/capture.pcap

1
emulator64_x86_64:/ # tcpdump -i any -p -n -s 0 -w /sdcard/capture.pcap

其中
-i是指定网卡为any,
-w表示保存为pacp,
s 0 : tcpdump 默认只会截取前 96 字节的内容,要想截取所有的报文内容,可以使用 -s number, number 就是你要截取的报文字节数,如果是 0 的话,表示截取报文全部内容。
-p : 不让网络接口进入混杂模式。默认情况下使用 tcpdump 抓包时,会让网络接口进入混杂模式。一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据。当网卡工作在混杂模式下时,网卡将来自接口的所有数据都捕获并交给相应的驱动程序。如果设备接入的交换机开启了混杂模式,使用 -p 选项可以有效地过滤噪声。
抓包结束后按Cltr+C中断后即可以保存文件。

 

我们在PC终端中把模拟器的抓取的包拿回来本地D盘。在本地终端执行

1
2
C:\Users\Administrator>adb pull /sdcard/capture.pcap d:/capture.pcap
/sdcard/capture.pcap: 1 file pulled, 0 skipped. 2.6 MB/s (108623 bytes in 0.040s)

通过wireshark分析如下:

 

首先会进行DNS请求,获取这个域名对应的IP:
发现请求了9h.app00app.com这个域名,且IP为204.11.56.48。
图片描述

 

checkmaintain执行完后会跳到下面这个回调函数里,
即请求服务器后会回到以下函数。

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
public static final class SplashActivity$checkAppMaintain$1 implements BaseWebApi.ResultListener {
    SplashActivity$checkAppMaintain$1(String param1String, boolean param1Boolean) {}
 
    //如果请求失败了调用OnError,说明当前请求的域名失效了
    public void onError(@NotNull ErrorOutput param1ErrorOutput) {
      Intrinsics.checkParameterIsNotNull(param1ErrorOutput, "error");
      Log.e("SplashActivity", "APP );
      if (this.$isFailToGetHost) {
        SplashActivity.this.getHost(); //调用这个获取新的url,然后发送请求:是https://9h.开头的
        return;
      }
      SplashActivity.this.showErrorRetryDialog(");
    }
 
    //如果请求成功了:以下代码有两个case:case49,case48。
    public void onResult(@NotNull String param1String) {
      Context context;
      Intrinsics.checkParameterIsNotNull(param1String, "response");
      Log.i("SplashActivity", "APP );
      switch (param1String.hashCode()) {
        case 49:
          if (param1String.equals("1")) {
            context = (Context)SplashActivity.this;
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(WebServerUrl.getBaseUrl());
            stringBuilder.append("/wh.html"); //通过wh.html可以看出wh是维护的缩写,且下面也标识了“维护中”,
            //所以推测这是服务器维护时会返回一个code,此时会执行下面代码的跳转,
            //比如跳转到9h.app00app.com/wh.html
            JumpUtil.ToWeb(context, stringBuilder.toString(), "维护中“);
            SplashActivity.this.finish();
            return;
          }
          break;
        case 48:
          if (context.equals("0")) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("Selected host url: ");
            stringBuilder.append(this.$selectedHostUrl);
            Log.i("SplashActivity", stringBuilder.toString());
            WebServerUrl.setBaseUrl(this.$selectedHostUrl);
            SplashActivity.this.getAppConfig(); //将服务器返回的参数用来设置APP
            return;
          }
          break;
      }
      onError(new ErrorOutput());
    }
  }

发现请求这个IP,连接不上。所以会执行上面Onerror函数去获取另外一个域名。
图片描述

 

Onerror函数会调用gethost()

1
2
3
private final void getHost() {
  this.mHostApi.getHost(new SplashActivity$getHost$1());
}

继续跟踪

1
2
3
4
5
public void getHost(BaseWebApi.ResultListener paramResultListener) {
  this.i = 0;
  this.mClientResultListener = paramResultListener;
  sendGetHostRequest(getNextServerUrl());
}

看getNextServerUrl,它会将”https://9h.“拼接域名list中一个域名。

1
2
3
4
5
6
7
8
9
10
11
12
private String getNextServerUrl() {
  try {
    WebServerUrl.setCurrentServerUrl(WebServerUrl.SERVER_URL_LIST.get(this.i));
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("https://9h.");
    stringBuilder.append(WebServerUrl.SERVER_URL_LIST.get(this.i));
    stringBuilder.append("/api/getAppConfig.do");
    return stringBuilder.toString();
  } catch (Exception exception) {
    exception.printStackTrace();
    return "";
  }

跟踪SERVER_URL_LIST,发现这是一个域名的list,包含以下域名。猜测这种读博网站域名经常被封,所以要多准备几个域名。

1
public static final List<String> SERVER_URL_LIST = Arrays.asList(new String[] { "app00app.com", "app66app.com", "app99app.vip", "app66app.vip", "app88app.vip" });

获取到域名之后,又会继续去执行checkAppMaintain这个函数去检测域名,即重复上面步骤,直到找到一个可以连接的域名,然后会进入上面的Onresult函数的case48。
通过抓包分析,这里请求了上面域名list中的第二个域名app66app.com
图片描述
然后与得到的IP地址进行TCP连接,以下为三次握手和密钥协商过程。
图片描述为了学习TLS协议,下面我们分析一下协议的过程。
图片描述
可以看到是TLS1.3协议,先看看Client hello这条消息
图片描述
我们可以看到Transport layer Security就是传输层安全(TLS),
TLS1.3总共有两层,分别是握手协议(handshake protocol)和记录协议(record protocol),握手协议在记录协议的上层,记录协议是一个分层协议。其中握手协议中还包括了警告协议(alert protocol)。
图片描述
(图来自https://blog.csdn.net/SkyChaserYu/article/details/104716229#t3,以下部分转自该博客)
接下来看一下Handshake protocol:Hello中的内容:
图片描述
Handshake Type:ClientHello,表示握手消息类型,此处是ClientHello
Length:508,即长度为508
Version:TLS1.2(0x0303),表示版本号为1.2,TLS1.3中规定此处必须置为0x0303,即TLS1.2,起到向后兼容的作用。1.3版本用来协商版本号的部分在扩展当中,而之前的版本就在此处进行。
Random,随机数,是由安全随机数生成器生成的32个字节。
Session ID Length:会话ID的长度。
Session ID,会话ID,TLS 1.3之前的版本支持“会话恢复”功能,该功能已与1.3版本中的预共享密钥合并。为了兼容以前的版本,该字段必须是非空的,因此不提供TLS 1.3之前会话的客户端必须生成一个新的32字节值。该值不必是随机的,但应该是不可预测的,以避免实现固定在特定值,否则,必须将其设置为空。
Cipher Suites Length,即下面Cipher Suites的长度

 

Cipher Suites是密码套件,表示客户端提供可选择的加密方式,如图所示:
图片描述
每个加密套件都包含,密钥交换,签名算法,加密算法,哈希算法。

 

Compression Methons (1 method)表示压缩方法,长度为1,内容为空

 

Exentisons扩展部分,是TLS1.3才开始使用,是TLS1.3的显著特征。每一个扩展都包含类型(type),长度(length)和数据(data)三个部分。

 

下面分析几个相对重要的扩展:
1)key_share

 

key_share 是椭圆曲线类型对应的公钥,如图所示
图片描述
此处包含一个KeyShareEntry,是x25519曲线组,这是客户端生成的代表自己支持的DH组,具体数据在KeyExchange字段中;每个KeyShareEntry都代表一组密钥交换参数,对于有限域DH来说是g和p的值,对于椭圆域DH是椭圆曲线和基点的值,很明显这里是用了椭圆曲线的DH。同选定加密组件一样,TLS 1.3定义了几组gp值,双方只需要协商想要使用的gp对即可。具体实施过程,为每个组生成一个DH密钥交换的参数,将其组名和参数值封装在key_share扩展中,服务端选定DH组后,返回一个封装好的key_share,双方根据交换的公钥参数和自己持有的私钥参数计算出DH最终密钥。
理论上,客户端应该将所有与密钥协商有关的扩展(pre_shared_key、shared_key)都发送给服务端,服务端选定哪一种,再将对应选定的扩展返还给客户端,如果服务端同时使用两种密钥协商,则返还所有扩展,

 

然后我们来看下EXDH的密钥协商过程,首先EC的意思是椭圆曲线,这个EC提供了一个很厉害的性质,你在曲线上找一个点P,给定一个整数k,求解另外一个点Q=kP很容易,给定两个点P,Q,知道Q =kP,求k却是个难题。在这个背景下,给定一个大家都知道的大数G,client在每次需要和server协商秘钥时,生成一段随机数a,然后发送A=aG给server,server收到这段消息(aG)后,生成一段随机数b,然后发送B=bG给client,然后server端计算(aG)b作为对称秘钥,client端收到后bG后计算a(Gb),因为(aG)b = a(Gb),所以对称秘钥就是aGb啦,攻击者只能截获A=aG和B=bG,由于椭圆曲线难题,知道A和G是很难计算a和b的,也就无法计算aGb了(当然,实际上的计算过程和原理证明不是这么简单的,中间还有一个取模的过程,以及取模过程的交换律和结合律证明,但是本质思想和这个是差不多的)。留意下TLS1.3图中的key_share,这段的功能就是直接记录了aG,然后包含在client_hello中。然后server收到后在server_hello的key_share段中记录bG。所以TLS1.3一个RTT就搞定握手了。
参考:https://blog.csdn.net/zk3326312/article/details/80245756
2)signature_algorithms
图片描述
Signature_algorithms扩展是,客户端提供签名算法,让服务器选择
以第一个签名算法为例,ecdsa_secp256r1_sha256,使用sha256作为签名中的哈希,签名算法为ecdsa。

 

3)psk_key_exchange_modes
图片描述
TLS 1.3 与之前的协议有较大差异,相比过去的的版本,引入了新的密钥协商机制 — PSK。TLS 1.3支持DH、PSK两种密钥协商机制,也支持同时使用两者进行密钥协商。
psk_key_exchange_modes表示psk密钥交互模式选择
此处的PSK模式为(EC)DHE下的PSK(貌似就是使用上面的ECDHE进行密钥协商),客户端和服务器必须提供KeyShare。
如果是仅PSK模式,则服务器不需要提供KeyShare。
下面分析server hello:
图片描述
可以看到Record layer下面有三个协议:
1.握手协议:确定了加密套件为TLS_AES_128_GCM_SHA256,确定了密钥协商的随机数bG.
2.密钥交换协议
3.应用数据协议-https
4).SNI Service name indication
图片描述
由于服务器能力的增强,在一台物理服务器上部署多个虚拟主机已经成为十分流行的做法了。在过去的 HTTP 时代,解决基于名称的主机同一 ip 地址上托管多个网站的问题并不难。当一个客户端请求某特定网站时,把请求的域名作为主机头(host)放在 http header 中,从而服务器根据域名可以知道把该请求引向哪个域名服务,并把匹配的网站传送给客户端。但是此方式到 https 就失效了,因为 SSL 在握手的过程中,不会有 host 信息,所以服务端通常返回配置中的第一个可用证书,这就导致不同虚拟主机上的服务不能使用不同证书(但在实际中,证书通常是与服务对应。)
所以通过这个字段我们有可能能识别出APP对应的服务是什么。
参考:
https://blog.csdn.net/u010217394/article/details/121713758

 

当APP检测到请求成功,即网站还能被访问时,会调用getAppConfig()。

1
2
3
4
5
6
private final void getAppConfig() {
    TextView textView = (TextView)_$_findCachedViewById(R.id.tv_progress_msg);
    Intrinsics.checkExpressionValueIsNotNull(textView, "tv_progress_msg");
    textView.setText("正在获取平台配置,请稍等“);
    this.mHomeApi.getAppConfig(new SplashActivity$getAppConfig$1());
  }

继续跟踪this.mHomeApi.getAppConfig,发现它向服务器请求了一个json文件,猜测会将服务器返回的配置信息用来配置APP。

1
2
3
4
5
6
7
public void getAppConfig(BaseWebApi.ResultListener paramResultListener) {
  StringBuilder stringBuilder = new StringBuilder();
  stringBuilder.append(WebServerUrl.getBaseUrl());
  stringBuilder.append("/static/data/config.json");
  StringRequest stringRequest = createStringRequest(0, stringBuilder.toString(), null, paramResultListener);
  getRequestQueue().add((Request)stringRequest);
}

抓包如下:
图片描述
发现本地请求的数据是TLS传输,下面能看到数据是加密的数值。

 

请求完之后进入回调函数如下:

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
public static final class SplashActivity$getAppConfig$1 implements BaseWebApi.ResultListener {
  //请求失败
  public void onError(@NotNull ErrorOutput param1ErrorOutput) {
    Intrinsics.checkParameterIsNotNull(param1ErrorOutput, "error");
    Log.e("SplashActivity", "Gat App Config );
    SplashActivity.this.showErrorRetryDialog(");
  }
  //请求成功,则这里传进来的参数param1String即为服务器返回的响应。
  public void onResult(@NotNull String param1String) {
    Intrinsics.checkParameterIsNotNull(param1String, "response");
    Log.i("SplashActivity", "Gat App Config );
    try {
      //把获取到json配置给APP
      AppConfigOutput appConfigOutput = (AppConfigOutput)(new Gson()).fromJson(param1String, AppConfigOutput.class);
      AppConfigManager.INSTANCE.setAppConfig(appConfigOutput);
 
      SplashActivity splashActivity = SplashActivity.this;
      String str = appConfigOutput.defaultSkin;
      Intrinsics.checkExpressionValueIsNotNull(str, "appConfigOutput.defaultSkin");
 
      //这里就会跳转到mainactivity,即APP的主页面
      splashActivity.judgeSkin(str);
 
 
      return;
    } catch (Exception exception) {
      exception.printStackTrace();
      onError(new ErrorOutput());
      return;
    }
  }
}

跟踪judgeSkin

1
2
3
4
5
6
7
8
9
10
11
12
13
private final void judgeSkin(String paramString) {
    if (AppConfigManager.INSTANCE.loadIsShowDefaultSkin()) {
      if (Intrinsics.areEqual(paramString, SystemSettingsManager.SkinStyle.BLUE.toString())) {
        SystemSettingsManager.INSTANCE.setColorSkin(SystemSettingsManager.SkinStyle.BLUE);
      } else if (Intrinsics.areEqual(paramString, SystemSettingsManager.SkinStyle.RED.toString())) {
        SystemSettingsManager.INSTANCE.setColorSkin(SystemSettingsManager.SkinStyle.RED);
      } else if (Intrinsics.areEqual(paramString, SystemSettingsManager.SkinStyle.DARK.toString())) {
        SystemSettingsManager.INSTANCE.setColorSkin(SystemSettingsManager.SkinStyle.DARK);
      }
      AppConfigManager.INSTANCE.saveIsShowDefaultSkin(false);
    }
    goHomePage();
  }

跟踪goHomePage:
这里就会跳转到mainactivity,即APP的主页面MainActivity.class
并且结束当前页面。

1
2
3
4
5
private final void goHomePage() {
  if (!isFinishing()) {
    startActivity(new Intent((Context)this, MainActivity.class));
    finish();
  }

以下即跳入主页面
图片描述

 

以下我们看看MainActivity.class的oncreate函数

1
2
3
4
5
6
7
8
9
protected void onCreate(@Nullable Bundle paramBundle) {
    super.onCreate(paramBundle);
    setContentView(2131492931); //设置布局
    initFragment();
    initNavigation(); //设置导航栏
    setNavigationListener(); //设置导航栏的监听
    if (HostManager.INSTANCE.isNeedGetHost())
      getHost();
  }

其gethost函数会执行以下函数:

1
2
3
private final void getHost() {
  (new HostApi()).getHost(new MainActivity$getHost$1());
}

继续跟踪getHost

1
2
3
4
5
6
  public void getHost(BaseWebApi.ResultListener paramResultListener) {
    this.i = 0;
    this.mClientResultListener = paramResultListener;
    sendGetHostRequest(getNextServerUrl());
  }
}

跟踪getNextServerUrl
这里是请求服务器去获取/api/getAppConfig.do这个配置文件(上面是获取/static/data/config.json文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
private String getNextServerUrl() {
  try {
    WebServerUrl.setCurrentServerUrl(WebServerUrl.SERVER_URL_LIST.get(this.i));
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("https://9h.");
    stringBuilder.append(WebServerUrl.SERVER_URL_LIST.get(this.i));
    stringBuilder.append("/api/getAppConfig.do");
    return stringBuilder.toString();
  } catch (Exception exception) {
    exception.printStackTrace();
    return "";
  }
}

到这里好像也没配置什么信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final class MainActivity$getHost$1 implements BaseWebApi.ResultListener {
  public void onError(@NotNull ErrorOutput param1ErrorOutput) {
    Intrinsics.checkParameterIsNotNull(param1ErrorOutput, "error");
    Log.e("MainActivity", "Get Host );
    MainActivity.this.showErrorCloseDialog("获取服务器失败,请先检查网络“);
  }
 
  public void onResult(@NotNull String param1String) {
    Intrinsics.checkParameterIsNotNull(param1String, "resultAppUrl");
    Log.i("MainActivity", "Get Host );
    if ((Intrinsics.areEqual(param1String, HostManager.INSTANCE.loadHostUrl()) ^ true) != 0) {
      HostManager.INSTANCE.saveHostUrl(param1String);
      MainActivity.this.showErrorCloseDialog("线路有更新,需要重启APP”);
    }
  }
}

下面我又点击了导航栏,跳转到其他页面
图片描述
通过抓包发现,它又请求了其他的域名,并且后面和该域名进行了TCP连接,并且传输了加密的数据。
图片描述
图片描述
此时server name与上面不同
后面又有client hello请求
图片描述
又出现了新的server name.

 

总结:
1.同一个APP会请求多个host的资源,所以在识别APP的时候不能只通过单一的IP流去识别。
2.在TLS1.3下只有协议头和握手信息能被看到,其他都是加密状态。
3.SNI字段是一个比较有用的信息。


看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

最后于 2021-12-31 15:55 被bigeast编辑 ,原因: 更新了网盘链接
收藏
点赞8
打赏
分享
最新回复 (6)
雪    币: 861
活跃值: 活跃值 (185)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
射到深處 活跃值 2021-11-15 16:14
2
0
期待更新
雪    币: 205
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
放个烟花好美 活跃值 2021-11-20 18:55
3
0
招安卓逆向,有兴趣私聊我
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
泰伯 活跃值 2021-12-30 22:45
4
0
网盘链接失效了,能再发一个不
雪    币: 1002
活跃值: 活跃值 (1646)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
bigeast 活跃值 2021-12-31 15:57
5
0
泰伯 网盘链接失效了,能再发一个不
已更新网盘链接,但貌似这个APP已经失效了
雪    币: 1002
活跃值: 活跃值 (1646)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
bigeast 活跃值 2021-12-31 15:57
6
0
泰伯 网盘链接失效了,能再发一个不


最后于 2021-12-31 15:58 被bigeast编辑 ,原因:
雪    币: 1180
活跃值: 活跃值 (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_gfeonlex 活跃值 2022-1-12 15:59
7
0
大佬,招安卓逆向,能私聊一下不
游客
登录 | 注册 方可回帖
返回