调试实战 | 通过转储文件分析程序无响应之使用 windbg + IDA 逆向篇


缘起

最近,接连在项目中遇到了两个界面无响应的问题。都只发生在客户特定机器上,不方便直接调试,只能抓取 dump 进行事后分析了。

抓取 dump

远程连上可以重现问题的机器,使用 process explorer 初步观察卡死的进程,发现 CPU 占用率很低,经过一段时间的观察,基本确定是一个死锁问题。在卡死的进程上右键,保存完整转储,压缩,发回本地进行分析。

使用 windbg 进行分析

双击抓取的 dump 文件,因为之前已经执行过 windbg.exe -IA,所以默认会通过 windbg 打开 dump 文件。先使用 ~*kvn 粗略浏览一下每个线程的调用栈(因为比较长,这里就不截图了)。经过观察,很快锁定了两个值得进一步查看的线程:一个是主线程(界面线程),因为是界面无响应,肯定要关注界面线程。另外一个是 7 号工作线程。分别看一下这两个线程的调用栈。

主线程的调用栈如下图所示:

注意上图红色高亮部分,主线程通过 SleepConditionVariableCS() 进入等待。

看完主线程,再看 7 号工作线程的调用栈,如下图所示:

7 号线程对 SendMessage() 的调用非常值得怀疑。

猜测整个流程是这样的:主线程不知由于什么原因进入等待状态,而工作线程由于各种各样的原因也进入了等待状态。其中 7 号线程最明显,因为它正在发消息,而主线程此时是无论如何也不会响应这个消息的。

于是,典型的死锁再一次发生了。


加载 AssemblyDesign_Tools.dll 的符号后,查看对应的源码,消除对 SendMessage() 的调用,问题解决!so easy!

说明:工作线程中并没有直接调用 SendMessage(),而是调用了操作界面的相关 API,间接调用了 SendMessage()  给界面线程发消息。

死锁的问题解决了,但是为什么向主线程发个消息就死锁了呢?秉着打破砂锅问到底的原则,我又开始折腾了。下面的内容适合喜欢调试逆向的极客阅读。

深入调查

最开始的思路是:查看主线程在等待的条件变量,然后再调查哪个工作线程会唤醒这个条件变量。奈何 64 位下,前四个参数通过寄存器 RCX, RDX, r8, r9 进行传递,如果这些寄存器没有在栈上存储一份的话,很难查看具体的值。折腾一番后,确实没找到有用的信息,而且就算找到了,也很难找出是哪个线程会执行唤醒操作。

这个死锁问题不像关键段死锁解决起来那么直接。不能直接通过命令(!cs -l),或者查看调用栈就能直接理出头绪。看来只能硬着头皮逆向分析相关代码了。

0 号线程和 7 号线程最值得怀疑,其它线程基本可以排除。先看看主线程为什么会等待吧。

主线程逻辑

找到调用 BentleyG!Bentley::BeConditionVariable::WaitOnCondition()  的地方,也就是 5 号栈帧。

IDA 中打开 MobileDgn.dll,并找到这个函数,然后按下神奇的 F5

可以看到,主线程在陷入等待之前,向工作线程发送了一个任务,也就是 sub_7FEDAC749A0,传递的参数是 v5v5偏移 88的位置保存了 BeConditonVariable类型的变量,也就是 WaitOnCondition()所等待的变量。猜测:sub_7FEDAC749A0内部会唤醒这个BeConditionVariable, 如果 sub_7FEDAC749A0被顺利执行,那么主线程的等待自然就结束了。

先看看 sub_7FEDAC749A0 的反汇编代码,当然是直接看 F5 后的伪代码了。

可以很明显的看到 sub_7FEDAC749A0内部调用了 Bentley::BeConditionVariable::Wake((CMFCRibbonInfo::XElementButtonUndo *)((char *)v1 + 88), 1);

从函数名就可以猜到是用来唤醒 BeConditionVariable 的。

如此看来,sub_7FEDAC749A0 很有可能还没有被执行,工作线程就挂起了。一起来看看 SendToWorkThread 是怎么把任务发送到工作线程的。

SendToWorkThread()会先判断是否在工作线程运行,如果是则直接执行对应的函数,否则就根据参数生成一个 RpcMessage,然后发送这个新生成的 RpcMessage到工作线程的任务队列中。

再来看一下生成 RpcMessage 的函数,我把这个函数命名为 MakeMobileDgnRPCMessage()

一定要记住这里的关键信息,后面会根据这里的关键信息验证。HandlePaint()传过来的函数地址是 0x7FEDAC749A0,保存在了 rpcMsg偏移 128的位置。MobileDgnRPCGenericMessage类的虚表地址是 000007FEDAD19658

继续追踪 SendAsynToWorkThread(),如下图:

继续追踪 HandleRpcMessage()(这个名字是我命名的,不是 IDA识别的),传给它的第一个参数是一个全局变量(姑且命名为 g_taskQueueManager),第二个参数是要发送的 rpcMsg,第三个参数 v3的值是 2,第四个参数是 1

这个函数比较长,我只逆了个大概,大体思路是先检查是否存在,不存在则插入。如果队列已经满了,需要等待工作线程从队列中取走一些任务才返回。

至此,基本理清了主线程相关的逻辑。大体是这样的:主线程在处理 HandlePaint()的时候,先发送一个任务给工作线程,(通过 SendToWorkThread()发送到工作线程的任务队列中),然后通过 BeConditionVariable::WaitOnCondition()等待这个任务结束。

看完界面线程,再来再看 7 号工作线程的相关逻辑。

工作线程逻辑

7 号工作线程,只需要关心 9 号栈帧对应的函数。

注意,_RunThread+0x1a3c,这个偏移有点大。由于缺少符号,这里很有可能只是以 _RunThread作为参照得到的一个偏移,实际对应的是另外一个函数的代码。使用 ub向前查看反汇编,很快定位到正确的函数首地址。
从上图可知,9号栈帧对应的函数起始地址是 000007fe dac5fee0。怎么在 IDA中找到这个函数呢?如果知道这个函数相对于其所在模块的偏移,就可以算出在 IDA中的地址了。该怎么获取这个地址对应的模块基址呢?在 windbg中执行 lma address就可以知道一个地址对应的模块信息了。
得到模块基址(0x000007fe dac10000)后,可以很简单的计算出偏移量为 0x4fee0。有了这些信息就可以在 IDA中找到这个函数了。

小贴士:也可以在 IDA 中调整模块基址,使其与 windbg 保持一致。这样就不用根据偏移在 IDA 中手动计算地址了。

得到要查看的函数地址后(我在 IDA 中执行了 Rebase program,所以是 000007fe dac10000),在 IDA 中直接按快捷键 g,输入地址后即可跳转到输入的地址处。

注意:如果在 IDA 中以 16 进制输入地址,请加上 0x 前缀,而且不要带重音连接符。

再次按下神奇的 F5

至此,工作线程的逻辑也理清了。简单总结如下:工作线程是一个循环,不断从任务队列取任务执行,如果设置了唤醒标记位,那么需要在执行完任务函数后,唤醒等待的线程。

验证猜想

好了,花费了这么多精力终于理清了主线程和工作线程的交互逻辑。目的只有一个,就是为了更好的验证之前的猜想:工作线程还没有来得及执行主线程过来的任务就挂起了。

如果猜测是正确的,那么工作线程的任务队列中应该还保留着这个未执行的任务。接下来的任务就是来找到这个未执行的任务。

通过上面对主线程和工作线程的分析,工作线程的任务队列中应该有类型为 MobileDgnRPCGenericMessage 的对象,并且该对象偏移 128 的位置的值为 0x7FEDAC749A0。根据这两条关键信息在内存中搜寻一下符合条件的记录。

windbg 中输入命令 s -q 0 L?fffffffffffffff 000007FEDAD19658,根据虚表地址搜寻 MobileDgnRPCGenericMessage 类型的对象。找到了两条符合条件的记录。

再输入 s -q 0 L?fffffffffffffff 7FEDAC749A0 根据 sub_7FEDAC749A0 的地址搜寻包含这个地址的对象。找到四条符合条件的记录。

我们关心的是这两次搜寻结果相差 128 的记录(因为根据之前的分析,workProc 存储在偏移为 128 的位置)。经过肉眼观察及在 windbg 中计算(? 00000000300b4ea0 - 00000000300b4f20),得到了一个符合条件的对象的地址 00000000 300b4ea0。整个过程如下图:

找到了满足上面条件的对象地址,还需要确认这个对象是否在工作线程的任务队列中。工作线程的任务队列由全局变量 g_taskQueueManager管理,该变量是一个指针,指针所在的地址是 000007FEDADAA380,指针的值是 00000000024d9fa0

根据之前的分析猜测,偏移为 8 的位置记录了任务队列的开始位置,偏移 16 的位置记录了任务队列的结束位置,偏移 24 的位置记录了任务队列缓冲区结尾的位置,这个任务队列很有可能是通过 vector 管理的。在 windbg 中查看 g_taskQueueManager 的内容。

查看任务队列起始位置保存的记录信息,输入 dq 0000000037e33ce0,然后与 s -q 0 L?fffffffffffffff 00000000300b4ea0得到的搜索记录对比,发现有一条是吻合的。
看来,主线程发送给工作线程的任务确实还没有被执行,工作线程就挂起了。

总结

这个偶发的挂起 bug 终于算是解决了。整个过程多亏了强大的 IDA 的强力支持。使整个分析过程简单了 N 倍。最后,对整个分析过程中用到的技术点做一个简单的总结:

  • IDAF5 真香。

  • 可以在 IDA 中通过 Rebase program 调整模块基址。

  • IDA 中按 g 可以跳转到输入的地址。

  • IDA 中的地址需要有 0x 前缀,不要包含 windbg64 位地址的地址连接符。

  • 可以在 windbg 中通过 lm a address 得到一个地址对应的模块信息。

  • windbg 中可以通过 ub 进行反向反汇编。

  • windbg 中可以根据虚表地址搜寻对应类型的变量在内存中的位置。

  • 在工作线程调用界面相关的 API 时,是通过给界面线程发消息的方式实现的。

参考资料

  • SleepConditionVariableCS msdn[1]

References:

[1]

SleepConditionVariableCS msdn: https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepconditionvariablecs

感谢你的分享,点赞和在看

欢迎留言交流!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306181.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

跟我一起学Redis之高可用从主从复制开始

前言现在遇到高并发场景时,缓存技术应该算是性能优化的第一步,缓解数据库压力的同时还能提高访问效率,而Redis应该是绝大多数应用场景的首选。但是尽快Redis性能再优秀,在当今高并发场景下,一台服务器负责读写&#xf…

【.Net core】EFCore——Code First生成数据库与表

Code First——领域设计模式中非常有用。使用 Code First 模式,专注于领域设计,创建领域类,然后生成数据库。1.创建数据模型类一般就是数据库里面有哪些表,就创建哪些模型, POCO 类就够了。public partial class SmsPu…

mysql 备份配置文件_mySQL配置文件、备份与恢复

mysql配置文件mysql的配置文件为/etc/my.cnf配置文件查找次序:若在多个配置文件中均有设定,则最后找到的最终生效/etc/my.cnf --> /etc/mysql/my.cnf --> --default-extra-file/PATH/TO/CONF_FILE --> ~/.my.cnfmysql常用配置文件参数&#xff…

oracle java写目录权限_oracle分配权限 学习笔记--转载

在全局数据库ORCL下创建一个用户首先在开始--》运行——》sqlplus,然后输入 sys/change_on_install as sysdba以sys权限登陆进去然后可以进行操作:创建用户 create user test indentified by test;这样就创建了一个用户名密码都为test的用户但这个时候te…

龙芯.NET正式发布 开源共享与开发者共成长

2020年12月19日,2020中国. NET开发者大会于苏州盛大开幕。本次大会以“开源、共享、创新”为主题,以线下城市苏州为中心,覆盖北京、上海、深圳、广州、长沙、成都、厦门、胶东等地区,是中国 .NET 开发者的大聚会,线上线…

划入 .NET 6版本目标,微软鼓励开发人员信任第三方库

喜欢就关注我们吧!鉴于许多 .NET 社区的开发人员都不愿意使用非微软官方打造的库,近日,微软 .NET Framework 团队的项目经理 Immo Landwerth 发布了一个名为”Growing the .NET ecosystem“的文档,以鼓励 .NET 开发人员加强对第三…

归并排序 java 迭代_经典排序算法之归并排序(示例代码)

归并排序(英语:Merge sort,或mergesort),是创建在归并操作上的一种有效的排序算法,效率为(大O符号)。1945年由约翰冯诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以…

新鲜高频笔面试题分享,Redis、MongoDB、ElasticSearch...

2020年可能是最短的一年,上班没几个月就发现要元旦了;2020年可能是最难的一年,各种降薪被离职风波;然而好消息是,2021年马上来了,跳槽季也要来了,一起开始备战吧,新年薪资up up&…

java读取整数列表_Java-检查整数列表中的X类整数

由于我没有足够的声誉来编辑我的文章,下面是我的答案gblodgett:嘿!谢谢你的回复。我正在做一个法克尔的游戏,因此我需要找出有多少1,2,3,4,5,6在列表中,然后给分(遵循规则)。我做了一个代码,似乎工作,尽管必须有一个更有效的方法来做。请随便看一看并给我一些反馈:公共int calc…

在 Windows 服务中托管 ASP.NET Core

概述众所周知,ASP.NET Core采用了和传统ASP.NET不同的托管和HTTP处理方式,即把服务器和托管环境完全解耦。ASP.NET Core内置了两个HTTP服务器实现,一个是基于libuv实现的Kestrel(支持跨平台),一个是基于Win…

java对文件的操作详解_Java 对 Properties 文件的操作详解及简单实例

Java 对 Properties 文件的操作详解及简单实例发布于 2020-8-7|复制链接摘记: Java 对 Properties 文件的操作简介在 Java 中,我们常用 java.util.Properties.Properties 类来解析 Properties 文件,Properties 格式文件是 Java 常用的配置文件&#xff0…

目录 | 数据结构与剑指Offer系列推文合集

【目录合集】| 作者 / Edison ZhouC#刷数据结构剑指Offer不知不觉,C#刷剑指Offer的系列推文就结束了,今天就把它们整理成目录合集。此外,考虑到剑指Offer和数据结构的关联,建议没有数据结构基础的童鞋先看看数据结构,再…

mysql数据库5120_超傻瓜 H3C S5120 限速配置

1出方向限速system-viewinterface GigabitEthernet ?/?/? (?是所要限制的端口号)qos lr outbound cir 数字 (cir是速率的意思,数字是kbps,如果按字节需要x8转换,必须是16的整数倍)出方向配置完毕2入方向限速1.system-viewacl n…

ricky java photos_【Melee】Ricky blog updates and new photos

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼Blog FirstThursday, August 21, 2008Celebrate!Ok, so my computer is finally back in full swing, but sadly, Im missing a couple weeks of updates, which will be slightly complicated, racking my brain to remember, but…

Abp vNext异常处理的缺陷/改造方案

之前吐槽Abp的用户/租户管理模块!今天我又来了,这次我给Abp官方repo提了一个issue。目前Website使用Abp vNext开发,免不了要全局处理异常、提示服务器异常信息。1. Abp官方异常处理Abp项目默认会启动内置的异常处理,默认不将异常信…

java原生的编译软件_Java 转原生平台代码 RoboVM

软件介绍编者注:RoboVM 项目已经关闭,目前有开源替代产品BugVM。RoboVM 编译器可以将 Java 字节码翻译成 ARM 或者 x86 平台上的原生代码,应用可直接在 CPU 上运行,无需其他解释器或者虚拟机。RoboVM 同时包含一个 Java 到 Object…

开源项目葫芦藤:IdentityServer4的实现及其运用

前言本篇文章主要是讲解葫芦藤项目中对IdentityServer的实践使用,为了使您对本篇文章中所讲述的内容有深刻的认识,并且在阅读时避免感到乏味,文中的内容不会涉及太多的基础理论知识,而更多的是采用动手实践的方式进行讲解&#xf…

facade java_Java设计模式之Facade模式

Java设计模式之Facade模式 GOF《设计模式》一书对Facade模式是这样描述的:为子系统中的一组接口提供一个统一接口。Facade模式定义了一个更高层的接口,使子系统更加容易使用。大致意思是说:使用一种比原有方式更简单的办法与系统交互。例如,…

如何在 ASP.NET Core Web API 中以三种方式返回数据

在 ASP.NET Core 中有三种返回 数据 和 HTTP状态码 的方式,最简单的就是直接返回指定的类型实例,如下代码所示:[ApiController][Route("[controller]")]public class WeatherForecastController : ControllerBase{[HttpGet]public …

java io 读取多个对象_Java IO系列(五):读写对象ObjectOutputStream和ObjectInputStream详解...

有必要多看几遍的关于字符和字节,例如文本文件,XML这些都是用字符流来读取和写入。而如RAR,EXE文件,图片等非文本,则用字节流来读取和写入。读写对象,传输对象在Java中很常使用,在javaBean中就经…