一个lldbtrace的demo

用一个demo来聊聊动态trace

这个仓库能做什么?

帮助理解动态trace的思想。仓库内的demo,可操作,可实践。
动态trace核心思想:
动态记录一个函数内每一条指令的执行中产生的关键信息,并导入IDA,用来弥补IDA等静态分析工具的不足。

反编译看一下

先clone仓库,把hellolldb.app拖到IDA里分析一下。这个hellolldb.app是我自己造的demo。demo只有按钮,点击按钮之后会触发回调onBtnClick,回调里会弹个窗,并打印一行log。我手动对demo加了一些常见的混淆。下面的截图是混淆后的代码。自己逆一下自己的demo,IDA7.0 :

  • Function Window里看不到函数名
  • 有DCQ
  • 有br切开整个函数的body
  • 整个函数(开头结尾)没被识别,无法F5

开始动态执行:

把helllolldb.app拖到MonkeyDev里,并且在onBtnClick下个断点。点击按钮,断点断住。
lldb-trace里的导出json的路径改成你自己的:


26 filename = "/Users/bliss_ddo/Desktop/%d.json"%(time.time())

输入script 进入交互式脚本执行环境:


(lldb)script

在交互式环境导入lldb-trace.py


>> exec(open('/Users/bliss_ddo/Desktop/lldb2ida/lldb-trace.py').read())

执行脚本里的函数:


Tracer().tracehere(True)

回车,执行,得到一份运行时的json。把json改个名,叫1.json

这个脚本做了什么?

  • 记录函数的开头和结尾(后续在IDA里修复函数)
  • 对每一行指令打个断点,并注册断点hit的回调。
    • 记录每一行指令的内存地址,值
    • 对于br指令,记录读取br后寄存器的值,用来修复成b指令
    • 记录断点hit的count,后续修复用。
  • 找到当前断点的上级,也就是lr寄存器的值,在这个地方断点并注册回调,用来标记trace结束。trace结束时,把上述的运行记录导出到一份json里。

看一下json里的内容

用json-formatter.py来看一下这个json里的内容,终端运行


python /Users/bliss_ddo/Desktop/lldb2ida/json-formatter.py

第一列,index,

第二列,激发次数

第三列,带aslr的地址

第四列,不带aslr的地址

第五列,汇编助记词和操作数

第六列,内存里的值

第n列,任何逆向trace的值,脚本自己扩展

把这个JSON搞到IDA里

打开IDA,在下方的交互式环境里输入


exec(open('/Users/bliss_ddo/Desktop/lldb2ida/ida-fix.py').read())

脚本做了什么事?看脚本的最后几行。


fixer = Fixer("/Users/bliss_ddo/Desktop/1.json") //读取动态trace的json

fixer.processJSON() //解析一下

fixer.fix_function_range() // 调用idc的函数,重新确定函数开始和结尾

fixer.fix_br() // 判断br的寄存器值是否落在函数内,如果在函数内,将其修复成b (reg-pc) b的计算规则见下文

fixer.fix_unknow_as_nop() // 讲DCQ里无法被mark as code的变成nop

# fixer.fix_unexec_as_nop() // 这句比较危险,把没被执行的代码变成nop,可以去掉虚假控制流,当然也有可能误杀。在逆一些签名加密之类的比较管用。这里就认为动态执行的路径,就是最最最正确的路径

# fixer.restore() //出错的时候的时候把IDA恢复回去。。。

记得把patch应用到原来的二进制里


IDA菜单-->Edit-->Patch program --> Apply.......

再次再MonkeyDev里运行一下修复后的二进制,ok能用。

后记

也就仅仅是用手头工具做了个demo。

这个过程中调研了很多的工具,也涨了很多姿势,工具没有十分趁手的,就自己做了个小demo。

道阻且长,一起加油。

仓促行文,代码杂乱,见谅。

明天更新b指令计算方式。

8 Likes

一个疑问:基于寄存器的br跳转,这个br如果是在循环里,是把每次br都记录了吗?

如果br 在循环里,并且是每次xn 的值不一样,那这种patch就不行了。
目前我的经验来看,一般都是用br 来对抗动态分析

膜拜大佬~_~

1 Like

牛批 牛批

牛逼牛逼 改天试一下.

更新一下b指令的计算方式。
arm文档有写
https://developer.arm.com/documentation/ddi0596/2021-06/Index-by-Encoding/Branches--Exception-Generating-and-System-instructions

arm的指令是4字节,一共32个bit,第31位是0或者1决定指令是b还是bl,然后00101是写死的,后边的26个bit是立即数,也就是b后边的相对地址。

所以一段python搞定

def b(op):
	return 0b00010100000000000000000000000000|(int(op/4) & 0b00000011111111111111111111111111)

为什么op要除以4?
因为arm的指令是4字节,pc+1其实是加了1(单位),也就是4字节。不管是1还是4,把单位搞一致就行了。

终端里测试一下

ojbk

上班路上我又想了一下这个问题。其实这是一类通用问题。for循环里的br。把问题再往外拓展一下,就变成了 对于运行时的变化的指令(每次运行操作数不一样),如何解决。但是这其实是另外一个问题了。这种问题就要具体问题具体分析了,没有什么银弹能够一劳永逸的解决。
比如,trace的时候,对断点引用计数,对于一个函数,采样1000次,也就是给1000个输入,最后这1000次的采样结果里,始终没被执行的指令,大概率就是虚假控制流。 当然也有可能是一些异常的判断的代码,这些代码也可以通过造输入被覆盖。
如果采样过程中,某些代码的引用计数是常数,比如某一行代码就被执行了50次,那么这个50明显就是个循环次数,进行了50轮加密。这些都得靠经验一点点去分析了。

总之,采样就是尽可能多的拿到运行时的信息,然后去弥补动态分析信息不足的缺陷。

说实话,这种断点的方式还是山寨了一点,不过也就是验证个概念,俗称poc。
要想做成生产力工具,还是要花点功夫。这个过程中我看了一些乱七八糟的,比如xcode代码覆盖率检测工具llvm-con,frida-stalker,ida自带的trace功能,retdec把bin转伪码(之前我想的是在xcode的lldb里做个F5功能),以及一大堆乱七八糟的工具。还是有一定收获的。

2 Likes