DMCK-ptrace
Comment稍微总结一下技术路线:
首先,被测方注册信号处理函数,并分配好信号执行的栈空间(sigaltstack)。两个处理函数,分别用于保存和恢复进程。
保存,具体来说就是把堆、栈、全局变量全都dump到文件。对于例子而言,这个文件大约是500KiB。压缩后是5KiB。
恢复,从文件中把数据再拷进来。首先不能破坏信号执行的栈区,要保留好 restore 函数的执行环境;其次要修改 restore 结束后返回的位置(主要是栈指针。pop rbp 之后,rbp要回到 dump 发生的位置)。
1 | pushq %rbp // rbp 是 restore 发生时的 user_stack 上的地址 |
- 总结下来,restore 不能破坏 restore 的栈结构,但是要把 push 的 rbp 换成 dump 下来的。之后 rsp 回到正常位置后,自然会取到正确的返回地址。
问题:
1、FILE * 是分配在堆上的。这带来的问题是,恢复了上次的堆空间之后,FILE * 所指的内容随着执行进度的不同,也会发生变化
解决A:使用操作系统的文件描述符表。此处用栈上 fd 代替堆上 FILE *。仍然需要管理操作系统对象,并且不能 kill 进程,每一个分支都得保留一个 proxy 进程用来维持系统资源。尝试过感觉稍微有点困难。
- 困难点在于,fork() 的 proxy 进程,脱离了 tracer。在有 tracer 的情况下,当 read() 系统调用被 SIGRESTORE 打断后,可以控制 read() 重新执行。在没有 tracer 的情况下,打断了就是打断了。
- 当然我们可以直接在打断失败的调用基础上修改返回的数据,这样单一调用的结果没问题。但是文件描述符的状态就没有发生变化了。
解决B:不使用操作系统的文件描述符表,而是全部自己模拟(全模拟有大量细节要考虑。类似于 select/epoll 的行为仍不明确。但是估计最终是这样)
2、随机性的产生 (choose())
取决于被测程序使用何种随机。考虑以下简单情况:
1 | int devrandom; // open("/dev/urandom", O_RDONLY); |
这种还是有一个系统调用可以控制的。但是如果面对这种使用场景:
1 | int determ = rand() % 2; |
read 的返回值很多,但是最终我们只要两个分支。根据概率分布的不同,我们可能需要走好多遍才能遍历两个分支。
故而,此类函数全都需要劫持或从代码上改写为 choose(N)。
choose(N) 要做什么?
- 构造一棵执行路径树。每个节点都需要有可能的取值范围,这个可能需要手工确定。
- 获取当前的执行路径。产生新的取值
- 让被测程序走到新的节点
rand.invoke() –> 被 ptrace 截停并通知父进程 –> 父进程发信号使 rand 被拦截,并保存程序状态
–> 父进程根据当前状态查表得知此处 rand 调用的取值可能
–> 对每种可能,新启动实例并给予返回值,运行至下一个选择的状态然后被 ptrace 捕获
ptrace 如何拦截用户函数?查询符号表,在函数开头插入 0xcc,这样就会触发 SIGTRAP… 接着只需要设置 rax 作为返回值,然后 ret。
1 | void *rand = symlookup("rand"); |
所以这个 rand 也不是真正执行的
只要算好了 rand 的可能取值,就可以刚刚好跑过所有分支不重复。
3、怎么对其他程序应用此方案
- 如果不用 signal handler,而是用 PEEKDATA 的方式 dump 进程,可以想象到效率
- 但进程信号表并不能 survive execve。如果原来的 elf 里没有这个 handler,没有办法后加
- 魔改elf感觉最稳。硬往里加text和data,有一定希望
目前就局限在 C/C++ 代码的单进程、单线程程序。
4、牵扯到线程调度怎么办
还没有调查如何控制并发调度。甚至还不知道 ptrace 一个多线程程序会发生什么。