目录
脱壳
寻找特征& frida hook
过frida检测
修复dex
重打包
修改smail
去签名校验
文章同步发表在看雪上
新版某数字壳脱壳,过frida检测,及重打包https://bbs.kanxue.com/thread-282858.htm
正文
大家好,我是小生,这次的app是一个国内某计划app, 功能相当全,界面也很美观,很实用,这个app我很欣赏。总共花了有三天晚上加一个白天,抓包分析,脱壳,过检测,手撕smail, 调试等, 做开发好久了,逆向有段时间没有接触了,很生疏了
就是会员太贵了,终身会员300多嘞!
【为该公司的权益考虑,不提供成品,也不提供app相关 信息】
(现在大大小小的app全都加壳,甚至一些颜色灰产的也加国内的这些壳!!! 动不动就抽取,dex2c,都不能愉快的好好玩耍了)
脱壳
还有asserts目录下的 libjiagu.so 就知道是数字壳无疑了!
apktool解包,发现6月份的新版数字加固
脱壳用的fart改的脱壳机,详细过程就不赘述了
总共脱下来21个dex,一个个先脱进jadx中看看是否都是有用的,发现有两个全是壳相关的,剩下了19个
发现脱下来还是相对较完整的,里面也有损坏的部分,但影响不太大,
寻找特征& frida hook
因为这次我需要的是里面的vip功能,按照惯例先搜isVip等字样
发现搜出来很多结果,不影响,排除掉本app的广告sdk和依赖的库,一个个看,看和用户相关的,发现两个类都是相关的
直接写hook,
只截取部分,
然后 frida启动!
我是先attach启动的,发现会闪退
换成去掉部分特征的strongr frida发现还是如此,
1.我又尝试了换端口,span启动,
2.hook libc.so中的 strstr,strcmp来去掉内存里的frida,gmain,gdbus等字样
3.hook 重定向/proc/xxx/maps
4.hook libc.so的exit
5.hook android.os.Process的killProcess
再配合上常用的几个过检测脚本还是一样闪退,感觉事情不简单了
过frida检测
提前说一嘴,这个frida检测不是在壳里,是在app的so里,还有hook这个业务代码要延迟一段时间执行,不然classloader还没有加载相关类。
既然不是在java层,那就是在native层检测的了,通常是hook android_dlopen_ext,观察加载到哪个so的时候退出就可以定位到了,
function hook_dlopenAndExt() {
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
//console.log("dlopen:", path);
// if (path.indexOf("libart.so") >= 0) {
// // this.can_hook_libart = true;
// console.log("[dlopen:]", path);
// }
console.log("load " + path);
}
},
onLeave: function (retval) {
// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
})
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("android_dlopen_ext:", path);
// if (path.indexOf("libart.so") >= 0) {
// // this.can_hook_libart = true;
// console.log("[android_dlopen_ext:]", path);
// }
console.log("load " + path);
}
},
onLeave: function (retval) {
// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
});
}
可以定位到是在libmxxxdesc.so中
然后hook pthread_create函数,尝试找到来自libmxxxdesc.so创建的检测线程
然后就一直卡在那了,一直也找不到来自该so的创建线程的调用,
下面的部分借鉴看雪的看雪bilibili frida过检测
把so放进ida中也没有发现有创建线程的导入符号
尝试从更早的时机,通过hook dlsym函数来看是否有通过dlsym来获取pthread_create地址来进行调用
发现确实调用了创建线程的函数,只不过不是直接调用,而是采用通过dlsym获取地址再调用
下面采用创建一个虚假的创建函数的地址返回,来欺骗目标so(还是来源于看雪bilibili frida过检测的思路和代码)
function create_fake_pthread_create() {
const fake_pthread_create = Memory.alloc(4096)
Memory.protect(fake_pthread_create, 4096, "rwx")
Memory.patchCode(fake_pthread_create, 4096, code => {
const cw = new Arm64Writer(code, { pc: ptr(fake_pthread_create) })
cw.putRet()
})
return fake_pthread_create
}
function hook_dlsym() {
var count = 0
console.log("=== HOOKING dlsym ===")
var interceptor = Interceptor.attach(Module.findExportByName(null, "dlsym"),
{
onEnter: function (args) {
const name = ptr(args[1]).readCString()
console.log("[dlsym]", name)
if (name == "pthread_create") {
count++
}
},
onLeave: function(retval) {
if (count == 1) {
retval.replace(fake_pthread_create)
}
else if (count == 2) {
retval.replace(fake_pthread_create)
// 完成2次替换, 停止hook dlsym
interceptor.detach()
}
}
}
)
return Interceptor
}
function hook_dlopen() {
var interceptor = 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)
if (path.indexOf("libmxxxxxec.so") > -1) {
hook_dlsym()
}
}
}
}
)
return interceptor
}
// 创建虚假pthread_create
var fake_pthread_create = create_fake_pthread_create()
var dlopen_interceptor = hook_dlopen()
就过掉了检测
修复dex
通过hook关键的函数发现确实可以达到付费vip的效果,但是部分界面显示的vip样式还是有点问题,
我逐个把dex脱进jadx中,进行查看,去除掉没用的dex, 发现可以去除掉两个全是数字壳的特征dex,
1.然后使用MT管理器把这21个dex替换了原来的dex
2.然后把asserts文件夹中的libjiagu.so 那四个数字壳的so文件删掉
3.然后把AndroidMinfest.xml中原来的com.stub.StubApp
为程序真正的入口com.xxxxxx
这个app是真的大,光androidMinfest文件就干出去将近5000行!!(后面改smail的时候很痛苦)
重打包
然后重打包编译,进行jarsinger签名,一气呵成,安装,闪退! 漂亮!
我一开始以为是不是有签名验证啊,我就再jadx中进行搜素packagemanager相关的,但都关系不太大,最后发现是脱壳还有数字的残留特征,
就是下面这种效果,1000多条!
invoke-static {p0, p1, p2}, Lcom/stub/StubApp;->interface24(Landroid/app/Activity;[Ljava/lang/String;I)V
可以用正则的方式匹配替换掉,这里很麻烦,我替换了整整有半个多小时,各种各样的,真恶心!
这里我是使用 一键正则 工具走捷径了(尽管这样,也很慢)
下面这两个可以通用替换掉一些,但还是会有很多很多很多漏网之鱼
invoke-static/range \{.* \.\. .*\}, Lcom/stub/StubApp;->getOrigApplicationContext\(Landroid/content/Context;\)Landroid/content/Context;\s+move-result-object .*
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
其实这个app算我运气好,onCreate函数没有被抽取掉,很赏脸了!
再都完成替换之后,确认没有stubapp, stub/stub等数字壳特征之后,再进行重打包,签名,发现可以打开了,我测试了一下,里面有两个子页面有点问题,打开会闪退,不过我会用到的页面都正常,(这个app大大小小加起来有62个页面,那两个无所谓)
修改smail
首先声明一下,我不会smail(以下纯现学现用,所以看着像屎一样很正常)
这一步就没有什么技术含量了,(对于我这种小卡拉米以及 这种简单的app而言),主要是耐心和细心,
这里我是采用mt管理器来进行编辑的,不得不说,确实很方便,但是改smail也很麻烦,要操作寄存器,改完还不知道,只能重打包后安装才能验证出来,一不小心改错就会闪退,前文说到有两个相关的类,一个有get set方法,很好处理,get的话直接
const/4 v1, 0x1
然后return 或者赋值都可以,
public class Uxxxxfo implements Serializable {
public List<AdX> adxList;
public boolean axxxxeVip;
public String alxxxcon;
public String axxxge;
public CheckFreeVipInfo cxxxnfo;
public boolean evxxp;
public int exxxxay;
public String id;
public boolean isPoxxxp;
public boolean isxxxit;
public boolean isVip;
......
......
还有一个bean全是public字段,没有get set方法,而且引用的地方相当多,我没有办法在构造函数中进行赋值,因为后续会被覆盖掉,这里我有想到用抓包改包的方式,我在有root和xposed的测试机上试验过,没问题,但我想在没有root和xposed的环境使用,这种方案显然不可行
我只能在每一个用到的地方都进行修改,比如
Lcom/dxxxx/lxxxxon/model/Uxxxxfo;->id:Ljava/lang/String;
check-cast v2, Ljava/lang/CharSequence;
invoke-static {v2}, Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z
move-result v2
const-string v8, "mContext"
if-nez v2, :cond_218
iget-boolean v2, v1, Lcom/xxx/xxxxx/model/Usxxxxxfo;->vstate:Z
if-nez v2, :cond_7f
goto/16 :goto_218
我要保证vstate一直为true
我改成下面的
const/4 v2, 0x1 # 将常量 1(true)存储到寄存器 v2
iput-boolean v2, v1, Lcom/xxx/xxxxx/model/Usxxxxxfo;->vstate:Z
iget-boolean v2, v1, Lcom/xxx/xxxxx/model/Usxxxxxfo;->vstate:Z
if-nez v2, :cond_7f
放一张成品吧
后记
之前一直采用的charles+postern方式抓包,用花哥的话说,走socket,靠近底层,能获取更多的上层流量
现在我改成了 Reqable小黄鸟来抓包,头一次用,挺方便的,也是要root,这个没得跑,
关于证书安装的问题,安卓7以后要手动remount,把证书移动到/system/etc/security/cacerts目录下
我试了好几次,小黄鸟都识别不到证书已安装,尽管64xxxk.0已经在系统证书目录中,后来我尝试移除charles证书,试了两次,重启过后可以了! 至于没有网络或者其他的问题导致无法安装证书,我的博客里有记载。
总结
敢于尝试,就有成功的可能
代码
文中所用到的部分过检测代码
function loadGson() {
Java.openClassFile("/data/local/tmp/xiaosheng-dex-tool.dex").load();
var js = Java.use("com.xiaosheng.tool.json.Gson");
var gson = js.$new();
return gson;
}
function hook_dlopen_ext() {
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_dlopenAndExt() {
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
//console.log("dlopen:", path);
// if (path.indexOf("libart.so") >= 0) {
// // this.can_hook_libart = true;
// console.log("[dlopen:]", path);
// }
console.log("load " + path);
}
},
onLeave: function (retval) {
// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
})
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("android_dlopen_ext:", path);
// if (path.indexOf("libart.so") >= 0) {
// // this.can_hook_libart = true;
// console.log("[android_dlopen_ext:]", path);
// }
console.log("load " + path);
}
},
onLeave: function (retval) {
// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
});
}
function hook_open() {
var pth = Module.findExportByName(null, "open");
Interceptor.attach(ptr(pth), {
onEnter: function (args) {
this.filename = args[0];
console.log("", this.filename.readCString())
if (this.filename.readCString().indexOf(".so") != -1) {
args[0] = ptr(0)
}
}, onLeave: function (retval) {
return retval;
}
})
}
function hookProcess() {
var process = Java.use("android.os.Process");
process.killProcess.implementation = function (pid) {
console.log("kill process:" + pid)
}
}
function hookExit() {
var ByPassTracerPid = function () {
var fgetsPtr = Module.findExportByName("libc.so", "fgets");
var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);
Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {
var retval = fgets(buffer, size, fp);
var bufstr = Memory.readUtf8String(buffer);
if (bufstr.indexOf("TracerPid:") > -1) {
Memory.writeUtf8String(buffer, "TracerPid:\t0");
console.log("tracerpid replaced: " + Memory.readUtf8String(buffer));
}
return retval;
}, 'pointer', ['pointer', 'int', 'pointer']));
};
}
function hook_Pthreadfunc() {
var pthread_creat_addr = Module.findExportByName("libc.so", "pthread_create")
Interceptor.attach(pthread_creat_addr, {
onEnter(args) {
console.log("call pthread_create...")
let func_addr = args[2]
console.log("The thread function address is " + func_addr)
try {
console.log('pthread_create called from:\n'
+ Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
.join('\n')
+ '\n');
} catch (e) {
}
}
})
}
function hookBaseExit() {
function main() {
const openPtr = Module.getExportByName('libc.so', 'open');
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var readPtr = Module.findExportByName("libc.so", "read");
var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
// var fakePath = "/sdcard/app/maps/maps";
var fakePath = "/data/local/tmp/fakeMap";
var file = new File(fakePath, "w");
var buffer = Memory.alloc(512);
Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
var pathname = Memory.readUtf8String(pathnameptr);
var realFd = open(pathnameptr, flag);
if (pathname.indexOf("maps") != 0) {
while (parseInt(read(realFd, buffer, 512)) !== 0) {
var oneLine = Memory.readCString(buffer);
if (oneLine.indexOf("tmp") === -1) {
file.write(oneLine);
}
}
var filename = Memory.allocUtf8String(fakePath);
return open(filename, flag);
}
var fd = open(pathnameptr, flag);
return fd;
}, 'int', ['pointer', 'int']));
}
setImmediate(main)
}
function replace_str() {
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
Interceptor.attach(pt_strstr, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1 ||
str2.indexOf("linjector") !== -1) {
// console.log("strcmp-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
});
Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1 ||
str2.indexOf("linjector") !== -1) {
// console.log("strcmp-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
})
}
// 定义一个函数anti_maps,用于阻止特定字符串的搜索匹配,避免检测到敏感内容如"Frida"或"REJECT"
function anti_maps() {
// 查找libc.so库中strstr函数的地址,strstr用于查找字符串中首次出现指定字符序列的位置
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
// 查找libc.so库中strcmp函数的地址,strcmp用于比较两个字符串
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
// 使用Interceptor模块附加到strstr函数上,拦截并修改其行为
Interceptor.attach(pt_strstr, {
// 在strstr函数调用前执行的回调
onEnter: function (args) {
// 读取strstr的第一个参数(源字符串)和第二个参数(要查找的子字符串)
var str1 = args[0].readCString();
var str2 = args[1].readCString();
// 检查子字符串是否包含"REJECT"或"frida",如果包含则设置hook标志为true
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
// 在strstr函数调用后执行的回调
onLeave: function (retval) {
// 如果之前设置了hook标志,则将strstr的结果替换为0(表示未找到),从而隐藏敏感信息
if (this.hook) {
retval.replace(0);
}
}
});
// 对strcmp函数做类似的处理,防止通过字符串比较检测敏感信息
Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
onLeave: function (retval) {
if (this.hook) {
// strcmp返回值为0表示两个字符串相等,这里同样替换为0以避免匹配成功
retval.replace(0);
}
}
});
}
const STD_STRING_SIZE = 3 * Process.pointerSize;
class StdString {
constructor() {
this.handle = Memory.alloc(STD_STRING_SIZE);
}
dispose() {
const [data, isTiny] = this._getData();
if (!isTiny) {
Java.api.$delete(data);
}
}
disposeToString() {
const result = this.toString();
this.dispose();
return result;
}
toString() {
const [data] = this._getData();
return data.readUtf8String();
}
_getData() {
const str = this.handle;
const isTiny = (str.readU8() & 1) === 0;
const data = isTiny ? str.add(1) : str.add(2 * Process.pointerSize).readPointer();
return [data, isTiny];
}
}
function prettyMethod(method_id, withSignature) {
const result = new StdString();
Java.api['art::ArtMethod::PrettyMethod'](result, method_id, withSignature ? 1 : 0);
return result.disposeToString();
}
function hook_libc_exit() {
var exit = Module.findExportByName("libc.so", "exit");
console.log("native:" + exit);
Interceptor.attach(exit, {
onEnter: function (args) {
try {
console.log(Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join("\n"));
} catch (e) {
console.log(e)
}
},
onLeave: function (retval) {
//send("gifcore so result value: "+retval);
}
});
}
function anti_exit() {
const exit_ptr = Module.findExportByName(null, '_exit');
// DMLog.i('anti_exit', "exit_ptr : " + exit_ptr);
console.log("anti_kill, kill_ptr:" + exit_ptr)
if (null == exit_ptr) {
return;
}
Interceptor.replace(exit_ptr, new NativeCallback(function (code) {
if (null == this) {
return 0;
}
// var lr = FCCommon.getLR(this.context);
// DMLog.i('exit debug', 'entry, lr: ' + lr);
console.log("kill debug entry,lr")
return 0;
}, 'int', ['int', 'int']));
}
function anti_kill() {
const kill_ptr = Module.findExportByName(null, 'kill');
// DMLog.i('anti_kill', "kill_ptr : " + kill_ptr);
console.log("anti_kill, kill_ptr:" + kill_ptr)
if (null == kill_ptr) {
return;
}
Interceptor.replace(kill_ptr, new NativeCallback(function (ptid, code) {
if (null == this) {
return 0;
}
// var lr = FCCommon.getLR(this.context);
// DMLog.i('kill debug', 'entry, lr: ' + lr);
console.log("kill debug entry,lr")
// FCAnd.showNativeStacks(this.context);
return 0;
}, 'int', ['int', 'int']));
}
// FCCommon哪个库我引用一直有问题,就把那段代码注释掉了