Android实战-逆向某li某li视频APP 当然不是bilibili,是某不良视频APP,没想到不良软件都能上梆梆加固企业版了,因此xposed模块编写失败,被检测了,进修后再来看看吧
Frida hook分析 MT看了下是梆梆加固企业版(不知道真假),先看看能不能一把梭
frida-dexdump出现了点问题
JavaScript agent(Frida 注入脚本)在执行过程中被目标进程销毁/结束,导致 RPC 调用失败。
为什么RPC会失败?
原因是:
JS Agent 已经死亡,但 Python 还在尝试 RPC 调用 。
1 2 3 4 5 searchdex → RPC 调用 ↓ Frida JS agent 被杀死(反调试 / 崩溃) ↓ script has been destroyed
RPC = 电脑端调用 Frida 注入手机里的 JS 函数。
JS 崩溃 → RPC 失败 → InvalidOperationError。
在安卓中,应用大多运行在不同的进程中 ,彼此不能直接访问对方的内存。于是 Android 设计了 Binder IPC(进程间通信) ,它使用 RPC 模型 :
就像你调用一个函数,但这个函数实际上在另一个进程里执行。
可能导致rpc失败的点很多
ptrace 反调试
native 层 anti-frida
Java 层 isDebuggerConnected
hook 监控
detect Frida server 端口
detect /data目录中 frida 名称
判断 frida-gadget 特征内存
主动 kill 注入线程
导致 Frida agent 被瞬间 kill → script 被销毁
经过测试,不启动frida时是可以正常启动的
问题可能就是frida-server被识别
56.al
网站试了下,梭出来了点信息
jadx查看了下,是广告vip和一些简单的视频vip
而不包含userVip
要凭借这些信息绕过也行,但最根本的还得凭借user的一些注入和hook
用了Releases · hzzheyang/strongR-frida-android
还是被kill掉了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Java .perform (function ( ){ var process_Obj_Module_Arr = Process .enumerateModules (); for (var i = 0 ; i < process_Obj_Module_Arr.length ; i++) { if (process_Obj_Module_Arr[i].path .indexOf ("lib" )!=-1 ) { console .log ("模块名称:" ,process_Obj_Module_Arr[i].name ); console .log ("模块地址:" ,process_Obj_Module_Arr[i].base ); console .log ("大小:" ,process_Obj_Module_Arr[i].size ); console .log ("文件系统路径" ,process_Obj_Module_Arr[i].path ); } } });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Java .perform (function ( ){ var dlopen_interceptor = hook_dlopen (); function hook_dlopen ( ) { Interceptor .attach (Module .findExportByName (null , "android_dlopen_ext" ), { onEnter : function (args ) { this .fileName = args[0 ].readCString () console .log (`dlopen onEnter: ${this .fileName} ` ) }, onLeave : function (retval ){ console .log (`dlopen onLeave fileName: ${this .fileName} ` ) } } ); } });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook_dlopen ( ) { Interceptor .attach (Module .findExportByName (null , "android_dlopen_ext" ), { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr !== undefined && pathptr != null ) { var path = ptr (pathptr).readCString (); console .log ("load " + path); } } } ); } Java .perform (function ( ){ hook_dlopen (); });
hookandroid_dlopen_ext>查看so文件的加载流程
1 2 3 4 5 6 dlopen onEnter: libmonochrome_64.so dlopen onLeave fileName: libmonochrome_64.so dlopen onEnter: /product/app/TrichromeLibrary64/TrichromeLibrary64.apk!/lib/arm64-v8a/libmonochrome_64.so dlopen onLeave fileName: /product/app/TrichromeLibrary64/TrichromeLibrary64.apk!/lib/arm64-v8a/libmonochrome_64.so dlopen onEnter: /system/lib64/libwebviewchromium_plat_support.so dlopen onLeave fileName: /system/lib64/libwebviewchromium_plat_support.so
libwebviewchromium_plat_support.so
这个里面可能放反调试的东西?
但是之前用MT看的时候是某梆加固企业版怪问题是用查壳工具并没有发现特征
越看越奇怪
1 2 3 ./hs -l 0.0.0.0:9999 adb forward tcp:9999 tcp:9999(需要另外开一个终端),前面的是电脑端口 frida -H 127.0.0.1:9999 -f xxx -l r0tracer.js
直接换了个端口就不闪退了
frida默认端口号是20742
1 frida-dexdump -H 127.0.0.1:9999 -f com.ctf -o E:\Desktop\dump\
成功运行
有几处错误地方很正常
看来那个某梆企业版感觉是吓唬人的…
抓下包
1 2 @o("v1/user/info") pl.d<vo.o<String>> g(@zo .a w wVar);
@o 很可能是 Retrofit 的 @POST
@zo.a 很可能是 Retrofit 的 @Body
通过抓包字段viptril找到了ResponseUserInfo
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 package com.palipali.model.response;import a0.b;import android.support.v4.media.d;import ib.e;import java.io.Serializable;import ym.f;public final class ResponseUserInfo implements Serializable { private final int _id; private boolean binding_alert; private int checkin_recounting_times; private String checkin_time; private int download_limit; private long expiry; private String forever_code; private ResponseInnerAnnouncement informAnnouncement; private String invite_code; private int invite_deadline; private int invite_total; private int level; private String login_type; private String mail; private boolean new_avkey_success; private int no_purchase; private ResponseOrderGa orderGa; private ResponseInnerAnnouncement paymentAnnouncement; private String phone; private String platform; private boolean set_password; private String share_url; private int showVipTime; private String trialMessage; private boolean trialResult; private String unicode; private int user_id; private int verify_mail; private int verify_phone; private boolean vipTrial; public ResponseUserInfo () { this (0 , 1 , null ); } public ResponseUserInfo (int i10) { this ._id = i10; this .login_type = "" ; this .mail = "" ; this .phone = "" ; this .platform = "" ; this .invite_code = "" ; this .share_url = "" ; this .unicode = "" ; this .forever_code = "" ; this .trialMessage = "" ; this .checkin_time = "" ; } public static ResponseUserInfo copy$default (ResponseUserInfo responseUserInfo, int i10, int i11, Object obj) { if ((i11 & 1 ) != 0 ) { i10 = responseUserInfo._id; } return responseUserInfo.copy(i10); } public final int component1 () { return this ._id; } public final ResponseUserInfo copy (int i10) { return new ResponseUserInfo (i10); } public boolean equals (Object obj) { if (this == obj) { return true ; } return (obj instanceof ResponseUserInfo) && this ._id == ((ResponseUserInfo) obj)._id; } public final boolean getBinding_alert () { return this .binding_alert; } public final int getCheckin_recounting_times () { return this .checkin_recounting_times; } public final String getCheckin_time () { return this .checkin_time; } public final int getDownload_limit () { return this .download_limit; } public final long getExpiry () { return this .expiry; } public final String getForever_code () { return this .forever_code; } public final ResponseInnerAnnouncement getInformAnnouncement () { return this .informAnnouncement; } public final String getInvite_code () { return this .invite_code; } public final int getInvite_deadline () { return this .invite_deadline; } public final int getInvite_total () { return this .invite_total; } public final int getLevel () { return this .level; } public final String getLogin_type () { return this .login_type; } public final String getMail () { return this .mail; } public final boolean getNew_avkey_success () { return this .new_avkey_success; } public final int getNo_purchase () { return this .no_purchase; } public final ResponseOrderGa getOrderGa () { return this .orderGa; } public final ResponseInnerAnnouncement getPaymentAnnouncement () { return this .paymentAnnouncement; } public final String getPhone () { return this .phone; } public final String getPlatform () { return this .platform; } public final boolean getSet_password () { return this .set_password; } public final String getShare_url () { return this .share_url; } public final int getShowVipTime () { return this .showVipTime; } public final String getTrialMessage () { return this .trialMessage; } public final boolean getTrialResult () { return this .trialResult; } public final String getUnicode () { return this .unicode; } public final int getUser_id () { return this .user_id; } public final int getVerify_mail () { return this .verify_mail; } public final int getVerify_phone () { return this .verify_phone; } public final boolean getVipTrial () { return this .vipTrial; } public final int get_id () { return this ._id; } public int hashCode () { return Integer.hashCode(this ._id); } public final void setBinding_alert (boolean z10) { this .binding_alert = z10; } public final void setCheckin_recounting_times (int i10) { this .checkin_recounting_times = i10; } public final void setCheckin_time (String str) { e.g(str, "<set-?>" ); this .checkin_time = str; } public final void setDownload_limit (int i10) { this .download_limit = i10; } public final void setExpiry (long j10) { this .expiry = j10; } public final void setForever_code (String str) { e.g(str, "<set-?>" ); this .forever_code = str; } public final void setInformAnnouncement (ResponseInnerAnnouncement responseInnerAnnouncement) { this .informAnnouncement = responseInnerAnnouncement; } public final void setInvite_code (String str) { e.g(str, "<set-?>" ); this .invite_code = str; } public final void setInvite_deadline (int i10) { this .invite_deadline = i10; } public final void setInvite_total (int i10) { this .invite_total = i10; } public final void setLevel (int i10) { this .level = i10; } public final void setLogin_type (String str) { e.g(str, "<set-?>" ); this .login_type = str; } public final void setMail (String str) { e.g(str, "<set-?>" ); this .mail = str; } public final void setNew_avkey_success (boolean z10) { this .new_avkey_success = z10; } public final void setNo_purchase (int i10) { this .no_purchase = i10; } public final void setOrderGa (ResponseOrderGa responseOrderGa) { this .orderGa = responseOrderGa; } public final void setPaymentAnnouncement (ResponseInnerAnnouncement responseInnerAnnouncement) { this .paymentAnnouncement = responseInnerAnnouncement; } public final void setPhone (String str) { e.g(str, "<set-?>" ); this .phone = str; } public final void setPlatform (String str) { e.g(str, "<set-?>" ); this .platform = str; } public final void setSet_password (boolean z10) { this .set_password = z10; } public final void setShare_url (String str) { e.g(str, "<set-?>" ); this .share_url = str; } public final void setShowVipTime (int i10) { this .showVipTime = i10; } public final void setTrialMessage (String str) { e.g(str, "<set-?>" ); this .trialMessage = str; } public final void setTrialResult (boolean z10) { this .trialResult = z10; } public final void setUnicode (String str) { e.g(str, "<set-?>" ); this .unicode = str; } public final void setUser_id (int i10) { this .user_id = i10; } public final void setVerify_mail (int i10) { this .verify_mail = i10; } public final void setVerify_phone (int i10) { this .verify_phone = i10; } public final void setVipTrial (boolean z10) { this .vipTrial = z10; } public String toString () { return b.a(d.a("ResponseUserInfo(_id=" ), this ._id, ')' ); } public ResponseUserInfo(int i10, int i11, f fVar) { this ((i11 & 1 ) != 0 ? -1 : i10); } }
注入似乎只能改变某些东西
如用户页面vip显示,视频页面的vip高清和快速通道选项
广告vip和视频时长似乎仍是1分钟并没有被更改,因此要继续逆向跟踪
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 60 61 62 63 64 function hook_dlopen ( ) { Interceptor .attach (Module .findExportByName (null , "android_dlopen_ext" ), { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr !== undefined && pathptr != null ) { var path = ptr (pathptr).readCString (); console .log ("load " + path); } } } ); } function hook_user ( ) { Java .perform (function ( ) { var User = Java .use ("com.palipali.model.response.ResponseUserInfo" ); console .log ("[+] Hooking ResponseUserInfo ..." ); User .setVipTrial .implementation = function (v ) { console .log ("[VIP] force vipTrial = true" ); return this .setVipTrial (true ); }; User .setLevel .implementation = function (v ) { console .log ("[VIP] force level = 2" ); return this .setLevel (1 ); }; User .setExpiry .implementation = function (v ) { console .log ("[VIP] force expiry = 9999999999999" ); return this .setExpiry (9999999999999 ); }; User .setTrialResult .implementation = function (v ) { console .log ("[VIP] force trialResult = true" ); return this .setTrialResult (true ); }; User .setShowVipTime .implementation = function (v ) { console .log ("[VIP] force showVipTime = 300" ); return this .setShowVipTime (3000 ); }; User .toString .implementation = function ( ) { var ret = this .toString (); console .log ("[UserInfo] " + ret); console .log (" level =" , this .getLevel ()); console .log (" vipTrial =" , this .getVipTrial ()); console .log (" expiry =" , this .getExpiry ()); console .log (" trialResult =" , this .getTrialResult ()); return ret; }; }); } Java .perform (function ( ){ hook_dlopen (); hook_user (); });
陷入了漫长的寻找
ResponseUserInfo,可能只改了用户信息,真正用于播放鉴权的是 ik.v(MemberBean)。
发现黄鸟抓包要全一点
这两个类均有video_end和duration类似信息
先hook了ResponseVideoInfo
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 function hook_response_vip ( ) { var Info = Java .use ("com.palipali.model.response.ResponseVideoInfo" ); console .log ("[+] Hooking ResponseVideoInfo..." ); Info .getVideo_duration .implementation = function ( ) { var d = this .getVideo_duration (); console .log ("[VIDEO] getVideo_duration =" , d); return d; }; Info .getVideo_end .implementation = function ( ) { var end = this .getVideo_end (); console .log ("[VIDEO] getVideo_end =" , end); return end; }; Info .setVideo_end .implementation = function (v ) { console .log ("[VIDEO] setVideo_end called, value =" , v); return this .setVideo_end (v); }; }
证明其实不该hook这,在这hook也晚了,或者hook的字段不对,然后跟踪字段到这个类
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 package com.palipali .model .response ; import android.support .v4 .media .d ;import bi.t ;import com.google .gson .annotations .SerializedName ;import e2.b ;import ib.e ;import java.io .Serializable ;import miui.telephony .phonenumber .CountryCodeConverter ;import ym.f ;public final class ResponseVideoUrl implements Serializable { @SerializedName ("intro" ) private String introUrl; @SerializedName (CountryCodeConverter .GQ ) private String qvgaUrl; @SerializedName ("480" ) private String vgaUrl; public ResponseVideoUrl () { this (null , null , null , 7 , null ); } public ResponseVideoUrl (String str, String str2, String str3) { t.a (str, "introUrl" , str2, "qvgaUrl" , str3, "vgaUrl" ); this .introUrl = str; this .qvgaUrl = str2; this .vgaUrl = str3; } public static ResponseVideoUrl copy$default (ResponseVideoUrl responseVideoUrl, String str, String str2, String str3, int i10, Object obj) { if ((i10 & 1 ) != 0 ) { str = responseVideoUrl.introUrl ; } if ((i10 & 2 ) != 0 ) { str2 = responseVideoUrl.qvgaUrl ; } if ((i10 & 4 ) != 0 ) { str3 = responseVideoUrl.vgaUrl ; } return responseVideoUrl.copy (str, str2, str3); } public final String component1 ( ) { return this .introUrl ; } public final String component2 ( ) { return this .qvgaUrl ; } public final String component3 ( ) { return this .vgaUrl ; } public final ResponseVideoUrl copy (String str, String str2, String str3 ) { e.g (str, "introUrl" ); e.g (str2, "qvgaUrl" ); e.g (str3, "vgaUrl" ); return new ResponseVideoUrl (str, str2, str3); } public boolean equals (Object obj ) { if (this == obj) { return true ; } if (!(obj instanceof ResponseVideoUrl )) { return false ; } ResponseVideoUrl responseVideoUrl = (ResponseVideoUrl ) obj; return e.b (this .introUrl , responseVideoUrl.introUrl ) && e.b (this .qvgaUrl , responseVideoUrl.qvgaUrl ) && e.b (this .vgaUrl , responseVideoUrl.vgaUrl ); } public final String getIntroUrl ( ) { return this .introUrl ; } public final String getQvgaUrl ( ) { return this .qvgaUrl ; } public final String getVgaUrl ( ) { return this .vgaUrl ; } public int hashCode ( ) { return this .vgaUrl .hashCode () + g1.e .a (this .qvgaUrl , this .introUrl .hashCode () * 31 , 31 ); } public final void setIntroUrl (String str ) { e.g (str, "<set-?>" ); this .introUrl = str; } public final void setQvgaUrl (String str ) { e.g (str, "<set-?>" ); this .qvgaUrl = str; } public final void setVgaUrl (String str ) { e.g (str, "<set-?>" ); this .vgaUrl = str; } public String toString ( ) { StringBuilder a10 = d.a ("ResponseVideoUrl(introUrl=" ); a10.mo3321append (this .introUrl ); a10.mo3321append (", qvgaUrl=" ); a10.mo3321append (this .qvgaUrl ); a10.mo3321append (", vgaUrl=" ); return b.a (a10, this .vgaUrl , ')' ); } public ResponseVideoUrl (String str, String str2, String str3, int i10, f fVar) { this ((i10 & 1 ) != 0 ? "" : str, (i10 & 2 ) != 0 ? "" : str2, (i10 & 4 ) != 0 ? "" : str3); } }
字段
含义
可能内容
introUrl
试看/简介视频(通常几十秒 → 限制版)
60 秒试看
qvgaUrl
低清视频(完整)
240P 正片
vgaUrl
标清视频(完整)
480P 正片
看了下免费的:
写了个新的hook方法
因为1分钟短片是intro
qvga是240p
vga是480p
我看了免费的验证了这之间只差一个参数,因此获取intro之后可以重新拼接,只要再给vga或qvag即可
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 function hook_response_url ( ) { var Url = Java .use ("com.palipali.model.response.ResponseVideoUrl" ); console .log ("[+] Hooking ResponseVideoUrl with auto full URL..." ); var url_240 = '' var url_480 = '' Url .getIntroUrl .implementation = function ( ) { var intro = this .getIntroUrl (); console .log ("[URL] intro =" , intro); var filename = intro.substring (intro.lastIndexOf ("/" ) + 1 ); console .log ("[URL] extracted filename =" , filename); url_240 = "/media/240/" + filename; url_480 = "/media/480/" + filename; console .log ("[URL] FULL video =" , url_240); console .log ("[URL] FULL video 480 =" , url_480); this .setQvgaUrl (url_240); this .setVgaUrl (url_480); console .log ("[URL] qvgaUrl overridden to FULL" ); return intro; }; Url .getQvgaUrl .implementation = function ( ) { var q = this .getQvgaUrl (); if (url_240) q = url_240; console .log ("[URL] qvga =" , q); return q; }; Url .getVgaUrl .implementation = function ( ) { var vga = this .getVgaUrl (); if (url_480) vga = url_480; console .log ("[URL] vga =" , vga); return vga; }; }
这个方法只是治标不治本,根本上还得分析谁调用了video,因为什么信息才导致不是vip,之前的hook_user用不了
分析了下,这个是后端校验为主
Hook 改写的是:
ResponseUserInfo.level
isVip
video_end
ResponseVideoUrl
这些都只是 本地数据模型 。
但 app 一旦执行需要写入服务器的操作,例如:
收藏视频(POST 请求)
取消收藏(DELETE)
点赞视频
评论
这些都是发请求给服务器的。
服务器当然不会根据你本地代码判断你是不是 VIP,它根据:
服务器数据库真正记录的账号状态
如
1 2 3 4 5 6 { "user_id" : 12345 , "level" : 0 , "is_vip" : false , "vip_expiry" : null }
解决方案有哪些
Hook 收藏接口的 请求参数 —— 伪造 VIP token
若收藏接口需要带 token:
1 2 3 POST /favorite token=xxxxxx video_id=123
可以 Hook OkHttp / retrofit,把 token 换成一个真正 VIP 的 token(需要你自己有 VIP 账号)。
缺点:需要一个 VIP 账号。
有点脱裤子放屁了
Hook OkHttp,让收藏接口永远“本地成功”
跟之前hook本地model一样
Hook retrofit/okhttp,劫持整个后端返回,伪造 ResponseUserInfo
这样整个 app 都以为你是真 VIP,甚至收藏接口服务器也不会再拒绝 (因为它会读取 local token?取决于接口)。
但通常收藏/点赞是根据真实服务器账号认证,不是本地字段。
即便伪造本地 ResponseUserInfo,服务器还是知道:
user_id=12345 的账号不是 VIP
没啥用
总代码:
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 function hook_dlopen ( ) { Interceptor .attach (Module .findExportByName (null , "android_dlopen_ext" ), { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr) { var path = ptr (pathptr).readCString (); console .log ("[dlopen] load => " + path); } } }); } function hook_JNI_OnLoad ( ){ let module = Process .findModuleByName ("libAppGuard.so" ) Interceptor .attach (module .base .add (0x32ADC ), { onEnter (args ){ console .log ("call JNI_OnLoad" ) } }) } function hook_dlopen2 (soName = '' ) { Interceptor .attach (Module .findExportByName (null , "android_dlopen_ext" ), { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr !== undefined && pathptr != null ) { var path = ptr (pathptr).readCString (); if (path.indexOf (soName) >= 0 ) { this .is_can_hook = true ; } } }, onLeave : function (retval ) { if (this .is_can_hook ) { hook_JNI_OnLoad () } } } ); } setImmediate (hook_dlopen2, "libAppGuard.so" )function hook_user ( ) { Java .perform (function ( ) { var User = Java .use ("com.palipali.model.response.ResponseUserInfo" ); console .log ("[+] Hooking ResponseUserInfo ..." ); User .setLevel .implementation = function (v ) { console .log ("[VIP] force level = 2" ); return this .setLevel (2 ); }; User .setExpiry .implementation = function (v ) { console .log ("[VIP] force expiry = 9999999999999" ); return this .setExpiry (1799999999 ); }; User .toString .implementation = function ( ) { var ret = this .toString (); console .log ("[UserInfo] " + ret); console .log (" level =" , this .getLevel ()); console .log (" vipTrial =" , this .getVipTrial ()); console .log (" expiry =" , this .getExpiry ()); console .log (" trialResult =" , this .getTrialResult ()); return ret; }; }); } function hook_response_vip ( ) { var Info = Java .use ("com.palipali.model.response.ResponseVideoInfo" ); console .log ("[+] Hooking ResponseVideoInfo..." ); Info .getVideo_duration .implementation = function ( ) { var d = this .getVideo_duration (); console .log ("[VIDEO] getVideo_duration =" , d); return d; }; Info .getVideo_end .implementation = function ( ) { var end = this .getVideo_end (); console .log ("[VIDEO] getVideo_end =" , end); return end; }; Info .setVideo_end .implementation = function (v ) { console .log ("[VIDEO] setVideo_end called, value =" , v); return this .setVideo_end (v); }; } function hook_response_url ( ) { var Url = Java .use ("com.palipali.model.response.ResponseVideoUrl" ); console .log ("[+] Hooking ResponseVideoUrl with auto full URL..." ); var url_240 = '' var url_480 = '' Url .getIntroUrl .implementation = function ( ) { var intro = this .getIntroUrl (); console .log ("[VID] intro =" , intro); var filename = intro.substring (intro.lastIndexOf ("/" ) + 1 ); console .log ("[VID] extracted filename =" , filename); if (filename){ url_240 = "/media/240/" + filename; url_480 = "/media/480/" + filename; console .log ("[VID] FULL video =" , url_240); console .log ("[VID] FULL video 480 =" , url_480); this .setQvgaUrl (url_240); this .setVgaUrl (url_480); console .log ("[VID] qvgaUrl overridden to FULL" ); } return intro; }; Url .getQvgaUrl .implementation = function ( ) { var q = this .getQvgaUrl (); if (url_240) q = url_240; console .log ("[URL] qvga =" , q); return q; }; Url .getVgaUrl .implementation = function ( ) { var vga = this .getVgaUrl (); var q = this .getQvgaUrl (); var filename = q.substring (q.lastIndexOf ("/" ) + 1 ); console .log ("[URL] extracted filename =" , filename); if (filename) url_480 = "/media/480/" + filename; if (url_480) vga = url_480; console .log ("[URL] vga =" , vga); return vga; }; } function hook_missing_video_urls ( ) { Java .perform (function ( ) { var Info = Java .use ("com.palipali.model.response.ResponseVideoInfo" ); var Url = Java .use ("com.palipali.model.response.ResponseVideoUrl" ); console .log ("[+] Hooking fallback URL generator..." ); Info .getVideo_urls .implementation = function ( ) { var urls = this .getVideo_urls (); var vid = this .getVideo_id (); console .log ("ID:" + vid); if (!vid || vid.length === 0 ) { console .log ("[VID] video_id missing, return raw urls" ); return urls; } var intro = urls.getIntroUrl (); var qvga = urls.getQvgaUrl (); var vga = urls.getVgaUrl (); if (!intro && !qvga && !vga) { console .log ("[VID] Missing URLs for video:" , vid); var file = vid + ".m3u8" ; var url240 = "/media/240/" + file; var url480 = "/media/480/" + file; console .log ("[VID] fallback 240 =" , url240); console .log ("[VID] fallback 480 =" , url480); urls.setQvgaUrl (url240); urls.setVgaUrl (url480); urls.setIntroUrl (url240); }else if (intro){ console .log ("[VID] Missing URLs for video:" , vid); var file = vid + ".m3u8" ; var url240 = "/media/240/" + file; var url480 = "/media/480/" + file; console .log ("[VID] fallback 240 =" , url240); console .log ("[VID] fallback 480 =" , url480); urls.setQvgaUrl (url240); urls.setVgaUrl (url480); } return urls; }; }); } Java .perform (function ( ){ hook_user (); hook_response_vip (); hook_missing_video_urls (); });
可能要改数据库只能渗透了hhh
渗透有空再学吧
Xpoed模块编写 [失败] 到此为止想收个尾,写个模块
没想到直接被kill了
因为没找到如何定位哪kill的,找了很久,看了挺多博客都是java层的
但是java层没发现什么,后面猜测是native层的
直接找so
发现了可以文件libAppGuard.so
似乎有反调但不确定
ida分析,发现这几个函数
是检测,但是a1点进去只是个偏移并不知道是对什么做了匹配
猜测是xposed这种?因为使用frida并没有出现这个情况
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 package com.example.prometronome;import java.io.BufferedReader;import java.io.FileReader;import de.robv.android.xposed.IXposedHookLoadPackage;import de.robv.android.xposed.callbacks.XC_LoadPackage;import de.robv.android.xposed.XC_MethodHook;import de.robv.android.xposed.XposedHelpers;import de.robv.android.xposed.XposedBridge;public class MainHook implements IXposedHookLoadPackage { private String url240 = "" ; private String url480 = "" ; @Override public void handleLoadPackage (final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.palipali" )) return ; XposedBridge.log("[+] Loaded com.palipali, start hooking..." ); dumpProcMaps(); hookUserInfo(lpparam); hookResponseUrl(lpparam); } private void dumpProcMaps () { try { BufferedReader br = new BufferedReader (new FileReader ("/proc/self/maps" )); String line; while ((line = br.readLine()) != null ) { XposedBridge.log("[MAPS] " + line); } br.close(); } catch (Throwable e) { XposedBridge.log("ERROR dumpProcMaps: " + e); } } private void hookUserInfo (XC_LoadPackage.LoadPackageParam lpparam) { try { Class<?> cls = XposedHelpers.findClass( "com.palipali.model.response.ResponseUserInfo" , lpparam.classLoader ); XposedBridge.log("[+] Hooking ResponseUserInfo ..." ); XposedHelpers.findAndHookMethod(cls, "setLevel" , int .class, new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) { XposedBridge.log("[VIP] force level = 2" ); param.args[0 ] = 2 ; } }); XposedHelpers.findAndHookMethod(cls, "setExpiry" , long .class, new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) { XposedBridge.log("[VIP] force expiry = 1799999999" ); param.args[0 ] = 1799999999L ; } }); XposedHelpers.findAndHookMethod(cls, "toString" , new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { Object thiz = param.thisObject; int level = (int ) XposedHelpers.callMethod(thiz, "getLevel" ); boolean vipTrial = (boolean ) XposedHelpers.callMethod(thiz, "getVipTrial" ); long expiry = (long ) XposedHelpers.callMethod(thiz, "getExpiry" ); boolean trialResult = (boolean ) XposedHelpers.callMethod(thiz, "getTrialResult" ); XposedBridge.log("[UserInfo] " + param.getResult()); XposedBridge.log(" level = " + level); XposedBridge.log(" vipTrial = " + vipTrial); XposedBridge.log(" expiry = " + expiry); XposedBridge.log(" trialResult = " + trialResult); } }); } catch (Throwable e) { XposedBridge.log("ERROR hookUserInfo: " + e); } } private void hookResponseUrl (XC_LoadPackage.LoadPackageParam lpparam) { try { Class<?> cls = XposedHelpers.findClass( "com.palipali.model.response.ResponseVideoUrl" , lpparam.classLoader ); XposedBridge.log("[+] Hooking ResponseVideoUrl ..." ); XposedHelpers.findAndHookMethod(cls, "getIntroUrl" , new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { String intro = (String) param.getResult(); if (intro == null ) return ; XposedBridge.log("[URL] intro = " + intro); String filename = intro.substring(intro.lastIndexOf("/" ) + 1 ); url240 = "/media/240/" + filename; url480 = "/media/480/" + filename; XposedBridge.log("[URL] FULL video 240 = " + url240); XposedBridge.log("[URL] FULL video 480 = " + url480); XposedHelpers.callMethod(param.thisObject, "setQvgaUrl" , url240); XposedHelpers.callMethod(param.thisObject, "setVgaUrl" , url480); XposedBridge.log("[URL] Overridden qvga/vga to FULL" ); } }); XposedHelpers.findAndHookMethod(cls, "getQvgaUrl" , new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { param.setResult(url240); XposedBridge.log("[URL] qvga = " + url240); } }); XposedHelpers.findAndHookMethod(cls, "getVgaUrl" , new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) { param.setResult(url480); XposedBridge.log("[URL] vga = " + url480); } }); } catch (Throwable e) { XposedBridge.log("ERROR hookResponseUrl: " + e); } } }
看了下用的这个so的是脱壳时才会用到,闪退也是没进入广告就闪退了
因此就是脱壳时so文件做了检测
向上追溯ida就是一个init方法
现在的问题是如何绕过
为了方便可以先用fridahook下确定
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 const so_name = "libAppGuard.so" ;const sub_2F02F8_offset = 0x2F02F8 ;function hexDump (ptr, len=64 ) { try { return hexdump (ptr, {length : len}); } catch (e) { return "(hex dump failed)" ; } } setTimeout (function ( ) { const module = Process .findModuleByName (so_name); if (!module ) { console .log ("[-] Cannot find module:" , so_name); return ; } console .log ("[+] Module base =" , module .base ); let target = module .base .add (sub_2F02F8_offset); console .log ("[+] sub_2F02F8 address =" , target); Interceptor .attach (target, { onEnter (args ) { this .arg_i = args[0 ]; this .arg_a1 = args[1 ]; console .log ("\n========== sub_2F02F8 called ==========" ); console .log ("i =" , this .arg_i ); console .log ("a1 ptr =" , this .arg_a1 ); try { let s = Memory .readUtf8String (this .arg_a1 ); console .log ("[+] a1 string:" , s); } catch (e) { console .log ("[-] a1 is not valid UTF-8:" , e); } console .log ("[+] Hex dump a1:" ); console .log (hexDump (this .arg_a1 , 128 )); } }); }, 1000 );
跟xposed配合使用调试
果然在libAppGuard.so被kill了
在这里被kill了
解密出来
分析不动了
解混淆代码
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 from pathlib import Pathpath = Path("libAppGuard.so" ).read_bytes() src_off, src_len, dst_len = 0x2fad0 , 0x126bf2 , 0x289144 s = path[src_off:src_off + src_len] dst = bytearray (dst_len) w7 = w9 = w4 = 0 w11 = 0x7fffffff w5 = 1 while True : w6 = w4 & 0x7f w4 = (w4 << 1 ) & 0xffffffff if w6 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 if w4 & 0x100 : if w9 >= len (s): break dst[w7] = s[w9]; w9 += 1 ; w7 += 1 continue w6 = 1 while True : w8 = w4 & 0x7f w6 = (w6 << 1 ) & 0xffffffff w4 = (w4 << 1 ) & 0xffffffff if w8 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 w8 = (w4 >> 8 ) & 1 ; w6 = (w6 + w8) & 0xffffffff w8 = w4 & 0x7f w4 = (w4 << 1 ) & 0xffffffff if w8 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 if w4 & 0x100 : break w8 = w4 & 0x7f w6 = (w6 + w11) & 0xffffffff w6 = (w6 << 1 ) & 0xffffffff w4 = (w4 << 1 ) & 0xffffffff if w8 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 w8 = (w4 >> 8 ) & 1 ; w6 = (w6 + w8) & 0xffffffff continue if w6 != 2 : if w9 >= len (s): break w6 = (w6 << 8 ) & 0xffffffff w5 = s[w9]; w9 += 1 w6 = (w6 - 0x300 ) & 0xffffffff w5 = (w5 + w6) & 0xffffffff if w5 == 0xffffffff : break w6 = (w5 & 1 ) ^ 1 w5 = (w5 >> 1 ) + 1 else : w8 = w4 & 0x7f w4 = (w4 << 1 ) & 0xffffffff if w8 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 w6 = (w4 >> 8 ) & 1 while True : w8 = w4 & 0x7f w6 = (w6 << 1 ) & 0xffffffff w4 = (w4 << 1 ) & 0xffffffff if w8 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 w8 = (w4 >> 8 ) & 1 ; w6 = (w6 + w8) & 0xffffffff if w6 != 0 : break w6 = 1 while True : w8 = w4 & 0x7f w6 = (w6 << 1 ) & 0xffffffff w4 = (w4 << 1 ) & 0xffffffff if w8 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 w8 = (w4 >> 8 ) & 1 ; w6 = (w6 + w8) & 0xffffffff w8 = w4 & 0x7f w4 = (w4 << 1 ) & 0xffffffff if w8 == 0 : if w9 >= len (s): break w4 = ((s[w9] << 1 ) + 1 ) & 0xffffffff ; w9 += 1 if not (w4 & 0x100 ): continue w6 = (w6 + 2 ) & 0xffffffff break break if w5 > 0x500 : w6 = (w6 + 1 ) & 0xffffffff dst[w7] = dst[w7 - w5]; w7 += 1 x8 = 0 while True : dst[w7 + x8] = dst[w7 - w5 + x8 + 1 ] x8 += 1 if w6 == x8: break w7 = (w7 + w6) & 0xffffffff patched = bytearray (path) patched[src_off:src_off + len (dst)] = dst Path("libAppGuard_unpacked.so" ).write_bytes(patched) print ("done" , w7, "bytes" )
暂时告一段落了
Frida持久化也不太好搞…