收录于话题
本文为看雪论坛优秀文章
看雪论坛作者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( 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 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
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( 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
2.一个数据加密恶意样本分析
3.windows64位分页机制分析-隐藏可执行内存方法
4.某系统漏洞挖掘之授权绕过到rce
5.office 分析笔记 —— rtf解析器(wwlib)的不完全解读
6.某系统漏洞挖掘之固件分析


点击“阅读原文”了解更多
最新评论
Thanks for your blog, nice to read. Do not stop.
1
1
你图片显示不了
你的网站怎么注册啊