首页
社区
课程
招聘
[原创]FartExt之优化更深主动调用的FART10
发表于: 2021-8-5 11:09 68780

[原创]FartExt之优化更深主动调用的FART10

2021-8-5 11:09
68780

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

以及看雪高研3W班课程看完后的整理与优化

寒冰大佬的FART带动了不少新的主动调用思想的抽取壳方案。看了上面这篇文章,感觉意犹未尽,当然是要动手实践一翻来优化一波。于是我重新翻阅FART和Youpk的源码,我准备和那位大佬一样,参考Youpk在FART的基础上进行升级改造。开工前先明确出我的需求。

还未了解过的请看原作者对于fart的介绍

FART:ART环境下基于主动调用的自动化脱壳方案

FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法

拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点

关于fart源码的调用流程可以看看我以前整理的一篇文章

fart的理解和分析过程

简单总结:

这是一个基于主动调用来脱抽取壳的方案。

简述实现原理:

在进程启动的时候通过双亲委派机制遍历所有classloader,然后遍历里面的所有class,取出所有函数,直接调用。然后在ArtMethod的Invoke函数这里根据参数判断出这是主动调用触发的,然后就取消函数的正常执行,并执行脱壳操作。

首先看看FART源码中,脱壳线程的入口点ActivityThread.java的performLaunchActivity这个函数中开始的FART处理。

也就是说,所有的进程都会执行脱壳的流程。所以这个地方我觉得还是有必要优化的。应该是对指定的进程脱壳会更加好一些。Youpk中已经有了这个优化。所以我直接拿Youpk的处理来使用。下面是修改后的入口启动部分

这里主要是做了两点修改:

1、把启动FART线程的处理放到了handleBindApplication函数,这是因为performLaunchActivity这个函数调用有的时候可能会触发两次,而handleBindApplication确定只会触发一次。

2、FART线程启动前加了个判断,在配置文件中的进程才需要脱壳。这样基本第一个优化就完成了。

由于应用的有些类可能是被特殊处理了,主动调用的情况会导致程序崩溃或者退出。所以最好是可以单独对某些类进行主动调用。我调整了FART线程启动的逻辑,先是上面的判断是不是要脱壳的目标进程,然后判断有没有设定类列表,如果有类列表就只脱壳类列表,否则就完整主动调用

直接将FART修改的代码部分直接替换到AOSP10中。毫不意外的出现了一堆错误。不过问题比较集中。主要是对于CodeItem的成员访问方式发生了变化。这里可以参考下面的文章

Android ART 虚拟机 - dex 文件格式要旨

根据这篇文章中对CodeItem对象新的访问方式。对FART的源码部分做出修改

修改文件是art_method.cc。我这里只贴上部分关键修改的代码

需要修改的不止上面这几个地方。但是主要都是针对CodeItem的使用以及命名空间的修改。这里我就不全部贴出了。最后我会贴出改完后的版本。

由于FART的知名度还是挺高的,所以最好还是把FART中特有的一些函数名和文件保存路径给修改一下。下面整理下我参考Youpk做的一些修改。

1、将ActivityThread中的FART相关函数全部单独放到一个类cn.mik.Fartext中。这样如果别人对ActivityThread的函数检测就找不到FART相关的了。

2、将DexFile中的dumpMethodCode函数名修改为fartextMethodCode

3、将myfartInvoke函数名改成fartextInvoke

4、将所有使用/sdcard/fart的这个路径全部修改成/sdcard/fext

把这些常见的可能识别的方式都修改之后。一般就识别不出来了。我这种完全没知名度的,想必不会被人检测到了。暗自欣喜。

抽取壳的应用在脱壳后,有两种文件,一个是在当前时机dump出来的dex文件。另一个是保存codeitem出来的bin文件。

FART的修复组件是使用开源项目FART中那个py的脚本来解析dex文件,将bin的codeitem修复打印。对于里面的代码解析部分我之前也写了文章。感兴趣可以看看

dex起步探索

但是我仔细研究后,发现修复组件只进行了打印。并没有修复成dex,而是直接解析打印。最理想的还是修复到dex,方便使用静态分析工具查看。有大佬也已经写了这个工具。那就是前面参考的Youpk。他的内部有个dexfixer的目录,就是实现了对导出的codeitem数据修复到dex中。不过他的codeitem的保存结构和FART的并不大一样。不过没关系,修改一下codeitem文件的解析部分就好了。下面贴上Youpk和我修改后专门处理fart结果的dexfixer。

Youpk

dexfixer

首先当然是看看FART的主动调用的深度是在哪里,这里的深度其实就是在函数的主动执行过程中,FART是在执行到哪个流程时,进行的脱壳处理。下面贴上FART主动调用的脱壳位置

上面可以看到。当函数执行流程到达ArtMethod::Invoke时,就根据参数判断是主动调用的情况,就脱壳并结束了。

一般的函数抽取壳,在执行到ArtMethod::Invoke前就已经对抽取函数还原了。但是也有一些抽取壳,执行到Invoke时依然还没有还原函数。譬如下面这种抽取壳。

可以看到这个抽取的函数,进来之后就goto,然后执行invoke-static。接着在goto到函数的开始位置。

也就是说。这个抽取壳,必须在函数执行了之后,才会还原出真实的函数。回想一下前面说的FART的主动调用深度。发现函数真正执行前就已经被我们直接结束掉了。所以我们需要更深的主动调用才能够解决这个抽取壳。

我们回头看看上面的抽取壳,我们的目标是要判断如果这个函数的第一个指令是goto,就正常执行,然后执行到invoke-static的指令。这个指令完成之后就直接结束掉函数调用。避免真实函数调用会出现异常。

先参考Youpk的看看他是如何实现更深的主动调用来解决这个问题的。下面是第一步,先修改默认的解释器为Switch的解释器。这是因为Switch解释器的可读性更加高,方便我们直接修改源码来达到目的。

然后我们看看主动调用时Youpk是怎么模拟参数的。

这里可以看到Youpk的参数是模拟赋值进去的。而寒冰大佬的做法不大一样。看看FART的函数调用模拟。

这样肯定没法顺利往后执行。我们先继续参考Youpk的后续。

然后看看Youpk的ArtMethod::Invoke的处理,如果是主动调用并且非Native函数就正常执行。

接下来看解释器的EnterInterpreterFromInvoke函数处理。这里Youpk没有什么处理。

继续看看函数Execute。

然后这ExecuteSwitchImpl就是关键的解释指令的函数了。到这里终于有Youpk修改的部分了。先看看修改的代码

PREAMBLE这个函数基本每个指令执行前都会调用beforeInstructionExecute来判断下。如果这里dump脱壳了,就直接结束掉,这个函数不再往下执行了。如果是上面那种特殊壳,这里就可以暂时先不要dump。让他正常执行先。下面看看里面的逻辑处理

这里可以看到。如果是INVOKE_STATIC就让指令正常执行。其他正常的抽取壳的深度就是在这里。这相当于就是指令执行前进行dump了。但是这里依然没解决特殊壳的深度问题。必须执行完INVOKE_STATIC之后。再进行脱壳并结束掉函数。继续看Youpk下面的处理

这里就看到每个指令都执行了PREAMBLE函数。然后每个指令执行完都执行了afterInstructionExecute这个函数。在这里就可以判断,如果执行完的指令是INVOKE_STATIC。就可以直接return结束掉函数执行了。看看Youpk的处理

这里留意了一下。这个函数固定返回的false。但是通过设置enableFakeInvoke和disableRealInvoke来控制下一个指令执行的时候来进行退出函数。我感觉这里退出应该也没啥问题。

到这里基本就走完大致的流程了。那么欣赏完别人的代码。可以开始我们的改造工作了。

和Youpk一样。第一步就是先把解释器给改成使用Switch解释器。但是由于我使用的是AOSP10。所以发现修改部分果然不大一样了。

发现这里变成可以通过编译参数来控制的了。搜索一下ART_USE_CXX_INTERPRETER的使用

发现这个好像可以通过cflags来配置了。所以我修改了下runtime下的Android.pb。如果不想改全局的。也可以在源码里面直接判断是主动调用就强制走switch解释器。

接着就是ArtMethod::Invoke的时候不要直接结束了。但是这里我们需要留意的是。第一个参数的Thread是fart用来判断是否为主动调用的。为了让后面能正常执行,我就直接把第一个参数给赋值了。而后面的调用流程也是需要判断当前执行函数是否为主动调用。Youpk是用线程和一个变量来控制判断是否为主动调用的。这里使用result=111111在后续判断是否为主动调用

这里有个问题是上面这种模拟参数的方式,碰到引用类型的参数会报错。所以在处理参数入栈的时候,也要进行判断处理一下。

接下来就开始修改解释器部分的逻辑了。我们只要做到几点处理。就可以搞定这种壳了。

1、如果是主动调用并且第一个指令如果不是GOTO的。就直接脱壳并结束

2、如果是主动调用并且第一个指令是GOTO的。让他继续执行

3、如果第三个指令是INVOKE-STATIC的执行完后直接结束掉

接下来准备改代码。然后碰到一个问题。同样也是AOSP10的版本导致的。Switch解释器的逻辑发生了较大的变动。先看看变成了啥样子

看到了这两个部分都发生了较大的变化。那个超大的case都不见了。不过也只是处理的方式发生变化。我们跟着调整下就行了。

在测试FART的主动调用中发现,主动调用的耗时较长,根据上面的流程图。我们可以看到调用最耗时最核心的函数dumpArtMethod。就是在这里进行脱壳的。先看看FART里面做了什么

这里可以看到dump的规则是相同大小的dex就跳过。非相同的就写入文件保存。相当于算是一个整体脱壳非常晚的时机。不过这个时机的调用频率较多。相对会影响性能,好处是这个整体脱壳点的时机够晚,绝对能脱掉除抽取函数外的整体壳。而Youpk中的优化是不使用这个整体脱壳点,只单纯的把codeitem写入bin文件,这样就能提高一定的效率。这里我就暂时不修改了。毕竟在这里脱整体壳也有一定的优势。如果想要更快的速度。也可以选择过滤主动调用的范围,来降低调用频率。

编译完成之后,我们就可以来试试深度主动调用+dex修复的效果。为了防止风险,就不放测试的apk样本了。

安装好apk后,先去/data/local/tmp/fext.config中填入我们的目标进程。如果主动调用出现崩溃的情况。可以将class.txt的文件复制到/data/local/tmp/进程名称 来对指定的类进行主动调用。然后打开应用静静的等待脱壳结果。

或者是使用整合怪来对指定类进行处理

Ps1:如果第二次打开应用发现没有触发主动调用,请清理应用:adb shell pm clear packageName

Ps2:如果不想等待60秒,想自己触发fart的主动调用。可以使用frida扩展

Ps3:如果想看logcat日志。搜索fartext即可,日志统一都添加了这个头部。方便查日志。

修复前的数据

使用前面我修改的修复工具,用下面的命令来修复

java -jar dexfixer.jar dexpath binpath outpath

或者是使用我整合怪的工具来修复

修复后的函数结果如下

可以结合frida来直接调用FART中准备的函数来对单个类或者类列表进行脱壳。

同时我的整合怪里面也添加了对我这个rom的主动调用和类列表主动调用支持

整个流程梳理完成后,我们可以由此来借鉴来思考延伸一下。

比如,包装一些属于自己的系统层api调用。便于我们使用xposed或者是frida来调用一些功能。

再比如,加载应用时,读取配置文件作为开关,我们来对网络流量进行拦截写入保存,或者对所有的jni函数调用,或者是java函数调用进行trace。这种就属于是rom级别的打桩。

再比如,可以做一个应用来读写作为开关的配置文件,而rom读取配置文件后,对一些流程进行调整。例如控制FART是否使用更深调用。控制是否开启rom级别的打桩。

以上纯属个人瞎想。刚刚入门,想的有点多,以后了解更深了,我再看看如何定制一个专属的rom逆向集合

整理一下前面所有的资料。

FART:ART环境下基于主动调用的自动化脱壳方案

FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法

拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点

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

fart的理解和分析过程

Android ART 虚拟机 - dex 文件格式要旨

dex起步探索

FART

Youpk

dexfixer

fridaUiTools

因刚梳理完,难免有些纰漏。我会慢慢修补的。

FartExt因为特殊原因延时开源。感兴趣的可以参考文章自己整整先。

 
 
 
 
 
 
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        fartthread();
              ...
    }
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        fartthread();
              ...
    }
//判断这个进程是否应该脱壳
    public static boolean shouldUnpack() {
    boolean should_unpack = false;
    String processName = ActivityThread.currentProcessName();
    BufferedReader br = null;
    String configPath="/data/local/tmp/fext.config";
    try {
        br = new BufferedReader(new FileReader(configPath));
        String line;
        while ((line = br.readLine()) != null) {
            if (processName.equals(line))) {
                should_unpack = true;
                break;
            }
        }
        br.close();
    }
    catch (Exception ignored) {
 
    }
    return should_unpack;
}
    //启动FART脱壳线程
public static void fartthread() {
 
    if (!shouldUnpack()) {
        return;
    }
 
    new Thread(new Runnable() {
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                Log.e("ActivityThread", "start sleep......");
                Thread.sleep(1 * 60 * 1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            Log.e("ActivityThread", "sleep over and start fart");
            fart();
            Log.e("ActivityThread", "fart run over");
 
        }
    }).start();
}
private void handleBindApplication(AppBindData data) {
    ...
    app = data.info.makeApplication(data.restrictedBackupMode, null);
      app.setAutofillOptions(data.autofillOptions);
      app.setContentCaptureOptions(data.contentCaptureOptions);
      mInitialApplication = app;
      fartthread();
            ...
}
//判断这个进程是否应该脱壳
    public static boolean shouldUnpack() {
    boolean should_unpack = false;
    String processName = ActivityThread.currentProcessName();
    BufferedReader br = null;
    String configPath="/data/local/tmp/fext.config";
    try {
        br = new BufferedReader(new FileReader(configPath));
        String line;
        while ((line = br.readLine()) != null) {
            if (processName.equals(line))) {
                should_unpack = true;
                break;
            }
        }
        br.close();
    }
    catch (Exception ignored) {
 
    }
    return should_unpack;
}
    //启动FART脱壳线程
public static void fartthread() {
 
    if (!shouldUnpack()) {
        return;
    }
 
    new Thread(new Runnable() {
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                Log.e("ActivityThread", "start sleep......");
                Thread.sleep(1 * 60 * 1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            Log.e("ActivityThread", "sleep over and start fart");
            fart();
            Log.e("ActivityThread", "fart run over");
 
        }
    }).start();
}
private void handleBindApplication(AppBindData data) {
    ...
    app = data.info.makeApplication(data.restrictedBackupMode, null);
      app.setAutofillOptions(data.autofillOptions);
      app.setContentCaptureOptions(data.contentCaptureOptions);
      mInitialApplication = app;
      fartthread();
            ...
}
 
 
//读取类列表
public static String getClassList() {
        String processName = ActivityThread.currentProcessName();
        BufferedReader br = null;
        String configPath="/data/local/tmp/"+processName;
        Log.e("ActivityThread", "getClassList processName:"+processName);
        StringBuilder sb=new StringBuilder();
        try {
            br = new BufferedReader(new FileReader(configPath));
            String line;
            while ((line = br.readLine()) != null) {
 
                if(line.length()>=2){
                    sb.append(line+"\n");
                }
            }
            br.close();
        }
        catch (Exception ex) {
            Log.e("ActivityThread", "getClassList err:"+ex.getMessage());
            return "";
        }
        return sb.toString();
    }
        //对指定类进行主动调用
    public static void fartWithClassList(String classlist){
        ClassLoader appClassloader = getClassloader();
        if(appClassloader==null){
            Log.e("ActivityThread", "appClassloader is null");
            return;
        }
        Class DexFileClazz = null;
        try {
            DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Error e) {
            e.printStackTrace();
        }
        Method dumpMethodCode_method = null;
        for (Method field : DexFileClazz.getDeclaredMethods()) {
            if (field.getName().equals("fartextMethodCode")) {
                dumpMethodCode_method = field;
                dumpMethodCode_method.setAccessible(true);
            }
        }
        String[] classes=classlist.split("\n");
        for(String clsname : classes){
            String line=clsname;
            if(line.startsWith("L")&&line.endsWith(";")&&line.contains("/")){
                line=line.substring(1,line.length()-1);
                line=line.replace("/",".");
            }
            loadClassAndInvoke(appClassloader, line, dumpMethodCode_method);
        }
    }
 
    public static void fartthread() {
        if (!shouldUnpack()) {
            return;
        }
          //获取类列表。如果有的话就不要完整主动调用了
        String classlist=getClassList();
        if(!classlist.equals("")){
            fartWithClassList(classlist);
            return;
        }
                ...
    }
//读取类列表
public static String getClassList() {
        String processName = ActivityThread.currentProcessName();
        BufferedReader br = null;
        String configPath="/data/local/tmp/"+processName;
        Log.e("ActivityThread", "getClassList processName:"+processName);
        StringBuilder sb=new StringBuilder();
        try {
            br = new BufferedReader(new FileReader(configPath));
            String line;
            while ((line = br.readLine()) != null) {
 
                if(line.length()>=2){
                    sb.append(line+"\n");
                }
            }
            br.close();
        }
        catch (Exception ex) {
            Log.e("ActivityThread", "getClassList err:"+ex.getMessage());
            return "";
        }
        return sb.toString();
    }
        //对指定类进行主动调用
    public static void fartWithClassList(String classlist){
        ClassLoader appClassloader = getClassloader();
        if(appClassloader==null){
            Log.e("ActivityThread", "appClassloader is null");
            return;
        }
        Class DexFileClazz = null;
        try {
            DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Error e) {
            e.printStackTrace();
        }
        Method dumpMethodCode_method = null;
        for (Method field : DexFileClazz.getDeclaredMethods()) {
            if (field.getName().equals("fartextMethodCode")) {
                dumpMethodCode_method = field;
                dumpMethodCode_method.setAccessible(true);
            }
        }
        String[] classes=classlist.split("\n");
        for(String clsname : classes){
            String line=clsname;
            if(line.startsWith("L")&&line.endsWith(";")&&line.contains("/")){
                line=line.substring(1,line.length()-1);
                line=line.replace("/",".");
            }
            loadClassAndInvoke(appClassloader, line, dumpMethodCode_method);
        }
    }
 
    public static void fartthread() {
        if (!shouldUnpack()) {
            return;
        }
          //获取类列表。如果有的话就不要完整主动调用了
        String classlist=getClassList();
        if(!classlist.equals("")){
            fartWithClassList(classlist);
            return;
        }
                ...
    }
 
 
extern "C" void dumpArtMethod(ArtMethod* artmethod)  REQUIRES_SHARED(Locks::mutator_lock_) {
                ...
        const dex::CodeItem* code_item = artmethod->GetCodeItem();
              const DexFile* dex_=artmethod->GetDexFile();
              CodeItemDataAccessor accessor(*dex_, dex_->GetCodeItem(artmethod->GetCodeItemOffset()));
              if (LIKELY(code_item != nullptr))
        {
              int code_item_len = 0;
              uint8_t *item=(uint8_t *) code_item;
              if (accessor.TriesSize()>0) {
                  const uint8_t *handler_data = accessor.GetCatchHandlerData();
                  uint8_t * tail = codeitem_end(&handler_data);
                  code_item_len = (int)(tail - item);
            }else{
                  code_item_len = 16+accessor.InsnsSizeInCodeUnits()*2;
            }
                        ...
            }
            ...
}
extern "C" void dumpArtMethod(ArtMethod* artmethod)  REQUIRES_SHARED(Locks::mutator_lock_) {
                ...
        const dex::CodeItem* code_item = artmethod->GetCodeItem();
              const DexFile* dex_=artmethod->GetDexFile();
              CodeItemDataAccessor accessor(*dex_, dex_->GetCodeItem(artmethod->GetCodeItemOffset()));
              if (LIKELY(code_item != nullptr))
        {
              int code_item_len = 0;
              uint8_t *item=(uint8_t *) code_item;
              if (accessor.TriesSize()>0) {
                  const uint8_t *handler_data = accessor.GetCatchHandlerData();
                  uint8_t * tail = codeitem_end(&handler_data);
                  code_item_len = (int)(tail - item);
            }else{
                  code_item_len = 16+accessor.InsnsSizeInCodeUnits()*2;
            }
                        ...
            }
            ...
}
 
 
 
 
 
 
 
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty) {
    if (self== nullptr) {
        dumpArtMethod(this);
        return;
    }
      ...
 }
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty) {
    if (self== nullptr) {
        dumpArtMethod(this);
        return;
    }
      ...
 }
.method public constructor <init>()V
    .registers 2
 
    goto :goto_c
 
    :goto_1
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    return-void
 
    :goto_c
    const v0, 0x1669
 
    invoke-static {v0}, Ls/h/e/l/l/H;->i(I)V
 
    goto :goto_1
.end method
.method public constructor <init>()V
    .registers 2
 
    goto :goto_c
 
    :goto_1
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    nop
 
    return-void
 
    :goto_c
    const v0, 0x1669
 
    invoke-static {v0}, Ls/h/e/l/l/H;->i(I)V
 
    goto :goto_1
.end method
 
 
static constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;
static constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;
void Unpacker::invokeAllMethods() {
          ...
      auto methods = klass->GetDeclaredMethods(pointer_size);
      Unpacker::enableFakeInvoke();
      for (auto& m : methods) {
        ArtMethod* method = &m;
        if (!method->IsProxyMethod() && method->IsInvokable()) {
          //获取参数个数
          uint32_t args_size = (uint32_t)ArtMethod::NumArgRegisters(method->GetShorty());
          if (!method->IsStatic()) {
            args_size += 1;
          }
          //模拟参数
          JValue result;
          std::vector<uint32_t> args(args_size, 0);
          if (!method->IsStatic()) {
            mirror::Object* thiz = klass->AllocObject(self);
            args[0] = StackReference<mirror::Object>::FromMirrorPtr(thiz).AsVRegValue(); 
          }
          method->Invoke(self, args.data(), args_size, &result, method->GetShorty());
        }
      }
      Unpacker::disableFakeInvoke();
      cJSON_ReplaceItemInObject(current, "status", cJSON_CreateString("Dumped"));
      writeJson();
    }
  }
}
void Unpacker::invokeAllMethods() {
          ...
      auto methods = klass->GetDeclaredMethods(pointer_size);
      Unpacker::enableFakeInvoke();
      for (auto& m : methods) {
        ArtMethod* method = &m;
        if (!method->IsProxyMethod() && method->IsInvokable()) {
          //获取参数个数
          uint32_t args_size = (uint32_t)ArtMethod::NumArgRegisters(method->GetShorty());
          if (!method->IsStatic()) {
            args_size += 1;
          }
          //模拟参数
          JValue result;
          std::vector<uint32_t> args(args_size, 0);
          if (!method->IsStatic()) {
            mirror::Object* thiz = klass->AllocObject(self);
            args[0] = StackReference<mirror::Object>::FromMirrorPtr(thiz).AsVRegValue(); 
          }
          method->Invoke(self, args.data(), args_size, &result, method->GetShorty());
        }
      }
      Unpacker::disableFakeInvoke();
      cJSON_ReplaceItemInObject(current, "status", cJSON_CreateString("Dumped"));
      writeJson();
    }
  }
}
extern "C" void myfartInvoke(ArtMethod* artmethod)  REQUIRES_SHARED(Locks::mutator_lock_) {
    JValue *result=nullptr;
    Thread *self=nullptr;
    uint32_t temp=6;
    uint32_t* args=&temp;
    uint32_t args_size=6;
    artmethod->Invoke(self, args, args_size, result, "fart");
}
extern "C" void myfartInvoke(ArtMethod* artmethod)  REQUIRES_SHARED(Locks::mutator_lock_) {
    JValue *result=nullptr;
    Thread *self=nullptr;
    uint32_t temp=6;
    uint32_t* args=&temp;
    uint32_t args_size=6;
    artmethod->Invoke(self, args, args_size, result, "fart");
}
 
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty) {
  ...
  //patch by Youlor
  //++++++++++++++++++++++++++++
  //如果是主动调用fake invoke并且不是native方法则强制走解释器
  if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this)
      || (Unpacker::isFakeInvoke(self, this) && !this->IsNative()))) {
  //++++++++++++++++++++++++++++
    if (IsStatic()) {
      art::interpreter::EnterInterpreterFromInvoke(
          self, this, nullptr, args, result, /*stay_in_interpreter*/ true);
    } else {
      mirror::Object* receiver =
          reinterpret_cast<StackReference<mirror::Object>*>(&args[0])->AsMirrorPtr();
      art::interpreter::EnterInterpreterFromInvoke(
          self, this, receiver, args + 1, result, /*stay_in_interpreter*/ true);
    }
  } else {
    //patch by Youlor
    //++++++++++++++++++++++++++++
    //如果是主动调用fake invoke并且是native方法则不执行
    if (Unpacker::isFakeInvoke(self, this) && this->IsNative()) {
      // Pop transition.
      self->PopManagedStackFragment(fragment);
      return;
    }
    //++++++++++++++++++++++++++++
    ...
  }
    ...
}
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
                       const char* shorty) {
  ...
  //patch by Youlor
  //++++++++++++++++++++++++++++
  //如果是主动调用fake invoke并且不是native方法则强制走解释器
  if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this)
      || (Unpacker::isFakeInvoke(self, this) && !this->IsNative()))) {
  //++++++++++++++++++++++++++++
    if (IsStatic()) {
      art::interpreter::EnterInterpreterFromInvoke(
          self, this, nullptr, args, result, /*stay_in_interpreter*/ true);
    } else {
      mirror::Object* receiver =
          reinterpret_cast<StackReference<mirror::Object>*>(&args[0])->AsMirrorPtr();
      art::interpreter::EnterInterpreterFromInvoke(
          self, this, receiver, args + 1, result, /*stay_in_interpreter*/ true);
    }
  } else {
    //patch by Youlor
    //++++++++++++++++++++++++++++
    //如果是主动调用fake invoke并且是native方法则不执行
    if (Unpacker::isFakeInvoke(self, this) && this->IsNative()) {
      // Pop transition.
      self->PopManagedStackFragment(fragment);
      return;
    }
    //++++++++++++++++++++++++++++
    ...
  }
    ...
}
void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver,
                                uint32_t* args, JValue* result,
                                bool stay_in_interpreter) {
         ...
    JValue r = Execute(self, code_item, *shadow_frame, JValue(), stay_in_interpreter);
    ...
}
void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver,
                                uint32_t* args, JValue* result,
                                bool stay_in_interpreter) {
         ...
    JValue r = Execute(self, code_item, *shadow_frame, JValue(), stay_in_interpreter);
    ...
}
static inline JValue Execute(
    Thread* self,
    const DexFile::CodeItem* code_item,
    ShadowFrame& shadow_frame,
    JValue result_register,
    bool stay_in_interpreter = false) SHARED_REQUIRES(Locks::mutator_lock_) {
      ...
    } else if (kInterpreterImplKind == kSwitchImplKind) {
      if (transaction_active) {
        return ExecuteSwitchImpl<false, true>(self, code_item, shadow_frame, result_register,
                                              false);
      } else {
        return ExecuteSwitchImpl<false, false>(self, code_item, shadow_frame, result_register,
                                               false);
      }
    }
        ...
}
static inline JValue Execute(
    Thread* self,
    const DexFile::CodeItem* code_item,
    ShadowFrame& shadow_frame,
    JValue result_register,
    bool stay_in_interpreter = false) SHARED_REQUIRES(Locks::mutator_lock_) {
      ...
    } else if (kInterpreterImplKind == kSwitchImplKind) {
      if (transaction_active) {
        return ExecuteSwitchImpl<false, true>(self, code_item, shadow_frame, result_register,
                                              false);
      } else {
        return ExecuteSwitchImpl<false, false>(self, code_item, shadow_frame, result_register,
                                               false);
      }
    }
        ...
}
//patch by Youlor
//++++++++++++++++++++++++++++
#define PREAMBLE()                                                                              \
  do {                                                                                          \
    inst_count++;                                                                               \
    bool dumped = Unpacker::beforeInstructionExecute(self, shadow_frame.GetMethod(),            \
                                                     dex_pc, inst_count);                       \
    if (dumped) {                                                                               \
      return JValue();                                                                          \
    }                                                                                           \
    if (UNLIKELY(instrumentation->HasDexPcListeners())) {                                       \
      instrumentation->DexPcMovedEvent(self, shadow_frame.GetThisObject(code_item->ins_size_),  \
                                       shadow_frame.GetMethod(), dex_pc);                       \
    }                                                                                           \
  } while (false)
//++++++++++++++++++++++++++++
//patch by Youlor
//++++++++++++++++++++++++++++
#define PREAMBLE()                                                                              \
  do {                                                                                          \
    inst_count++;                                                                               \
    bool dumped = Unpacker::beforeInstructionExecute(self, shadow_frame.GetMethod(),            \
                                                     dex_pc, inst_count);                       \
    if (dumped) {                                                                               \
      return JValue();                                                                          \
    }                                                                                           \
    if (UNLIKELY(instrumentation->HasDexPcListeners())) {                                       \
      instrumentation->DexPcMovedEvent(self, shadow_frame.GetThisObject(code_item->ins_size_),  \
                                       shadow_frame.GetMethod(), dex_pc);                       \
    }                                                                                           \
  } while (false)
//++++++++++++++++++++++++++++
//继续解释执行返回false, dump完成返回true
bool Unpacker::beforeInstructionExecute(Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) {
  if (Unpacker::isFakeInvoke(self, method)) {
    const uint16_t* const insns = method->GetCodeItem()->insns_;
    const Instruction* inst = Instruction::At(insns + dex_pc);
    uint16_t inst_data = inst->Fetch16(0);
    Instruction::Code opcode = inst->Opcode(inst_data);
 
    //对于一般的方法抽取(非ijiami, najia), 直接在第一条指令处dump即可
    if (inst_count == 0 && opcode != Instruction::GOTO && opcode != Instruction::GOTO_16 && opcode != Instruction::GOTO_32) {
      Unpacker::dumpMethod(method);
      return true;
    }
    //ijiami, najia的特征为: goto: goto_decrypt; nop; ... ; return; const vx, n; invoke-static xxx; goto: goto_origin;
    else if (inst_count == 0 && opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) {
      return false;
    } else if (inst_count == 1 && opcode >= Instruction::CONST_4 && opcode <= Instruction::CONST_WIDE_HIGH16) {
      return false;
    } else if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE)) {
      //让这条指令真正的执行
      Unpacker::disableFakeInvoke();
      Unpacker::enableRealInvoke();
      return false;
    } else if (inst_count == 3) {
      if (opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) {
        //写入时将第一条GOTO用nop填充
        const Instruction* inst_first = Instruction::At(insns);
        Instruction::Code first_opcode = inst_first->Opcode(inst->Fetch16(0));
        CHECK(first_opcode >= Instruction::GOTO && first_opcode <= Instruction::GOTO_32);
        ULOGD("found najia/ijiami %s", PrettyMethod(method).c_str());
        switch (first_opcode)
        {
        case Instruction::GOTO:
          Unpacker::dumpMethod(method, 2);
          break;
        case Instruction::GOTO_16:
          Unpacker::dumpMethod(method, 4);
          break;
        case Instruction::GOTO_32:
          Unpacker::dumpMethod(method, 8);
          break;
        default:
          break;
        }
      } else {
        Unpacker::dumpMethod(method);
      }
      return true;
    }
    Unpacker::dumpMethod(method);
    return true;
  }
  return false;
}
//继续解释执行返回false, dump完成返回true
bool Unpacker::beforeInstructionExecute(Thread *self, ArtMethod *method, uint32_t dex_pc, int inst_count) {
  if (Unpacker::isFakeInvoke(self, method)) {
    const uint16_t* const insns = method->GetCodeItem()->insns_;
    const Instruction* inst = Instruction::At(insns + dex_pc);
    uint16_t inst_data = inst->Fetch16(0);
    Instruction::Code opcode = inst->Opcode(inst_data);
 
    //对于一般的方法抽取(非ijiami, najia), 直接在第一条指令处dump即可
    if (inst_count == 0 && opcode != Instruction::GOTO && opcode != Instruction::GOTO_16 && opcode != Instruction::GOTO_32) {
      Unpacker::dumpMethod(method);
      return true;
    }
    //ijiami, najia的特征为: goto: goto_decrypt; nop; ... ; return; const vx, n; invoke-static xxx; goto: goto_origin;
    else if (inst_count == 0 && opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) {
      return false;
    } else if (inst_count == 1 && opcode >= Instruction::CONST_4 && opcode <= Instruction::CONST_WIDE_HIGH16) {
      return false;
    } else if (inst_count == 2 && (opcode == Instruction::INVOKE_STATIC || opcode == Instruction::INVOKE_STATIC_RANGE)) {
      //让这条指令真正的执行
      Unpacker::disableFakeInvoke();
      Unpacker::enableRealInvoke();
      return false;
    } else if (inst_count == 3) {
      if (opcode >= Instruction::GOTO && opcode <= Instruction::GOTO_32) {
        //写入时将第一条GOTO用nop填充
        const Instruction* inst_first = Instruction::At(insns);
        Instruction::Code first_opcode = inst_first->Opcode(inst->Fetch16(0));
        CHECK(first_opcode >= Instruction::GOTO && first_opcode <= Instruction::GOTO_32);
        ULOGD("found najia/ijiami %s", PrettyMethod(method).c_str());
        switch (first_opcode)
        {
        case Instruction::GOTO:
          Unpacker::dumpMethod(method, 2);
          break;
        case Instruction::GOTO_16:
          Unpacker::dumpMethod(method, 4);
          break;
        case Instruction::GOTO_32:
          Unpacker::dumpMethod(method, 8);
          break;
        default:
          break;
        }
      } else {
        Unpacker::dumpMethod(method);
      }
      return true;
    }
    Unpacker::dumpMethod(method);
    return true;
  }
  return false;
}
template<bool do_access_check, bool transaction_active>
JValue ExecuteSwitchImpl(Thread* self, const DexFile::CodeItem* code_item,
                         ShadowFrame& shadow_frame, JValue result_register,
                         bool interpret_one_instruction) {
  ...
  //patch by Youlor
  //++++++++++++++++++++++++++++
  int inst_count = -1;
  //++++++++++++++++++++++++++++
  do {
    dex_pc = inst->GetDexPc(insns);
    shadow_frame.SetDexPC(dex_pc);
    TraceExecution(shadow_frame, inst, dex_pc);
    inst_data = inst->Fetch16(0);
    switch (inst->Opcode(inst_data)) {
      ...
      case Instruction::GOTO: {
        PREAMBLE();
        int8_t offset = inst->VRegA_10t(inst_data);
        BRANCH_INSTRUMENTATION(offset);
        if (IsBackwardBranch(offset)) {
          HOTNESS_UPDATE();
          self->AllowThreadSuspension();
        }
        inst = inst->RelativeAt(offset);
        break;
      }
      ...
      case Instruction::INVOKE_STATIC: {
        PREAMBLE();
        bool success = DoInvoke<kStatic, false, do_access_check>(
            self, shadow_frame, inst, inst_data, &result_register);
        POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx);
        break;
      }
      case Instruction::INVOKE_STATIC_RANGE: {
        PREAMBLE();
        bool success = DoInvoke<kStatic, true, do_access_check>(
            self, shadow_frame, inst, inst_data, &result_register);
        POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx);
        break;
      }
      ...
    }
    //patch by Youlor
    //++++++++++++++++++++++++++++
    bool dumped = Unpacker::afterInstructionExecute(self, shadow_frame.GetMethod(), dex_pc, inst_count);
    if (dumped) {
      return JValue();
    }
    //++++++++++++++++++++++++++++
  } while (!interpret_one_instruction);
  // Record where we stopped.
  shadow_frame.SetDexPC(inst->GetDexPc(insns));
  return result_register;
// NOLINT(readability/fn_size)
template<bool do_access_check, bool transaction_active>
JValue ExecuteSwitchImpl(Thread* self, const DexFile::CodeItem* code_item,
                         ShadowFrame& shadow_frame, JValue result_register,
                         bool interpret_one_instruction) {
  ...
  //patch by Youlor
  //++++++++++++++++++++++++++++
  int inst_count = -1;
  //++++++++++++++++++++++++++++
  do {
    dex_pc = inst->GetDexPc(insns);
    shadow_frame.SetDexPC(dex_pc);
    TraceExecution(shadow_frame, inst, dex_pc);
    inst_data = inst->Fetch16(0);
    switch (inst->Opcode(inst_data)) {
      ...
      case Instruction::GOTO: {
        PREAMBLE();
        int8_t offset = inst->VRegA_10t(inst_data);
        BRANCH_INSTRUMENTATION(offset);
        if (IsBackwardBranch(offset)) {
          HOTNESS_UPDATE();
          self->AllowThreadSuspension();
        }
        inst = inst->RelativeAt(offset);
        break;
      }
      ...
      case Instruction::INVOKE_STATIC: {
        PREAMBLE();
        bool success = DoInvoke<kStatic, false, do_access_check>(
            self, shadow_frame, inst, inst_data, &result_register);
        POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx);
        break;
      }
      case Instruction::INVOKE_STATIC_RANGE: {
        PREAMBLE();
        bool success = DoInvoke<kStatic, true, do_access_check>(
            self, shadow_frame, inst, inst_data, &result_register);
        POSSIBLY_HANDLE_PENDING_EXCEPTION(!success, Next_3xx);
        break;
      }
      ...
    }
    //patch by Youlor
    //++++++++++++++++++++++++++++

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-8-5 15:17 被misskings编辑 ,原因:
收藏
免费 24
支持
分享
最新回复 (78)
雪    币: 1230
活跃值: (1765)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
高产呀,给你点个赞!
2021-8-5 11:43
0
雪    币: 1310
活跃值: (727)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
3
都是折腾。思路决定眼界,眼界决定高度。
2021-8-5 11:55
0
雪    币: 1
活跃值: (1072)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
大佬,好厉害啊。我也在学习fart。能不能帮忙解答个问题,问题链接在:
https://bbs.pediy.com/thread-268744.htm
非常感谢
2021-8-5 14:33
0
雪    币: 1490
活跃值: (9913)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
5
wx_阿达西 大佬,好厉害啊。我也在学习fart。能不能帮忙解答个问题,问题链接在: https://bbs.pediy.com/thread-268744.htm 非常感谢
已经在你帖子回答了
2021-8-5 14:55
0
雪    币: 3836
活跃值: (4142)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
坐等加精
2021-8-5 15:02
0
雪    币: 638
活跃值: (1772)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
7
rom打桩机大佬,dddd
2021-8-5 15:22
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
支持一下
2021-8-5 20:02
0
雪    币: 14824
活跃值: (6063)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
这个优化是针对某加固?其它加固不一定是goto+static?百度加固就是另一种。
2021-8-6 08:57
0
雪    币: 1490
活跃值: (9913)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
10
tDasm 这个优化是针对某加固?其它加固不一定是goto+static?百度加固就是另一种。
流程和原理通了,其他的加固可以自己调整和改良。当然,我后续也会优化针对其他加固的处理
2021-8-6 09:22
0
雪    币: 14824
活跃值: (6063)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
misskings 流程和原理通了,其他的加固可以自己调整和改良。当然,我后续也会优化针对其他加固的处理
网上找了一个最新dexhelper加固的壳,,你测试一下?
https://app.mi.com/details?id=com.hn.catv&ref=search
2021-8-6 11:35
0
雪    币: 1490
活跃值: (9913)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
12
tDasm 网上找了一个最新dexhelper加固的壳,,你测试一下? https://app.mi.com/details?id=com.hn.catv&ref=search
好的。晚上有空了我再测试下。
2021-8-6 11:38
0
雪    币: 576
活跃值: (2035)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
mark
2021-8-6 20:19
0
雪    币: 1490
活跃值: (9913)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
14
tDasm 网上找了一个最新dexhelper加固的壳,,你测试一下? https://app.mi.com/details?id=com.hn.catv&ref=search

你发的这个例子我测试了下。确实还需要优化。
启动应用的时候被检测环境,然后直接退出了。目前看到的检测比较简单。就是搜索包名,找一些敏感的数据。

另外发现好像是下面这个函数的地方检测的环境。frida试了下没法用,不知道是不是也有frida检测。想要正常脱壳得先过掉环境检测才行

public boolean h() {

        return g() || f() || a(H.k()) || b() || c() || e() || d();

    }


最后于 2021-8-6 23:04 被misskings编辑 ,原因:
2021-8-6 21:17
0
雪    币: 2089
活跃值: (3933)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
tDasm 网上找了一个最新dexhelper加固的壳,,你测试一下? https://app.mi.com/details?id=com.hn.catv&ref=search
X梆的检测很简单的啊,脱壳用开源的blackdex都可以
2021-8-8 01:38
0
雪    币: 14824
活跃值: (6063)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
lhxdiao X梆的检测很简单的啊,脱壳用开源的blackdex都可以
愿闻其详!检测部分
2021-8-8 07:56
0
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17

大佬你这包怎么刷的。刷system的时候报错

2021-8-8 16:34
0
雪    币: 1490
活跃值: (9913)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
18
小明是咸鱼 大佬你这包怎么刷的。刷system的时候报错
我是下载官包。然后自己写了个脚本打包。最后用官方的flash-all.sh来刷的。
rm ./image-sailfish-qp1a.191005.007.a3/*.img
rm ./image-sailfish-qp1a.191005.007.a3.zip
cp ~/aosp_src/aosp1000r2/out/target/product/sailfish/boot.img ./image-sailfish-qp1a.191005.007.a3/
cp ~/aosp_src/aosp1000r2/out/target/product/sailfish/system.img ./image-sailfish-qp1a.191005.007.a3/
cp ~/aosp_src/aosp1000r2/out/target/product/sailfish/system_other.img ./image-sailfish-qp1a.191005.007.a3/
cp ~/aosp_src/aosp1000r2/out/target/product/sailfish/vendor.img ./image-sailfish-qp1a.191005.007.a3/
cd ./image-sailfish-qp1a.191005.007.a3
zip image-sailfish-qp1a.191005.007.a3.zip ./*
mv image-sailfish-qp1a.191005.007.a3.zip ../
2021-8-8 16:49
0
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
misskings 我是下载官包。然后自己写了个脚本打包。最后用官方的flash-all.sh来刷的。 rm ./image-sailfish-qp1a.191005.007.a3/*.img rm ./image- ...
有没有win一键刷的方法
2021-8-8 17:26
0
雪    币: 1490
活跃值: (9913)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
20
小明是咸鱼 有没有win一键刷的方法
你去下载aosp的官方刷机包。然后把里面的img全部替换成你自己的。然后他的脚本里面也有.bat的一键线刷的。名字就是flash-all.bat
2021-8-8 22:35
0
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21
misskings 你去下载aosp的官方刷机包。然后把里面的img全部替换成你自己的。然后他的脚本里面也有.bat的一键线刷的。名字就是flash-all.bat

那个我之前试过,也会报错

2021-8-9 01:16
0
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22

替换官方.img会这样

2021-8-9 01:17
0
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
23
ok,是我自己搞错了。忘了这是一代的镜像。弄好了
谢谢大佬
2021-8-9 02:22
0
雪    币: 4233
活跃值: (3813)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
D-t
24
tDasm 网上找了一个最新dexhelper加固的壳,,你测试一下? https://app.mi.com/details?id=com.hn.catv&ref=search

classes0.jar直接静态解密 RC4算法万年不变

jar解密后就是全部dex 然后算法解密dgc就好了

由于企业壳原因 就不贴出代码了 自己研究下就好了

上传的附件:
2021-8-9 03:33
1
雪    币: 3269
活跃值: (2964)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
25
D-t classes0.jar直接静态解密 RC4算法万年不变jar解密后就是全部dex 然后算法解密dgc就好了由于企业壳原因 就不贴出代码了 自己研究下就好了

DGC指的是classes.dgc文件,新版企业壳去除了classes0.jar,应该是放到classes.dex中了

最后于 2021-10-7 00:23 被xhyeax编辑 ,原因:
2021-8-9 07:41
0
游客
登录 | 注册 方可回帖
返回
//