Ollydbg 入门、使用 Ollydbg 从零开始 Cracking、玩玩破解,写给新人看-程序员宅基地

技术标签: 网络安全  OllyDbg  

Ollydbg 超强入门级:链接:https://pan.baidu.com/s/1fLTdBn9400rkx_iAgJ7bZg 提取码:dvi5

使用 Ollydbg 从零开始 Cracking ( 58章):https://www.52pojie.cn/thread-1045228-1-1.html

Ollydbg 帮助文档翻译:https://www.0xaa55.com/thread-1122-1-1.html

OllyDBG 视频教程:https://www.bilibili.com/video/av6889190
动态调试工具之OllyDbg(OD)教程:https://www.bilibili.com/video/av70600053

1、ollydbg 简介

Ollydbg 又叫 OllyDebug,通常称作 OD,诞生于2000年,曾经代替 SoftICE 成为最为流行的调试解密工具,是很不错的调试器 (现在早已经停止更新)。OD 和 W32dasm 被誉为屠龙刀和倚天剑,是反汇编工作的常用工具。OD 可以看作是 IDA 与 SoftICE 结合起来 Ring 3 级的调试器,OD 还附带了200脱壳脚本和各类插件,功能非常强大,可以过 SE,VMP3.0,深受逆向圈内人士的喜爱。

  • 调试自己的程序因为有源码,一般用 vc
  • 破解别人的程序,因为没有源码,只能使用 OllyDebug

od、x64Dbg、Windbg、IDA 区别

没有符号信息时,动态调试别人代码用 OllyDbg 和 x64Dbg

  • OllyDbg 和 x64Dbg 大致属于同一类别。它们的主要优势是在没有符号信息的情况下进行调试(尽管它们也可以使用符号信息进行调试)。OllyDbg (封闭源代码) 已经很久没有维护,并且仅限于 x86 32 位。
  • x64Dbg 开源,并且可以处理 x86 和 x64。

动态调试自己的或系统程程序时用 Windbg

  • Windbg 是由 Microsoft 维护,主要是一个符号调试器(它可以在没有任何符号的情况下工作)。当符号可用时,它非常强大。它支持插件(C 或 C++),脚本(它有自己的脚本语言,它也支持官方JavaScript 和第三方 python脚本加载器).NET 调试,并且可以进行内核调试(在这方面它是事实上的 Windows 系统上的内核调试器)。在其最新版本中,它还支持 TTD(时间旅行调试)。

静态分析代码结构用 IDA

  • IDA 的主要优势在于它是一个交互式反汇编程序。您可以通过 python 脚本以多种方式“询问”二进制文件(更准确地说,是从二进制文件生成的数据库)。它还支持自行调试或通过其他引擎(例如 gdb 或 windbg 引擎)进行调试。

总结

  • OllyDbg - OllyDbg 是一个 32 位汇编器级别的分析调试器。
  • IDA - 同类最佳的二进制代码分析工具,是世界级软件分析师、逆向工程师、恶意软件分析师和网络安全专业人员工具箱中不可或缺的项目。
  • Immunity Debugger - Immunity Debugger 是一种强大的新方法来编写漏洞利用、分析恶意软件和逆向工程...
  • Ghidra - 软件逆向工程 (SRE) 框架
  • SoftICE - SoftICE 是一款适用于 Windows 和 DOS 的调试软件,可以分析您的所有程序并进行修复。
  • Nirsoft Simple Program Debugger - Nirsoft Simple Program Debugger 是一款调试软件,可在连接到正在运行的程序或在调试模式下启动新程序后,分析并显示计算机上的所有主要调试事件。

弃用 Ollydbg 转战 x64dbg 的10个理由

弃用 Ollydbg,转战 x64dbg 的10个理由:https://www.52pojie.cn/thread-1573909-1-1.html

OllyDbg vs x64dbg - OllyDbg 是否比 x64dbg 有什么特别的优势https://52sbl.cn/discussion/25999.html

OD只支持 x86 32位程序,并且早已经停止更新。如果分析 x86 32位程序,OD 还是很强悍的。

x64dbg 修订简体中文绿色版 / OllyDBG吾爱破解专用版 / PYG官方专用版

x64dbg 同时支持 x86、x64 程序,并且开源在不断更新。如果是 x64 程序则只能使用 x64dbg,如果是 x86 ( 32位程序 ) 则可以先用 x64dbg,如果出问题,则再用OD打开两个作为对照参考。

Github x64dbg:https://github.com/search?q=x64dbg&type=repositories
x64dbg-Plugin-Manager:https://github.com/horsicq/x64dbg-Plugin-Manager
爱盘 Debuggers 工具集:https://down.52pojie.cn/Tools/Debuggers/
爱盘 反汇编 工具集:https://down.52pojie.cn/Tools/Disassemblers/

x64dbg 汉化、插件

x64dbg 简体中文版:https://www.52pojie.cn/thread-1374894-1-1.html

IDA, Ghidra, x64DBG, GDB,OllyDBG 插件:https://github.com/fr0gger/awesome-ida-x64-olly-plugin
x64dbg 官网插件列表:https://github.com/x64dbg/x64dbg/wiki/Plugins

吾爱的专版OD的插件就挺全,完全够在破解中使用

2、OD 界面


OD 结合了动态调试和静态分析;调试Ring 3级程序的首选工具;可识别大量被C和Windows频繁使用的函数,并能将其参数注释出;

OD 主界面

下载 OllyDBG吾爱破解专用版 ,然后进入目录。

首先执行 路径修复工具.exe,然后再打开 LCG.exe,打开后界面如图:

打开OD 时,默认窗口是CPU窗口,调试的大部分操作在这个窗口中进行;它包含5个面板窗口:反汇编面板,寄存器面板,信息面板,数据面板,堆栈面板。调整各个窗口的大小,只需左键按住边框拖动,调整好后重启 OllyDBG 就可以生效。OllyDBG 中各个窗口的功能:

  • 汇编指令:就是 "反汇编窗口",显示被调试程序的反汇编代码。标题栏上的地址、 HEX 数据、反汇编、注释可以通过在窗口中右击出现的菜单 界面选项->隐藏标题 或 显示标题 来进行切换是否显示。用鼠标左键点击注释标签可以切换注释显示的方式。
  • 寄存器窗口 :显示当前所选线程的 CPU 寄存器内容。同样点击标签 寄存器 (FPU) 可以切换显示寄存器的方式。

    几个通用寄存器
    ESP:指向堆栈栈顶
    EBP:大部分用来定位局部变量和参数
    其余的就各自拆开来用
    另外, 点击标签寄存器 (FPU) 可以切换显示寄存器的方式
  • 信息窗口 :显示反汇编窗口中选中的第一个命令的参数及一些跳转目标地址、字串等。
  • 数据窗口 :包括 "16进制的值、对应的 ASCII",显示内存或文件的内容。右键菜单可切换显示方式。 

  • 堆栈窗口 :显示当前线程的堆栈。用于动态调试。

  • 上面的地址就是程序的内存地址,hex即为hex数据,反汇编就是程序的汇编代码,注释即为od分析出来的,双击即可编辑,

OD 菜单栏

各种窗口 按钮:

  • L (Log):日志窗口;
  • E ():可执行模块窗口;
  • M ():内存映射窗口;
  • W (Windows):窗口列表;指被反汇编的程序所包含的窗口;
  • H (Handle):句柄 窗口
  • T (Thread):线程 窗口;查看被反汇编的程序所包含的线程;
  • C (CPU):CPU 窗口;默认打开
  • P (Patches):Patches 窗口
  • K ():调用堆栈 窗口;
  • B (Breakpoint):断点 窗口
  • R (Reference):搜索结果 窗口;
  • ... (Run trace):运行跟踪 窗口;
  • S (Source):

调试 按钮

  • F2:设置断点,只要在光标定位的位置按F2键即可,再按一次F2键则会删除断点。
  • F8:单步步过。每按一次这个键执行一条反汇编窗口中的一条指令,遇到 CALL 等子程序不进入其代码。
  • F7:单步步入。功能同单步步过(F8)类似,区别是遇到 CALL 等子程序时会进入其中,进入后首先会停留在子程序的第一条指令上。
  • F4:运行到选定位置。作用就是直接运行到光标所在位置处暂停。
  • F9:运行。按下这个键如果没有设置相应断点的话,被调试的程序将直接开始运行。
  • CTR+F9:执行到返回。此命令在执行到一个 ret (返回指令)指令时暂停,常用于从系统领空返回到我们调试的程序领空。
  • ALT+F9:执行到用户代码。可用于从系统领空快速返回到我们调试的程序领空。

上面提到的几个快捷键对于一般的调试基本上已够用了。要开始调试只需设置好断点,找到你感兴趣的代码段再按 F8 或 F7 键来一条条分析指令功能就可以了。

OD 配置

OD 配置在菜单的 Options 中。有界面选项、调试选项,配置保存在ollydbg.ini文件里。

  • 调试设置:一般按默认;
  • 异常(Exceptions),可以设置让OllyDbg忽略或不忽略哪些异常;可以全选;

插件 目录

OllyDBG 支持插件功能,插件的安装也很简单,只要把下载的插件(一般是个 DLL 文件)复制到 OllyDBG 安装目录下的 PLUGIN 目录中就可以了,OllyDBG 启动时会自动识别。要注意的是 OllyDBG 1.10 对插件的个数有限制,最多不能超过 32 个,否则会出错。建议插件不要添加的太多。到这里基本配置就完成了,OllyDBG 把所有配置都放在安装目录下的 ollydbg.ini 文件中。

查看插件所在目录:菜单栏 ---> 选项 ---> 界面选项 ---> 目录。也可以设置字体、颜色

UDD 目录

UDD 目录的作用是保存你调试的工作。比如你调试一个软件,设置了断点,添加了注释,一次没做完,这时 OllyDBG 就会把你所做的工作保存到这个 UDD 目录,以便你下次调试时可以继续以前的工作。如果不设置这个 UDD 目录,OllyDBG 默认是在其安装目录下保存这些后缀名为 udd 的文件,时间长了就会显的很乱,所以还是建议专门设置一个目录来保存这些文件。

调试选项

菜单栏 ---> 选项 ---> 调试设置,默认配置即可。

新手一般不需更改这里的选项,默认已配置好,可以直接使用。建议在对 OllyDBG 已比较熟的情况下再来进行配置。上面那个异常标签中的选项经常会在脱壳中用到,建议在有一定调试基础后学脱壳时再配置这里。

添加 右键菜单

把 OllyDBG 添加到资源管理器右键菜单,这样就可以直接在 .exe 及 .dll 文件上点右键选择 " 用Ollydbg打开 " 菜单来进行调试

从右键菜单中删除也很简单,还是这个对话框,点击 " 从系统资源管理器菜单删除 Ollydbg 即可。

OllyDbg 基本操作

3、 玩玩破解,写给新人看

玩玩破解,写给新人看(第一集)
玩玩破解,写给新人看(第二集)
玩玩破解,写给新人看(第三集)
玩玩破解,写给新人看(第四集)
玩玩破解,写个新人看(第五集)
玩玩破解,写给新人看(第六集)
玩玩破解,写给新人看(第七集)
玩玩破解,写给新人看(第八集)
玩玩破解,写给新人看(第九集)
玩玩破解,写给新人看(第十集)
玩玩破解,写给新人看(第十一集)
玩玩破解,写给新人看(第十二集)
玩玩破解,写给新人看(阶段练习1)
玩玩破解,写给新人看(第十三集)
玩玩破解,写给新人看(第十四集)
玩玩破解,写给新人看(第十五集)
玩玩破解,写给新人看(第十六集)
玩玩破解,写给新人看(第十七集)
玩玩破解,写给新人看(第十八集)
玩玩破解,写给新人看(第十九集)
玩玩破解,写给新人看(第二十集)
玩玩破解,写给新人看(第二十一集)
玩玩破解,写给新人看(第二十二集)
玩玩破解,写给新人看(第二十三集)
玩玩破解,写给新人看(第二十四集)
闲谈如何破解软件?
玩玩破解——小白实战1,你也行!
玩玩破解——小白实战2,巧搜字符串
玩玩破解——小白实战3,易语言字符串比较通杀
玩玩破解——小白实战4,冰火两重天
对新人们学习《玩玩破解》系列教程的集中解答
玩玩破解—加点油,再前行!
玩玩破解—小白实战5,易语言变脸
玩玩破解——小白实战6,再上一层楼,做个注册机
新年送“心”礼,易语言逆向分析助手6.0
重新认识Delphi程序按钮事件特征码
玩玩破解番外篇1—快速分析汇编代码
玩玩破解之逆向工具篇

4、使用 ollydbg 调试

调试 符号 (符号文件)

当应用程序被链接以后,代码被逐一地翻译为一个个的地址。使用vs或者windbg等微软的调试工具进行调试的时候,可以方便地使用变量名来查看内存、可以使用函数名称来下断点、可以指定某个文件的某一行来下断点。这一切背后,是符号在指导调试器工作,pdb或者dbg文件。(.NET自己有元数据,符号不需要元数据已有的信息)。

程序运行的时候,计算机只需要逐条执行指令即可。而与源代码对应的关系是完全不需要知道的。这就给调试带来了困难。无论什么编译都有自己的一套用于对应代码和可执行程序。各种编译器都有自己保存类似这种对应关系的办法,有的直接嵌入可执行文件,有的则是独立出来的。而微软的编译器则是独立产生了这种文件,它就被成为符号文件。符号文件一般都是pdb文件。根据微软官方的解释,pdb文件包含有: 全局变量;局部变量;函数名及入口点;FPO记录;源代码行号。

加载符号文件,可以让OllyDbg以函数名显示DLL中的函数。例如:MFC42.DLL是以序号输出函数,在OllyDbg显示的是序号,如果加载MFC42.DLL调试符号,则以函数名显示相关输出函数;

基本调试方法

OllyDBG 有三种方式来载入程序进行调试:

  1. 一种是点击 菜单 -> 文件 -> 打开 ( 快捷键是 F3 )来打开一个可执行文件进行调试,
  2. 另一种是点击 菜单 -> 文件 -> 附加,来附加到一个已运行的进程上进行调试。注意这里要附加的程序必须已运行。
  3. 第三种就是用右键菜单来载入程序(勉强算一种)。

一般用第1种方式。示例:选择一个 test.exe 来调试,通过菜单 文件->打开 来载入这个程序,OllyDBG 中显示的内容将会是这样:

调试中经常用的快捷键有这些:

  • F2:设置断点。只要在光标定位的位置(上图中灰色条)按F2键即可,再按一次F2键则会删除断点。(相当于 SoftICE 中的 F9)
  • F8:单步跳过。每按一次这个键执行一条反汇编窗口中的一条指令,遇到 CALL 等子程序不进入其代码。(相当于 SoftICE 中的 F10)
  • F7:单步步入。功能同单步步过(F8)类似,区别是遇到 CALL 等子程序时会进入其中,进入后首先会停留在子程序的第一条指令上。(相当于 SoftICE 中的 F8)
  • F4:运行到选定位置。作用就是直接运行到光标所在位置处暂停。(相当于 SoftICE 中的 F7)
  • F9:运行。按下这个键如果没有设置相应断点的话,被调试的程序将直接开始运行。(相当于 SoftICE 中的 F5)
  • CTR + F9:执行到返回。此命令在执行到一个 ret (返回指令)指令时暂停,常用于从系统领空返回到我们调试的程序领空。(相当于 SoftICE 中的 F12)
  • ALT + F9:执行到用户代码。可用于从系统领空快速返回到我们调试的程序领空。(相当于 SoftICE 中的 F11)

上面提到的几个快捷键对于一般的调试基本上已够用了。要开始调试只需设置好断点,找到你感兴趣的代码段再按 F8 或 F7 键来一条条分析指令功能就可以了

OllyDbg 常见问题

  • 跟踪程序时乱码:这是因为OllyDbg将一段代码当成数据;没有进行反汇编识别;右键快捷菜单,执行 Analysis/Analyse code (分析/分析代码);如果不行,则执行菜单 Analysis/Remove analysis from module (分析/从模块中删除分析);或在UDD目录中删除相应的.udd文件;
  • 快速回到当前领空:如果查看代码翻页到其他地方,想快速回到当前CPU所在的指令上,双击寄存器面板中的EIP或单击
  • 修改EIP:将光标移到需要的地址;执行右键菜单 New origin here (此处新建EIP);
  • 在反汇编窗口键入汇编代码,输入 push E000,提示 未知标识符,不能识别E是字母还是数字;输入 push 0E000;

搜索 字串

Crackme #3 链接:https://pan.baidu.com/s/1AVJliU_L9BrqIrTVJBL8dw    提取码:un9n 

现在开始正式进入破解。今天的目标程序是看雪兄《加密与解密》第一版附带光盘中的 crackmes.cjb.NET 镜像打包中的 CFF Crackme #3,采用用户名/序列号保护方式。原版加了个 UPX 的壳。刚开始学破解先不涉及壳的问题,主要是熟悉用 OllyDBG 来破解的一般方法。这里把壳脱掉来分析,附件是脱壳后的文件,直接就可以拿来用。

一般软件破解的流程:

  • 拿到一个软件先别马上用 OllyDBG 调试,先运行一下,有帮助文档的最好先看一下帮助,熟悉一下软件的使用方法,
  • 再看看注册的方式。如果是序列号方式可以先输个假的来试一下,看看有什么反应,也给破解留下一些有用的线索。
  • 如果没有输入注册码的地方,要考虑一下是不是读取注册表或 Key 文件(一般称 keyfile,就是程序读取一个文件中的内容来判断是否注册),这些可以用其它工具来辅助分析。
  • 如果这些都不是,原程序只是一个功能不全的试用版,那要注册为正式版本就要自己来写代码完善了。
  • 获得程序的一些基本信息后,还要用查壳的工具来查一下程序是否加了壳,若没壳的话看看程序是什么编译器编的,如 VC、Delphi、VB 等。这样的查壳工具有 PEiD 和 FI。有壳的话要尽量脱了壳后再来用 OllyDBG 调试,特殊情况下也可带壳调试。

先来运行一下这个 crackme(用 PEiD 检测显示是 Delphi 编的),界面如图:

这个 crackme 已经把用户名和注册码都输好了,省得我们动手^_^。我们在那个“Register now !”按钮上点击一下,将会跳出一个对话框:

好了,今天就从这个错误对话框中显示的 “Wrong Serial, try again!” 来入手。启动 OllyDBG,选择菜单 文件->打开 载入 CrackMe3.exe 文件,会停在这里:

在反汇编窗口中右击,出来一个菜单,我们在 查找->所有参考文本字串 上左键点击:

当然如果用上面那个 超级字串参考+ 插件会更方便。但我们的目标是熟悉 OllyDBG 的一些操作,我就尽量使用 OllyDBG 自带的功能,少用插件。好了,现在出来另一个对话框,我们在这个对话框里右击,选择”查找文本”菜单项,输入“Wrong Serial, try again!”的开头单词“Wrong”(注意:查找内容要区分大小写)来查找,找到一处:

在我们找到的字串上右击,再在出来的菜单上点击”反汇编窗口中跟随”,我们来到这里:

见上图,为了看看是否还有其他的参考,可以通过选择右键菜单查找参考->立即数,会出来一个对话框:

分别双击上面标出的两个地址,我们会来到对应的位置:

00440F79 |. BA 8C104400       MOV EDX,CrackMe3.0044108C             ; ASCII “Wrong Serial,try again!”
00440F7E |. A1 442C4400       MOV EAX,DWORD PTR DS:[442C44]
00440F83 |. 8B00                     MOV EAX,DWORD PTR DS:[EAX]
00440F85 |. E8 DEC0FFFF     CALL CrackMe3.0043D068
00440F8A |. EB 18                   JMP SHORT CrackMe3.00440FA4
00440F8C |> 6A 00                  PUSH 0
00440F8E |. B9 80104400       MOV ECX,CrackMe3.00441080             ; ASCII “Beggar off!”
00440F93 |. BA 8C104400       MOV EDX,CrackMe3.0044108C             ; ASCII “Wrong Serial,try again!”
00440F98 |. A1 442C4400       MOV EAX,DWORD PTR DS:[442C44]
00440F9D |. 8B00                    MOV EAX,DWORD PTR DS:[EAX]
00440F9F |. E8 C4C0FFFF     CALL CrackMe3.0043D068

在反汇编窗口中向上滚动一下再看看:

00440F2C |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]
00440F2F |. BA 14104400     MOV EDX,CrackMe3.00441014             ; ASCII “Registered User”
00440F34 |. E8 F32BFCFF     CALL CrackMe3.00403B2C                ; 关键,要用F7跟进去
00440F39 |. 75 51           JNZ SHORT CrackMe3.00440F8C           ; 这里跳走就完蛋
00440F3B |. 8D55 FC         LEA EDX,DWORD PTR SS:[EBP-4]
00440F3E |. 8B83 C8020000   MOV EAX,DWORD PTR DS:[EBX+2C8]
00440F44 |. E8 D7FEFDFF     CALL CrackMe3.00420E20
00440F49 |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]
00440F4C |. BA 2C104400     MOV EDX,CrackMe3.0044102C             ; ASCII “GFX-754-IER-954”
00440F51 |. E8 D62BFCFF     CALL CrackMe3.00403B2C                ; 关键,要用F7跟进去
00440F56 |. 75 1A           JNZ SHORT CrackMe3.00440F72           ; 这里跳走就完蛋
00440F58 |. 6A 00 PUSH 0
00440F5A |. B9 3C104400     MOV ECX,CrackMe3.0044103C             ; ASCII “CrackMe cracked successfully”
00440F5F |. BA 5C104400     MOV EDX,CrackMe3.0044105C             ; ASCII “Congrats! You cracked this CrackMe!”
00440F64 |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F69 |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F6B |. E8 F8C0FFFF     CALL CrackMe3.0043D068
00440F70 |. EB 32           JMP SHORT CrackMe3.00440FA4
00440F72 |> 6A 00           PUSH 0
00440F74 |. B9 80104400     MOV ECX,CrackMe3.00441080             ; ASCII “Beggar off!”
00440F79 |. BA 8C104400     MOV EDX,CrackMe3.0044108C             ; ASCII “Wrong Serial,try again!”
00440F7E |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F83 |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F85 |. E8 DEC0FFFF     CALL CrackMe3.0043D068
00440F8A |. EB 18           JMP SHORT CrackMe3.00440FA4
00440F8C |> 6A 00           PUSH 0
00440F8E |. B9 80104400     MOV ECX,CrackMe3.00441080             ; ASCII “Beggar off!”
00440F93 |. BA 8C104400     MOV EDX,CrackMe3.0044108C             ; ASCII “Wrong Serial,try again!”
00440F98 |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F9D |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F9F |. E8 C4C0FFFF     CALL CrackMe3.0043D068

大家注意看一下上面的注释,我在上面标了两个关键点。有人可能要问,你怎么知道那两个地方是关键点?其实很简单,我是根据查看是哪条指令跳到 “wrong serial,try again” 这条字串对应的指令来决定的。如果你在 调试选项->CPU 标签中把”显示跳转路径”及其下面的两个”如跳转未实现则显示灰色路径”、”显示跳转到选定命令的路径”都选上的话,就会看到是从什么地方跳到出错字串处的:

我们在上图中地址 00440F2C 处按 F2 键设个断点,现在我们按 F9 键,程序已运行起来了。我在上面那个编辑框中随便输入一下,如 CCDebuger,下面那个编辑框我还保留为原来的“754-GFX-IER-954”,我们点一下那个“Register now !”按钮,呵,OllyDBG 跳了出来,暂停在我们下的断点处。我们看一下信息窗口,你应该发现了你刚才输入的内容了吧?我这里显示是这样:

堆栈 SS:[0012F9AC]=00D44DB4, (ASCII “CCDebuger”)
EAX=00000009

上面的内存地址 00D44DB4 中就是我们刚才输入的内容,我这里是 CCDebuger。你可以在 堆栈 SS:[0012F9AC]=00D44DB4, (ASCII “CCDebuger”) 这条内容上左击选择一下,再点右键,在弹出菜单中选择”数据窗口中跟随数值”,你就会在下面的数据窗口中看到你刚才输入的内容。而 EAX=00000009 指的是你输入内容的长度。如我输入的 CCDebuger 是9个字符。如下图所示:

现在按 F8 键一步步分析一下:

00440F2C |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]          ; 把我们输入的内容送到EAX,我这里是“CCDebuger”
00440F2F |. BA 14104400     MOV EDX,CrackMe3.00441014             ; ASCII “Registered User”
00440F34 |. E8 F32BFCFF     CALL CrackMe3.00403B2C                ; 关键,要用F7跟进去
00440F39 |. 75 51           JNZ SHORT CrackMe3.00440F8C           ; 这里跳走就完蛋

当我们按 F8 键走到 00440F34 |. E8 F32BFCFF     CALL CrackMe3.00403B2C 这一句时,我们按一下 F7 键,进入这个 CALL,进去后光标停在这一句:

我们所看到的那些 PUSH EBX、 PUSH ESI 等都是调用子程序保存堆栈时用的指令,不用管它,按 F8 键一步步过来,我们只关心关键部分:

00403B2C /$ 53              PUSH EBX
00403B2D |. 56              PUSH ESI
00403B2E |. 57              PUSH EDI
00403B2F |. 89C6            MOV ESI,EAX                         ; 把EAX内我们输入的用户名送到 ESI
00403B31 |. 89D7            MOV EDI,EDX                         ; 把EDX内的数据“Registered User”送到EDI
00403B33 |. 39D0            CMP EAX,EDX                         ; 用“Registered User”和我们输入的用户名作比较
00403B35 |. 0F84 8F000000   JE CrackMe3.00403BCA                ; 相同则跳
00403B3B |. 85F6            TEST ESI,ESI                        ; 看看ESI中是否有数据,主要是看看我们有没有输入用户名
00403B3D |. 74 68           JE SHORT CrackMe3.00403BA7          ; 用户名为空则跳
00403B3F |. 85FF            TEST EDI,EDI
00403B41 |. 74 6B           JE SHORT CrackMe3.00403BAE
00403B43 |. 8B46 FC         MOV EAX,DWORD PTR DS:[ESI-4]        ; 用户名长度送EAX
00403B46 |. 8B57 FC         MOV EDX,DWORD PTR DS:[EDI-4]        ; “Registered User”字串的长度送EDX
00403B49 |. 29D0            SUB EAX,EDX                         ; 把用户名长度和“Registered User”字串长度相减
00403B4B |. 77 02           JA SHORT CrackMe3.00403B4F          ; 用户名长度大于“Registered User”长度则跳
00403B4D |. 01C2            ADD EDX,EAX                         ; 把减后值与“Registered User”长度相加,即用户名长度
00403B4F |> 52              PUSH EDX
00403B50 |. C1EA 02         SHR EDX,2                           ; 用户名长度值右移2位,这里相当于长度除以4
00403B53 |. 74 26           JE SHORT CrackMe3.00403B7B          ; 上面的指令及这条指令就是判断用户名长度最少不能低于4
00403B55 |> 8B0E            MOV ECX,DWORD PTR DS:[ESI]          ; 把我们输入的用户名送到ECX
00403B57 |. 8B1F            MOV EBX,DWORD PTR DS:[EDI]          ; 把“Registered User”送到EBX
00403B59 |. 39D9            CMP ECX,EBX                         ; 比较
00403B5B |. 75 58           JNZ SHORT CrackMe3.00403BB5         ; 不等则完蛋

根据上面的分析,我们知道用户名必须是“Registered User”。我们按 F9 键让程序运行,出现错误对话框,点确定,重新在第一个编辑框中输入“Registered User”,再次点击那个“Register now !”按钮,被 OllyDBG 拦下。因为地址 00440F34 处的那个 CALL 我们已经分析清楚了,这次就不用再按 F7 键跟进去了,直接按 F8 键通过。我们一路按 F8 键,来到第二个关键代码处:

00440F49 |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]         ; 取输入的注册码
00440F4C |. BA 2C104400     MOV EDX,CrackMe3.0044102C            ; ASCII “GFX-754-IER-954”
00440F51 |. E8 D62BFCFF     CALL CrackMe3.00403B2C               ; 关键,要用F7跟进去
00440F56 |. 75 1A           JNZ SHORT CrackMe3.00440F72          ; 这里跳走就完蛋

大家注意看一下,地址 00440F51 处的 CALL CrackMe3.00403B2C 和上面我们分析的地址 00440F34 处的 CALL CrackMe3.00403B2C 是不是汇编指令都一样啊?这说明检测用户名和注册码是用的同一个子程序。而这个子程序 CALL 我们在上面已经分析过了。我们执行到现在可以很容易得出结论,这个 CALL 也就是把我们输入的注册码与 00440F4C 地址处指令后的“GFX-754-IER-954”作比较,相等则 OK。好了,我们已经得到足够的信息了。现在我们在菜单 查看->断点 上点击一下,打开断点窗口(也可以通过组合键 ALT+B 或点击工具栏上那个“B”图标打开断点窗口):

为什么要做这一步,而不是把这个断点删除呢?这里主要是为了保险一点,万一分析错误,我们还要接着分析,要是把断点删除了就要做一些重复工作了。还是先禁用一下,如果经过实际验证证明我们的分析是正确的,再删不迟。现在我们把断点禁用,在 OllyDBG 中按 F9 键让程序运行。输入我们经分析得出的内容:
用户名:Registered User
注册码:GFX-754-IER-954
点击“Register now !”按钮,呵呵,终于成功了:

搜索 函数

怎样使用 OllyDBG 搜索函数? 仍然选择 crackmes.cjb.Net 镜像打包中的一个名称为 CrackHead 的 crackme。老规矩,先运行一下这个程序看看:

竟然没找到输入注册码的地方!别急,点一下程序上的那个菜单“Shit”(真是 Shit 啊,呵呵),在下拉菜单中选“Try It”,会来到如下界面:

点一下那个“Check It”按钮试一下,哦,竟然没反应!再输个“78787878”试试,还是没反应。再试试输入字母或其它字符,输不进去。由此判断注册码应该都是数字,只有输入正确的注册码才有动静。用 PEiD 检测一下,结果为 MASM32 / TASM32,怪不得程序比较小。

信息收集的差不多了,现在关掉这个程序,用 OllyDBG 载入,按 F9 键直接让它运行起来,依次点击上面图中所说的菜单,使被调试程序显示如上面的第二个图。先不要点那个“Check It”按钮,保留上图的状态。

现在没有什么字串能搜索,那就在 API 函数上下断点,来让被调试程序中断在我们希望的地方。在 OllyDBG 的反汇编窗口中右击鼠标,在弹出菜单中选择 查找->当前模块中的名称 (标签),或者通过按 CTR+N 组合键也可以达到同样的效果(注意在进行此操作时要在 OllyDBG 中保证是在当前被调试程序的领空,如这里调试这个程序时 OllyDBG 的标题栏显示的就是“[CPU - 主线程, 模块 - CrackHea]”,这表明当前在被调试程序的领空)。通过上面的操作后会弹出一个对话框:

对于这样的编辑框中输注册码的程序,要设断点首选的 API 函数就是 

  • GetDlgItemText 及 GetWindowText。每个函数都有两个版本,一个是 ASCII 版,在函数后添加一个 A 表示,如 GetDlgItemTextA,另一个是 UNICODE 版,在函数后添加一个 W 表示。如 GetDlgItemTextW。对于编译为 UNCODE 版的程序可能在 Win98 下不能运行,因为 Win98 并非是完全支持 UNICODE 的系统。而 NT 系统则从底层支持 UNICODE,它可以在操作系统内对字串进行转换,同时支持ASCII和UNICODE版本函数的调用。一般我们打开的程序看到的调用都是 ASCII 类型的函数,以“A”结尾。

现在回到我们调试的程序上来,我们现在就是要找一下我们调试的程序有没有调用 GetDlgItemTextA 或 GetWindowTextA 函数。还好,找到一个 GetWindowTextA。在这个函数上右击,在弹出菜单上选择 "在每个参考上设置断点",然后会在 OllyDBG 窗口最下面的那个状态栏里看到 "已设置 2 个断点"。另一种方法就是那个 GetWindowTextA 函数上右击,在弹出菜单上选择 "查找输入函数参考"(或者按回车键),将会出现下面的对话框:

看上图,我们可以把两条都设上断点。这个程序只需在第一条指令设断点就可以了。现在按前面提到的第一条方法,就是”在每个参考上设置断点”,这样上图中的两条指令都会设上断点。断点设好后,转到调试的程序上来,现在点击那个“Check It”按钮,被 OllyDBG 断下:

00401323 |. E8 4C010000         CALL <JMP.&USER32.GetWindowTextA>           ; GetWindowTextA
00401328 |. E8 A5000000         CALL CrackHea.004013D2                      ; 关键,要按F7键跟进去
0040132D |. 3BC6                CMP EAX,ESI                                 ; 比较
0040132F |. 75 42               JNZ SHORT CrackHea.00401373                 ; 不等则完蛋
00401331 |. EB 2C               JMP SHORT CrackHea.0040135F
00401333 |. 4E 6F 77 20 7>      ASCII “Now write a keyg”
00401343 |. 65 6E 20 61 6>      ASCII “en and tut and y”
00401353 |. 6F 75 27 72 6>      ASCII “ou&apos;re done.”,0
0040135F |> 6A 00               PUSH 0                                      ; Style = MB_OK|MB_APPLMODAL
00401361 |. 68 0F304000         PUSH CrackHea.0040300F                      ; Title = “Crudd&apos;s Crack Head”
00401366 |. 68 33134000         PUSH CrackHea.00401333                      ; Text = “Now write a keygen and tut and you&apos;re done.”
0040136B |. FF75 08             PUSH DWORD PTR SS:[EBP+8]                   ; hOwner
0040136E |. E8 19010000         CALL <JMP.&USER32.MessageBoxA>              ; MessageBoxA

从上面的代码,我们很容易看出 00401328 地址处的 CALL CrackHea.004013D2 是关键,必须仔细跟踪。而注册成功则会显示一个对话框,标题是“Crudd&apos;s Crack Head”,对话框显示的内容是“Now write a keygen and tut and you&apos;re done.”现在我按一下 F8,准备步进到 00401328 地址处的那条 CALL CrackHea.004013D2 指令后再按 F7 键跟进去。等等,怎么回事?怎么按一下 F8 键跑到这来了:

00401474 - FF25 30204000      JMP DWORD PTR DS:[<&USER32.LoadCursorA>]     ; USER32.LoadCursorA
00401480 - FF25 20204000      JMP DWORD PTR DS:[<&USER32.LoadMenuA>]       ; USER32.LoadMenuA
0040148C $- FF25 24204000      JMP DWORD PTR DS:[<&USER32.MessageBoxA>]     ; USER32.MessageBoxA

原来是跳到另一个断点了。这个断点我们不需要,按一下 F2 键删掉它吧。删掉 00401474 地址处的断点后,我再按 F8 键,呵,完了,跑到 User32.dll 的领空了。看一下 OllyDBG 的标题栏:“[CPU - 主线程, 模块 - USER32],跑到系统领空了,OllyDBG 反汇编窗口中显示代码是这样:

77D3213C 6A 0C                 PUSH 0C
77D3213E 68 A021D377           PUSH USER32.77D321A0
77D32143 E8 7864FEFF           CALL USER32.77D185C0

怎么办?别急,我们按一下 ALT+F9 组合键,呵,回来了:

00401328 |. E8 A5000000        CALL CrackHea.004013D2                     ; 关键,要按F7键跟进去
0040132D |. 3BC6               CMP EAX,ESI                                ; 比较
0040132F |. 75 42              JNZ SHORT CrackHea.00401373                ; 不等则完蛋

光标停在 00401328 地址处的那条指令上。现在我们按 F7 键跟进:

004013D2 /$ 56                PUSH ESI                                    ; ESI入栈
004013D3 |. 33C0              XOR EAX,EAX                                 ; EAX清零
004013D5 |. 8D35 C4334000     LEA ESI,DWORD PTR DS:[4033C4]               ; 把注册码框中的数值送到ESI
004013DB |. 33C9              XOR ECX,ECX                                 ; ECX清零
004013DD |. 33D2              XOR EDX,EDX                                 ; EDX清零
004013DF |. 8A06              MOV AL,BYTE PTR DS:[ESI]                    ; 把注册码中的每个字符送到AL
004013E1 |. 46                INC ESI                                     ; 指针加1,指向下一个字符
004013E2 |. 3C 2D             CMP AL,2D                                   ; 把取得的字符与16进制值为2D的字符(即“-“)比较,这里主要用于判断输入的是不是负数
004013E4 |. 75 08             JNZ SHORT CrackHea.004013EE                 ; 不等则跳
004013E6 |. BA FFFFFFFF       MOV EDX,-1                                  ; 如果输入的是负数,则把-1送到EDX,即16进制FFFFFFFF
004013EB |. 8A06              MOV AL,BYTE PTR DS:[ESI]                    ; 取“-“号后的第一个字符
004013ED |. 46                INC ESI                                     ; 指针加1,指向再下一个字符
004013EE |> EB 0B             JMP SHORT CrackHea.004013FB
004013F0 |> 2C 30             SUB AL,30                                   ; 每位字符减16进制的30,因为这里都是数字,如1的ASCII码是“31H”,减30H后为1,即我们平时看到的数值
004013F2 |. 8D0C89            LEA ECX,DWORD PTR DS:[ECX+ECX*4]            ; 把前面运算后保存在ECX中的结果乘5再送到ECX
004013F5 |. 8D0C48            LEA ECX,DWORD PTR DS:[EAX+ECX*2]            ; 每位字符运算后的值与2倍上一位字符运算后值相加后送ECX
004013F8 |. 8A06              MOV AL,BYTE PTR DS:[ESI]                    ; 取下一个字符
004013FA |. 46                INC ESI                                     ; 指针加1,指向再下一个字符
004013FB |> 0AC0              OR AL,AL
004013FD |.^ 75 F1            JNZ SHORT CrackHea.004013F0                 ; 上面一条和这一条指令主要是用来判断是否已把用户输入的注册码计算完
004013FF |. 8D040A            LEA EAX,DWORD PTR DS:[EDX+ECX]              ; 把EDX中的值与经过上面运算后的ECX中值相加送到EAX
00401402 |. 33C2              XOR EAX,EDX                                 ; 把EAX与EDX异或。如果我们输入的是负数,则此处功能就是把EAX中的值取反
00401404 |. 5E                POP ESI                                     ; ESI出栈。看到这条和下一条指令,我们要考虑一下这个ESI的值是哪里运算得出的呢?
00401405 |. 81F6 53757A79     XOR ESI,797A7553                            ; 把ESI中的值与797A7553H异或
0040140B \. C3                RETN


这里留下了一个问题:那个 ESI 寄存器中的值是从哪运算出来的?先不管这里,我们接着按 F8 键往下走,来到 0040140B 地址处的那条 RETN 指令(这里可以通过在调试选项的”命令”标签中勾选”使用 RET 代替 RETN”来更改返回指令的显示方式),再按一下 F8,我们就走出 00401328 地址处的那个 CALL 了。现在我们回到了这里:

0040132D |. 3BC6             CMP EAX,ESI                                  ; 比较
0040132F |. 75 42            JNZ SHORT CrackHea.00401373                  ; 不等则完蛋

光标停在了 0040132D 地址处的那条指令上。根据前面的分析,我们知道 EAX 中存放的是我们输入的注册码经过计算后的值。我们来看一下信息窗口:

ESI=E6B5F2F9
EAX=FF439EBE

左键选择信息窗口中的 ESI=E6B5F2F9,再按右键,在弹出菜单上选”修改寄存器”,我们会看到这样一个窗口:

可能你的显示跟我不一样,因为这个 crackme 中已经说了每个机器的序列号不一样。关掉上面的窗口,再对信息窗口中的 EAX=FF439EBE 做同样操作:

由上图我们知道了原来前面分析的对我们输入的注册码进行处理后的结果就是把字符格式转为数字格式。我们原来输入的是字串“12345666”,现在转换为了数字 12345666。这下就很清楚了,随便在上面那个修改 ESI 图中显示的有符号或无符号编辑框中复制一个,粘贴到我们调试的程序中的编辑框中试一下:

成功了。且慢高兴,这个 crackme 是要求写出注册机的。我们先不要求写注册机,但注册的算法我们要搞清楚。还记得我在前面说到的那个 ESI 寄存器值的问题吗?现在看看我们上面的分析,其实对做注册机来说是没有多少帮助的。要搞清注册算法,必须知道上面那个 ESI 寄存器值是如何产生的,这弄清楚后才能真正清楚这个 crackme 算法。今天就先说到这里,关于如何追出 ESI 寄存器的值我就留到下一篇-内存断点 中再讲吧。

内存断点

上面分析后发现一个 ESI 寄存器值不知是从什么地方产生的,要弄清这个问题必须要找到生成这个 ESI 值的计算部分。现在任务就是使用 OllyDBG 的内存断点功能找到这个地方,搞清楚这个值是如何算出来的。这次分析的目标程序还是 crackme,温习一下:

00401323 |. E8 4C010000         CALL <JMP.&USER32.GetWindowTextA>           ; GetWindowTextA
00401328 |. E8 A5000000         CALL CrackHea.004013D2                      ; 关键,要按F7键跟进去
0040132D |. 3BC6                CMP EAX,ESI                                 ; 比较
0040132F |. 75 42               JNZ SHORT CrackHea.00401373                 ; 不等则完蛋

我们重新用 OllyDBG 载入目标程序,F9运行来到上面代码所在的地方(你上次设的断点应该没删吧?),我们向上看看能不能找到那个 ESI 寄存器中最近是在哪里赋的值。哈哈,原来就在附近啊:

我们现在知道 ESI 寄存器的值是从内存地址 40339C 中送过来的,那内存地址 40339C 中的数据是什么时候产生的呢?大家注意,我这里信息窗口中显示的是 DS:[0040339C]=9FCF87AA,你那可能是 DS:[0040339C]=XXXXXXXX,这里的 XXXXXXXX 表示的是其它的值,就是说与我这里显示的 9FCF87AA 不一样。我们按上图的操作在数据窗口中看一下:

从上图我们可以看出内存地址 40339C 处的值已经有了,说明早就算过了。现在怎么办呢?我们考虑一下,看情况程序是把这个值算出来以后写在这个内存地址,那我们要是能让 OllyDBG 在程序开始往这个内存地址写东西的时候中断下来,不就有可能知道目标程序是怎么算出这个值的吗?说干就干,我们在 OllyDBG 的菜单上点 调试->重新开始,或者按 CTR+F2 组合键(还可以点击工具栏上的那个有两个实心左箭头的图标)来重新载入程序。这时会跳出一个”进程仍处于激活状态”的对话框(我们可以在在调试选项的安全标签下把”终止活动进程时警告”这条前面的勾去掉,这样下次就不会出现这个对话框了),问我们是否要终止进程。这里我们选”是”,程序被重新载入,我们停在下面这一句上:

00401000 >/$ 6A 00              PUSH 0                                      ; pModule = NULL

现在我们就要来设内存断点了。在 OllyDBG 中一般我们用到的内存断点有内存访问和内存写入断点。内存访问断点就是指程序访问内存中我们指定的内存地址时中断,内存写入断点就是指程序往我们指定的内存地址中写东西时中断。更多关于断点的知识大家可以参考 论坛精华7->基础知识->断点技巧->断点原理 这篇 Lenus 兄弟写的《如何对抗硬件断点之一 — 调试寄存器》文章,也可以看这个帖:http://bbs.pediy.com/showthread.PHP?threadid=10829。根据当前我们调试的具体程序的情况,我们选用内存写入断点。还记得前面我叫大家记住的那个 40339C 内存地址吗?现在我们要用上了。我们先在 OllyDBG 的数据窗口中左键点击一下,再右击,会弹出一个如下图所示的菜单。我们选择其中的转到->表达式(也可以左键点击数据窗口后按 CTR+G 组合键)。如下图:

现在将会出现这样一个对话框:
 
我们在上面那个编辑框中输入我们想查看内容的内存地址 40339C,然后点确定按钮,数据窗口中显示如下:

我们可以看到,40339C 地址开始处的这段内存里面还没有内容。我们现在在 40339C 地址处后面的 HEX 数据或 ASCII 栏中按住左键往后拖放,选择一段。内存断点的特性就是不管你选几个字节,OllyDBG 都会分配 4096 字节的内存区。这里我就选从 40339C 地址处开始的四个字节,主要是为了让大家提前了解一下硬件断点的设法,因为硬件断点最多只能选 4 个字节。选中部分会显示为灰色。选好以后松开鼠标左键,在我们选中的灰色部分上右击:

经过上面的操作,内存断点就设好了(这里还有个要注意的地方:内存断点只在当前调试的进程中有效,就是说你如果重新载入程序的话内存断点就自动删除了,且内存断点每一时刻只能有一个。就是说你不能像按 F2 键那样同时设置多个断点)。现在按 F9 键让程序运行 OllyDBG 中断

7C932F39 8808                   MOV BYTE PTR DS:[EAX],CL                    ; 这就是我们第一次断下来的地方
7C932F3B 40                     INC EAX
7C932F3C 4F                     DEC EDI
7C932F3D 4E                     DEC ESI
7C932F3E ^ 75 CB                JNZ SHORT ntdll.7C932F0B
7C932F40 8B4D 10                MOV ECX,DWORD PTR SS:[EBP+10]

上面就是我们中断后反汇编窗口中的代码。如果你是其它系统,如 Win98 的话,可能会有所不同。没关系,这里不是关键。我们看一下领空,原来是在 ntdll.dll 内。系统领空,我们现在要考虑返回到程序领空。返回前我们看一下数据窗口:

现在我们转到反汇编窗口,右击鼠标,在弹出菜单上选择断点->删除内存断点,这样内存断点就被删除了。

现在我们来按一下 ALT+F9 组合键,我们来到下面的代码:

00401431 |. 8D35 9C334000      LEA ESI,DWORD PTR DS:[40339C]               ; ALT+F9返回后来到的位置
00401437 |. 0FB60D EC334000    MOVZX ECX,BYTE PTR DS:[4033EC]
0040143E |. 33FF               XOR EDI,EDI

我们把反汇编窗口往上翻翻,呵,原来就在我们上一篇分析的代码下面啊?

现在我们在 0040140C 地址处那条指令上按 F2 设置一个断点,现在我们按  CTR+F2 组合键重新载入程序,载入后按 F9 键运行,我们将会中断在我们刚才在 0040140C 地址下的那个断点处:

0040140C /$ 60                 PUSHAD
0040140D |. 6A 00              PUSH 0                                      ; /RootPathName = NULL
0040140F |. E8 B4000000        CALL <JMP.&KERNEL32.GetDriveTypeA>          ; \GetDriveTypeA
00401414 |. A2 EC334000        MOV BYTE PTR DS:[4033EC],AL                 ; 磁盘类型参数送内存地址4033EC
00401419 |. 6A 00              PUSH 0                                      ; /pFileSystemNameSize = NULL
0040141B |. 6A 00              PUSH 0                                      ; |pFileSystemNameBuffer = NULL
0040141D |. 6A 00              PUSH 0                                      ; |pFileSystemFlags = NULL
0040141F |. 6A 00              PUSH 0                                      ; |pMaxFilenameLength = NULL
00401421 |. 6A 00              PUSH 0                                      ; |pVolumeSerialNumber = NULL
00401423 |. 6A 0B              PUSH 0B                                     ; |MaxVolumeNameSize = B (11.)
00401425 |. 68 9C334000        PUSH CrackHea.0040339C                      ; |VolumeNameBuffer = CrackHea.0040339C
0040142A |. 6A 00              PUSH 0                                      ; |RootPathName = NULL
0040142C |. E8 A3000000        CALL <JMP.&KERNEL32.GetVolumeInformationA>  ; \GetVolumeInformationA
00401431 |. 8D35 9C334000      LEA ESI,DWORD PTR DS:[40339C]               ; 把crackme程序所在分区的卷标名称送到ESI
00401437 |. 0FB60D EC334000    MOVZX ECX,BYTE PTR DS:[4033EC]              ; 磁盘类型参数送ECX
0040143E |. 33FF               XOR EDI,EDI                                 ; 把EDI清零
00401440 |> 8BC1               MOV EAX,ECX                                 ; 磁盘类型参数送EAX
00401442 |. 8B1E               MOV EBX,DWORD PTR DS:[ESI]                  ; 把卷标名作为数值送到 EBX
00401444 |. F7E3               MUL EBX                                     ; 循环递减取磁盘类型参数值与卷标名值相乘
00401446 |. 03F8               ADD EDI,EAX                                 ; 每次计算结果再加上上次计算结果保存在EDI中
00401448 |. 49                 DEC ECX                                     ; 把磁盘类型参数作为循环次数,依次递减
00401449 |. 83F9 00            CMP ECX,0                                   ; 判断是否计算完
0040144C |.^ 75 F2             JNZ SHORT CrackHea.00401440                 ; 没完继续
0040144E |. 893D 9C334000      MOV DWORD PTR DS:[40339C],EDI               ; 把计算后值送到内存地址40339C,这就是我们后来在ESI中看到的值
00401454 |. 61                 POPAD
00401455 \. C3                 RETN

通过上面的分析,我们知道基本算法是这样的:先用 GetDriveTypeA 函数获取磁盘类型参数,再用 GetVolumeInformationA 函数获取这个 crackme 程序所在分区的卷标。如我把这个 Crackme 程序放在 F:\OD教程\crackhead\ 目录下,而我 F 盘设置的卷标是 GAME,则这里获取的就是 GAME,ASCII 码为“47414D45”。但我们发现一个问题:假如原来我们在数据窗口中看到的地址 40339C 处的 16 进制代码是“47414D45”,即“GAME”,但经过地址 00401442 处的那条 MOV EBX,DWORD PTR DS:[ESI] 指令后,我们却发现 EBX 中的值是“454D4147”,正好把我们上面那个“47414D45”反过来了。为什么会这样呢?如果大家对 x86系列 CPU 的存储方式了解的话,这里就容易理解了。我们知道“GAME”有四个字节,即 ASCII 码为“47414D45”。我们看一下数据窗口中的情况:

0040339C     47 41 4D 45 00 00 00 00 00 00 00 00 00 00 00 00     GAME…………

大家可以看出来内存地址 40339CH 到 40339FH 分别按顺序存放的是 47 41 4D 45。
如下图:

系统存储的原则为”高高低低”,即低字节存放在地址较低的字节单元中,高字节存放在地址较高的字节单元中。比如一个字由两个字节组成,像这样:12 34 ,这里的高字节就是 12 ,低字节就是 34。上面的那条指令 MOV EBX,DWORD PTR DS:[ESI] 等同于 MOV EBX,DWORD PTR DS:[40339C]。注意这里是 DWORD,即”双字”,由 4 个连续的字节构成。而取地址为 40339C 的双字单元中的内容时,我们应该得到的是“454D4147”,即由高字节到低字节顺序的值。因此经过 MOV EBX,DWORD PTR DS:[ESI] 这条指令,就是把从地址 40339C 开始处的值送到 EBX,所以我们得到了“454D4147”。好了,这里弄清楚了,我们再接着谈这个程序的算法。前面我们已经说了取磁盘类型参数做循环次数,再取卷标值 ASCII 码的逆序作为数值,有了这两个值就开始计算了。现在我们把磁盘类型值作为 n,卷标值 ASCII 码的逆序数值作为 a,最后得出的结果作为 b,有这样的计算过程:
第一次:b = a * n
第二次:b = a * (n - 1) + b
第三次:b = a * (n - 2) + b

第 n 次:b = a * 1 + b
可得出公式为 b = a * [n + (n - 1) + (n - 2) + … + 1] = a * [n * (n + 1) / 2]
还记得上一篇我们的分析吗?看这一句:

00401405 |. 81F6 53757A79     XOR ESI,797A7553                            ; 把ESI中的值与797A7553H异或

这里算出来的 b 最后还要和 797A7553H 异或一下才是真正的注册码。只要你对编程有所了解,这个注册机就很好写了。如果用汇编来写这个注册机的话就更简单了,很多内容可以直接照抄。
到此已经差不多了,最后还有几个东西也说一下吧:
1、上面用到了两个 API 函数,一个是 GetDriveTypeA,还有一个是 GetVolumeInformationA,关于这两个函数的具体用法我就不多说了,大家可以查一下 MSDN。这里只要大家注意函数参数传递的次序,即调用约定。先看一下这里:

00401419 |. 6A 00              PUSH 0                                      ; /pFileSystemNameSize = NULL
0040141B |. 6A 00              PUSH 0                                      ; |pFileSystemNameBuffer = NULL
0040141D |. 6A 00              PUSH 0                                      ; |pFileSystemFlags = NULL
0040141F |. 6A 00              PUSH 0                                      ; |pMaxFilenameLength = NULL
00401421 |. 6A 00              PUSH 0                                      ; |pVolumeSerialNumber = NULL
00401423 |. 6A 0B              PUSH 0B                                     ; |MaxVolumeNameSize = B (11.)
00401425 |. 68 9C334000        PUSH CrackHea.0040339C                      ; |VolumeNameBuffer = CrackHea.0040339C
0040142A |. 6A 00              PUSH 0                                      ; |RootPathName = NULL
0040142C |. E8 A3000000        CALL <JMP.&KERNEL32.GetVolumeInformationA>  ; \GetVolumeInformationA

把上面代码后的 OllyDBG 自动添加的注释与 MSDN 中的函数原型比较一下:
BOOL GetVolumeInformation(
LPCTSTR lpRootPathName,             // address of root directory of the file system
LPTSTR lpVolumeNameBuffer,          // address of name of the volume
DWORD nVolumeNameSize,              // length of lpVolumeNameBuffer
LPDWORD lpVolumeSerialNumber,       // address of volume serial number
LPDWORD lpMaximumComponentLength,   // address of system&apos;s maximum filename length
LPDWORD lpFileSystemFlags,          // address of file system flags
LPTSTR lpFileSystemNameBuffer,      // address of name of file system
DWORD nFileSystemNameSize           // length of lpFileSystemNameBuffer
);

大家应该看出来点什么了吧?函数调用是先把最后一个参数压栈,参数压栈顺序是从后往前。这就是一般比较常见的 stdcall 调用约定。
2、我在前面的 00401414 地址处的那条 MOV BYTE PTR DS:[4033EC],AL 指令后加的注释是”磁盘类型参数送内存地址4033EC”。为什么这样写?大家把前一句和这一句合起来看一下:

0040140F |. E8 B4000000        CALL <JMP.&KERNEL32.GetDriveTypeA>          ; \GetDriveTypeA
00401414 |. A2 EC334000        MOV BYTE PTR DS:[4033EC],AL                 ; 磁盘类型参数送内存地址4033EC

地址 0040140F 处的那条指令是调用 GetDriveTypeA 函数,一般函数调用后的返回值都保存在 EAX 中,所以地址 00401414 处的那一句 MOV BYTE PTR DS:[4033EC],AL 就是传递返回值。查一下 MSDN 可以知道 GetDriveTypeA 函数的返回值有这几个:

Value                     Meaning                                        返回在EAX中的值
DRIVE_UNKNOWN             The drive type cannot be determined.               0
DRIVE_NO_ROOT_DIR         The root directory does not exist.                 1
DRIVE_REMOVABLE           The disk can be removed from the drive.            2
DRIVE_FIXED               The disk cannot be removed from the drive.         3
DRIVE_REMOTE              The drive is a remote (network) drive.             4
DRIVE_CDROM               The drive is a CD-ROM drive.                       5
DRIVE_RAMDISK             The drive is a RAM disk.                           6

上面那个”返回在EAX中的值”是我加的,我这里返回的是 3,即磁盘不可从驱动器上删除。
3、通过分析这个程序的算法,我们发现这个注册算法是有漏洞的。如果我的分区没有卷标的话,则卷标值为 0,最后的注册码就是 797A7553H,即十进制 2038068563。而如果你的卷标和我一样,且磁盘类型一样的话,注册码也会一样,并不能真正做到一机一码。

消息断点、RUN 跟踪

找了几十个不同语言编写的 crackme,发现只用消息断点的话有很多并不能真正到达要找的关键位置,想想还是把消息断点和 RUN 跟踪结合在一起讲,更有效一点。关于消息断点的更多内容大家可以参考 jingulong 兄的那篇《几种典型程序Button处理代码的定位》的文章,堪称经典之作。今天仍然选择 crackmes.cjb.net 镜像打包中的一个名称为 cycle 的 crackme。按照惯例,先运行一下这个程序看看:

输入用户名 CCDebuger,序列号 78787878,点上面那个“Check”按钮,没反应!看来是要注册码正确才有动静。现在关掉 crackme,用 PEiD 查一下壳,原来是 MASM32 / TASM32 [Overlay]。启动 OllyDBG 载入这个程序,F9让它运行。这个程序按我们前面讲的采用字串参考或函数参考的方法都很容易断下来。但我们今天主要学习的是消息断点及 RUN 跟踪,就先用消息断点来断这个程序吧。

在设消息断点前,有两个内容我们要简单了解一下:

  • 首先要了解的是消息。Windows 的中文翻译就是”窗口”,而 Windows 上面的应用程序也都是通过窗口来与用户交互的。现在就有一个问题,应用程序是如何知道用户作了什么样的操作的?这里就要用到消息了。Windows 是个基于消息的系统,它在应用程序开始执行后,为该程序创建一个”消息队列”,用来存放该程序可能创建的各种不同窗口的信息。比如你创建窗口、点击按钮、移动鼠标等等,都是通过消息来完成的。通俗的说,Windows 就像一个中间人,你要干什么事是先通知它,然后它才通过传递消息的方式通知应用程序作出相应的操作。
  • 说到这,又有个问题了,在 Windows 下有多个程序都在运行,那我点了某个按钮,或把某个窗口最大化,Windows 知道我是点的哪个吗?这里就要说到另一个内容:句柄(handle)了。句柄一般是个 32 位的数,表示一个对象。Windows 通过使用句柄来标识它代表的对象。比如你点击某个按钮,Windows 就是通过句柄来判断你是点击了那一个按钮,然后发送相应的消息通知程序。

用 OllyDBG 把这个 crackme 载入并按 F9 键运行,输入用户名“CCDebuger”,序列号“78787878”,先不要点那个“Check”按钮,我们来到 OllyDBG 中,点击菜单 查看->窗口(或者点击工具栏上那个“W”的图标),我们会看到以下内容:

我们在选中的条目上点右键,再选择上图所示的菜单项,会来到下面这个窗口:

现在我们点击图上的那个下拉菜单,呵,原来里面的消息真不少。这么多消息我们选哪个呢?注册是个按钮,我们就在按下按钮再松开时让程序中断。查一下 MSDN,我们知道这个消息应该是 WM_LBUTTON_UP,看字面意思也可以知道是左键松开时的消息:

从下拉菜单中选中那个 202 WM_LBUTTON_UP,再按确定按钮,我们的消息断点就设好了。现在我们还要做一件事,就是把 RUN 跟踪打开。有人可能要问,这个 RUN 跟踪是干什么的?简单的说,RUN 跟踪就是把被调试程序执行过的指令保存下来,让你可以查看被调试程序运行期间干了哪些事。RUN 跟踪会把地址、寄存器的内容、消息以及已知的操作数记录到 RUN 跟踪缓冲区中,你可以通过查看 RUN 跟踪的记录来了解程序执行了那些指令。在这还要注意一个缓冲区大小的问题,如果执行的指令太多,缓冲区满了的话,就会自动丢弃前面老的记录。我们可以在调试选项->跟踪中设置:

现在我们回到 OllyDBG 中,点击菜单调试->打开或清除 RUN 跟踪(第一次点这个菜单是打开 RUN 跟踪,在打开的情况下点击就是清除 RUN 跟踪的记录,对 RUN 跟踪熟悉时还可以设置条件),保证当前在我们调试的程序领空,在反汇编窗口中点击右键,在弹出菜单中选择 RUN 跟踪->添加所有函数过程的入口:

我们可以看到 OllyDBG 把识别出的函数过程都在前面加了灰色条:

现在我们回到那个 crackme 中按那个“Check”按钮,被 OllyDBG 断下了:

这时我们点击菜单查看->内存,或者点击工具栏上那个“M”按钮(也可以按组合键 ALT+M),来到内存映射窗口:

为什么在这里设访问断点,我也说一下。我们可以看一下常见的 PE 文件,没加过壳的用 PEiD 检测是这样:

点一下 EP 段后面那个“>”符号,我们可以看到以下内容:

看完上面的图我们应该了解为什么在 401000 处的代码段下访问断点了,我们这里的意思就是在消息断点断下后,只要按 F9 键运行时执行到程序代码段的指令我们就中断,这样就可以回到程序领空了(当然在 401000 处所在的段不是绝对的,我们主要是要看程序的代码段在什么位置,其实在上面图中 OllyDBG 内存窗口的”包含”栏中我们就可以看得很清楚了)。设好访问断点后我们按 F9 键,被 OllyDBG 断下:

现在我们先不管,按 F9 键(或者按 CTR+F12 组合键跟踪步过)让程序运行,再点击菜单查看->RUN 跟踪,或者点击工具栏上的那个“…”符号,打开 RUN 跟踪的记录窗口看看:

我们现在再来看看统计的情况:

在地址 401082 处的那条指令上双击一下,来到以下位置:

现在我们在地址 4010A6 处的那条指令上按 F2,删除所有其它的断点,点菜单调试->关闭 RUN 跟踪,现在我们就可以开始分析了:

004010E2 |. 8BFE             MOV EDI,ESI                                         ; 用户名送 EDI
004010E4 |. 03F8             ADD EDI,EAX
004010E6 |. FC               CLD
004010E7 |. F3:A4            REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
004010E9 |. 33C9             XOR ECX,ECX                                         ; 清零,设循环计数器
004010EB |. BE 71214000      MOV ESI,cycle.00402171                              ; 注册码送ESI
004010F0 |> 41               INC ECX
004010F1 |. AC               LODS BYTE PTR DS:[ESI]                              ; 取注册码的每个字符
004010F2 |. 0AC0             OR AL,AL                                            ; 判断是否为空
004010F4 |. 74 0A            JE SHORT cycle.00401100                             ; 没有则跳走
004010F6 |. 3C 7E            CMP AL,7E                                           ; 判断字符是否为非ASCII字符
004010F8 |. 7F 06            JG SHORT cycle.00401100                             ; 非ASCII字符跳走
004010FA |. 3C 30            CMP AL,30                                           ; 看是否小于30H,主要是判断是不是数字或字母等
004010FC |. 72 02            JB SHORT cycle.00401100                             ; 小于跳走
004010FE |.^ EB F0           JMP SHORT cycle.004010F0
00401100 |> 83F9 11          CMP ECX,11                                          ; 比较注册码位数,必须为十进制17位
00401103 |. 75 1A            JNZ SHORT cycle.0040111F
00401105 |. E8 E7000000      CALL cycle.004011F1                                 ; 关键,F7跟进去
0040110A |. B9 01FF0000      MOV ECX,0FF01
0040110F |. 51               PUSH ECX
00401110 |. E8 7B000000      CALL cycle.00401190                                 ; 关键,跟进去
00401115 |. 83F9 01          CMP ECX,1
00401118 |. 74 06            JE SHORT cycle.00401120
0040111A |> E8 47000000      CALL cycle.00401166                                 ; 注册失败对话框
0040111F |> C3               RETN
00401120 |> A1 68214000      MOV EAX,DWORD PTR DS:[402168]
00401125 |. 8B1D 6C214000    MOV EBX,DWORD PTR DS:[40216C]
0040112B |. 33C3             XOR EAX,EBX
0040112D |. 3305 82214000    XOR EAX,DWORD PTR DS:[402182]
00401133 |. 0D 40404040      OR EAX,40404040
00401138 |. 25 77777777      AND EAX,77777777
0040113D |. 3305 79214000    XOR EAX,DWORD PTR DS:[402179]
00401143 |. 3305 7D214000    XOR EAX,DWORD PTR DS:[40217D]
00401149 |.^ 75 CF           JNZ SHORT cycle.0040111A                             ; 这里跳走就完蛋
0040114B |. E8 2B000000      CALL cycle.0040117B                                  ; 注册成功对话框

写到这准备跟踪算法时,才发现这个 crackme 还是挺复杂的,具体算法我就不写了,实在没那么多时间详细跟踪。有兴趣的可以跟一下,注册码是17位,用户名采用复制的方式扩展到 16 位,如我输入“CCDebuger”,扩展后就是“CCDebugerCCDebug”。大致是先取扩展后用户名的前 8 位和注册码的前 8 位,把用户名的前四位和后四位分别与注册码的前四位和后四位进行运算,算完后再把扩展后用户名的后 8 位和注册码的后 8 位分两部分,再与前面用户名和注册码的前 8 位计算后的值进行异或计算,最后结果等于 0 就成功。注册码的第 17 位我尚未发现有何用处。对于新手来说,可能这个 crackme 的难度大了一点。没关系,我们主要是学习 OllyDBG 的使用,方法掌握就可以了。

最后说明一下:
1、这个程序在设置了消息断点后可以省略在代码段上设访问断点那一步,直接打开 RUN 跟踪,消息断点断下后按 CTR+F12 组合键让程序执行,RUN 跟踪记录中就可以找到关键地方。
2、对于这个程序,你可以不设消息断点,在输入用户名和注册码后先不按那个“Check”按钮,直接打开 RUN 跟踪,添加”所有函数过程的入口”后再回到程序中点“Check”按钮,这时在 OllyDBG 中打开 RUN 跟踪记录同样可以找到关键位置。

汇编功能

目标程序是 MyUninstaller 1.34 版。这是一个非常小的程序卸载工具,VC6编写,大小只有61K。我拿到的这个是上次闪电狼兄弟给我的,附带在里面的简体中文语言文件是由六芒星制作的。这个程序有个毛病:就是在列出的可卸载程序上双击查看属性时,弹出的属性窗口的字体非常难看,应该就是系统字体(SYSTEM_FONT):

今天的目标就是利用 OllyDBG 的汇编功能把上面显示的字体改成我们常见的9号(小五)宋体。首先我们用 OllyDBG 载入程序,按 CTR+N 组合键查找一下有哪些 API 函数,只发现一个和设置字体相关的 CreateFontIndirectA。现在我们按鼠标右键,选择”在每个参考上设置断点”,关掉名称对话框,F9运行,程序已经运行起来了。我们在程序的列表框中随便找一项双击一下,很不幸,那个字体难看的界面又出现了,OllyDBG 没有任何动作。可见创建这个窗口的时候根本没调用 CreateFontIndirectA,问题现在就变得有点复杂了。先点确定把这个字体难看的对话框关闭,现在我们从另一个方面考虑:既然没有调用设置字体的函数,那我们来看看这个窗口是如何创建的,跟踪窗口创建过程可能会找到一些对我们有用的信息。现在我们再回到我们调试程序的领空,按 CTR+N 看一下,发现 CreateWindowExA 这个 API 函数比较可疑。我们在 CreateWindowExA 函数的每个参考上设上断点,在 MyUninstaller 的列表框中再随便找一项双击一下,被 OllyDBG 断下:

  00408F5E  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; \断在这里

上下翻看一下代码:

  00408F3B  |.  50              |PUSH EAX                                         ; |hInst
  00408F3C  |.  8B45 C0         |MOV EAX,DWORD PTR SS:[EBP-40]                    ; |
  00408F3F  |.  6A 00           |PUSH 0                                           ; |hMenu = NULL
  00408F41  |.  03C6            |ADD EAX,ESI                                      ; |
  00408F43  |.  FF75 08         |PUSH DWORD PTR SS:[EBP+8]                        ; |hParent
  00408F46  |.  FF75 D0         |PUSH DWORD PTR SS:[EBP-30]                       ; |Height
  00408F49  |.  57              |PUSH EDI                                         ; |Width
  00408F4A  |.  50              |PUSH EAX                                         ; |Y
  00408F4B  |.  FF75 BC         |PUSH DWORD PTR SS:[EBP-44]                       ; |X
  00408F4E  |.  FF75 EC         |PUSH DWORD PTR SS:[EBP-14]                       ; |Style
  00408F51  |.  68 80DE4000     |PUSH myuninst.0040DE80                           ; |WindowName = ”“
  00408F56  |.  68 DCD94000     |PUSH myuninst.0040D9DC                           ; |Class = “STATIC”
  00408F5B  |.  FF75 D4         |PUSH DWORD PTR SS:[EBP-2C]                       ; |ExtStyle
  00408F5E  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; \断在这里
  00408F64  |   6A 00           |PUSH 0                                           ;  第一处要修改的地方
  00408F66  |   8945 F4         |MOV DWORD PTR SS:[EBP-C],EAX
  00408F69  |.  E8 A098FFFF     |CALL <myuninst.sub_40280E>
  00408F6E  |.  50              |PUSH EAX                                         ; |hInst
  00408F6F  |.  8B45 DC         |MOV EAX,DWORD PTR SS:[EBP-24]                    ; |
  00408F72  |.  6A 00           |PUSH 0                                           ; |hMenu = NULL
  00408F74  |.  03F0            |ADD ESI,EAX                                      ; |
  00408F76  |.  FF75 08         |PUSH DWORD PTR SS:[EBP+8]                        ; |hParent
  00408F79  |.  FF75 CC         |PUSH DWORD PTR SS:[EBP-34]                       ; |Height
  00408F7C  |.  53              |PUSH EBX                                         ; |Width
  00408F7D  |.  56              |PUSH ESI                                         ; |Y
  00408F7E  |.  FF75 D8         |PUSH DWORD PTR SS:[EBP-28]                       ; |X
  00408F81  |.  FF75 E8         |PUSH DWORD PTR SS:[EBP-18]                       ; |Style
  00408F84  |.  68 80DE4000     |PUSH myuninst.0040DE80                           ; |WindowName = ”“
  00408F89  |.  68 D4D94000     |PUSH myuninst.0040D9D4                           ; |Class = “EDIT”
  00408F8E  |.  FF75 B8         |PUSH DWORD PTR SS:[EBP-48]                       ; |ExtStyle
  00408F91  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; \CreateWindowExA
  00408F97  |   8945 F0         |MOV DWORD PTR SS:[EBP-10],EAX                    ;  第二处要修改的地方
  00408F9A  |   8B45 F8         |MOV EAX,DWORD PTR SS:[EBP-8]
  00408F9D  |.  FF30            |PUSH DWORD PTR DS:[EAX]                          ; /<%s>
  00408F9F  |.  8D85 B0FEFFFF   |LEA EAX,DWORD PTR SS:[EBP-150]                   ; |
  00408FA5  |.  68 D0D94000     |PUSH myuninst.0040D9D0                           ; |format = ”%s:”
  00408FAA  |.  50              |PUSH EAX                                         ; |s
  00408FAB  |.  FF15 90B14000   |CALL DWORD PTR DS:[<&MSVCRT.sprintf>]            ; \sprintf
  00408FB1  |.  8B35 84B24000   |MOV ESI,DWORD PTR DS:[<&USER32.SetWindowTextA>]  ;  USER32.SetWindowTextA
  00408FB7  |.  83C4 0C         |ADD ESP,0C
  00408FBA  |.  8D85 B0FEFFFF   |LEA EAX,DWORD PTR SS:[EBP-150]
  00408FC0  |.  50              |PUSH EAX                                         ; /Text
  00408FC1  |.  FF75 F4         |PUSH DWORD PTR SS:[EBP-C]                        ; |hWnd
  00408FC4  |.  FFD6            |CALL ESI                                         ; \SetWindowTextA
  00408FC6  |.  8D85 ACFAFFFF   |LEA EAX,DWORD PTR SS:[EBP-554]
  00408FCC  |.  50              |PUSH EAX                                         ; /Arg3
  00408FCD  |.  FF75 FC         |PUSH DWORD PTR SS:[EBP-4]                        ; |Arg2
  00408FD0  |.  FF35 00EF4000   |PUSH DWORD PTR DS:[40EF00]                       ; |Arg1 = 00BEADCC
  00408FD6  |.  E8 1884FFFF     |CALL  <myuninst.sub_4013F3>                      ; \sub_4013F3
  00408FDB  |.  83C4 0C         |ADD ESP,0C
  00408FDE  |.  50              |PUSH EAX
  00408FDF  |.  FF75 F0         |PUSH DWORD PTR SS:[EBP-10]
  00408FE2  |.  FFD6            |CALL ESI
  00408FE4  |.  FF45 FC         |INC DWORD PTR SS:[EBP-4]
  00408FE7  |.  8345 F8 14      |ADD DWORD PTR SS:[EBP-8],14
  00408FEB  |.  837D FC 0F      |CMP DWORD PTR SS:[EBP-4],0F
  00408FEF  |.^ 0F8C 32FFFFFF   \JL <myuninst.loc_408F27>
  00408FF5  |.  5F              POP EDI
  00408FF6  |.  5E              POP ESI
  00408FF7  |.  5B              POP EBX
  00408FF8  |.  C9              LEAVE
  00408FF9  \.  C3              RETN
 

我想上面的代码我不需多做解释,OllyDBG 自动给出的注释已经够清楚的了。我们双击 MyUninstaller 列表框中的的某项查看属性时,弹出的属性窗口上的 STATIC 控件和 EDIT 控件都是由 CreateWindowExA 函数创建的,然后再调用 SetWindowTextA 来设置文本,根本没考虑控件上字体显示的问题,所以我们看到的都是系统默认的字体。我们要设置控件上的字体,可以考虑在 CreateWindowExA 创建完控件后,在使用 SetWindowTextA 函数设置文本之前调用相关字体创建函数来选择字体,再调用 SendMessageA 函数发送 WM_SETFONT 消息来设置控件字体。思路定下来后,我们就开始来实施。首先我们看一下这个程序中的导入函数,CreateFontIndirectA 这个字体创建函数已经有了,再看看 SendMessageA,呵呵,不错,原程序也有这个函数。这样我们就省事了。有人可能要问,如果原来并没有这两个导入函数,那怎么办呢?其实这也很简单,我们可以直接用 LordPE 来在程序中添加我们需要的导入函数。我这里用个很小的 PE 工具 zeroadd 来示范一下,这个程序里面没有 CreateFontIndirectA 和 SendMessageA 函数(这里还有个问题说一下,其实我们编程时调用这两个函数时都是直接写 CreateFontIndirect 及 SendMessage,一般不需指定。但在程序中写补丁代码时我们要指定这是什么类型的函数。这里在函数后面加个“A”表示这是 ASCII 版本,同样 UNICODE 版本在后面加个“W”,如 SendMessageW。在 Win9X 下我们一般都用 ASCII 版本的函数,UNICODE 版本的函数很多在 Win9X 下是不能运行的。而NT 系统如 WinXP 一般都是 UNICODE 版本的,但如果我们用了 ASCII 版本的函数,系统会自动转换调用 UNICODE 版本。这样我们写补丁代码的时候就可以直接指定为 ASCII 版本的函数,可以兼容各个系统):我们用 LordPE 的 PE 编辑器载入 zeroadd 程序,选择”目录”,再在弹出的目录表对话框中选择输入表后面的那个“…”按钮,会弹出一个对话框:

因为 SendMessageA 在 USER32.dll 中,我们在右键菜单中点击按钮”添加导入表”,来到下面:

按上面的提示完成后点”确定”,我们回到原先的那个”输入表”对话框:

从上图中我们可以看出多出了一个 USER32.dll,这就是我们添加 SendMessageA 的结果。这也是用工具添加的一个缺点。我们一般希望把添加的函数直接放到已存在的 DLL 中,而不是多出来一个,这样显得不好看。但用工具就没办法,LordPE 默认是建一个 1K 的新区段来保存添加后的结果,由此出现了上图中的情况。如果你对 PE 结构比较熟悉的话,也可以直接用 16进制编辑工具来添加你需要的函数,这样改出来的东西好看。如果想偷懒,就像我一样用工具吧,呵呵。在上图中我还标出了要注意 FirstThunk 及那个 ThunkRVA 的值,并且要把”总是查看 FirstThunk”那个选项选上。有人可能不理解其作用,我这里也解释一下:一般讲述 PE 格式的文章中对 FirstThunk 的解释是这样的:FirstThunk 包含指向一个 IMAGE_THUNK_DATA 结构数组的 RVA 偏移地址,当把 PE 文件装载到内存中时,PE装载器将查找 IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME 这些结构数组来决定导入函数的地址,随后用导入函数真实地址来替代由 FirstThunk 指向的 IMAGE_THUNK_DATA 数组里的元素值。这样说起来还是让人不明白,我举个例子:比如你有个很要好的朋友,他是个大忙人,虽然你知道他的家庭住址,可他很少回家。如果你哪天想找他,直接去他家,很可能吃个闭门羹,找不到他人。怎么办?幸好你有他的手机号码,你就给他拨了一个电话:”小子,你在哪呢?”,他告诉你:”我正在XXX饭店喝酒呢!”这时你怎么办?(当然是杀到他说的那家饭店去蹭饭了!^_^)这里的 ThunkRVA 就相当于你朋友的手机号码, SendMessageA 就相当于你那个朋友。而 FirstThunk 就是你手机里的号码分组。你把你的多个朋友都放在 FirstThunk 这样的号码分组里,每个 ThunkRVA 就是你一个朋友的手机号码。你要找他们,就是通过 ThunkRVA 这样的手机号码来和他们联系,直接去他家找他你很可能要碰壁。而移动或联通就相当于操作系统,他们负责把你的手机号码和你的朋友对应上。而 FirstThunk 这样的号码分组还有一个好处就是你可以不记你某个朋友的具体号码,只要记得 FirstThunk 号码分组的值,你的朋友会按顺序在里面排列。比如上图中 USER32.dll 中的第一个函数是 SendMessageA,它的 ThunkRVA 值就是 FirstThunk 值。如果还有第二个函数,比如是 MessageBoxA,它的值就是 FirstThunk 值加上 4,其余类推。你只要记住各个函数的位置,也可以通过 FirstThunk 加上位置对应值来找到它。当然这比不上直接看 ThunkRVA 来得方便。说了上面这些,我们就要考虑怎么在程序中调用了。你可能会说,我在 OllyDBG 中直接在我们要修改的程序中这样调用:CALL SendMessageA。哦,别这样。这等于我上面说的都是废话,会让我感到伤心的。你这里的 CALL SendMessageA 就相当于也不跟你朋友打个招呼就直接去他家找他,很有可能你会乘兴而去,败兴而归。别忘了他的手机号码,我们只有通过号码才知道他到底在什么地方。我们应该这样:CALL DWORD PTR [40B01A],这里的 40B01A 就是上面的 SendMessageA 在程序载入后的所在的地方,由基址 00400000 加上 ThunkRVA 0000B01A 得到的。这就是你要找的人所在的地方,不管他跑到哪,你有他的手机号码就能找到他。同样道理,你只要记住了 ThunkRVA 值,就按这个来调用你需要的函数,在别的 Windows 系统下也是没有问题的。系统会自动把你要找到函数和 ThunkRVA 值对应上。而你在 OllyDBG 中写 CALL SendMessageA,可能你在你的系统上成功了,可放到别的系统下就要出错了。为什么?因为你找的人已经不在原来的位置了,他跑到别的地方去了。你还到老地方找他,当然看不见人了。说了这么多废话,也不知大家听明白了没有,别越听越糊涂就行了。总之一句话,别像 CALL SendMessageA 这样直接调用某个函数,而应该通过 ThunkRVA 值来调用它。下面我们回到我们要修改的 MyUninstaller 上来,先用 LordPE 打开看一下,呵呵,原来 CreateFontIndirectA 和 SendMessageA 原程序里面都有了,省了我们不少事情。看一下这两个函数的 ThunkRVA 值,CreateFontIndirectA 在 GDI32.dll 里面,ThunkRVA 值是 0000B044,这样我们就知道在程序中调用它的时候就是 CALL DWORD PTR [0040B044]。同样,SendMessageA 的ThunkRVA 值是 0000B23C,调用时应该是这样:CALL DWORD PTR [0040B23C]。了解了这些东西我们就来考虑怎么写代码了。首先我们来看一下 CreateFontIndirectA 和 SendMessageA 这两个函数的定义:

CreateFontIndirectA:

HFONT CreateFontIndirect(
CONST LOGFONT *lplf // pointer to logical font structure
);
CreateFontIndirect的返回值就是字体的句柄。

对于这个函数我们需要的参数就是给它一个 LOGFONT 的字体结构指针,我们只要在要修改程序的空白处建一个标准的9号(小五)宋体的 LOGFONT 字体结构,再把指针给 CreateFontIndirectA 就可以了。

SendMessageA:

LRESULT SendMessage(
HWND hWnd, // handle of destination window
UINT Msg, // message to send
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
上面的第一个参数是窗口句柄,我们知道 CreateWindowExA 返回的就是窗口句柄,我们可以直接拿来用。第二个消息参数我们这里是设置字体,选WM_SETFONT,这个值是 30H。第三个参数是字体句柄,可以由上面的 CreateFontIndirectA 获得。第四个参数我们不需要,留空。现在我们准备开始写代码,首先我们要在程序中建一个标准9号宋体的 LOGFONT,以便于我们调用。对于 LOGFONT,我们再来看一下定义:

typedef struct tagLOGFONT { // lf 
LONG lfHeight; 
LONG lfWidth; 
LONG lfEscapement; 
LONG lfOrientation; 
LONG lfWeight; 
BYTE lfItalic; 
BYTE lfUnderline; 
BYTE lfStrikeOut; 
BYTE lfCharSet; 
BYTE lfOutPrecision; 
BYTE lfClipPrecision; 
BYTE lfQuality; 
BYTE lfPitchAndFamily; 
TCHAR lfFaceName[LF_FACESIZE]; 
} LOGFONT;

这样我们的标准9号宋体的 LOGFONT 值应该是32字节,16进制就像这样:F4FFFFFF000000000000000000000000900100000000008600000000CBCECCE5。现在在程序中找个空地。我们用 PEiD 来帮助我们寻找,用 PEiD 打开程序,点 EP 段后面的那个 > 号,随便选择一个区段右击,选”搜索全0处”(原版好像是cave什么的):

我们看到 PEiD 把搜索到的空间都给我们列出来了:

现在我们用 WinHEX 打开我们要修改的程序,转到偏移 9815 处,从 9815 处选择 32 字节(16进制是0X20)的一个选块,把光标定位到 9815 处,右键选择菜单 剪贴板数据->写入(从当前位置覆写),随后的格式选择 ASCII Hex,把我们 LOGFONT 的 16 进制值

 F4FFFFFF000000000000000000000000900100000000008600000000CBCECCE5 

写入保存。现在我们用 OllyDBG 载入已添加了 LOGFONT 数据的程序,先转到 VA 40A415 处(从上图中看到的)往下看一下:
 

因为 SendMessageA 还要用到一个窗口句柄,我们可以通过前面的 CreateWindowExA 来获得。现在我们就把前一张图中的 .rdata 区段中的地址 0040C56E 作为我们保存窗口句柄 HWND 值的临时空间。一切就绪,开始写代码。先回顾一下我们最先说的那两个要修改的地方:

第一个要改的地方:

  00408F5E  |.  FF15 98B24000 |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]  ; \CreateWindowExA
  00408F64      6A 00         PUSH 0                                          ;  修改前
  00408F66      8945 F4       MOV DWORD PTR SS:[EBP-C],EAX
  00408F69  |.  E8 A098FFFF   |CALL <myuninst.sub_40280E>

修改后:

  00408F5E  |.  FF15 98B24000 |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]  ; \CreateWindowExA
  00408F64      E9 D5140000   JMP myuninst.0040A43E                           ;  跳转到我们的补丁代码处
  00408F69  |.  E8 A098FFFF   |CALL <myuninst.sub_40280E>

第二个要改的地方:

  00408F91  |.  FF15 98B24000    |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; \CreateWindowExA
  00408F97      8945 F0           MOV DWORD PTR SS:[EBP-10],EAX                     ;  改这里
  00408F9A      8B45 F8           MOV EAX,DWORD PTR SS:[EBP-8]
  00408F9D  |.  FF30             |PUSH DWORD PTR DS:[EAX]                          ; /<%s>
  00408F9F  |.  8D85 B0FEFFFF    |LEA EAX,DWORD PTR SS:[EBP-150]                   ; |
  00408FA5  |.  68 D0D94000      |PUSH myuninst.0040D9D0                           ; |format = ”%s:”
  00408FAA  |.  50               |PUSH EAX                                         ; |s
  00408FAB  |.  FF15 90B14000    |CALL DWORD PTR DS:[<&MSVCRT.sprintf>]            ; \sprintf
  00408FB1  |.  8B35 84B24000    |MOV ESI,DWORD PTR DS:[<&USER32.SetWindowTextA>]  ;  USER32.SetWindowTextA

修改后:

  00408F91  |.  FF15 98B24000    |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; \CreateWindowExA
  00408F97      E9 D4140000      JMP myuninst.0040A470                             ;  跳到我们的第二部分补丁代码处
  00408F9C      90               NOP
  00408F9D  |.  FF30             |PUSH DWORD PTR DS:[EAX]                          ; /<%s>
  00408F9F  |.  8D85 B0FEFFFF    |LEA EAX,DWORD PTR SS:[EBP-150]                   ; |
  00408FA5  |.  68 D0D94000      |PUSH myuninst.0040D9D0                           ; |format = ”%s:”
  00408FAA  |.  50               |PUSH EAX                                         ; |s
  00408FAB  |.  FF15 90B14000    |CALL DWORD PTR DS:[<&MSVCRT.sprintf>]            ; \sprintf
  00408FB1  |.  8B35 84B24000    |MOV ESI,DWORD PTR DS:[<&USER32.SetWindowTextA>]  ;  USER32.SetWindowTextA

这两个地方的修改都是把原代码改成跳转,跳到我们的补丁代码那继续执行。在修改之前先把原代码复制下来,以便恢复。我们在 OllyDBG 中按 CTR+G 组合键,来到 0040A43E 地址处,开始输补丁代码:

同样,我们也在 0040A470 地址处输入我们另一部分的补丁代码。两部分的补丁代码分别如下:

补丁代码1:

  0040A43E      60               PUSHAD                                            ;  保护现场
  0040A43F      A3 6EC54000      MOV DWORD PTR DS:[40C56E],EAX                     ;  保存窗口句柄
  0040A444      68 15A44000      PUSH myuninst.0040A415                            ;  传递字体句柄LOGFONT
  0040A449      FF15 44B04000    CALL DWORD PTR DS:[<&GDI32.CreateFontIndirectA>]  ;  GDI32.CreateFontIndirectA
  0040A44F      6A 00            PUSH 0                                            ;  lParam 参数留空
  0040A451      50               PUSH EAX                                          ;  字体句柄LOGFONT
  0040A452      6A 30            PUSH 30                                           ;  WM_SETFONT
  0040A454      8B0D 6EC54000    MOV ECX,DWORD PTR DS:[40C56E]                     ;  窗口句柄送ECX
  0040A45A      51               PUSH ECX                                          ;  压入窗口句柄参数
  0040A45B      FF15 3CB24000    CALL DWORD PTR DS:[<&USER32.SendMessageA>]        ;  USER32.SendMessageA
  0040A461      61               POPAD                                             ;  恢复现场
  0040A462      6A 00            PUSH 0                                            ;  恢复原代码
  0040A464      8945 F4          MOV DWORD PTR SS:[EBP-C],EAX
  0040A467    ^ E9 FDEAFFFF      JMP myuninst.00408F69                             ;  返回

补丁代码2:

  0040A470   > \60            PUSHAD
  0040A471   .  A3 6EC54000   MOV DWORD PTR DS:[40C56E],EAX
  0040A476   .  68 15A44000   PUSH myuninst.0040A415                             ; /pLogfont = myuninst.0040A415
  0040A47B   .  FF15 44B04000 CALL DWORD PTR DS:[<&GDI32.CreateFontIndirectA>]   ; \CreateFontIndirectA
  0040A481   .  6A 00         PUSH 0                                             ; /lParam = 0
  0040A483   .  50            PUSH EAX                                           ; |wParam
  0040A484   .  6A 30         PUSH 30                                            ; |Message = WM_SETFONT
  0040A486   .  8B0D 6EC54000 MOV ECX,DWORD PTR DS:[40C56E]                      ; |
  0040A48C   .  51            PUSH ECX                                           ; |hWnd => NULL
  0040A48D   .  FF15 3CB24000 CALL DWORD PTR DS:[<&USER32.SendMessageA>]         ; \SendMessageA
  0040A493   .  61            POPAD
  0040A494   .  8945 F0       MOV DWORD PTR SS:[EBP-10],EAX
  0040A497   .  8B45 F8       MOV EAX,DWORD PTR SS:[EBP-8]
  0040A49A   .^ E9 FEEAFFFF   JMP myuninst.00408F9D

补丁代码2因为与补丁代码1类似,我就不做详细解释了。现在我们的代码都写完了,现在我们开始保存我们的工作,选中我们修改的代码,点击鼠标右键,会出来一个菜单:

我们左键选所有修改(当然选它了,要不然只会保存我们选定的这一部分。关于这个地方还要说一下,有的时候我们修改完程序选”复制到可执行文件”时只有”选择”菜单,没有”所有修改”菜单项。按 OllyDBG 帮助里关于备份功能的说法,好像是受内存块限制的,补丁功能也同样是这样。对于备份及补丁功能我用的比较少,并不是很了解,这方面的内容还是大家自己去研究吧,有什么好的心得也希望能共享一下。我遇到不能保存所有修改的情况就是先把补丁代码全部复制下来,同时利用二进制功能复制代码,先选一段补丁代码保存为文件,再用 OllyDBG 打开保存后的文件,转到相应位置分别把我们复制下来的补丁二进制代码粘贴上去后保存。纯属笨办法,当然你也可以用 HexView 这样的工具来修改代码),随后会出来一个”把选中的内容复制到可执行文件”的对话框,我们选”全部复制”,又出来一个对话框,我们在上面点右键,在弹出的菜单上选”保存文件”:

这时会出来一个另存文件的对话框,我们另选一个名字如 myuninst1.exe 来保存,不要直接覆盖原文件 myuninst.exe,以便于出错后好修改。现在关闭 OllyDBG,先不要急着运行刚刚修改过的文件,因为我们还有个地方要改一下。大家还记得我们在 .rdata 中用了个地方作为我们保存临时变量的地方吧?原先的 .rdata 段属性设置是不可写的,现在我们写入了数据,运行时是会出错的。现在我们要修改一下 .rdata 段的属性。用 LordPE 的 PE 编辑器打开我们修改后的程序,点”区段”按钮,在弹出的对话框中点击 .rdata 段,右键选择弹出菜单中的”编辑区段”:

在弹出的对话框中选标志后面那个“…”按钮:

现在我们把区段标志添加一个可写入的属性:

完成后按确定保存我们所做的工作,运行一下修改后的程序,呵呵,终于把字体改过来了:

如果你运行出错也没关系,用 OllyDBG 调试一下你修改后的程序,看看错在什么地方。这一般都是输入补丁代码时造成的,你只要看一下你补丁代码运行的情况就可以了。到这里我们的任务似乎也完成了,但细心的朋友可能会发现补丁代码1和补丁代码2前面的代码基本上是相同的。一个两个这样的补丁还好,如果要是多的话,这样重复就要浪费不少空间了,况且工作量也相应加大了。既然前面有很多代码都是重复的,为什么我们不把这些重复的代码做成一个子程序呢?这样调用起来要方便的多。下面我们把前面的补丁代码修改一下,我们先把补丁代码1的代码改成这样:

  0040A43E      60              PUSHAD                                            ;  保护现场
  0040A43F      A3 6EC54000     MOV DWORD PTR DS:[40C56E],EAX                     ;  保存窗口句柄
  0040A444      68 15A44000     PUSH myuninst.0040A415                            ;  我们建的LOGFONT对应指针
  0040A449      FF15 44B04000   CALL DWORD PTR DS:[<&GDI32.CreateFontIndirectA>]  ;  GDI32.CreateFontIndirectA
  0040A44F      6A 00           PUSH 0                                            ;  lParam 参数留空
  0040A451      50              PUSH EAX                                          ;  字体句柄
  0040A452      6A 30           PUSH 30                                           ;  WM_SETFONT
  0040A454      8B0D 6EC54000   MOV ECX,DWORD PTR DS:[40C56E]                     ;  窗口句柄
  0040A45A      51              PUSH ECX                                          ;  窗口句柄压栈
  0040A45B      FF15 3CB24000   CALL DWORD PTR DS:[<&USER32.SendMessageA>]        ;  USER32.SendMessageA
  0040A461      61              POPAD                                             ;  恢复现场
  0040A462      C3              RETN                                              ;  返回

这样我们的子程序代码就写好了。现在我们再在子程序代码后面写上两个补丁代码,当然不要忘了改前面原程序中的跳转:

修改后的补丁代码1:

  0040A467      E8 D2FFFFFF     CALL myuninst.0040A43E                            ;  调用子程序
  0040A46C      6A 00           PUSH 0                                            ;  恢复前面修改过的代码
  0040A46E      8945 F4         MOV DWORD PTR SS:[EBP-C],EAX
  0040A471    ^ E9 F3EAFFFF     JMP myuninst.00408F69                             ;  返回继续执行

修改后的补丁代码2:
  0040A47A      E8 BFFFFFFF     CALL myuninst.0040A43E
  0040A47F      8945 F0         MOV DWORD PTR SS:[EBP-10],EAX
  0040A482      8B45 F8         MOV EAX,DWORD PTR SS:[EBP-8]
  0040A485    ^ E9 13EBFFFF     JMP myuninst.00408F9D

我在每个补丁代码片断间留了4个字节来分隔。同样,我们还要修改一下我们前面的跳转:
第一个要修改跳转的地方:
  00408F5E  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; \断在这里
  00408F64      E9 FE140000     JMP myuninst.0040A467                             ;  跳到我们的第一部分补丁代码处
  00408F69  |.  E8 A098FFFF     |CALL <myuninst.sub_40280E>

第二个要修改跳转的地方:
  00408F91  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; \CreateWindowExA
  00408F97      E9 DE140000     JMP myuninst.0040A47A                             ;  跳到我们的第二部分补丁代码处
  00408F9C      90              NOP
  00408F9D  |.  FF30            |PUSH DWORD PTR DS:[EAX]                          ; /<%s>
修改好后保存,同样不要忘了再修改一下 .rdata 区段的属性。运行一下,一切OK!

原文作者:CCDebuger(再次感谢CCDebuger)

上傳者后記:再次感谢作者 CCDebuger 

!!!希望對您能起到幫助,如果你需要文中所涉及的軟件(包括OllyDBG以及相關的crackme等,可與我聯系,我將盡快發給你,聯系QQ:20003138,謝謝!)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/freeking101/article/details/101180621

智能推荐

移动机器人运动规划(四)--动力学约束下的路径规划_机器人轨迹规划算法 动力学约束-程序员宅基地

文章浏览阅读8.2k次,点赞15次,收藏98次。本节课_机器人轨迹规划算法 动力学约束

nginx部署vue项目出现404查找不到js,css文件解决方法_路由跳转上线服务器显示找不到某个js和cs-程序员宅基地

文章浏览阅读1.2w次。nginx部署vue项目出现查找不到js,css文件解决方法1.先测试你的vue文件本地是否正常显示打开,正常再进行第二步,否则是你vue打包后的文件出现错误了2.测试nginx是否正常,可以在ftp中查找你vue项目中的文件,浏览器打开nginx打开那个文件,能正常打开则进入第三部,否则就是你nginx配置问题,nginx无法映射此vue文件夹3.重新配置一个新的nginx的conf文件, # 前端vue转发 location / {root /data/frontend/di_路由跳转上线服务器显示找不到某个js和cs

MICCAI-iseg2017挑战赛小结与婴儿脑组织分割总结-程序员宅基地

文章浏览阅读5.6k次,点赞3次,收藏13次。按照数据集进行划分:关于自动分割工具,婴儿脑MR图像来自单个时间点,其中纵向数据集不可用,因此必须开发不针对纵向数据集的分割工具,目前提出了一些机器学习方法,但这些方法的效果并不令人满意。iseg-2017:竞赛结果一共评估了两次,第一次评估中的TOP-1为MSL_SKKU,第二次评估中降为了第二名,但由于第二次评估的时间较晚,因此已发表的论文大多与第一次评估中的冠军作对比,在以下的总..._iseg2017

TI电赛无人机_电赛无人机怎么准备-程序员宅基地

文章浏览阅读2.8k次,点赞2次,收藏49次。一、材料准备1.机架(F330机架便宜耐摔,初期调试时使用)2.电调(好盈40A电调)3.电机(新西达、朗宇)4.桨叶(乾丰8045)5.航模动力电池(格氏)6.接收机与遥控器(PPM款)————————————————7.TI主控板(TM4C123G核心板)8.激光雷达(北醒、空循环)9.MPU6050+气压计10.openmv11.光流模块二、软件准备编程:keil建模、3D打印:solidwork+cura上位机:匿名、无名上位机三、飞控程序飞控程序准备与熟悉四、_电赛无人机怎么准备

python基础学习笔记<面向对象编程>_attributeerror: 'dict' object has no attribute 're-程序员宅基地

文章浏览阅读2.6k次。来自:http://www.liaoxuefeng.com/类和实例面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。仍以Student类为例,在Python中,定义类是通过class关键字:class_attributeerror: 'dict' object has no attribute 'read

win32创建控件的一些问题_tooltip控件createwindows失败,其它控件成功-程序员宅基地

文章浏览阅读631次。在我们使用CreateWindow();像一般控件建Windows扩展控件的时候我们会发现控件没有创建成功这是因为我们没有对Windows扩展控件库进行初始化,这要我们使用InitCommonControlsEx();函数来对Windows扩展控件库进行初始化,代码如下:INITCOMMONCONTROLSEX InitCtrls; InitCtrls.dwSize =_tooltip控件createwindows失败,其它控件成功

随便推点

克里金插值算法java实现_kriging.jar-程序员宅基地

文章浏览阅读2.2k次,点赞2次,收藏5次。什么是克里金插值算法IDW(反距离加权法)和样条函数法插值工具被称为确定性插值方法,因为这些方法直接基于周围的测量值或确定生成表面的平滑度的指定数学公式。第二类插值方法由地统计方法(如克里金法)组成,该方法基于包含自相关(即,测量点之间的统计关系)的统计模型。因此,地统计方法不仅具有产生预测表面的功能,而且能够对预测的确定性或准确性提供某种度量。克里金法假定采样点之间的距离或方向可以反映可用于说明表面变化的空间相关性。克里金法工具可将数学函数与指定数量的点或指定半径内的所有点进行拟合以确定每个位置的输_kriging.jar

No qualifying bean of type问题解决_no qualifying bean of type 'me.chanjar.weixin.mp.a-程序员宅基地

文章浏览阅读6.5k次。对于一个java开发新手来说,应该经常会遇到这个问题,而且还不好意思询问周围同事,不要怕,今天就让我们解决掉它。_no qualifying bean of type 'me.chanjar.weixin.mp.api.wxmpservice' available:

Allegro-修改Shape边界_shape edit boundary-程序员宅基地

文章浏览阅读4.2k次。Shape画好后并不能一次成形想要小修小改怎么办?解决方案:方案一:单击 Shape select 图标想要修改的Shape边界拐点出现小方框可以拖动小方框到想要的位置也可以把鼠标放在边界线上出现双向箭头后可以整体拖动方案二:单击 Shape Edit Boundary 图标单击想要修改的边界线按照想要修改的外形重新画边界欢迎关注公众号学习更多硬件知识..._shape edit boundary

如何将网络监控摄像头中的网络流 RTSP 转换成 M3U8 流并嵌入微信公众号_channel=ttlhd001-程序员宅基地

文章浏览阅读1.9w次,点赞7次,收藏17次。现在的网络监控摄像头一般都是支持输出网络流 RTSP 协议,当然现在有一些也直接支持输出 RTMP 协议流,目的是更好的适配主流流媒体服务器系统的接收如 adobe 的 FMS,Wowza 或 800Li Media Server。从节省成本来说输出 RTSP 协议的摄像头都在 100-500 元之间,性价比高,受众群更多。很多人买了监控摄像头会希望嵌入自己的网站,现在很多销售 IPCam 的厂家也提供直播云服务了,不过他们考虑带宽成本,会有很多限制码率,流量等。 所以现在很多人都在找能够嵌入自己网站或手机_channel=ttlhd001

Java学习笔记目录索引 (持续更新中)_孙帅 springboot-程序员宅基地

文章浏览阅读10w+次,点赞491次,收藏3.3k次。Java学习路线目录索引一、Java基础(省略)Lambda表达式及函数式接口二、Java数据库MySQL 一 概念、DDL、DML、DQL、事务、约束等数据库设计 一 多表关系、三大范式JDBC 一 基本使用、DAO组件、连接池、JDBCTemplate三、JavaWebHTML相关学习CSS — 常用属性CSS — 选择器及三大特性CSS — 网页的布局方式C..........................._孙帅 springboot

因果卷积(causal Convolutions)和扩展卷积(Dilated Convolutions)-程序员宅基地

文章浏览阅读2.9w次,点赞47次,收藏164次。背景对于序列问题(Sequence Modeling)的处理方法,通常采用RNN或者LSTM,例如处理一段视频/音频,往往会沿着时间方向(时序)进行操作。通常CNN网络都被认为适合处理图像数据而不适合处理sequence modeling问题;而今年来,由于RNN及LSTM这类模型的瓶颈,越来越多的人开始发现其实CNN对于这种序列问题的处理是被大大低估了,CNN建立的model要比之前人们之前用..._因果卷积

推荐文章

热门文章

相关标签