篇幅有限 完整内容及源码关注公众号:ReverseCode,发送 冲
移动TV npm install –save @types/frida-gum 配置vscode的frida自动代码提示
1 2 3 4 5 6 adb install movetv.apk git clone https://github.com/hluwa/FRIDA-DEXDump.git python main.py 打开app操作一会积分查看视频开始脱壳 cd com.cz.babySister grep -ril "LoginActivity" * 查看该类在那个dex中 frida -UF -l hookEvent.js 点击登录时,触发打印LoginActivity完整路径
通过jadx-gui 查看com.cz.babySister.activity.LoginActivity
的onClick方法
1 2 3 4 5 6 7 8 objection -g com.cz.babySister explore android hooking search classes user 查找和用户相关的类 android hooking search classes person android hooking watch class com.cz.babySister.javabean.UserInfo 查看该类在jadx中哪些地方调用了 plugin load /root/.objection/plugins/Wallbreaker plugin wallbreaker objectsearch com.cz.babySister.javabean.UserInfo 内存搜索UserInfo plugin wallbreaker objectdump --fullname 0x112swa 查看内存中UserInfo信息 android hooking list class_methods com.cz.babySister.javabean.UserInfo
frida -U -f com.cz.babySister -l jifen.js --no-pause 有壳不能spawn,从登录开始hook
frida -UF -l jifen.js 服务器检测校验了无法查看视频,客户端修改无效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hook_jifen(){ Java.perform(function(){ var javaString = Java.use("java.lang.String") Java.use("com.cz.babySister.javabean.UserInfo").setJifen.implementation = function(str) { var result = this.setJifen(javaString.$new("1000")) console.log("setJifen is :" ,str) console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); return result; } Java.use("com.cz.babySister.javabean.UserInfo").setJifen.implementation = function() { var result = this.getJifen() console.log("getJifen is :",result) console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); return javaString.$new("1000") } }) } function main(){ hook_jifen() } setImmediate(main)
通过console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
打印调用栈,找到最下面的com.cz.babySister.alipay.k.run
,查看com.cz.babySister.alipay下的所有类,jadx搜索支付失败
1 2 3 4 5 6 7 8 9 10 11 12 function hook_jifen2(){ Java.perform(function(){ var javaString = Java.use("java.lang.String") Java.use("com.cz.babySister.alipay.o").b.implementation=function(){ console.log("success") return javaString.$new("9000") } }) } function main(){ hook_jifen2() }
依旧失败,可能做了支付订单校验。
调用栈中害打印了com.cz.babySister.utils.ParseJson.parseRegisterName
,查看com.cz.babySister.utils.ParseJson类,所有内容都是从该类中出现,jadx查看该类的调用处,出现queryJifen
进入a.a方法,a2就是json,所有查询从a.a中返回json,通过objection对该类进行hook,登录时查看调用的方法基本都是a.a
1 2 android hooking watch class com.cz.babySister.c.a --dump-args --dump-return android hooking watch_method class com.cz.babySister.c.a.a --dump-args --dump-return
每次看成人台时都会queryJifen判断积分是否不足,不过Return Value是服务器判断了积分是否充足,充足才返回可以看的地址,没有逻辑漏洞了。
搜索memi1的来源
1 2 android hooking watch class_method android.content.Context.getString --dump-args --dump-return android hooking watch class_method android.content.Context.getText --dump-args --dump-return
context在app启动后立即生成且销毁,因为有壳的原因,无法hook,只有一次捕捉机会
1 2 3 4 android hooking watch class android.provider.Settings$Secure --dump-args --dump-return 每次context实时获取的 android hooking watch class_method android.provider.Settings$Secure.getString --dump-args --dump-return --dump-backtrace plugin wallbreaker objectsearch android.app.ContextImpl$ApplicationContentResolver plugin wallbreaker objectdump --fullname 0x2502
记一次frida实战——对某视频APP的脱壳、hook破解、模拟抓包、协议分析一条龙服务
fulao2 adb install -r fulao2.apk
1 2 3 4 5 6 7 8 9 10 11 ./fs128arm64 frida -UF -l hookSocket.js -o /root/Desktop/img.txt 启动app后开始抓包,GET到的地址结合域名发现图片是被加密的 objection -g com.ilulutv.fulao2 explore -P ~/.objection/plugins android hooking search classes imageView plugin wallbreaker objectsearch android.widget.ImageView 查看内存中的对象 plugin wallbreaker objectdump --fullname 0x20212 打印内存对象属性内容 android hooking search classes Bitmap 把所有Bitmap相关的类拷贝到文件bitmap.txt中,前面加上android hooking watch class plugin wallbreaker objectdump --fullname 0x2045 查看内存Bitmap属性,即图片属性 objection -g com.ilulutv.fulao2 explore -c bitmap.txt 批量hook,如果app崩了查看最后一个hook的类,在文件中删除重新挂上objection android hooking watch class_method android.graphics.BitmapFactory.decodeStream --dump-args --dump-backtrace --dump-return 下拉加载图片,堆栈中加载了glide图片加载库 android hooking watch class_method android.graphics.BitmapFactory.decodeByteArray --dump-args --dump-backtrace --dump-return 堆栈中打印了业务代码com.ilulutv.fulao2.other.helper.glide.b.a
jadx打开fulao2,搜索com.ilulutv.fulao2.other.helper.glide.b类的a方法
1 android hooking list class_methods android.graphics.BigmapFactory 查看decodeByteArray返回值
其中byte[] c2 = com.ilulutv.fulao2.other.i.b.c(decode, Base64.decode(bytes2, 0), encodeToString);
解密
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 function hookRAW(){ Java.perform(function(){ console.log("hooking RAW...") Java.use("com.ilulutv.fulao2.other.i.b").a.overload('java.nio.ByteBuffer').implementation = function(bytebuffer){ var result = this.a(bytebuffer) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("result is => ",ByteString.of(result).hex()) return result } }) } function hook_SSLsocketandroid8(){ Java.perform(function(){ console.log("hook_SSLsocket") // concrypt本质对libssl.so进行操作 Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function(bytearry,int1,int2){ var result = this.write(bytearry,int1,int2); console.log("HTTPS write result,bytearry,int1,int2=>",result,bytearry,int1,int2) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("bytearray contents=>", ByteString.of(bytearry).hex()) //console.log(jhexdump(bytearry,int1,int2)); // console.log(jhexdump(bytearry)); return result; } Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream").read.overload('[B', 'int', 'int').implementation = function(bytearry,int1,int2){ var result = this.read(bytearry,int1,int2); console.log("HTTPS read result,bytearry,int1,int2=>",result,bytearry,int1,int2) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("bytearray contents=>", ByteString.of(bytearry).hex()) //console.log(jhexdump(bytearry,int1,int2)); // console.log(jhexdump(bytearry)); return result; } }) } function main(){ hookRAW() hook_SSLsocketandroid8() } setImmediate(main)
frida -UF -l fulao2.js -o hookRAW.txt 下拉内容查看log图片,对比抓包hook_SSLsocketandroid8结果和hookRAW结果的内容是否一致,说明com.ilulutv.fulao2.other.i.b.a((ByteBuffer) obj)
确实是要加密的内容,明文在com.ilulutv.fulao2.other.i.b.b(decode, Base64.decode(bytes2, 0), encodeToString)
根据return com.bumptech.glide.load.q.d.e.a(BitmapFactory.decodeByteArray(b2, 0, b2.length), this.f11769a);
,hook系统库查看解密后图片内容
1 2 3 4 5 6 7 8 9 10 11 function hookClean(){ Java.perform(function(){ // hook 系统库 不会被混淆 ,图片文件头jpeg都是ffd8ff Java.use("android.graphics.BigmapFactory").decodeByteArray.overload('[B','int','int','android.graphics.BitmapFactory$Options').implementation = function (ba,int1,int2,op){ var result = this.decodeByteArray(ba,in1,int2,op) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("ba is=>",ByteString.of(ba).hex()) return result; } }) }
byte下载图片,frida -UF -l fulao2.js -o hookRAW.txt 下拉加载图片
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 function guid() { function S4() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); } return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); } function hookClean(){ Java.perform(function(){ // hook 系统库 不会被混淆 ,图片文件头jpeg都是ffd8ff Java.use("android.graphics.BigmapFactory").decodeByteArray.overload('[B','int','int','android.graphics.BitmapFactory$Options').implementation = function (ba,int1,int2,op){ var result = this.decodeByteArray(ba,in1,int2,op) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("ba is=>",ByteString.of(ba).hex()) var path = "/sdcard/Download/tmp/"+guid()+".jpg" console.log("path=> ",path) // android hooking search classes File var file = Java.use("java.io.File").$new(path) // android hooking search classes FileOutputStream var fos = Java.use("java.io.FileOutputStream").$new(file); fos.write(data); fos.close(); fos.close(); return result; } }) }
根据正常图片访问方式,利用安卓api,通过Bigmap对象压缩到文件输出流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hookClean(){ Java.perform(function(){ // hook 系统库 不会被混淆 ,图片文件头jpeg都是ffd8ff Java.use("android.graphics.BigmapFactory").decodeByteArray.overload('[B','int','int','android.graphics.BitmapFactory$Options').implementation = function (ba,int1,int2,op){ var result = this.decodeByteArray(ba,in1,int2,op) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("ba is=>",ByteString.of(ba).hex()) var path = "/sdcard/Download/tmp/"+guid()+".jpg" console.log("path=> ",path) // android hooking search classes File var file = Java.use("java.io.File").$new(path) // android hooking search classes FileOutputStream var fos = Java.use("java.io.FileOutputStream").$new(file); // android hooking list class_methods android.graphics.Bitmap 查看compress实例方法 result.compress(Java.use("android.graphics.Bitmap$CompressFormat").JPEG.value,100,fos) // fos.write(data); fos.close(); fos.close(); return result; } }) }
compress占用主线程资源,阻塞主线程导致程序崩溃,可以另起线程专门用来下载图片
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 function hookImage() { Java.perform(function () { var Runnable = Java.use("java.lang.Runnable"); var saveImg = Java.registerClass({ name: "com.roysue.runnable", implements: [Runnable], fields: { bm: "android.graphics.Bitmap", }, methods: { $init: [{ returnType: "void", argumentTypes: ["android.graphics.Bitmap"], implementation: function (bitmap) { this.bm.value = bitmap; } }], run: function () { var path = "/sdcard/Download/tmp/" + guid() + ".jpg" console.log("path=> ", path) var file = Java.use("java.io.File").$new(path) var fos = Java.use("java.io.FileOutputStream").$new(file); this.bm.value.compress(Java.use("android.graphics.Bitmap$CompressFormat").JPEG.value, 100, fos) console.log("success!") fos.flush(); fos.close(); } } }); Java.use("android.graphics.BitmapFactory").decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory$Options').implementation = function (data, offset, length, opts) { var result = this.decodeByteArray(data, offset, length, opts); var ByteString = Java.use("com.android.okhttp.okio.ByteString"); var runnable = saveImg.$new(result); runnable.run() return result; } }) }
不建议多线程在手机端运行,可以将线程发送PC端执行
fulao2.js
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 function hookImage() { Java.perform(function () { var Runnable = Java.use("java.lang.Runnable"); var saveImg = Java.registerClass({ name: "com.roysue.runnable", implements: [Runnable], fields: { bm: "android.graphics.Bitmap", }, methods: { $init: [{ returnType: "void", argumentTypes: ["android.graphics.Bitmap"], implementation: function (bitmap) { this.bm.value = bitmap; } }], run: function () { var path = "/sdcard/Download/tmp/" + guid() + ".jpg" console.log("path=> ", path) var file = Java.use("java.io.File").$new(path) var fos = Java.use("java.io.FileOutputStream").$new(file); this.bm.value.compress(Java.use("android.graphics.Bitmap$CompressFormat").JPEG.value, 100, fos) console.log("success!") fos.flush(); fos.close(); } } }); Java.use("android.graphics.BitmapFactory").decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory$Options').implementation = function (data, offset, length, opts) { var result = this.decodeByteArray(data, offset, length, opts); send(data) return result; } }) } function main(){ hookImage() } setImmediate(main)
fulao2.py
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 import frida import json import time import uuid import base64 import re def my_message_handler(message, payload): print(message) print(payload) if message["type"] == "send": #image = re.findall("(-?\d+)", message["payload"]) image = message["payload"] print(image) # 保存image intArr = [] # 位数的转换 for m in image: ival = int(m) if ival < 0: ival += 256 intArr.append(ival) bs = bytes(intArr) fileName = str(uuid.uuid1()) + ".jpg" f = open(fileName, 'wb') f.write(bs) f.close() device = frida.get_usb_device() target = device.get_frontmost_application() session = device.attach(target.pid) # 加载脚本 with open("fulao2.js") as f: script = session.create_script(f.read()) script.on("message", my_message_handler) # 调用错误处理 script.load() # 脚本会持续运行等待输入 input()
接下来尝试从抓到的包中解密协议,获取解密后的图片在hookRAW中实现脱机
fulao2.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function hookRAW(){ Java.perform(function(){ console.log("hooking RAW...") Java.use("com.ilulutv.fulao2.other.i.b").a.overload('java.nio.ByteBuffer').implementation = function(bytebuffer){ var result = this.a(bytebuffer) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("result is => ",ByteString.of(result).hex()) send(result) return result } }) } function main(){ hookRAW() } setImmediate(main)
fulao2.py中image = message["payload"]
需要解密,进入decodeImageKey实现
进入CipherCore
发现从so库中加载的加密协议
1 2 memory list modules 找到libcipher-lib.so memory list exports libcipher-lib.so 搜索getString 分析拿到byte之后通过base64加密
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 import frida import json import time import uuid import base64 import re from Crypto.Cipher import AES import base64 def IMGdecrypt(bytearray): # hook String decodeImgKey = CipherClient.decodeImgKey(); imgkey = base64.decodebytes( bytes("svOEKGb5WD0ezmHE4FXCVQ==", encoding='utf8')) imgiv = base64.decodebytes( bytes("4B7eYzHTevzHvgVZfWVNIg==", encoding='utf8')) cipher = AES.new(imgkey, AES.MODE_CBC, imgiv) # enStr += (len(enStr) % 4)*"=" # decryptByts = base64.urlsafe_b64decode(enStr) msg = cipher.decrypt(bytearray) def unpad(s): return s[0:-s[-1]] return unpad(msg) def my_message_handler(message, payload): print(message) print(payload) if message["type"] == "send": #image = re.findall("(-?\d+)", message["payload"]) image = message["payload"] print(image) intArr = [] # 位数的转换 for m in image: ival = int(m) if ival < 0: ival += 256 intArr.append(ival) bs = bytes(intArr) # 拿到数据后Base64解密 bs = IMGdecrypt(bs) fileName = str(uuid.uuid1()) + ".jpg" f = open(fileName, 'wb') f.write(bs) f.close() device = frida.get_usb_device() target = device.get_frontmost_application() session = device.attach(target.pid) # 加载脚本 with open("fulao2.js") as f: script = session.create_script(f.read()) script.on("message", my_message_handler) # 调用错误处理 script.load() # 脚本会持续运行等待输入 input()
VIP破解
切换高清视频,提示vip限定功能,无法切换vip,尝试hook按钮点击事件,frida -UF -l hookEvent.js,点击切换清晰度
jadx搜索com.ilulutv.fulao2.film.l.t
1 2 plugin wallbreaker objectsearch com.ilulutv.fulao2.film.l$t plugin wallbreaker objectdump --fullname 0x2a76
1 2 plugin wallbreaker objectdump --fullname 0x28f2 查看在看视频实例 android heap search instances com.ilulutv.fulao2.film.l
尝试将this.f11151d.q0
改为true,完成VIP功能中的清晰度切换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function hookVIP(){ Java.perform(function(){ Java.choose("com.ilulutv.fulao2.film.l",{ onMatch:function(ins){ console.log("found ins:=>",ins) ins.q0.value=true; },onComplete:function(){ console.log("search complete") } }) }) } function main(){ hookVIP() } setInterval(main,500)
抓取所有收发包
frida -U -f com.ilulutv.fulao2 -l hookSocket.js -o traffic.txt 所有通信都是加密了,不是在https层加密,而是在业务层,那就尝试hook所有的cipher
objection -g com.ilulutv.fulao2 explore -c cipher.txt
1 2 3 android hooking watch class com.cz.babySister.c.a android hooking watch class_method javax.crypto.Cipher.init --dump-args --dump-return --dump-backtrace android hooking list class_methods com.ilulutv.fulao2.other.i.b 将所有的方法复制到ciph.txt中,前面添加上android hooking watch class_method,后面添加--dump-args --dump-return --dump-backtrace
objection -g com.ilulutv.fulao2 explore -c cipher.txt 关闭app重新hook,打印出所有的通信内容包括加解密所有内容
查看com.ilulutv.fulao2.other.i.b.a ,改包将其中的vip改为true,expire修改过期时间
当所有业务返回都被加解密了,可以尝试hook系统库,不可被混淆加密
第一题.apk jadx-gui 第一题.apk
1 2 3 objection -g com.kanxue.pediy1 explore android hooking search classes com.kanxue.pediy1 android hooking watch classe com.kanxue.pediy1.VVVV
frida -UF -l question.js
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 var CONTEXT = null; // 获取类名 function getObjClassName(obj) { if (!jclazz) { var jclazz = Java.use("java.lang.Class"); } if (!jobj) { var jobj = Java.use("java.lang.Object"); } return jclazz.getName.call(jobj.getClass.call(obj)); } function hookReturn() { Java.perform(function () { Java.use("com.kanxue.pediy1.VVVVV").VVVV.implementation = function (context, str) { var result = this.VVVV(context, str) console.log("context,str,result => ", context, str, result); console.log("context className is => ", getObjClassName(context)); CONTEXT = context; return true; } }) } function invoke() { Java.perform(function () { //console.log("CONTEXT IS => ",CONTEXT) var MainActivity = null; Java.choose("com.kanxue.pediy1.MainActivity", { onMatch: function (instance) { MainActivity = instance; }, onComplete: function () { } }) var CONTEXT2 = Java.use("com.kanxue.pediy1.MainActivity$1").$new(MainActivity); var javaString = Java.use("java.lang.String").$new("12345"); for (var x = 0; x < (99999 + 1); x++) { // 静态函数VVVV 使用use直接调用 var result = Java.use("com.kanxue.pediy1.VVVVV").VVVV(CONTEXT2, String(x)); console.log("now x is => ", String(x)) if (result) { console.log("found result is => ", String(x)) break; } } }) } function main() { hookReturn() invoke() } setImmediate(main)
主动调用的参数构造两种方案:
1 2 cd ~/.pyenv tree -NCfhl | grep agent.js 加上构造函数hook,如com.kanxue.pediy1.MainActivity$1
第二题.apk jadx-gui 第二题.apk
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 function invoke2() { Java.perform(function () { // console.log("CONTEXT IS => ",CONTEXT) var MainActivity = null; Java.choose("com.kanxue.pediy1.MainActivity",{ onMatch:function(instance){ MainActivity = instance; }, onComplete:function(){} }) // var CONTEXT2 = Java.use("com.kanxue.pediy1.MainActivity$1").$new(MainActivity); var loader1 = null; var loader2 = null; Java.enumerateClassLoaders({ onMatch: function (loader) { try { if (loader.findClass("com.kanxue.pediy1.VVVVV")) { console.log("Successfully found loader") console.log(loader); // 切换classLoader loader2 = loader; Java.classFactory.loader = loader2; }else if(loader.findClass("com.kanxue.pediy1.MainActivity")){ console.log("Successfully found loader") console.log(loader); loader1 = loader; }else{ } } catch (error) { console.log("find error:" + error) } }, onComplete: function () { console.log("end1") } }) var javaString = Java.use("java.lang.String").$new("12345"); for (var x = 0; x < (99999 + 1); x++) { var result1 = MainActivity.stringFromJNI(String(100000 - x)); var result2 = Java.use("com.kanxue.pediy1.VVVVV").VVVV(String(result1)); console.log("now x is => ", String(x)) if (result2) { console.log("found result2 is => ", String(100000 - x)) break; } } }) }
frida -UF -l traceNativelibssl.js 拿到函数_ZTVNST3__19strstreamE
,修改if(exports[j].name.indexOf("ZTVNSt3")>=0){
,if(exports[j].name.indexOf("ZTVNSt3")>=0){
,
举杯邀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 // hook native函数,使其不要执行,Interceptor.replace function hookSTRSTR() { var kill_addr = Module.findExportByName("libc.so", "strcmp"); Interceptor.attach(strstr, { onEnter: function (args) { console.log("Entering =>") console.log("args[0] => ", args[0].readCString()) console.log("args[1] => ", args[1].readCString()) }, onLeave: function (retval) { } } )} function replaceKill(){ var kill_addr = Module.findExportByName("libc.so", "kill"); // var kill = new NativeFunction(kill_addr,"int",['int','int']); Interceptor.replace(kill_addr,new NativeCallback(function(arg0,arg1){ console.log("arg0=> ",arg0) console.log("arg1=> ",arg1) },"int",['int','int'])) }