在分析插件、业务组件、加密模块或者旧系统的接口库时,我们常常会碰到两个问题:一是怎么让IDA Pro动态调试一个dll文件,并把它附加到进程上;二是断点设了之后为什么总是不管用,该怎么排查,dll本身不像普通的exe程序那样可以直接跑起来,它需要被一个宿主进程加载进内存之后,才能进入被调试的状态,所以操作的关键并不是“打开dll就把断点打上”,而是要先找到正确的宿主进程,确认这个dll已经被加载进去了,然后再让IDA Pro在对应模块的地址上准确地把断点命中。
一、怎样把dll附加到进程上进行动态调试
用IDA Pro动态调试dll,第一步就要弄清楚这个dll是由哪一个进程加载的,这个进程可能是主程序的exe文件,也可能是某个系统服务、浏览器的插件容器、测试工具或者自动化脚本的进程,一旦进程选错了,后面就算下再多的断点,也不会被触发。
1、先拿IDA打开这个dll
在IDA Pro里点击“文件”菜单下的“打开”,选中你要分析的那个dll文件,然后等着它自动分析完,打开之后,先去看一看“导出”窗口,确认一下那些导出函数的名字、序号还有入口的位置,如果目标函数并不是导出的,那就需要通过字符串、交叉引用和调用链这些线索来把它的位置找出来。
2、选好本地的调试器
点击“调试器”菜单,再去选“选择调试器”,一般情况下就选“本地Windows调试器”,如果是要调试远程机器或者虚拟机里的东西,那就要先把远程调试的服务给配置好,这里还要注意一点,dll的位数必须和宿主进程的位数一致,32位的dll只能被32位的进程加载,64位的也只能进64位的进程。
3、附加到宿主进程上去
先把那个会加载这个dll的主程序运行起来,然后回到IDA,点击“调试器”菜单里的“附加到进程”,在跳出来的进程列表里面选到对应的那个进程,附加成功之后,IDA就会让这个进程暂停下来,这时你可以去看“调试器”菜单下的“模块”列表,确认一下目标dll是不是已经被加载进来了。
4、在模块被加载之后再去下断点
如果dll已经被加载了,那就可以在目标函数、DllMain、导出函数或者那些关键的调用位置按F2把断点设好,再按F9让程序继续跑,如果dll还没有被加载,那就先去主程序里触发一下相关的功能,等这个模块出现在“模块”列表里以后,再跳到模块基址所对应的位置去下断点。
二、断点失效的时候该怎么排查
用IDA Pro动态调试dll时发现断点没起作用,别光是反复地按F9,断点失效比较常见的原因,是模块还没加载、地址因为ASLR发生了变化、代码根本就没有执行到那个地方、断点下在了一个错误的副本上,或者是调试器没能够成功地把断点写进去。
1、查一下dll是不是真的被加载了
进到“调试器”菜单的“模块”里,看一看目标dll在不在这个列表当中,如果列表里根本没有,那就说明宿主进程当前还没有加载它,这时候可以先试着去执行一下能触发它加载的功能,或者去检查一下配置文件、插件目录、注册表路径、工作目录这些地方是不是对的。
2、查一下断点的地址是不是发生了偏移
很多dll都会启用ASLR,所以每次加载时的基址可能是变化的,你在静态分析里看到的地址,不一定就是它运行时的地址,应该在“模块”列表里查看dll运行时的基址,再通过函数的偏移重新把断点定位出来,不要只盯着静态窗口里面的那个原始地址。
3、查一下断点是不是被下在了一条不会走到的路径上
有些函数看起来像是关键逻辑,可实际运行的时候,程序走的却是另一个版本、另一个分支或者另一个导出函数,可以先在DllMain、导出函数的入口、有明显字符串引用的地方附近设下断点,先确认程序是不是真的进到了这个dll里面,然后再一步一步地把范围缩小。
4、查一下软件断点和硬件断点的情况
如果目标代码段被保护、被校验或者被恢复过,那普通的软件断点就有可能会被覆盖掉,你可以在断点上点右键,试着把它换成硬件断点,或者是在调用这个函数的上层位置先让程序停下来,再单步跟进去,注意不要在同一个地方堆上好几个断点,那样反而容易把排查的思路给弄乱。
三、怎样提高动态调试dll时的断点命中率
想让IDA Pro在调试dll时断点更容易命中,关键是要把加载的时机、模块的地址、触发的路径还有参数的来源都摸清楚,很多断点并不是失效了,而是始终没有等到正确的执行条件。
1、先在模块的入口处下断
可以先在DllMain或者导出函数的入口把断点打上,确认一下dll的加载、初始化以及调用过程是不是符合自己的预期,命中之后再去查看调用栈,判断是谁把这个dll加载起来的,接下来再从调用方那里继续往下追,这样会更稳当一些。
2、结合字符串和交叉引用来定位
如果目标功能会输出一些报错信息、日志文本、协议字段或者界面上的文字,可以先在“字符串”窗口里找到这些相关的字符串,再按X键去看看它被引用的位置,这样找起来,比凭感觉在大量函数里面到处下断点要快得多。
3、把运行时的参数记录下来
断点被命中之后,去看一看寄存器、栈、局部变量以及内存窗口里面的内容,特别是函数的参数、对象的指针、缓冲区的地址还有返回值,要把它们和静态分析的结果放到一起去对照,免得把一个包装用的函数,错当成了核心的逻辑。
4、尽量减少环境带来的干扰
在开始调试之前,把无关的插件和重复的进程都关掉,确认只运行着一个宿主程序的实例,如果系统里同时存在着好几个同名的dll,那就一定要在“模块”列表里把完整的路径检查清楚,免得IDA断在了A目录下的dll上,而实际程序加载的却是B目录下的dll。
总结
总的来说,IDA Pro动态调试dll时怎样去附加进程,以及断点失效了又该怎么去排查,核心都是要先找对宿主进程,然后再确认dll的加载状态和运行时的模块基址,附加进程之后,通过“模块”列表去核对路径和地址,在导出函数、DllMain或者关键的调用点上把断点设好,要是断点没有命中,就重点去检查模块是不是真的加载了、地址是不是发生了偏移、程序是不是真的走到了那条路径上、断点的类型合不合适,把这些环节都查清楚了,dll的动态调试就会比盲目下断点要稳定得多。
