学习笔记
1、准备工作
破解一个Windows程序要比破解一个Dos程序容易得多,因为Windows系统中,只要API函数被使用,想对那些试图寻找蛛丝马迹的人隐藏一些信息就比较困难了。
假设有一个Windows程序,几千行代码,这几千行代码中其实大部分是对API函数的调用。API是操作系统提供给开发人员的编程接口,如果把操作系统比作一部车,API就是操作这部车的方向盘、油门、刹车、变速器。
因此分析一个程序,采用什么API函数作为切入点就显得比较关键了,这也正是为什么说破解要靠耐心和经验的原因。
2、API
API函数名 | 功能 |
---|---|
GetDlgItemTextA | 获取对话框内容,为ASCII编码 |
GetDlgItemTextW | 获取对话框内容,为Unicode编码 |
lstrcmp | 比较字符串,是Windows对字符串比较的api封装,原理上与C语言的strcmp。 当相同时返回0,当不同时,返回第一次不相等的一对字符的差值 |
3、调试分析
开始第一个断点配置
设置OllyDbg中断在程序入口点(选项-调试-开始)
-
System Breakpoint(系统断点):OllyDbg使用CreateProcessA加载DEBUG_ONLY_THIS_PROCESS参数运行,程序执行之后会触发一个INT13,在系统空间里
-
Entry Point of main module:主模块入口点,即文件的入口点
-
WinMain:程序的WinMain()函数入口点
在C语言中在程序执行时系统默认最先寻找的函数就是main函数,而在Windows系统中默认最先寻找的就是Winmain()函数。
但是WIndows程序并不一定非要把Winmain()作为程序的入口,可以指定从任意一个函数作为入口,只是通常编译器约定把Winmain作为入口
反汇编窗口详解
-
虚拟地址:
-
0x0040xxxx等小地址,为用户代码,同一程序的同一条指令在不同系统环境中该值相同
-
0x77xxxxxx等大地址,为系统API函数,同一程序的同一条指令在不同系统环境中该值不同。
这是因为系统API函数是调用了dll动态链接库,地址是动态连接的,所以不同系统环境可能会不同,但是同一系统环境下重新调试一般会保持相同。
而且动态链接库函数的同一指令,它的操作码是一致的,操作数是地址的时候,由于地址动态链接,所以不同的系统环境下操作数可能会不同。
-
-
机器码:即CPU执行的机器代码
-
汇编指令:与机器代码对应的程序代码
错误示例
通常我们拿到一个程序,会用F8单步步过快速浏览程序,找到目标call下断点,重新调试,逐步定位到我们要找的目标指令,然而在《加密与解密》中TraceMe.exe,采用了输入框,在DialogBoxParamA调用中调用了DialogBoxIndirectParamAorW,之后陷入死循环循环,无论F8多少次在这个call里跳不出来。
其实这个call循环里每次循环都调用了user32的PeekMessageW函数,OllyDbg由于没有user32.lib,所以对user32.dll函数名的解析可能不全,所以可能会显示不出这个函数,而是显示了一个地址。
死循环的原因是因为Windows采用了消息队列机制:
- 别的进程或线程发送消息Message
- MessageQueue系统消息队列接收消息
- GetMessage()->DispatchMessage()->GetMessage()………………….循环从系统消息队列中获取属于自己的消息并分派自己的消息队列中来,然后由程序自行处理
TraceMe.exe中就是一直在GetMessage()->DispatchMessage()的循环中,因为我们根本没有操作它的窗口,所以并没有向它发送消息,它也就接收不到消息,它就一直在循环里试图获取消息跳不出来。
正确做法
- TraceMe.exe是要获取输入框的用户名和序列号,那么它很可能使用了GetDlgItemTextA或者GetDlgItemTextW,此时使用Ctrl+G跟踪表达式,输入API函数名可以快速定位到目标地址。
- 定位之后发现,在反汇编窗口的最左边子窗口即汇编代码对应的地址窗口有user32的GetDlgItemTextA函数名,那我们就可以直接定位函数,在这个位置F2打一个断点,让那些call在准备期间就被中断。
- Ctrl+F2重新加载程序,F9运行,输入窗口顺利弹出,此时输入用户名和序列号,点击Check,就会向消息队列发送消息,程序接收到之后进行处理,而处理这个消息就要调用GetDlgItemTextA函数,所以它被断在了GetDlgItemTextA的门口。
- 之后一直F8,直到看到和校验有关的信息。在本例中就是操作我们输入的用户名和序列号,寻找寄存器中某个寄存器出现我们输入的用户名和序列号的指针。
- 之后,找到了一个push用户名 push序列号的指令块,之后紧跟着一个call,按照常识,这个call一般就是校验身份的函数。
- 暴力破解只需处理该函数的返回值,所以不需要F7单步步入,而是F8单步步过即可。无论它如何验证,总会返回一个值,来表述该身份是否可信。
- 任何函数的返回值在汇编中都是存放在eax寄存器中,所以关注紧接着的处理eax的指令。在本例中是test eax,eax
- 后面有个je指令,应该是用来根据结果跳转,来返回结果的。此时Z=1,所以此时可以双击标志寄存器中的Z标志位,使其为0,就可以跳转到校验通过的路径上执行。(临时破解完成)
- 双击或者选中跳转指令点击空格,将这条指令修改为nop,令跳转指令失效。
- 选中修改后的汇编指令,右键复制到可执行文件->选择,此时会弹出可执行文件在内存中的镜像,dump出来就是我们的目标文件,此时在这个窗口,右键备份->保存数据到文件。(暴力破解完成)
Ctrl+G 不仅可以输入地址,还可以检索API函数名,所以无论选用合适的API函数作为切入点,都可以采用Ctrl+G来快速定位。
Ctrl+G定位到的位置实际上是在call这个函数之后准备所有环境的第一条指令
Ctrl+G也可以搜索esp+0x???,这种是代码中经常会有这种地址,在数据窗口中可以快速定位到地址,并查看当前地址中存放的地址。
至于为什么断在这,是因为这样既避免了之前的死循环,又找到了核心代码。因为核心代码必定在获取到输入数据之后。
5、快捷键
快捷键 | 作用 | |
---|---|---|
Alt+B | 打开断点编辑器。选中单一条目,空格切换选中断点状态,del删除选中断点 | |
Space | 修改选中的汇编指令 | |
Ctrl+N | 打开输入表,查看导入的dll中函数的名单,来推断下断点的位置 |
6、汇编指令
汇编指令 | 功能 |
---|---|
test | 将两个操作数进行逻辑与运算,运算结果只用来修改标志位,不会回写 |
je | ZF标志位=1时跳转 |
jne | ZF标志位=0时跳转 |
7、技巧总结
- F2下断点,Alt+B可以查看所有断点,空格切换断点状态
- 当位于某个call中,此时想返回调用这个call的地方时,可以按Ctrl+F9,这样OllyDbg会停在它接下来遇到的第一个返回指令处(ret、retf、iret),再按一次F8就回到了调用处。
- 如果跟进系统DLL提供的API函数中,此时想返回到应用程序领空里,可以按Alt+F9执行返回到用户代码指令。
- 领空,实际上就是指某一时刻,CPU执行的指令所在的代码段的所有者。
- 0x0040????等小地址一般为可执行文件领空,这些地址在不同系统环境中一般不变。
- 0x7C??????等大地址一般为系统DLL领空,这些地址在不同系统环境中可能会发生改变。
- 程序从文本框中获取字符串一般采用的是GetDlgItemTextA(GetDlgItemTextW)、GetWindowTextA(GetWindowTextW)。
- 使用Ctrl+G快速定位到表达式的位置。
- Ctrl+N打开应用程序的导入表,然后查看应用程序总共导入了哪些函数来推断在哪里下断点。
- 对于返回值,汇编代码的返回值约定存放在eax中,如果eax不够存放数据,则把数据存到内存的某个位置并把地址存储到eax中。
独立思考
1. 为什么别人的OllyDbg可以显示出User32模块下的函数名,我的OllyDbg只能显示User32.xxxxxxxx?
发现OllyDbg的调试菜单下有选择导入库,这里面有一些lib文件,点击处理之后,这些文件中的函数名能够显示出来了,但是user32仍然没办法解析,之后发现是缺少user32.lib。
每一个库可以被编译为两种形式:
-
动态链接库(产生两个文件,一个lib文件和一个dll文件)
- 隐式加载:lib文件添加到链接选项,dll放到主程序exe的相同目录下,编译链接即可
- 显示加载:不使用lib文件,只在主程序运行时调用LoadLibrary来加载该dll,并使用GetProcAddress来获得dll某函数的指针,最后使用FreeLibrary卸载dll库
-
静态链接库(只产生一个文件lib)
- 包含该库的所有函数
- 当使用该库时,需要将该lib文件添加到链接选项中
- 生成的exe包含该lib的全部内容
其中的区别在于:
- 将一个库编译成动态链接库时,产生的那个lib的文件内容是该库导出的函数和变量的声明
- 将一个库编译成静态链接库时,产生的lib文件包含了该库的所有内容
因此OllyDbg要根据模块内函数的地址偏移解析出函数名的话,少不了lib文件的帮助,因为动态链接库的lib文件内有该库所有的函数和变量声明。
解决方案是找到user32.lib在OllyDbg中调试-选择导入库
产生过的疑问
- 为什么别人的od可以显示出User32模块下的函数名,我的od只能显示User32.xxxxxxxx?