掘金 后端 ( ) • 2024-05-11 17:16

获取入口点

调试程序是首先要找到程序的入口点,方便查看。可执行文件(.exe)的入口点通常位于 <Module> 类或具体的启动类中,如 Program 类的 main 方法。Program 类也称为程序的主类。寻找入口点有以下几种方法:

  • 在解决方案资源管理器中寻找类似 Program.cs 或包含 static void Main(string[] args) 方法的文件。
  • 在 dnSpy 的搜索框中输入 Main ,查找可能的入口方法。
  • 在程序集名称 apri_cli (1.0.0.0) 或 物理文件名 apri_cli.exe 上,点击右键选择“转到入口点(E)”即可。

image-20240507143746830.png

知识点:

解决方案资源管理器中:

第一层标题是 程序集 名称,这里是 apri_cli (1.0.0.0)

第二层标题是 物理文件名 名称,这里是 apri_cli.exe

这些数据都存储在.NET程序编译时生成的程序集元数据中,不会改变。

附加进程

使用 dnSpy 附件进程时,通过 调试==>附加到进程(P) 弹出进程列表,在列表中选择要附加的进程即可。

这里新手要关注的点是,没有附加进程前,dnSpy 的页面没有任何内容,如下所示:

image-20240511134604708.png

进程列表:

image-20240511134718031.png

通过进程列表附加进程后,其实窗口中也不显示任何内容,但这时候其实已经附加成功。

image-20240511134820122.png

这时可以通过 调试==>窗口==>模块 打开“模块窗口”,在其中搜索要调试的模块。

image-20240511135028991.png

搜索目标模块:

image-20240511135207371.png

找到目标模块后双击,即可在“程序集资源管理器”出现要调试的目标模块。定位到目标代码所在行下断点,之后运行程序即可调试。

image-20240511135406697.png

返回当前执行指令行

使用 dnSpy 逆向调试程序,查看反编译出的代码时,经常会跳转到调用代码或上层代码中,查看具体的代码实现。这时要想快速返回当前执行的指令行(比如在 main() 函数中中断的位置),可通过堆栈的内容快速返回。

  1. 打开“调用堆栈”窗口:通过点击菜单栏的“调试”>“窗口”>“调用堆栈”。

  2. 选择当前执行的方法:在“调用堆栈”窗口中,找到位于最顶部的条目,这通常表示当前执行的方法。

  3. 双击条目:双击第一行,dnSpy 将自动跳转到反编译窗口中的当前执行的指令行。

image-20240511140728523.png

修改 IL 字节码

调试过程中,有时需要修改代码的执行流程,可以采用修改 IL 字节码的方法。选中要修改的代码行,右键选择“编辑IL指令(S)”。

image-20240511142402148.png

弹出的窗口中,比其它行代码颜色稍微深一点的 IL 指令,就对应之前选中的反编译代码。

image-20240511142537043.png

观察具体的操作码,看到三行中的第二行是 call 指令,第三行是 brtrue.s,说明就是要修改的目标 IL指令。点击该指令,在弹出的下拉列表中选择类似的 brfalse.s 指令,之后点“确定”。

image-20240511142656509.png

可以看到反编译的代码已经改变,之前的非运算符 ! 已经没有了。

image-20240511142829419.png

显示私有类

这里要感谢同事的帮忙,提供了一个好的解决方法。 调试某样本时,发现 setServerIp() 方法中对 Program.<setServerIp>d__9 实例化了一个对象,但没有具体的实现代码,点击 new Program.<setServerIp>d__9() 中的类名称时,又跳转回 setServerIp() 函数自身。

image-20240511144428468.png

通过GPT得知,这行代码是编译器生成的,用于实现异步方法或迭代器方法的部分。在C#中,当你写一个使用了await关键字的异步方法或者一个yield return语句的迭代器方法时,编译器会将这个方法转换成一个状态机。这个过程是自动的,旨在处理异步操作的执行流程或迭代器的状态管理。

Program.<setServerIp>d__9 <setServerIp>d__ = new Program.<setServerIp>d__9();

这里的Program.<setServerIp>d__9是编译器生成的一个类,用于实现名为setServerIp的方法的状态机。这个类名中的<setServerIp>d__9是编译器生成的,其中setServerIp是原始方法的名称,d__9是编译器用来标识这个特定状态机的内部名称。这种命名约定是编译器内部使用的,以确保名称的唯一性。

  • Program是包含setServerIp方法的类。
  • <setServerIp>d__9是编译器为setServerIp方法生成的状态机类。
  • 最后,<setServerIp>d__ = new Program.<setServerIp>d__9();这行代码创建了这个状态机的一个实例。

这种转换使得编译器能够自动处理异步方法的挂起和恢复,或者在迭代器方法中处理集合的迭代过程。这是C#语言支持异步编程和迭代器的一种底层机制,并且这一转换对于开发者来说是完全透明的。在使用像dnSpy这样的工具进行反编译时,你会看到这种由编译器生成的代码,但在原始的C#源代码中,这部分是看不到的。

但要调试恶意样本分析其功能时,我们必须知道该函数具体执行了哪些功能,这时可以通过修改 dnSpy 的设置解决这个问题。

image-20240511144048474.png

打开“显示编译器生成的隐藏类型和方法”选项后,setServerIp() 方法变成了 <setServerIp>d__9 状态机类,可以看到具体实现代码如下:

image-20240511143451827.png