其他
从分析一个赌球APP中入门安卓逆向、开发、协议分析
本文为看雪论坛优秀文章
看雪论坛作者ID:bigeast
分析要用到的工具:
2、WINRAR压缩包(提取apk文件中的.dex文件)
3、apktool(用WINRAR提取APK中的androidmanifest.xml文件时可能会导致乱码,所以要用它来提取)
4、d2j-dex2jar(将.dex文件转为.jar文件,后面会提到)
5、jd.gui(将.jar文件打开展示成java源代码,后面会提到)
6、Wireshark(用于分析流量)
1
APP开发的背景知识的介绍
注意,任何的activity都要在AndroidManifest.xml中定义。(一般androidstudio会自动完成)
视图
在xml中:
@id/id_name表示引用这个id。
@+id/button1 表示定义一个id。
逻辑
如果要对布局进行一些操作,也是在activity中定义。比如说监听按钮的点击事件,在Java中要使用findviewByID()方法获取布局文件中定义的元素,然后再定义该元素的函数的内容,比如按钮元素的话就可以定义其setonclicklistener函数。
(1)逻辑间如何跳转---intent
intent = Intet(this,另一个activity)
startactivity(intent)
(2)隐式intent
intent不仅可以打开activity,也可以打开网页。
intent = Intet(intent.action_view)
intent.data = uri.parse('www.baidu.com')
startactivity(intent)
(3)常用UI控件---textview
在activity的xml中定义,显示文字。
1、match_parent:和父布局大小一样(即和手机屏幕大小一样)
2、wrap_content:恰好包住里面内容。
3、固定值。
2
APP逆向过程
流程:
1、先用压缩包提取classes.dex文件。
执行完毕,查看dex2jar目录,会发现生成了classes.dex.dex2jar.jar文件。
上一步中生成的classes.dex.dex2jar.jar文件,可以通过JD-GUI工具直接打开查看jar文件中的代码。
查找某个字符串在哪个页面出现的小trick
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 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>
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();
}
private final void checkLocalHost() {
String str = HostManager.INSTANCE.loadHostUrl();//先取出url
HostManager.INSTANCE.setNeedGetHost(true);
checkAppMaintain(str, true); //然后验证url是否能连接
}
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", "");
}
在模拟器上运行该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}
C:\Users\Administrator>adb shell dumpsys window | findstr mCurrentFocus
mCurrentFocus=Window{ae65cbc u0 com.cxinc.app.n9h/com.happy.roulette.activity.login.LoginActivity}
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
private final void checkAppMaintain(String paramString, boolean paramBoolean) {
this.mHostApi.checkUrl(paramString, new SplashActivity$checkAppMaintain$1(paramString, paramBoolean));
}
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抓包
当用android studio自带的模拟器启动APP后,在电脑终端输入adb shell进入模拟器终端:
C:\Users\Administrator>adb shell
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中断后即可以保存文件。
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)
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());
}
}
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());
}
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 List<String> SERVER_URL_LIST = Arrays.asList(new String[] { "app00app.com", "app66app.com", "app99app.vip", "app66app.vip", "app88app.vip" });
然后与得到的IP地址进行TCP连接,以下为三次握手和密钥协商过程。
可以看到是TLS1.3协议,先看看Client hello这条消息。
TLS1.3总共有两层,分别是握手协议(handshake protocol)和记录协议(record protocol),握手协议在记录协议的上层,记录协议是一个分层协议。其中握手协议中还包括了警告协议(alert protocol)。
接下来看一下Handshake protocol:Hello中的内容:
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的长度。
每个加密套件都包含,密钥交换,签名算法,加密算法,哈希算法。
1)key_share
理论上,客户端应该将所有与密钥协商有关的扩展(pre_shared_key、shared_key)都发送给服务端,服务端选定哪一种,再将对应选定的扩展返还给客户端,如果服务端同时使用两种密钥协商,则返还所有扩展,
参考:https://blog.csdn.net/zk3326312/article/details/80245756
以第一个签名算法为例,ecdsa_secp256r1_sha256,使用sha256作为签名中的哈希,签名算法为ecdsa。
psk_key_exchange_modes表示psk密钥交互模式选择
此处的PSK模式为(EC)DHE下的PSK(貌似就是使用上面的ECDHE进行密钥协商),客户端和服务器必须提供KeyShare。
如果是仅PSK模式,则服务器不需要提供KeyShare。
可以看到Record layer下面有三个协议:
2、密钥交换协议
3、应用数据协议-https
4、SNI Service name indication
所以通过这个字段我们有可能能识别出APP对应的服务是什么。
参考:https://blog.csdn.net/u010217394/article/details/121713758
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());
}
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);
}
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;
}
}
}
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();
}
private final void goHomePage() {
if (!isFinishing()) {
startActivity(new Intent((Context)this, MainActivity.class));
finish();
}
protected void onCreate(@Nullable Bundle paramBundle) {
super.onCreate(paramBundle);
setContentView(2131492931); //设置布局
initFragment();
initNavigation(); //设置导航栏
setNavigationListener(); //设置导航栏的监听
if (HostManager.INSTANCE.isNeedGetHost())
getHost();
}
private final void getHost() {
(new HostApi()).getHost(new MainActivity$getHost$1());
}
public void getHost(BaseWebApi.ResultListener paramResultListener) {
this.i = 0;
this.mClientResultListener = paramResultListener;
sendGetHostRequest(getNextServerUrl());
}
}
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”);
}
}
}
4
总结
2、在TLS1.3下只有协议头和握手信息能被看到,其他都是加密状态。
3、SNI字段是一个比较有用的信息。
E N D
看雪ID:bigeast
https://bbs.pediy.com/user-home-859945.htm
# 往期推荐
1.内核漏洞学习-HEVD-NullPointerDereference
5.office 分析笔记 —— rtf解析器(wwlib)的不完全解读
点击“阅读原文”了解更多