1

[原创]Android通用脱壳机FUPK3

currwin 2018-8-7 16:52 8053

大家好,我是F8LEFT,潜水了这么久,抽空来发个技术帖子。这次要发的作品是我以前写的一个脱壳机FUPK3,这个脱壳机的思路应该是以前没人放过的,这里我不私藏了,放出来给大家来评评。
Android代码是开源的,那么通过直接修改Android源码,把运行时的所有dex数据dump出来,不就可以实现一个通用的脱壳机了吗?FUPk3就是基于上面的思路来实现的。FUPK3需要修改Android源码,导出数据接口.不过,最为核心的脱壳操作是在一个so中执行的,脱壳时应用会自动加载该so,这样好处在于可以通过替换这个so来达到动态修bug的效果。

  1. 代码注入
    程序启动后读取配置文件/data/local/tmp/FUpk3.txt,如果命中脱壳的配置就会直接加载so /data/local/tmp/libFupk3.so,进行脱壳。修改Android源码
    try {
     UpkConfig config = new UpkConfig();
     if (config.load() && config.mTargetPackage.equals(data.info.getPackageName())) {
         Fupk upk = new Fupk(config.mTargetPackage);
         upk.unpackAfter(10000);
     }
    } catch (Throwable t) {
    }
    
  2. cookie
    脱壳需要知道app当前加载的所有dex文件。每一个被加载到内存中的dex文件都会被记录为一个cookie值,存放在程序的loader中,所有类的加载都需要通过该loader进行。Java层的loader可能会比较复杂,比如会用到双亲委派机制等,不好遍历。但是,到了Jni层,一切都变得简单起来,在native中,会通过一个Hash表来存储所有dex文件。

    // 每个打开的dex文件都会存储到gDvm.userDexFiles中
    static void addToDexFileTable(DexOrJar* pDexOrJar) {
     u4 hash = (u4) pDexOrJar;
     void* result;
    
     dvmHashTableLock(gDvm.userDexFiles);
     result = dvmHashTableLookup(gDvm.userDexFiles, hash, pDexOrJar,
             hashcmpDexOrJar, true);
     dvmHashTableUnlock(gDvm.userDexFiles);
    
     if (result != pDexOrJar) {
         ALOGE("Pointer has already been added?");
         dvmAbort();
     }
    
     pDexOrJar->okayToFree = true;
    }
    

    因此,想要知道当前apk加载了那些dex就简单多了,只需要遍历这个userDexFile这个Hashtable就好了。直接在Android源码中导出接口。

    // @F8LEFT exported function
    HashTable* dvmGetUserDexFiles() {
     return gDvm.userDexFiles;
    }
    

    然后,直接在so里面调用接口,就可以取出所有dex文件的内存布局。

    auto fn = (HashTable* (*)())dlsym(libdvm, "dvmGetUserDexFiles");
    if (fn == nullptr) {
     goto bail;
    }
    gDvmUserDexFiles = fn();
    
  3. dex重建
    dex文件被加载起来后,一些值就不会再被用到,如文件头sig,数据偏移等,部分壳会把这些数据抹除。这些数据需要直接从应用的运行时数据 DvmDex, ClassObject, MethodObject等取出来,并进行重建。还有一些常量数据,StringPool,TypePool等,这些是不会加密的,直接dump出来。

    bool DexDumper::fixDexHeader() {
     DexFile *pDexFile = mDvmDex->pDexFile;
    
     mDexHeader.stringIdsOff = (u4) ((u1 *) pDexFile->pStringIds - (u1 *) pDexFile->pHeader);
     mDexHeader.typeIdsOff = (u4) ((u1 *) pDexFile->pTypeIds - (u1 *) pDexFile->pHeader);
     mDexHeader.fieldIdsOff = (u4) ((u1 *) pDexFile->pFieldIds - (u1 *) pDexFile->pHeader);
     mDexHeader.methodIdsOff = (u4) ((u1 *) pDexFile->pMethodIds - (u1 *) pDexFile->pHeader);
     mDexHeader.protoIdsOff = (u4) ((u1 *) pDexFile->pProtoIds - (u1 *) pDexFile->pHeader);
     mDexHeader.classDefsOff = (u4) ((u1 *) pDexFile->pClassDefs - (u1 *) pDexFile->pHeader);
     return true;
    }
    

    对于Method的重构要复杂得多,每一代加固的发展重点都是增强对Method的CodeItem的加密。从整体加密到函数抽取到最新的函数VMP,还原难度也越来越高。VMP暂且不说,函数抽取本身还是可以攻破的。有些壳做得非常复杂,会在函数执行时还原代码,执行完后又加密回去。针对这种情况,可以看到,在程序运行时的某一个时刻,dex的部分数据肯定是还原的,那么我们可以通过不断地手动构造代码,来遍历触发这些还原点,同时进行数据提取,最终完成修复。

// 从Android源码中导出方法
/* @F8LEFT
 * This method is used to export some data for fupk3 to dump dex file.
 * Fupk3 will hook this method and get data from it.
 */
void fupkInvokeMethod(Method* meth) {
    // it is no need to init or link class, the code of the method will
    // not exec actually, so just ignore it
    // anyway, I should make sure this method has code to execute
    if (dvmIsMirandaMethod(meth) || dvmIsAbstractMethod(meth)) {
        return;
    }
    dvmInvokeMethod((Object*)0xF88FF88F, meth, NULL, NULL, NULL, true);
}

bool fupkExportMethod(Thread* self, const Method* method) {
    return false;
}

FupkInterface gFupk = {
    NULL, NULL, NULL, NULL, 
    fupkExportMethod
};
// @F8LEFT add end
// 对解析器入口插桩
 void dvmInterpret(Thread* self, const Method* method, JValue* pResult)
 {
    // @F8LEFT insert point for Fupk
    if ((u4)pResult->i == 0xF88FF88F) {
        gFupk.ExportMethod(self, method);
        return;
    }
    // @F8LEFT add end
    ...
 }

在上面,直接在Android源码中构造了脱壳用到的接口 gFupk,它提供了4个void*的空间来存放临时数据,与一个可以替换的接口方法fupkExportMethod。这样,只需要手动调用fupkInvokeMethod方法,程序就会走函数正常的执行流程,从dvmInvokeMethod开始,进入dvmInterpret中,并最终调用完gFupk.ExportMethod后直接退出Interpret。这样当我们手动调用函数时,既可以让壳还原出原始代码,又不会影响到程序本身的稳定性。

// 在so中直接替换ExportMethod
auto interface = FupkImpl::gUpkInterface;
if (interface == nullptr) {
    FLOGE("Unable to found fupk interface");
    return;
}
// Hook all
interface->ExportMethod = fupk_ExportMethod;
// 传递参数,然后直接调用
gUpkInterface->reserved0 = &shared;
shared.mCurMethod = dexMethod;
FupkImpl::fupkInvokeMethod(m);
shared.mCurMethod = nullptr;
// 在ExportMethod中直接提取CodeItem数据
bool fupk_ExportMethod(void *thread, Method *method) {
    DexSharedData* shared = (DexSharedData*)gUpkInterface->reserved0;
    DexMethod* dexMethod = shared->mCurMethod;
    u4 ac = (method->accessFlags) & mask;
    if (method->insns == nullptr || ac & ACC_NATIVE) {
        if (ac & ACC_ABSTRACT) {
            ac = ac & ~ACC_NATIVE;
        }
        dexMethod->accessFlags = ac;
        dexMethod->codeOff = 0;
        return false;
    }

    if (ac != dexMethod->accessFlags) {
        dexMethod->accessFlags = ac;
    }
    dexMethod->codeOff = shared->total_point;
    DexCode *code = (DexCode*)((const u1*) method->insns - 16);

    u1 *item = (u1*) code;
    int code_item_len = 0;
    if (code->triesSize) {
        const u1*handler_data = dexGetCatchHandlerData(code);
        const u1 **phandler = (const u1**) &handler_data;
        u1 *tail = codeitem_end(phandler);
        code_item_len = (int)(tail - item);
    } else {
        code_item_len = 16 + code->insnsSize * 2;
    }
    shared->extra.append((char*)item, code_item_len);
    shared->total_point += code_item_len;
    while(shared->total_point & 3) {
        shared->extra.push_back(shared->padding);
        shared->total_point++;
    }
    return true;
}

这样,所有加密的数据都提取出来了,直接进行组合,以加密的方式dump出来。

size_t myfwrite(const void* buffer, size_t size, size_t count, FILE* stream) {
    char *tmp = new char[size * count];
    mymemcpy(tmp, buffer, size * count);
    for (size_t i = 0; i < size * count; ++i) {
        tmp[i] ^= encryptKey;
    }
    size_t rel = fwrite(tmp, size, count, stream);
    delete []tmp;
    return rel;
}
  1. dex修复
    上面dump下来的dex文件是非标准的,可能存在部分的class数据不合法,并且一些软件不认。所以需要一个写一个修复的server来跳过非法的数据。server主要修改了baksmali与smali的代码,自动跳过无法反编译的类。
  2. 其他
    Android加固发展到现在,经历了好几个大版本改动。最初的是dex整体加固,现在的是VMP加固,中间出了不少非常不错的脱壳机,其中最为经典的有2个:ZjDroid与dexHunter,这两个都是开源的,并且写得非常好,即使是放到今天来看,也具有相当的参考价值,想要学习脱壳的同学们可以拜读一下。另外,FUpk3是运行在dalvik上的,那么要在art下脱壳怎么办呢?道理还是一样的,只是art下会复杂很多, 跑解析的跑编译的都有,修复起来需要记录很多数据,这里由于某些原因就不公开art下的脱壳机了。

  3. 写在最后
    新手想要入门,需要什么?1. 一台谷歌亲儿子(nexus) 2. 一个Ubuntu系统 3. 一套完整的Android源码。平时有事没事可以多看看源码,刷刷机之类的。Android平台与Windows上的不同,到目前为止,Android上系统的优秀的教材实在是少,如果没人手把手地带入门的话,基本上就是在白做功,幸好大部分难点的解决方案答案都可以在源码里面找到,多熟悉一下总会有好处的。

7.https://github.com/F8LEFT/FUPK3
看雪上的md插件怪怪的,大家去我的github上看吧,欢迎各种star与follow,演示视频在这里 https://pan.baidu.com/s/1HH_-TQGca1NLoSqzvOPB3Q 密码:izm3

 

最后,美团招人啦,想学习业界最新的技术吗,想认识业界顶级大牛吗,快来加入我们吧。详情看地址http://www.expshell.com/?p=114



快讯:[看雪招聘]十八年来,看雪平台输出了大量安全人才,影响三代安全人才!

最后于 2018-8-8 19:52 被currwin编辑 ,原因:
本主题帖已收到 1 次赞赏,累计¥5.00
最新回复 (89)
jhyrathon 2018-8-7 16:54
2
厉害了大佬
wx_秋木 2018-8-7 16:54
3
老师  我第一
Imyang 2018-8-7 16:55
4
F8LEFT大佬 666
无名侠 2018-8-7 16:57
5
支持支持
mingxuan三千 2018-8-7 16:58
6
F8LEFT大佬 666 
yikuaiyingbi 2018-8-7 16:59
7
前排膜拜。另外.大佬艹粉吗
deadxing 2018-8-7 17:00
8
膜f8大佬
koflfy 2018-8-7 17:00
9
mark,跟F8大佬学习
鬼谷子c 2018-8-7 17:02
10
友情暖贴。
skyun 2018-8-7 17:03
11
支持支持
FIGHTING安 2018-8-7 17:04
12
git上没有内容额,是删了吗
小强! 2018-8-7 17:09
13
膜拜
默NJ 2018-8-7 17:22
14
666666666大佬
iamyrc 2018-8-7 17:24
15
gayhub是空的呀,已follow
春辉 2018-8-7 17:25
16
前排围观大神
junkboy 2018-8-7 17:26
17
支持~
hxdqq 2018-8-7 17:26
18
膜拜大佬
Lucaks 2018-8-7 17:37
19
膜拜
猫子 2018-8-7 17:40
20
膜拜F8大佬
猫子 2018-8-7 17:46
21
FIGHTING安 git上没有内容额,是删了吗
晚上放,大佬写的很清楚了
sqdebug 2018-8-7 17:52
22
大佬,膜拜
BinGzL 2018-8-7 17:56
23
我们招人是认真的,面试成功将与我们共事
StriveXjun 2018-8-7 18:02
24
膜拜大佬!!!  
tinxi 2018-8-7 18:36
25
牛逼F8老师
言肆คิดถึง 2018-8-7 18:39
26
厉害啦!
欧阳峰峰 2018-8-7 18:55
27
膜拜大佬666
wx_依然范特西 2018-8-7 19:04
28
楼上小姐姐要给你生猴子
wx_La0s 2018-8-7 19:50
29
学习!
xianhuimin 2018-8-7 23:28
30
膜拜F8老师
重庆rcbing 2018-8-8 01:34
31
大佬,膜拜,非常虔诚的那种
wulovebo 2018-8-8 09:29
32
取名FUPK3 什么意思?什么的缩写吗?
chmlqw 2018-8-8 09:42
33
围观学习。。!
wulovebo 2018-8-8 10:19
34
unpacker v3 by F8LEFT    =   FUPK3     哈哈哈
Ddddz 2018-8-8 10:33
35
大佬就是大佬
sogohere 2018-8-8 10:45
36
牛逼!万能脱壳神器
kiyaa 2018-8-8 11:39
37
请问这个错误我该从哪里入手, 好像是一个 so 被两次加载了
W/dalvikvm(9736): Shared lib '/data/local/tmp/libFupk3.so' already opened by CL 0x416e7500; can't open in 0x0
W/dalvikvm(9736): Exception Ljava/lang/UnsatisfiedLinkError; thrown while initializing Landroid/app/fupk3/Fupk;
W/dalvikvm(9661): No implementation found for native Lf8left/fupk3/core/Fupk;.unpackAll:(Ljava/lang/String;)V
W/dalvikvm(9661): threadid=11: thread exiting with uncaught exception

最后于 2018-8-8 11:39 被kiyaa编辑 ,原因:
wulovebo 2018-8-8 11:59
38
日志显示:release target so file failed







最后于 2018-8-8 12:00 被wulovebo编辑 ,原因:
kiyaa 2018-8-8 12:03
39
wulovebo 日志显示:release&nbsp;target&nbsp;so&nbsp;file&nbsp;failed
你应该打开全部日志看报错信息, 是不是没权限?
wulovebo 2018-8-8 14:32
40
kiyaa 你应该打开全部日志看报错信息, 是不是没权限?
确实是文件夹权限问题,修改了data/local/tmp就好了, 但是又遇到了[libsuperuser][O][SU*] su: uid 10053 not allowed to su , 但是我用adb shell 可以运行su的呀,奇怪!!!


kiyaa 2018-8-8 14:49
41
wulovebo 确实是文件夹权限问题,修改了data/local/tmp就好了,&nbsp;但是又遇到了[libsuperuser][O][SU*] su: uid 10053 not allowed to ...
打开 supersu 看看授权情况, 10053 是 FUPK3  ?
wulovebo 2018-8-8 14:56
42
kiyaa 打开 supersu 看看授权情况, 10053 是 FUPK3 ?
嗯  我现在重新刷了个supersu进去 没有出现刚才问题  现在尝试一下脱一个样本, 你测试成功了吗?
wulovebo 2018-8-8 15:11
43
wulovebo 嗯 我现在重新刷了个supersu进去 没有出现刚才问题 现在尝试一下脱一个样本, 你测试成功了吗?[em_1]
尝试脱一下【惠头条】,  日志提示 没有 information.json


zzcc 2018-8-8 15:55
44
支持一下。
厉害。
kiyaa 2018-8-8 17:23
45
wulovebo 尝试脱一下【惠头条】,&nbsp; 日志提示 没有 information.json
那你应该也有我上面那个错误吧?
currwin 2018-8-8 19:55
46
wulovebo 尝试脱一下【惠头条】,&nbsp; 日志提示 没有 information.json
更新了演示视频,看一下吧
yidia 2018-8-8 22:50
47
大佬,招聘信息还是认真有效的吗?
Umiade 2018-8-9 10:00
48
支持支持,感谢分享
十八垧 2018-8-9 10:10
49
有的函数抽取加固,是在原函数中插入一些还原相关的新指令,要真正执行时才能真正还原的吧。这种是不是不能拖。膜楼主,找时间拜读一下代码。DexHunter确实不错,给大家提供了一种脱壳思路,楼主在DexHunter基础上,增加了对每个method的调用,在DexHunter的文档里也提到了。
最后于 2018-8-9 10:24 被十八垧编辑 ,原因: 重新编辑
currwin 2018-8-9 11:38
50
十八垧 有的函数抽取加固,是在原函数中插入一些还原相关的新指令,要真正执行时才能真正还原的吧。这种是不是不能拖。膜楼主,找时间拜读一下代码。DexHunter确实不错,给大家提供了一种脱壳思路,楼主在DexH ...
对哦,这种太少见我都忘了。这个需要预读取一条指令,判断是否是壳的解密代码,如果是的话需要执行完再dump method。改一下代码就可以搞定。
返回