使用DNSpy 调试.net 服务
背景:
某个.net 应用以windows服务的形式对外提供服务,然后通过IIs的web应用和该服务进行交互,通过web的方式向用户提供业务。该项目有点年头,目前相当于自己维护,没厂家支持,而且自定义了很多开发在上面,但是有一个棘手的问题是该项目没有提供所有源码的,类似服务程序,使用的相关DLL还进行了混淆加密,所以其内部逻辑通过反编译看源码会非常绕,而且就算反混淆后,代码的组织和命名也会差很多,看起来会非常的累。
症状:
1. 某次为了和生产保持一致,就地升级了测试环境的操作系统(2012 r2 到2016),这个服务死活其不来,还好做了VM的快照。这个程序错误的时候就在日志里记了一条“输入字符串格式不对”,没头没脑,也不包含stack trace ,不知道哪里报的错。后来没有办法,拿快照回退,初步判定为操作系统兼容性问题(注意是.net 4.x 开发的,虽然有点怀疑这个结论)
2. 另外的同事为了熟悉这个系统,在新的测试环境,重新部署了一套这个系统(操作系统使用的2019),系统可以运行,没有问题。(所以我很怀疑1 中的兼容性问题,但目前我找不到原因)
3. 恢复快照的测试环境,后又经历过两次服务起不来的问题。有次打了补丁,有次是重启就不行了。但是因为没有太多思路去排错(因为配置什么的都没有变过,关键是也没有什么有用的日志协助排错,而且没技术支持,没源码),后面偷懒直接恢复了快照解决。
4. 测试环境因为需要更新补丁,重启,结果服务又挂了。还是一样的报错
尝试解决:
实在忍不了,我必须得把这个原因找出来,不然在生产上出现,真的没法处理,而且到时候肯定手忙脚乱。初步的想法是通过调试器直接调试这个服务,然后找到报错的位置,大致按逻辑找到可能出错的位置。从而找出到底是配置问题、还是兼容性问题,还是其他的什么。
由于是个.net 应用,带调试功能,且能反编译的DNSpy 就属于我的首选工具了。支持直接查看.net 语言编译的exe、dll 文件,而且支持直接调试,而且可以就地对dll、exe的代码进行修改并保存。
结果调试一个windows 服务没有你想的简单。当使用DNSpy调试运行一个windows 服务时,由于其不是通过net start 或者windows 服务管理器执行的,所以报这个错误。
这可如何是好,这个服务起来就挂掉,我没有通过attach 到进程的方式来调试啊。还好微软的文章提供了一些思路。https://docs.microsoft.com/en-us/dotnet/framework/windows-services/how-to-debug-windows-service-applications#debugging-tips-for-windows-services
两个选项:
1. 在服务的onstart或者main 里增加sleep ,延长服务执行实际代码的时间,在sleep 没报错的时候,使用调试器attch上去(加sleep 也就一两行代码的改动,应该可以直接使用dnspy搞定,即使混淆过了,但是程序入口这些还是很容易找到的,因此选择了该方法)
2. 把服务的程序逻辑改写,重新编译成一个console 程序。(没源码,修改较多,看来不太适合我的情况)
Dnspy 打开服务程序的exe文件,定位到onstart 方法,然后点击右键,选择”编辑方法(C#)“
增加一个sleep,我这里大概5秒钟,可以手快的执行完attach,另外在onstart处下断点。
保存修改。然后再DNspy的文件菜单中选择保存模块。这样会把更改写入到新的exe中(注意保留备份)
在服务管理器中启动服务。
使用dnspy 调试--附加到进程
注意点: 这里附加到进程能列出的是dnspy执行账号下的进程,加入你的服务以其他用户身份执行,可能列举不出来,所以可能要更换dnspy的运行账号。
后续由于涉及到应用内部的内容,就不写出来了,大概定位到是程序加载某些自生成的配置文件(二进制),然后某些目录的配置文件不知被谁拷贝了一份,名称为xx.yy-复制,导致程序加载报错(可能是文件名问题),但是报错内容只有一条“输入格式不正确”,因为程序本身对错误进行了catch ,然后没有记录下面stack信息,调试器里其实可以看到。虽然代码进行了混淆,但是还可以定位到大概的位置。
在 System.Version.VersionResult.SetFailure(ParseFailureKind failure, String argument)
在 System.Version.TryParseComponent(String component, String componentName, VersionResult& result, Int32& parsedComponent)
在 System.Version.TryParseVersion(String version, VersionResult& result)
在 System.Version.Parse(String input)
在 System.Version..ctor(String version)
在 OO.Server.OOProcess.a(String A_0, Version& A_1)
在 OO.Server.OOProcess.b(String A_0)
在 l.c(String A_0)
在 l.j()
在 l.b(Boolean A_0)
在 l.b(Boolean A_0)
在 s4.a(Boolean A_0, Boolean A_1)
在 aje.g()
在 OO.Server.Server.Start()
总结分析:
综合以上多个历史症状,可以得出问题原因是复制的自动生成的配置文件导致了应用crash,只是服务不重启的时候,这个配置文件不会重新加载,所以无论是升级操作系统、打补丁、重启都是由于间接重启了服务导致配置重新加载,然后触发错误。
更多思考和问题:
这个程序是一个.net 反编译和更改代码相对简单,但是如果服务是个C程序或者C++编译的,如何进行调试呢?如何增加sleep 呢,或者有其他更好的方法进行调试。而且程序本身catch了异常,退出也是相对优雅的退出,不能通过在程序无处理异常时打开调试器的方法。
- 如何从客户端 JavaScript 调用 .NET Web 服务使用 InternetExplorer 和 MSXML
- 使用 .NET 和后台智能传输服务 API 来编写自动更新应用程序
- 使用VC++使用开发Web服务(ISAPI extension - mod_gsoap.dll) 4- mod_gSoap 调试
- 以 Console 方式运行、调试、编译 .Net 编写的 Windows 服务
- .Net服务组件(ServicedComponent)简介及其使用
- [转]C#程序安装.net服务(不使用InstallUtil.exe)
- 使用Mdbg.exe 调试.Net 程序
- 使用 .NET 框架将现有代码作为 Web 服务提供
- .Net服务组件(ServicedComponent)简介及其使用
- .Net服务组件(ServicedComponent)简介及其使用
- 程序安装.net服务(不使用InstallUtil.exe)
- 一起谈.NET技术,使用WCF实现SOA面向服务编程—— 架构设计
- 使用程序代码安装/卸载.net服务(不使用InstallUtil.exe)
- .Net网络通讯编程[利用Socket实现字串、文件、序列化对象传输]--使用封装的网络服务2
- 【笔记】使用.net中的Debug与Trace加强调试能力(SamWang)
- 使用 .NET 框架将现有代码作为 Web 服务提供
- 在 ASP.NET 开发中使用非 .Net Web 服务
- .Net服务组件(ServicedComponent)简介及其使用
- .Net服务组件(ServicedComponent)简介及其使用
- .Net服务组件(ServicedComponent)简介及其使用