目录

脱壳

寻找特征& 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哪个库我引用一直有问题,就把那段代码注释掉了

浙公网安备33011302000604

辽ICP备20003309号