欢迎光临
我们一直在努力

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

收录于话题

本文为看雪论坛优秀文章
看雪论坛作者ID:bigeast


分析要用到的工具:

1、Android studio(需要安装好模拟器)
2、WINRAR压缩包(提取apk文件中的.dex文件)
3、apktool(用WINRAR提取APK中的androidmanifest.xml文件时可能会导致乱码,所以要用它来提取)
4、d2j-dex2jar(将.dex文件转为.jar文件,后面会提到)
5、jd.gui(将.jar文件打开展示成java源代码,后面会提到)
6、Wireshark(用于分析流量)


1

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(),直接使用元素的名字就可以调用该元素了。

(1)逻辑间如何跳转—intent

在activity中按钮的listen函数中定义
intent = Intet(this,另一个activity)startactivity(intent)
即可实现跳转页面。

(2)隐式intent

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

intent不仅可以打开activity,也可以打开网页。
intent = Intet(intent.action_view)intent.data = uri.parse(\'www.baidu.com\')startactivity(intent)
甚至可以通过intent向下一个或者上一个页面传递数据。

(3)常用UI控件—textview

在activity的xml中定义,显示文字。

width和height有三个可选值:
1、match_parent:和父布局大小一样(即和手机屏幕大小一样)
2、wrap_content:恰好包住里面内容。
3、固定值。

还有其他的属性可以选:是否居中,文字颜色,文字大小。


2

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的文件。
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


3

赌球APP分析实战

定位第一个APP界面

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

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

-<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函数是进入到一个新页面后要执行的第一个函数)。
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。
private final void checkLocalHost() {  String str = HostManager.INSTANCE.loadHostUrl();//先取出url  HostManager.INSTANCE.setNeedGetHost(true);  checkAppMaintain(str, true); //然后验证url是否能连接}
loadHostUrl函数会先取出url, 我们继续跟踪loadHostUrl。
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在主页面时运行命令得到的结果。
C:\\Users\\Administrator>adb shell dumpsys window | findstr mCurrentFocus  mCurrentFocus=Window{b007190 u0 com.cxinc.app.n9h/com.happy.roulette.activity.MainActivity}

以下为APP在登录页面时运行命令得到的结果:
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的文件目录下:
C:\\Users\\Administrator>adb devicesList of devices attachedemulator-5554   deviceC:\\Users\\Administrator>adb -s emulator-5554 shellemulator64_x86_64:/ $ run-as com.cxinc.app.n9hrun-as: package not debuggable: com.cxinc.app.n9h

发现无法进入这个APP的目录,因为不是debug版本的APP。所以后面会通过抓包来获取这个url。
 
我们继续看checkLocalHost函数,上面我们无法分析出loadHostUrl在本地哪个文件获取url。
我们继续看后面的函数调用过程,通过loadHostUrl函数获取到url后,会调用checkAppMaintain来检查这个url。
private final void checkAppMaintain(String paramString, boolean paramBoolean) {    this.mHostApi.checkUrl(paramString, new SplashActivity$checkAppMaintain$1(paramString, paramBoolean));  }
一直跟踪下去,发现这里会请求这个url。发现会在这个url后拼接/api/checkAppWh.do,然后发送请求。
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进入模拟器终端:
C:\\Users\\Administrator>adb shell
进入模拟器终端后用tcpdump命令进行抓包,包保存在/sdcard/capture.pcap
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盘,在本地终端执行。
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执行完后会跳到下面这个回调函数里,即请求服务器后会回到以下函数。
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()
private final void getHost() {  this.mHostApi.getHost(new SplashActivity$getHost$1());}
继续跟踪:
public void getHost(BaseWebApi.ResultListener paramResultListener) {  this.i = 0;  this.mClientResultListener = paramResultListener;  sendGetHostRequest(getNextServerUrl());}

看getNextServerUrl,它会将“https://9h.”拼接域名list中一个域名。
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,包含以下域名。猜测这种读博网站域名经常被封,所以要多准备几个域名。
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()。
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。
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传输,下面能看到数据是加密的数值。
 
请求完之后进入回调函数如下:
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:
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,并且结束当前页面。
private final void goHomePage() {  if (!isFinishing()) {    startActivity(new Intent((Context)this, MainActivity.class));    finish();  }
以下即跳入主页面:

以下我们看看MainActivity.class的oncreate函数:
protected void onCreate(@Nullable Bundle paramBundle) {    super.onCreate(paramBundle);    setContentView(2131492931); //设置布局    initFragment();    initNavigation(); //设置导航栏    setNavigationListener(); //设置导航栏的监听    if (HostManager.INSTANCE.isNeedGetHost())      getHost();  }
其gethost函数会执行以下函数:
private final void getHost() {  (new HostApi()).getHost(new MainActivity$getHost$1());}
继续跟踪getHost:
public void getHost(BaseWebApi.ResultListener paramResultListener) {    this.i = 0;    this.mClientResultListener = paramResultListener;    sendGetHostRequest(getNextServerUrl());  }}
跟踪getNextServerUrl,这里是请求服务器去获取/api/getAppConfig.do这个配置文件(上面是获取/static/data/config.json文件)。
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 \"\";  }}
到这里好像也没配置什么信息。
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。
 

4

总结

1、同一个APP会请求多个host的资源,所以在识别APP的时候不能只通过单一的IP流去识别。

2、在TLS1.3下只有协议头和握手信息能被看到,其他都是加密状态。

3、SNI字段是一个比较有用的信息。

E N D

 


看雪ID:bigeast

https://bbs.pediy.com/user-home-859945.htm

*本文由看雪论坛 bigeast 原创,转载请注明来自看雪社区


# 往期推荐

1.内核漏洞学习-HEVD-NullPointerDereference

2.一个数据加密恶意样本分析

3.windows64位分页机制分析-隐藏可执行内存方法

4.某系统漏洞挖掘之授权绕过到rce

5.office 分析笔记 —— rtf解析器(wwlib)的不完全解读

6.某系统漏洞挖掘之固件分析

点击“阅读原文”了解更多

赞(1) 打赏
未经允许不得转载:黑客技术网 » 从分析一个赌球APP中入门安卓逆向、开发、协议分析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏