精华内容
下载资源
问答
  • 代码插桩

    千次阅读 2019-05-19 09:33:16
    【转载】http://wiki.dzsc.com/info/5081.html 代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试 来说又是至关重要的。在对CodeTeST 中插桩技术研究的基础上,以G...

    https://blog.csdn.net/woshidujian19881029/article/details/6558260

    【转载】http://wiki.dzsc.com/info/5081.html

    •   代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试 来说又是至关重要的。在对CodeTeST 中插桩技术研究的基础上,以GCC 作为开发平台,应用并实现了新的插装器,采用增加一个词法语法分析器的方法,提高了插桩的效率。经过实验证明新的插装器具有代码膨胀率小,插桩速度块的优 点,在一定程度上做到了高效插桩。
    代码插桩

    代码插桩概述

    •   在实现覆盖测试的过程中,往往需要知道某些信息,如:程序中可执行语句 被执行(即被覆盖)的情况,程序执行的路径,变量的引用、定义等。要想获取这类信息,需要跟踪被测程序的执行过程,或者是由计算机在被测程序执行的过程中 自动记录。前者需要人工进行,效率低下且枯燥乏味;后者则需要在被测程序中插入完成相应工作的代码,即代码插桩技术。如今大多数的覆盖测试工具 均采用代码插桩技术。

        在对普通应用的软件进行测试时,由于现在电脑的配置越来越高,电脑的运行速度越来越快,代码插桩所 引起的问题还不是很明显或者说是在可以接受的范围之内。但是对于嵌入式软件来说这却是致命的问题。因为嵌入式软件的系统资源有限(内存较小、I/O 通道较少等),过大的代码膨胀率将使得程序不能在嵌入式系统中运行;同时嵌入式软件通常具有很强的实时性,程序的输出只在有限的时间内有效,迟到的“正确 的”结果是无用的甚至会变成错误的、有害的。

        代码插桩技术会破坏程序的时间特性等,导致软件执行的错误。因此我们需要更高效的代码插桩技术来完成覆盖测试,尤其是嵌入式软件的覆盖测试。

    代码插桩方式比较

    •   由于程序插桩技术是在被测程序中插入探针,然后通过探针的执行来获得程序的控制流和数据流信息,以此来实现测试的目的。因此,根据探针插入的时间可以分为目标代码插桩和源代码插桩。

        (1)目标代码插桩的前提是对目标代码进:

        行必要的分析以确定需要插桩的地点和内容。由于目标代码的格式主要和操作系统相关,和具体的编程语 言及版本无关,所以得到了广泛的应用,尤其是在需要对内存进行监控的软件中。但是由于目标代码中语法、语义信息不完整,而插桩技术需要对代码词法语法的分 析有较高的要求,故在覆盖测试工具中多采用源代码插桩。

        (2)源代码插桩是在对源文件进行完整的:

        词法分析和语法分析的基础上进行的,这就保证对源文件的插桩能够达到很高的准确度和针对性。但是源代码插桩需要接触到源代码,使得工作量较大,而且随着编码语言和版本的不同需要做一定的修改。在后面我们所提到的程序插桩均指源代码插桩。

    代码插桩设计

    •   (1)插桩位置:

        探针的植入要做到紧凑精干,才能保证在做到收集的信息全面而无冗余,减少代码的膨胀率。因此,在确定插桩位置时,要将程序划分,基本的划分方法是基于“块”结构。

        按照块结构的划分,探针的植入位置有以下几种情况:

        a. 程序的第一条语句;b. 分支语句的开始;c. 循环语句的开始;d. 下一个入口语句之前的语句;e. 程序的结束语句;f. 分支语句的结束;g. 循环语句的结束;除此之外,根据覆盖测试要求的不同,插桩的位置除了上面所说的几种情况外,也会随着覆盖测试要求的不同有所变化。

        (2)插桩策略:

        插桩策略是解决“如何插”的问题。传统的插桩策略是在所有需要插桩的位置插入探针,在程序运行过程 收集所有可能用到得程序信息,将其写入数据库进行分析和处理。这种方法对于大型的程序来说,将会造成相当大的工作量,效率很低,且会造成很大的代码膨胀 率。而我们会根据不同的测试要求,每次插入不同的探针,采用相应的插桩策略,这样就减少了代码的膨胀率,保证了程序执行的效率。下面简单介绍几种探针的插 桩策略。

        语句覆盖探针(基本块探针):在基本块的入口和出口处,分别植入相应的探针,以确定程序执行时该基本块是否被覆盖。

        分支覆盖探针:C/C++语言中,分支由分支点确定。对于每个分支,在其开始处植入一个相应的探针,以确定程序执行时该分支是否被覆盖。

        条件覆盖探针:C/C++语言中,if, swich,while, do-while, for 几种语法结构都支持条件判定,在每个条件表达式的布尔表达式处植入探针,进行变量跟踪取值,以确定其被覆盖情况。

        根据不同测试要求采用不用的插桩策略 ,每次在不同的位置植入相应的探针,使得每次只是植入有限的探针,这就更大大减少了代码的膨胀率和插桩的速度。

    展开全文
  • 代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试来说又是至关重要的。文章在对CodeTeST 中插桩技术研究的基础上,以GCC 作为开发平台,应用并实现了新的插装器,采用增加一个词法语法...
  • 代码插桩技术能够让我们在不更改已有源码的前提下,从外部注入、拦截各种自定的逻辑。这为施展各种黑魔法提供了巨大的想象空间。
  • 静态代码插桩

    2021-01-27 18:28:52
    静态代码插桩设置插桩参数 代码插桩是指根据一定的策略在代码中插入桩点来统计代码覆盖的技术手段.一般可以分为三个粒度: 函数(function): 按照函数为单位进行插桩; 基本块(basic block): 按照代码执行单元进行...


    代码插桩是指根据一定的策略在代码中插入桩点来统计代码覆盖的技术手段.一般可以分为三个粒度:

    • 函数(function): 按照函数为单位进行插桩;
    • 基本块(basic block): 按照代码执行单元进行分组的执行单元,单元内部的代码执行次数一定是相同的;
    • 边界(Edge): 按照代码执行路径进行插桩。

    针对iOS来说,clang支持以上粒度的插桩方式。这里先介绍一些函数粒度的插桩实现.

    函数覆盖

    Clang 是一个高度模块化开发的轻量级编译器。可以通过设置Clang的编译参数实现静态插桩.

    • 在Xcode->Build Settings中搜索"Other C Flags",然后在其中添加
    // 基本块覆盖可以使用参数:  -fsanitize-coverage=bb,trace-pc-guard
    // 边缘覆盖可以使用参数:  -fsanitize-coverage=edge,trace-pc-guard
    -fsanitize-coverage=func,trace-pc-guard
    

    同时在任意实现文件中添加一下两个函数:

    // 哨兵初始化函数,其中[*start,*end)表示了哨兵的标志,这里可以理解为每个哨兵guard是一个指针,保存了一个uint32_t的整形数据来作为自己的标记
    void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
      static uint32_t N;  // Counter for the guards.
      if (start == stop || *start) return;  // Initialize only once.
        for (uint32_t *x = start; x < stop; x++) {
            *x = ++N;  // Guards should start from 1.
        }
    }
    
    // 当每个函数开始调用时会被插入该回调,所以在方法调用开始就会执行该回调。函数中guard就是__sanitizer_cov_trace_pc_guard_init中[start, end)区间中一个
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    	printf("guard = %p, *guard=%d", guard, *guard);
    // 在这里可以尝试获取到执行函数的信息
    }
    

    然后运行项目会发现,回调正常执行.那么问题来了,如何在回调函数中获取到当前执行函数的信息呢?

    获取当前执行函数的信息

    在汇编语言中,如果函数包含了子函数,则方法执行过程中会在进行bl跳转指令之前将下一条指令的地址保存在特定的寄存器中(x30寄存器),而clang中封装了这样的获取方法:

    // 返回当前函数或其调用者的返回地址,LEVEL表示调用层级:
    // 0: 表示当前函数的返回地址;
    // 1:表示当前函数调用者的返回地址,依次类推
    void *__builtin_return_address(int LEVEL);
    

    拿到改地址之后如何获取该方法的信息呢?系统同样提供了开放的方法。在 dlfcn.h 中有一个方法如下 :

    typedef struct dl_info {
            const char      *dli_fname;     /* 所在文件 */
            void            *dli_fbase;     /* 文件地址 */
            const char      *dli_sname;     /* 符号名称 */
            void            *dli_saddr;     /* 函数起始地址 */
    } Dl_info;
    
    //这个函数能通过函数内部地址找到函数符号
    int dladdr(const void *, Dl_info *);
    

    这样就可以通过函数内部的地址获取到函数的符号信息:

    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
        // 获取寄存器中下一条指令的地址
        void *PC = __builtin_return_address(0);
        Dl_info info;
        // 通过函数内部地址获取当前函数的符号信息
        dladdr(PC, &info);
        printf("fname=%s \nfbase=%p \nsname=%s\nsaddr=%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
    }
    

    再次启动工程就可以发现,已经获取到了项目中执行方法的符号信息.

    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=main
    saddr=0x1008257d4 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[AppDelegate application:didFinishLaunchingWithOptions:]
    saddr=0x100825520 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate setWindow:]
    saddr=0x100825d60 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate scene:willConnectToSession:options:]
    saddr=0x1008259f4 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[ViewController setImageView:]
    saddr=0x10082541c 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[ViewController viewDidLoad]
    saddr=0x100824538 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate sceneWillEnterForeground:]
    saddr=0x100825c20 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate sceneDidBecomeActive:]
    saddr=0x100825b38 
    

    保存符号

    既然获得了符号,就需要对符号进行保存,以留作后续使用.这里尝试保存在第一个界面正式出现之前的所有符号。
    为了方便操作将上述两个方法从main中移动到第一个控制器中.

    • 由于回调函数可能出在多个线程中,为了线程安全,同时减少频繁加锁开锁的系统开销使用原子队列来进行数据存储.
      需要注意的是,原子队列只能添加结构体节点,不能直接添加oc对象节点。
    #import <libkern/OSAtomic.h>
    // 向队列中添加节点
    void  OSAtomicEnqueue( OSQueueHead *__list, void *__new, size_t __offset);
    
    // 可以使用循环从院子队列中取出元素
    void* OSAtomicDequeue( OSQueueHead *__list, size_t __offset);
    
    • 初始化原子队列,并定义结构体节点:
    static OSQueueHead queueHead = OS_ATOMIC_QUEUE_INIT;
    // 定义节点
    struct SYNode {
        char *symbol; // 记录符号
        struct SYNode *next; // 记录符号的写一个地址
    };
    
    • 向节点中写入数据:
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
        if (!*guard) return;  // Duplicate the guard check.
    
        void *PC = __builtin_return_address(0);
        Dl_info info;
        dladdr(PC, &info);
        
    //
    //    printf("fname=%s \nfbase=%p \nsname=%s\nsaddr=%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
        
        
        char *symbol = malloc(sizeof(char) * strlen(info.dli_sname));
        strcpy(symbol, info.dli_sname);
        struct SYNode *node = malloc(sizeof(struct SYNode));
        *node = (struct SYNode){symbol, NULL};
        // 向节点中写入数据
        OSAtomicEnqueue(&queueHead, node, offsetof(struct SYNode, next));
    }
    
    • 在控制器的viewDidAppear方法中,获取院子队列中的符号信息,并进行本地保存:
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        
        NSMutableArray<NSString *> *allSymbols = [NSMutableArray array];
        
        struct SYNode *node = OSAtomicDequeue(&queueHead, offsetof(struct SYNode, next));
        while (node) {
            NSLog(@"--->%s", node->symbol);
            
            [allSymbols addObject:[NSString stringWithUTF8String:node->symbol]];
            free(node);
            node = OSAtomicDequeue(&queueHead, offsetof(struct SYNode, next));
        }
        
        NSString *cacheRootPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true).firstObject;
        NSString *path = [cacheRootPath stringByAppendingPathComponent:@"symbols.txt"];
    
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            BOOL success = [allSymbols writeToFile:path atomically:true];
             NSLog(@"写入%@", success ? @"成功" : @"失败");
        });
    }
    
    • 使用Xcode连接真机,启动应用直至第一个控制器界面家在完成.使用快捷键cmd+shift+2进入Devices and Simulators界面,选择对应应用并点击Download Containers选择保存路径下载文件。
      在这里插入图片描述
    • 在下载的.xcappd中右键显示包内容,在AppData->Library->Caches路径下即可保存的.txt文件.
      由于队列的特性,这里的符号与实际调用顺序是相反的。
      在这里插入图片描述

    这样就可查看到在应用首个控制器显示之前系统调用的所有符号,从而为应用启动优化奠定基础.

    静态插桩作用

    通过静态插桩,可以查看项目中的代码执行情况,进而为项目优化提供依据.

    • 重排二进制文件:可以根据启动时调用的方法,存储在.order文件中,认为干预二进制文件的生成,优化启动速度;
    • 删除无用代码:可以根据项目中方法的执行情况,查看方法的覆盖率,将没有使用到的方法进行删除,减少二进制文件的大小;
    • 跟踪方法调用顺序:可以将调用的符号进行保存来查看应用中方法的调用顺序,跟踪异常。
    展开全文
  • 摘要:代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试来说又是至关重要的。文章在对CodeTest 中插桩技术研究的基础上,以GCC 作为开发平台,应用并实现了新的插装器,采用增加一个词...
  • 通过安卓修改大师可以很轻松的在任何apk中添加新的代码逻辑,实现额外添加的功能,本次教程通过对一款名为“VMware Horizon”的软件进行反编译,实现在登录界面添加自动登录功能(该登录界面之前已经有自动登录功能...

    通过安卓修改大师可以很轻松的在任何apk中添加新的代码逻辑,实现额外添加的功能,本次教程通过对一款名为“VMware Horizon”的软件进行反编译,实现在登录界面添加自动登录功能(该登录界面之前已经有自动登录功能,但是未实现相应的逻辑,且自动登录功能按钮在代码中自动隐藏掉了)。

     

    为了方便大家学习,文中用到的apk和java代码请点击后面的连接下载:点击这里下载

     

    1、 反编译安装包,将要反编译的安装包拖拽到安卓修改大师界面上,弹出的界面中选择反编译。系统将自动进行反编译打包。首次修改的时候,建议什么都不改,直接打包,看看有什么问题,并进行修复,确保可以打包运行才进行后续的操作。

     

    2、 修复打包过程错误,在打包的过程中会遇到图示的错误,尝试按照提示的自动修复进行打包,发现并不能进行自动修复。然后看底部的日志,按照日志的要求,按照文件路径打开文件,删除提示的不存在的属性,然后重新编译。

     

    3、 定位要修改的布局和Activity类。重新编译后,项目可以正常在手机上面运行,将手机浏览到登录界面,在安卓修改大师的左侧点击“代码/布局定位”,并点击打开界面的抓取界面布局按钮,就可以获得当前界面的Activity类名和布局文件。请记住上面的类名:

    com.vmware.view.client.android.WindowsPasswordPrompt,后续修改都是在这个文件。

    4、 点击上图右下角红框圈中的“定位布局和代码”按钮,将自动定位和搜索引用这个按钮的布局和代码文件,可以双击布局或者代码进入看布局逻辑或者代码的逻辑。

    5、 找到界面元素对应的在类里面映射的变量的方法,以找到上述的“保存密码”复选框对应的变量为例,按照同样的方法,需要自行找到界面上面的用户名、密码和登录按钮对应的变量。按照上面的步骤抓取界面后,做抓取界面左侧的预览窗口点击相应的元素,然后右侧就会显示这个元素相应的一些属性,如果这个元素有id,就会在右下角的列表中显示“查看布局”的按钮,点击后就是上面步骤看到的搜索界面。然后点击smali结尾的文件(就是程序处理源代码文件),看到的代码如下:

    最好把映射的变量记下来,同样找到用户名和密码的输入框,登录按钮对应的变量,记录下来。

     

    6、 上面是准备工作,下面切入正题。本教程的主要工作是实现自动登录,按照这个逻辑,需要实现两个方法,一个是点击登录后的保存登录信息的功能(保存用户名,密码和保存密码选择框的状态),一个是页面加载的时候读取保存的用户名、密码和保存密码的状态,并自动点击登录按钮进行登录。

    7、 要实现上述功能,需要额外添加代码来实现。一般的做法是先写好一个demo的Android studio项目,把要实现的功能写成一个类,然后确保实现功能并完成功能测试。下面就是实现该逻辑的代码,该代码也在附带的压缩包中。

     

    package com.kongyu.project;

    import android.app.Activity;
    import android.content.SharedPreferences;
    import android.widget.Button;
    import android.widget.CheckBox;
    import android.widget.EditText;

    public class AutoLogin {
        
    //实现自动登录
        public static void autoclick(final Activity context, CheckBox cbRemmber, EditText etUser, EditText etPwd, Button btnSave) {
            SharedPreferences shareData = context.getSharedPreferences(
    "Data", Activity.MODE_PRIVATE);//创建一个给全局使用
            etUser.setText(shareData.getString("UserName"""));
            etPwd.setText(shareData.getString(
    "Password"""));
            cbRemmber.setChecked(shareData.getString(
    "CheckedUser""").toLowerCase().equals("true"));
            
    if(cbRemmber.isChecked())btnSave.performClick();
        }

        
    //保存登录信息
        public static void saveinfo(final Activity context, CheckBox cbRemmber, EditText etUser, EditText etPwd) {
            SharedPreferences shareData = context.getSharedPreferences(
    "Data", Activity.MODE_PRIVATE);//创建一个给全局使用
            SharedPreferences.Editor editor = shareData.edit();//绑定sharePreferences对象
            editor.putString("UserName", etUser.getText().toString());
            editor.putString(
    "Password", etPwd.getText().toString());
            editor.putString(
    "CheckedUser""" + cbRemmber.isChecked());
            editor.commit();
    //注意保存数据
        }
    }

    下面是调用的java代码

    
    
    AutoLogin.saveinfo(this,checkBox,etuser,etpwd);
    
    AutoLogin.autoclick(this,checkBox,etuser,etpwd,btn);

     

     

    同时也要在代码里面写好这两个方法的调用方法,会利用下一步生成上述类和改调用方法对应的smali文件,这是插桩方式插入代码的关键地方。

     

    8、 然后将上述demo项目打包成apk,再用安装修改大师反编译,反编译后即可获得对应的smali代码。

     

     

    反编译后点击该项目后面的“打开目录”按钮,即打开上图显示的反编译后的文件目录,然后浏览smali目录下面,按照类路径依次找到上述的类文件对应的smali类文件(路径不一定 跟我的截图一样,按照实际的你自己定义的类路径来)。

    同时在调用这两个方法的地方,找到反编译后的对应的调用的smali调用方法,后面备用。

       保存信息的smali方式调用方法:

        invoke-static {p0, p1, v0, v1}, Lcom/kongyu/project/AutoLogin;->saveinfo(Landroid/app/Activity;Landroid/widget/CheckBox;Landroid/widget/EditText;Landroid/widget/EditText;)V

     

      自动登录的smali方式调用方法:

        invoke-static {p0, p1, v0, v1, v2}, Lcom/kongyu/project/AutoLogin;->autoclick(Landroid/app/Activity;Landroid/widget/CheckBox;Landroid/widget/EditText;Landroid/widget/EditText;Landroid/widget/Button;)V

     

    9、 下面就进行最核心的代码整合阶段了。代码整合的过程一般是先将写好的代码转变成的smali文件拷贝到目标项目的smali目录下面(只要放到smali下面就可以,具体在哪一个目录无所谓,你为了方便,也可以放到自定义目录例如plugin这样名称的目录下面),如果有资源文件,也照样拷贝到对应的res资源文件目录即可。如果你要整合的代码有依赖包,依赖包的相关反编译出来的smali文件也一并拷贝到新的项目即可,重点要强调的是,如果目标项目已经有了相关的类包,一定不要重复拷贝,会导致编译失败。

    按照上述方法,将上述获得的类smali文件拷贝到目标项目的smali目录下。到目前为止,类文件已经移植完毕。

    10、         下面进行核心的插桩操作。按照上面的分析,需要在登录按钮点击的时候插入保存信息的代码,在页面oncreate加载的地方插入自动登录的方法。大家还记得开头通过界面抓取获得类名吗,可以在那个界面直接点击转到smali,跳转到要处理的smali文件。

    跳转后的代码编辑界面,通过顶部的下拉,找到oncreate方法,并点击进入

    11、         然后在oncreate的末尾插入自动点击的方法

    自动登录的方法需要传入5个参数,分别是当前类p0,保存密码的复选框v0,用户名输入框v1,密码输入框v2,登录按钮v3,这几个界面元素对应的类里面的变量如何获取,请参考前面获取复选框的方法。

     

    12、         在登录按钮点击后,插入自动保存登录信息的调用方法。需要获得点击事件对应的代码位置,下面重点演示如何通过界面抓取来获得按钮对应的点击事件代码对应的位置。

    在界面抓取页面,点击左面的预览窗口的登录按钮,然后右侧会显示该按钮可以查看界面布局和代码,点击“查看页面布局和代码”按钮,跳转到搜索界面。

    搜索结果里面找到上述类名对应的文件,找到sswitch_0这一行,一般情况下,sswitch语句就是用来做判断跳转的,所以这个搜索结果对应的基本上就是该按钮对应的处理方法代码。

    然后双击这一行,并自动打开代码编辑页面,在代码编辑器搜索上述截图的sswitch_0,即可找到这个按钮的处理入口。下面的截图已经将保存登录信息的方法调用添加进去了。

    保存信息的方法需要四个参数,分别是当前的activity的p0,保存密码的复选框v0,保存密码的复选框v0,用户名输入框v1,密码输入框v2。

    到此为止,已经通过代码插桩的方式,将当初在android studio里面开发的java代码逻辑,转变为smali代码,并且在相应的位置插入了smali代码的调用,重新编译打包,在手机上面即可看到运行的效果。

    顺便说一句,原始的apk的保存密码选择框在布局里面有,但是运行后在界面上面看不到,通过分析代码,发现是代码中通过代码隐藏掉了,需要搜索复选框对应的变量,找到处理显示的方法setVisibility方法,然后删除或者注释掉这一行即可。Smali注释某一行的方法是在这一行的前面写#号。注释后,这句代码就不起作用了。

     

    到此为止,已经完整讲述了如何通过插桩的方式,在没有源代码的基础上,在apk里面添加新的功能。大家可以触类旁通,实现更多更复杂的功能。通过安卓修改大师,只要apk可以反编译打包运行,就可以额外添加任意逻辑。

    展开全文
  • 代码插桩技术能够让我们在不更改已有源码的前提下,从外部注入、拦截各种自定的逻辑。这为施展各种黑魔法提供了巨大的想象空间。下面我们将介绍浏览器环境中一些插桩技术的原理与应用实践。 插桩基础概念 前端插桩...

    代码插桩技术能够让我们在不更改已有源码的前提下,从外部注入、拦截各种自定的逻辑。这为施展各种黑魔法提供了巨大的想象空间。下面我们将介绍浏览器环境中一些插桩技术的原理与应用实践。

    插桩基础概念

    前端插桩的基本理念,可以用这个问题来表达:假设有一个被业务广泛使用的函数,我们是否能够在既不更改调用它的业务代码,也不更改该函数源码的前提下,在其执行前后注入一段我们自定义的逻辑呢?

    举个更具体的例子,如果业务逻辑中有许多 console.log 日志代码,我们能否在不改动这些代码的前提下,将这些 log 内容通过网络请求上报呢?一个简单的思路是这样的:

    1. 封装一个「先执行自定义逻辑,然后执行原有 log 方法的函数」。
    2. 将原生 console.log 替换为该函数。

    如果希望我们的解法具备通用性,那么不难将第一步中的操作泛化为一个高阶函数:

    function withHookBefore (originalFn, hookFn) {
      return function () {
        hookFn.apply(this, arguments)
        return originalFn.apply(this, arguments)
      }
    }
    复制代码

    于是,我们的插桩代码就很简洁了。只需要形如这样:

    console.log = withHookBefore(console.log, (...data) => myAjax(data))
    复制代码

    原生的 console.log 会在我们插入的逻辑之后继续。下面考虑这个问题:我们能否从外部阻断 console.log 的执行呢?有了高阶函数,这同样是小菜一碟:

    function withHookBefore (originalFn, hookFn) {
      return function () {
        if (hookFn.apply(this, arguments) === false) {
          return
        }
        return originalFn.apply(this, arguments)
      }
    }
    复制代码

    只要钩子函数返回 false,那么原函数就不会被执行。例如下面就给出了一种清爽化控制台的骚操作:

    console.log = withHookBefore(console.log, () => false)
    复制代码

    这就是在浏览器中「偷天换日」的基本原理了。

    对 DOM API 的插桩

    单纯的函数替换还不足以完成一些较为 HACK 的操作。下面让我们考虑一个更有意思的场景:如何捕获浏览器中所有的用户事件?

    你当然可以在最顶层的 document.body 上添加各种事件 listener 来达成这一需求。但这时的问题在于,一旦子元素中使用 e.stopPropagation() 阻止了事件冒泡,顶层节点就无法收到这一事件了。难道我们要遍历所有 DOM 中元素并魔改其事件监听器吗?比起暴力遍历,我们可以选择在原型链上做文章。

    对于一个 DOM 元素,使用 addEventListener 为其添加事件回调是再正常不过的操作了。这个方法其实位于公共的原型链上,我们可以通过前面的高阶插桩函数,这样劫持它:

    EventTarget.prototype.addEventListener = withHookBefore(
      EventTarget.prototype.addEventListener,
      myHookFn // 自定义的钩子函数
    )
    复制代码

    但这还不够。因为通过这种方式,真正添加的 listener 参数并没有被改变。那么,我们能否劫持 listener 参数呢?这时,我们实际上需要这样的高阶函数:

    1. 把原函数的参数传入自定义的钩子中,返回一系列新参数。
    2. 用魔改后的新参数来调用原函数。

    这个函数大概长这样:

    function hookArgs (originalFn, argsGetter) {
      return function () {
        var _args = argsGetter.apply(this, arguments)
        // 在此魔改 arguments
        for (var i = 0; i < _args.length; i++) arguments[i] = _args[i]
        return originalFn.apply(this, arguments)
      }
    }
    复制代码

    结合这个高阶函数和已有的 withHookBefore,我们就可以设计出完整的劫持方案了:

    • 使用 hookArgs 替换掉传入 addEventListener 的各个参数。
    • 被替换的参数中,第二个参数就是真正的 listener 回调。将这个回调替换为 withHookBefore 的定制版本。
    • 在我们为 listener 添加的钩子中,执行我们定制的事件采集代码。

    这个方案的基本逻辑结构大致形如这样:

    EventTarget.prototype.addEventListener = hookArgs(
      EventTarget.prototype.addEventListener,
      function (type, listener, options) {
        const hookedListener = withHookBefore(listener, e => myEvents.push(e))
        return [type, hookedListener, options]
      }
    )
    复制代码

    只要保证上面这段代码在所有包含 addEventListener 的实际业务代码之前执行,我们就能超越事件冒泡的限制,采集到所有我们感兴趣的用户事件了 :)

    对前端框架的插桩

    在我们理解了对 DOM API 插桩的原理后,对于前端框架的 API,就可以照猫画虎地搞起来了。比如,我们能否在 Vue 中收集甚至定制所有的 this.$emit 信息呢?这同样可以通过原型链劫持来简单地实现:

    import Vue from 'vue'
    
    Vue.prototype.$emit = withHookBefore(Vue.prototype.$emit, (name, payload) => {
      // 在此发挥你的黑魔法
      console.log('emitting', name, payload)
    })
    复制代码

    当然了,对于已经封装出一套完善 API 接口的框架,通过这种方式定制它,很可能有违其最佳实践。但在需要开发基础库或开发者工具的时候,相信这一技术是有其用武之地的。举几个例子:

    • 基于对 console.log 的插桩,可以让我们实现跨屏的日志收集(比如在你的机器上实时查看其他设备的操作日志)
    • 基于对 DOM API 的插桩,可以让我们实现对业务无侵入的埋点,以及用户行为的录制与回放。
    • 基于对组件生命周期钩子的插桩,可以让我们实现更精确而无痛的性能收集与分析。
    • ……

    总结

    到此为止,我们已经介绍了插桩技术的基本概念与若干实践。如果你感兴趣,一个好消息是我们已经将常用的插桩高阶函数封装为了开箱即用的 NPM 基础库 runtime-hooks,其中包括了这些插桩函数:

    • withHookBefore - 为函数添加 before 钩子
    • withHookAfter - 为函数添加 after 钩子
    • hookArgs - 魔改函数参数
    • hookOutput - 魔改函数返回值

    欢迎在 GitHub 上尝鲜我司这一开源项目,也欢迎大家关注这个前端专栏噢 :)

    P.S. 我们 base 厦门的前端团队活跃招人中,简历求砸 xuebi at gaoding.com 呀~

    展开全文
  • 行业分类-物理装置-代码插桩检测方法、装置、设备及介质.zip
  • 咨询方法-基于代码查询进行源代码插桩的方法.zip
  • Android 进阶——代码插桩必知必会之初识ASM7字节码操作库完全攻略(一) Android 进阶——代码插桩必知必会之ASM7字节码操作库实战完全攻略(二) 一、ASM库概述 ASM 是一个 Java 字节码 (.class)操控...
  • LLVM的核心思想是将各种语言解析成LLVM的中间表示语言(LLVM IR),然后LLVM通过各种pass在IR上进行优化,最后通过各种代码生成后端生成目标机器上的机器代码,这样就可以最大程度重用前端解析、中端优化以及后端代码...
  • 说到代码插桩,你可能会想到 AspectJ、Transfrom Api + ASM 等等。 代码插桩的用处自不必说,可以做埋点、热修复、组件化路由等等。 然而,AspectJ感觉不好用,ASM 比较复杂,需要自定义 gradle 插件。好在前段时间...
  • 代码插桩【转载】

    千次阅读 2011-06-21 11:59:00
    【转载】http://wiki.dzsc.com/info/5081.html   代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试来说又是至关重要的。在对CodeTeST 中插桩技术研究的基础上,以GCC ...
  • Java ——代码插桩必知必会之Java字节指令码和Java文件概述 Android 进阶——代码插桩必知必会之初识ASM7字节码操作库完全攻略(一) Android 进阶——代码插桩必知必会之ASM7字节码操作库实战完全攻略(二) 一、...
  • } } 通过如上代码我们发现最后调用AccessibilityDelegate实例对象方法sendAccessibilityEvent(this, eventType),那么是否可以重写AccessibilityDelegate类和sendAccessibilityEvent(this, eventType)来完成埋点...
  • dynamorio插桩工具

    2015-06-07 19:07:53
    此工具可以对被测程序进行自动插桩,用起来十分好用,方便,快捷,无痕迹。
  • 在android开发的过程中,不管是开发SDK...今天就来给大家介绍一个,利用AOP技术,字节码编译期方法插桩的方式,统计android耗时的工具。 github地址:https://github.com/miqt/PluginTools 这是一个android 方法耗时
  • smali代码插桩打印日志

    千次阅读 2016-10-26 14:02:08
    1.将以下代码复制并保存为"log.smali"到你本地,文件名可任意,后缀名必须为“smali”。 .class public Lcrack; .super Ljava/lang/Object; .source "crack.java" .method public static log1(Ljava/lang/String;)V...
  • 一、背景 ... 通过fiddle抓包,然后将想替换的那个js代码复制出来,然后更改代码! 在fiddler中设置替换规则! 启动规则: 备注:在这里可能会遇到一个问题!如下: 此问题,勾选unma...
  • 字节码注入 入口 想要不修改原始的java代码,并且实现SDK的代码的集成,实现初始化init方法的调用,就不得不在编译或者打包的过程中干预原始代码的编译结果。安卓APK文件的编译过程。从宏观上是: .java -->.class ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,014
精华内容 2,005
关键字:

代码插桩