查看原文
其他

一款炒茅台炒鞋抢购辅助软件Android版的逆向分析

wx_A.R 看雪学院 2021-03-07

本文为看雪论坛优秀文章

看雪论坛作者ID:wx_A.R





0x00 简介


这是一款抢购辅助软件,通过这个软件,可以获取国内外的热门抢购网站抢购款的补货、发售的精准提醒,主要以AJ、AF等热门鞋款为主。

也可以添加淘宝、天猫、京东的各种抢购活动链接以辅助计时跳转,提高抢购的时效性和成功率,最近也正好在各大平台抢茅台玩儿,听人说用这个APP抢购的成功率还是蛮高的,所以下载了一个试试,注册登录进入APP之后,点击“XX工具箱” 提示“仅限会员使用”,如下图:


果然“天下没有免费的午餐”。

现在就来试着逆向分析一下,看能否被破解或者找到明显漏洞。





0x01 逆向

打开JADX-GUI,将APK拖入:


很显然是加固了的,看一下主要的Activity吧:


有若干个activity,结合dumpsys activity top指令的结果,蓝色框内的就是APP启动之后的主Activity了,要想看到主要的Activity代码和关键逻辑就只有先把APP的壳脱掉了。

将APP安装到Fart脱壳机上,点击运行,等待脱壳完成。

A Few Moments Later~~~~

脱壳机的/storage/emulated/0/fart目录下生成了com.jason.skrman,这里面便是脱壳之后的dex文件了,pull到本地,先查一下MainNewActivity是包含在哪个DEX里面的,如下命令:


主要的DEX的文件应该就是8937780_dexfile.dex这个文件了,用dex2jar反编译dex为jar文件,完成之后用JD-GUI打开jar文件:


大概看了下,MainNewActivity.class的函数都是没有被抽取的,那就先分析一下逻辑,先搜一下“仅限会员使用”字符串,看能否直接定位到提示的地方,没想到直接就找到了提示的地方:


很简单的逻辑,MyApplication.user_vip变量如果是true,就会调用调起HyBridActivity页面,并给页面传递进了一个URL作为参数。

关键点就是这个user_vip这个参数是在什么地方赋值的了,很幸运,这些地方的函数都没有被抽取,全局搜索一下user_vip,发现这个变量是从本地shared_prefs/user.xml中去取的值,如下图:


于是乎去试着打开/data/data/com.jason.skrman/shared_prefs/user.xml看看文件结构,如下图:


果然user_vip是false,问题突然变得简单了起来,只需要改变user.xml里面的user_vip字段的值应该就可以进入到VIP使用界面了。

改变user_vip字段为true之后重新替换user.xml文件,重新启动APP,再点击“XX工具箱”,没有弹出“仅限会员使用”Toast了,顺利进入到了VIP工具箱,现在可以使用高级功能了,如下图:


点击右上角的BP使用指南得知,我只需要把淘宝抢购宝贝的淘口令链接复制到“淘宝客户端BP”这个模块里面去,APP就会识别宝贝信息并帮我计时,并可以校准本地时间和天猫服务器的时间误差,添加时间偏移,在抢购开始的时候能够精准实施跳转并快速下单。

那就先点击一下“淘宝客户端BP”试试,很遗憾,点击了很久,页面都没有出现跳转,这是为什么呢?

继续从跳转进入到这个界面开始分析一下到底发生了什么,先前已经知道,页面是跳转进入到了HyBridActivity,看看HyBridActivity.class的代码如下:


关键函数都被抽取掉了,不过看这些函数应该都是做一个初始化的操作,似乎没有什么关键逻辑,那就先不急着去还原代码,先观察一下。

类里面用到一个DWebView的组件,刚才在分析打开HyBridActivity页面的时候看到传入了一个URL参数,这样看来,打开HyBridActivity页面就加载了一个网页。

代码上很容易看到这个URL的拼接是用到了user.xml里的另一个参数auth,通过本地的user.xml文件,最终拼接成的URL是https://skrman.app/skrtool.html?auth=7ed41a2999256157a8c606a9254e3a23
,先将这个URL在PC上的浏览器中打开:


跟手机上WebView加载的页面相差无几,点击“淘宝客户端BP”,居然进入到了次级页面,加载了https://skrman.app/tool-tmallBP.html?auth=
7ed41a2999256157a8c606a9254e3a23这个链接,页面加载如下图:


为什么手机上点击没反应呢?先不管了,我先试着贴一个淘宝的抢购淘口令链接进去看看能否得到BP链接:


看到这个弹窗,事情又变得复杂起来了,试着看看网页的源码,看看里面的逻辑,部分关键网页源码如下:

<script type="text/javascript"> ;eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('3u(N).3v(A($){3 g=K;3 h="X";h=3w.3x("1K");4(!h||h==\'X\'){C.D(\'请前往官网Y://2g.2h更新最新版本\')}3 l;3 m;3 n=0;3 o=[];3 p=1;3 q="X";3 r;3 s;3 t=K;3 u=K;3 v=E;3 w=E;3 x=1L.2i.3y;4(x.B(\'3z.3A\')>-1||x.B(\'2g.2h\')>-1){v=E}H{v=K}$("#Z-10-1").L();$("#Z-10-2").F();$("#I-J-15").F();$("#I-J-G").F();$("#I-1d-G").F();$("#I-1y-O").F();z U=3B.U;4(U.B(\'3C\')>-1||U.B(\'1M\')>-1||U.B(\'3D\')>-1){g=E;4(U.B(\'1M 2j 3E\')>-1||U.B(\'3F\')>-1||U.B(\'1M 2j 3G\')>-1||U.B(\'3H\')>-1){g=K}}$(\'#2k\').1e(2l());3 y=3I(A(){2m()},3J);$("#1N-G").16(A(){4(!u){u=E;$("#1N-G").2n(\'2o\',\'3K\');C.D("已开启专业模式");$("#I-J-15").L();$("#2p-J-15").F();$("#I-1d-G").L();$("#I-J-G").L();$("#I-1y-O").L()}H{u=K;$("#1N-G").2n(\'2o\',\'3L\');C.D("已关闭专业模式");$("#I-J-15").F();$("#2p-J-15").L();$("#I-1d-G").F();$("#I-J-G").F();$("#I-1y-O").F()}});A 2m(){4(t){z 11=1f 1g();z 1m=r.1h()-11.1h();4(1m<=s){t=K;1O();$("#J-2q").P("开启跳转")}H{$("#J-2q").P("倒计时:"+1m+"1z")}}}A 1P(b){4(w){w=K}H{17}n=0;o=[];p=1;t=K;3 c=\'Y://3M.3N.3O/2r/2s?1K=\'+h+\'&2t=\'+b;4(v){c=\'Y://3P.3Q.1A/2r/2s?1K=\'+h+\'&2t=\'+b}$.2u({1n:"2v",2w:2x,1Q:\'18\',18:"2y",2z:\'18\',1B:c,2A:A(a){w=E;4(a.3R==0){l=a.19;1o();$("#Z-10-1").F();$("#Z-10-2").L();$("#1R").L();$("#1S").F()}H{4(a.2B.B(\'解析失败\')>-1){1T(b);w=E}H{C.D(a.2B)}}},2C:(1U,1n)=>{1T(b);w=E},})}A 1T(a){n=0;o=[];p=1;t=K;4(a.B(\'.1V.1A\')>-1){2D(a)}4(a.B(\'Q=\')>-1){3 b=a.R("Q=")[1];1W(b)}}A 2D(d){2E.2F("2G",{1B:d},A(a){4(a.B(\'3S\')==0){C.D("错误"+a);17}z S=a;3 b="X";4(S.B(\'&Q=\')>-1){3 c=S.R("&Q=")[1];b=c.R("&")[0]}H 4(S.B(\'M.V/i\')>-1){3 c=S.R("M.V/i")[1];b=c.R(".2H")[0]}H 4(S.B(\'2I.V/i\')>-1){3 c=S.R("2I.V/i")[1];b=c.R(".2H")[0]}4(b!=\'X\'){1W(b)}H{C.D("无法解析该淘口令")}})}A 1W(b){z 2J="3T";z 2K="3U";z 11=1f 1g();z 1X=11.1h()+"";3 c={"Q":b,"3V":b,"1p":b,"2L":{"Q":b},"3W":"8.0.0","3X":"1"};3 d=1Y.2M(c);d=2N(d);z 2O=2J+"&"+1X+"&2P&"+d;z 2Q=3Y(2O);z 2R="Y://3Z.m.M.V/1C/1Z.M.2S.2T/6.0/?"+"40=2.5.7&41=2P&t="+1X+"&42="+2Q+"&20=1Z.M.2S.2T&v=6.0&43=0&44=0&45=E&46=E&47=E&48=49@4a.9.9&1n=18&1Q=18&"+"19="+d;$.2u({1n:"2v",2w:2x,1Q:\'18\',18:"2y",2z:2K,1B:2R,2A:A(a){1q{z 2U={12:a.19.12,1a:a.19.1a};l=2U;1o();$("#Z-10-1").F();$("#Z-10-2").L();$("#1R").L();$("#1S").F()}1r(e){C.D("解析失败")}},2C:(1U,1n)=>{C.D(1U.4b+"网络错误,换个网络试试")},})}A 1o(){$("#21").P(l.12.4c);m=l.12.1p+"";3 a=\'\';3 b;3 c=\'\';1q{4(l.12.2V[0].1b>0){b=l.12.2V[0];4(b.B("1D")!=0){b="Y:"+b}22.23(b)}}1r(e){}1q{1i(3 i=0;i<l.1a.2W.1b;i++){z 1s=l.1a.2W[i];z 1E=1s.1c;z 1F=1s.1t;3 d=\'\';d=" <W 1j=\'12\' >\\n"+" <W 1j=\'21\' >"+1E+"</W>\\n"+" <O 1c=\'O\' 1j=\'2X\' 2Y=2Z("+1F+",24.30[24.30.4d].1k)>\\n";1i(3 j=0;j<1s.31.1b;j++){z 13=1s.31[j];z 1u=13.1v;3 f="X";4(13.32){f="Y:"+13.32}z 25=K;4(n==0&&j==0){4(i==0&&f!=\'X\'){b=f}c=c+1E+":"+13.1c;z 33={1t:1F,1v:1u};o.4e(33)}4(n!=0){1i(3 k=0;k<o.1b;k++){4(o[k].1t==1F&&o[k].1v==1u){c=c+1E+":"+13.1c;25=E;4(i==0&&f!=\'X\'){b=f}}}}4(25){d=d+" <1G 1k="+1u+" 34 = \'34\'>"+13.1c+"</1G>"}H{d=d+" <1G 1k="+1u+">"+13.1c+"</1G>"}}a=a+d+"</O></W>"}}1r(e){c=\'默认\'}a=a+" <W 1j=\'12\' >\\n"+"<W 1j=\'21\' >购买数量(谨慎输入)</W>\\n"+"<35 1c=\'O\' 1j=\'2X\' 2Y=36(24.1k) 1k=\'"+p+"\'>";a=a+"</W>";$("#4f").P(a);c="[下单确认]"+c+\' 购买数量:\'+p;$("#37").P(c);$("#4g").4h("4i",b);38();$("#39-3a").F()}A 38(){n=0;1q{1i(3 i=0;i<l.1a.26.1b;i++){z 27=l.1a.26[i].4j+\';\';3 a=E;1i(3 j=0;j<o.1b;j++){4(27.B(o[j].1t+\':\')==-1||27.B(o[j].1v+\';\')==-1){a=K}}4(a){n=l.1a.26[i].1w}}4(n==0){C.D("获取4k出错");$("#37").P("获取数据错误")}}1r(e){22.23("1w"+"---"+n)}};2Z=A(a,b){1i(3 i=0;i<o.1b;i++){4(o[i].1t==a){o[i].1v=b}}1o()};36=A(a){z 1H=3b(a);4(1H>0&&1H<3c){p=1H;22.23(p);1o()}};$("#4l-G").16(A(){z 1x=$("#4m").1e();4(1x.B(\'.1V.1A\')>-1){3 a=1x.R("1D")[1];a=a.R("?")[0];a=\'1D\'+a;1P(a)}H 4(1x.B(\'Q=\')>-1){3 a=1x.R("Q=")[1];a=\'Q=\'+a;1P(a)}H{C.D(\'确保口令内含有Y://m.1V.1A\')}});$("#I-J-G").16(A(){2E.2F("2G",{1B:"1D://20.m.M.V/4n/4o.4p?20=1Z.4q.4r"},A(a){1q{z 11=1f 1g();z 19=1Y.4s(a);z 3d=19.19.t;3 b=3d-11;3 c=28-b;4(b>0){C.D(\'淘宝时间比本地晚了【\'+b+\'1z】 , 时间偏移建议提前【\'+c+\'】(正负值已为您算好,数值仅供参考!实际视设备跳转速度自行调整!)\')}H{C.D(\'淘宝时间比本地早了【\'+(-b)+\'1z】 , 时间偏移建议设置【\'+c+\'】(正负值已为您算好,数值仅供参考!实际视设备跳转速度自行调整!)\')}}1r(e){}})});$("#4t-G").16(A(){t=K;$("#Z-10-1").L();$("#Z-10-2").F();$("#39-3a").L()});$("#4u-G").16(A(){z 3e=$("#2k").1e();r=1f 1g(3e);r.4v(0);4(g){r.4w(r.1h()-(3c*28*28*8))}q="M://1C.m.M.V/29/2a.P?1p="+m+"&3f=3g-8&2b=E&v=0&1w="+n+"&2c="+p;4(u){z 3h=3b($("#J-15-35").1e());s=3h;z 3i=$("#1y-O").1e();4(3i==\'2d\'){3 a={"Q":m,"4x":"2d","4y":"2d"};3 b=1Y.2M(a);b=2N(b);q=\'M://1C.m.M.V/29/2a.P?1w=\'+n+\'&2c=\'+p+\'&1p=\'+m+\'&2b=E&2L=\'+b}}H{s=$("#J-15-O").1e()}z 11=1f 1g();z 1m=r.1h()-11.1h();4(1m<=s){C.D("哎呀,开始时间不能比现在早");17}$("#1R").F();$("#1S").L();$("#J-4z").P("["+r.4A()+"(-"+s+"1z)] 自动下单");t=E});$("#4B-G").16(A(){1O()});$("#I-1d-G").16(A(){q="M://1C.m.M.V/29/2a.P?1p="+m+"&3f=3g-8&2b=E&v=0&1w="+n+"&2c="+p;3j(q.4C("M:","Y:"))});A 1O(){1L.2i.4D=q}A 3j(a){4(3k N.2e!=="A"){C.D(a);17}3 b=N.3l("4E");b.1k=a;b.3m(\'3n\',\'4F: 4G;4H: 1I;3o: 1I;\');N.S.3p(b);b.O();3 c=N.2e(\'1d\');N.S.4I(b);4(c){C.D(\'3q链接已经复制到剪切板中,请保存好使用【已有3q链接跳转 】\');17}4(3k N.3r!=="A"){C.D(a);17}3 d=N.3r();3 e=N.3l(\'W\');e.4J=a;e.3m(\'3n\',\'3o: 1I;4K: 1I;4L: 4M;\');N.S.3p(e);d.4N(e);4O 1J=1L.4P();4(1J.4Q>0){1J.4R()}1J.4S(d);N.2e(\'1d\');C.D(a)}A 2l(){z 14=1f 1g();z 2f="-";3 a=14.4T();3 b=14.4U()+1;3 c=14.4V();14.4W(14.3s()+1);z 3t=14.4X();z 1l=14.3s();4(b>=1&&b<=9){b="0"+b}4(c>=0&&c<=9){c="0"+c}4(1l>=0&&1l<=9){1l="0"+1l}3 d=a+2f+b+2f+c+\'T\'+3t+\':\'+1l;17 d}})',62,308,'|||var|if|||||||||||||||||||||||||||||||let|function|indexOf|weui|alert|true|hide|button|else|promode|time|false|show|taobao|document|select|html|id|split|body||userAgent|com|div|null|https|card|step|now|item|current_props_value|date|offset|click|return|jsonp|data|skuBase|length|name|copy|val|new|Date|getTime|for|class|value|strMinutes|timeDiff|type|reflashControlPad|itemId|try|catch|current_props|pid|current_props_vid|vid|skuId|_link|channel|ms|cn|url|h5|http|current_props_name|current_props_pid|option|_count|1px|selection|auth|window|iPhone|professional|startJump|queryItem|dataType|configPad|readyPad|queryItem2|err|tb|getTmallItemStyle|timeStramp|JSON|mtop|api|itemTitle|console|log|this|isSelect|skus|_propPath|60|cart|order|buyNow|quantity|bybtqdyh|execCommand|seperator1|SKRMAN|APP|location|OS|timeInput|getNowFormatDateTime|timeTask|css|color|basemode|count|aed0843961fb06dc|tmallBP|mlink|ajax|GET|timeout|5000|callback|jsonpCallback|success|errmsg|error|geiItemId|dsBridge|call|networkGet|htm|tmall|current_token|callbackString|exParams|stringify|encodeURIComponent|preSignStr|12574478|signStr|linkString|detail|getdetail|_result|images|props|itemSelect|onchange|onChange_Row|options|values|image|_mprops|selected|input|onChange_Count|itemStyle|findSkuId|tips|content|parseInt|1000|timestamp_tb|ready_time_str|_input_charset|utf|_ready_time_offset|buy_channel|copyTxt|typeof|createElement|setAttribute|style|height|appendChild|BP|createRange|getMinutes|hour|jQuery|ready|UrlParm|parm|host|skrman|app|navigator|AppleWebKit|iPad|4_3_3|SKRAND|7_0_1|IOS14|setInterval|50|red|black|bmobapi|hb174|top|cloud|bmob|errcode|errorjhandler|654a3a20023319a6dc4c755b151f082b|mtopjsonp|itemNumId|detail_v|utdid|hex_md5|h5api|jsv|appKey|sign|isSec|ecode|AntiFlood|AntiCreep|H5Request|ttid|2018|taobao_h5_9|statusText|title|selectedIndex|push|itemStylePad|itemPic|attr|src|propPath|skuid|query|link|rest|api3|do|common|getTimestamp|parse|reset|config|setSeconds|setTime|umpChannel|u_channel|confirm|toLocaleString|buy|replace|href|textarea|display|block|width|removeChild|innerHTML|fontSize|overflow|hidden|selectNode|const|getSelection|rangeCount|removeAllRanges|addRange|getFullYear|getMonth|getDate|setMinutes|getHours'.split('|'),0,{})); </script>


很遗憾,网页的关键逻辑脚本被混淆了,想理清这里面的逻辑比较困难。

面对这样的局面我的心理已经有一个猜测了,在用户注册并登陆的时候,通过账号等信息请求到服务器,服务器会通过某种算法生成一个auth值,最后写入到shared_prefs/user.xml配置里。

最后在进入到“XX工具箱”之后,打开各个BP页面的时候会带入保存在本地的auth,这个时候已经进入网页的逻辑,后台再根据网页请求的参数auth在后台查询当前的auth是否是高级会员,如果是高级会员,返回一个带有解析BP链接的页面,如果不是高级会员,直接提示警告弹窗。

这种情况下,如果判断会员逻辑在服务端进行,只要我们不知道auth的生成逻辑,就不能在本地伪造auth,进而此APP无法被破解。

下面通过抓包来验证一下是否是这个逻辑。





0x02 抓包


首先配置好Charles的HTTPS抓包,我们从登录的请求开始,我选择用QQ邮箱登录,如下图:


第一个请求是UserSign,请求很简单,构造了一个json,包含了时间戳timeStamp、初始auth、验证码code、邮箱地址address,以及一个简单的MD5 sign(通过代码中可以分析得到),这个时候服务端就会返回一个包含auth的json,程序将auth写入到本地的shared_prefs/user.xml中。

我们会发现,在进入到“淘宝客户端BP”或者天猫、京东BP模块之后,全程都会使用到这个auth,所以auth至关重要。

再来看看进入到“淘宝客户端BP”模块之后把淘口令粘贴到输入框之后点击提交按钮之后的抓包情况,如下图:


虽然网页端的JS脚本混淆之后想理清逻辑已经不那么容易了,从抓包上来看,实际上非常简单。

请求URL为https://cloud.bmob.cn/aed0843961fb06dc/tmallBP?auth=7ed41a2999256157a8c606a9254e3a23&mlink=https://m.tb.cn/h.VpgnK1d&callback=jsonp&_=1593173135285。

auth就是本地shared_prefs/user.xml里的auth,mlink是从淘口令里提取出的淘宝短链接,callback是传给服务端的回调函数指针,_参数是一个时间戳。

所以整个是否是会员的校验都是在服务端实现的,不是会员将不会返回任何有用的结果,只是出现一个Alert弹窗,所以我倾向于这个APP无解。





0x03 讨论


通过此样本的分析,再一次刷新了我对isVip的认知。

如果一切验证都在服务端进行,本人倾向于这个APP无解。

各位大佬有别的思路吗?小弟不才,希望大佬给出思路,也欢迎大家讨论啊。





0x04 声明


1、此案例分析只作为学习使用,因为能力有限本人无法破解也不会再进行后续的破解。如后续APP被破解或者因为破解版对版权方造成的损失,本人概不负责;

2、对于安全意识如此强的开发者,我只想对大佬说:“我开会员还不行吗?”。




- End -



看雪ID:wx_A.R

https://bbs.pediy.com/user-887043.htm

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



推荐文章++++

* 通过一款早期代码抽取壳入门学习 so 层分析

* 一个隐藏蛮深的白加黑样本分析

* 将FART和Youpk结合来做一次针对函数抽取壳的全面提升

* 一例Sential Ldk 7.10软加密狗处理的.net程序逆向处理过程

*  一个Unity游戏保护方案的分析和还原符号信息,偷学对global-metadata保护的思路


好书推荐













公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存