有一天在网上下载了一个上个世纪dos年代的光盘iso,里面有一个readme.exe的dos可执行文件,在过去,这一般是光盘里面的软件说明。

好奇想知道这光盘里面有一些什么软件,于是执行readme.exe,由于是虚拟机里面执行所以也不担心有病毒。执行的结果有点意外,文件竟然设置了访问密码。

设置了访问密码的readme.exe

这反而更好奇这个文件里面的内容了。

疫情在家游戏玩多了,不妨把破解密码的过程当作是一次解迷游戏,于是决定花点时间尝试对这个程序的访问密码进行破解。

为什么这些光盘说明不放在一个文本文件里呢?因为这是要方便没有中文dos环境的用户查看,因此有人会用工具生成一个可以显示这些中文说明的dos的exe文件。

在动手破解之前,要先介绍一些背景和破解思路。

先说背景知识:

小型的dos程序一般都是汇编语言开发,生成机器码比较精简相对容易用debug工具跟踪,而对于所谓密码的处理一般不会像现在这样用des、aes、rc4对密码进行加密,通常就是设一个字符串然后和用户输入的字符进行比较,如果字符串匹配上就继续执行,匹配不上就退出程序。

如果是用basic或者其他高级语言开发编译的程序生成的机器代码就相对复杂。

有一些防破解的dos软件会对自身进行加密,加载到内存后先做解密再去执行,对于这类程序要先经过一种叫“脱壳”过程后才可以跟踪,破解也就不那么容易。

说完背景知识就说破解思路:

先在linux用 strings 查看程序里面字符,看看有没疑似密码的字符串,经过尝试没发现找到像明文的密码字符串,看来密码不是以明文方式存放在程序里面的。

既然密码不是以明文方式存放的,那么怎么找到密码呢?

我想起我朋友和我说过他开发的软件是怎么被一个外国人破解的过程,朋友的软件需要用户输入注册码,因为在用户输入注册码之后,程序就要对注册码进行校验并根据校验的结果进行分支跳转,因此只要追踪程序弹出注册码的对话框或者是提示注册码成功/失败的信息的机器代码就能非常接近破解的目标,这时可以分析存放在内存中密码,如果破解密码很麻烦也可以用汇编语言修改内存中的机器指令,即使注册码校验不过也强制跳转到注册码校验成功的程序分支。

于是这里有了两个方案,

方案一:

跟踪程序显示 “please input password:” 或 “invalied password!” 的代码,找到对比密码字符串的代码这样就有可能找到存放密码的内存地址从而把密码找出来。

方案二:

如果程序对密码的处理过程比较复杂,例如用了一些加密算法,即使找到存放密码的内存地址也不太容易分析出密码,那么还可以跟踪跳转到显示 “invalied password!” 的分支代码,修改跳转的指令使得即使密码校验不正确也跳转到密码校验成功的代码分支上去。

用到的调试工具是dos下的 debug,里面有内存搜索的指令可以搜出程序中调用显示字符串的汇编代码。

在dos的字符界面上显示字符串有三种方式

1、直接写显卡内存b800

2、调用bios 中断 int 10h

3、调用dos 中断 int 21h

从程序滚动输出这些字符串来看,一般不会是用直接写显存的方式去实现,如果要这样做就要实现拷贝内存和操作显卡寄存器一整套功能,而这里仅是简单显示一些字符串,因此先排除直接写显卡内存。

经过搜索跟踪,排除了调用bios 中断 int 10h,这个过程略过细节,大约过程就是去找,但没找到。

剩下就是跟踪调用dos 中断 int 21h显示字符串这条线索,下面是详细的过程。

用 dos int 21h 显示字符串的过程大概是这样:

把以$为结束符的字符串的地址放到寄存器ds:dx里,把寄存器ah设为09, 09是dos int 21h功能号,用于显示ds:dx存放的地址的内容,最后调用 int 21h,这样dos就会在当前光标的位置显示这个字符串。

因此,我们只需用debug把 readme.exe 加载到内存后搜索哪里有 mov ah,09 和 int 21h 这两条指令,这样就能比较容易找到输出字符串的地方。

当然了,如果程序里大量使用int 21h输出字符串,那就会搜索很多条结果出来,这时就要先搜索 “please input password:” 和 “invalied password!” 这两个字符串在内存里面的地址,然后搜出加载这些字符串地址到ds:dx的代码,这样可以缩小跟踪分析的范围。

dos 的 debug 的 s 命令可以用16进制或 asc码 搜索内存,而我们不大可能记得 int 21h 这条汇编指令对应的x86 cpu的机器指令,因此可以用 debug 的汇编功能先输入int 21h,然后反汇编看这条指令对应的机器指令。

使用debug的汇编和反汇编命令

这里可以看到 int 21h 对应的x86 cpu 机器指令用16进制表示是 cd21,mov ah,09 对应的机器指令是b409。

有了这些准备,就可以开始用debug破解这个程序了,先用debug加载 readme.exe,然后用 r 指令查看当前的寄存器,其中

cs=121f

ip=0100

这表示cs寄存器的内容是 121f ip 寄存器为0100,这表明 readme.exe 的代码段被加载到段地址为121f,偏移地址为0100 这个地方,如果不清楚这里的意思的话,可以查资料去了解一下 intel x86 cpu 在16位实模式下内存寻址方式。

在这里,只需要记得readme.exe的代码就在这个区域里面,先重点对这个区域搜索。对于一些大型的dos程序,程序和数据可能会分布在多个段里,这样就不能只搜索一个段了。

分别输入搜指令

-s 121f:0100 ffff b4 09
-s  121f:0100 ffff cd 10

使用debug的搜功能

符合条件的地址会被列出来,根据搜索的结果可以看到,

在 121f:01b8 和 121f:01f3 这两个地方有机器码b409(mov ah,09) ,而121f:01bd和121f:01f8有机器码 cd10(int 21h),这两组地址(121f:01b8和121f:01bd,121f:01f3和121f:01f8)靠得很近,这是很关键的线索,根据这个信息可以初步判断121f:01bd和121f:01f8这两个地方的代码调用了dos 21h显示字符串。

这时可以用 debug 的 u 命令反汇编代码看一下。

用debug的反汇编和内存查看功能

-u 121f:01f3

从反汇编的结果可以看到,这个确实是一段调用dos 21h显示字符串的代码,要显示的字符串的段偏移地址是 0263 ,在显示完这字符串之后,就调用 int 21的4c号(mov ah,4c 和 int21h)功能退出了程序。

没有看到操作ds寄存器的内容,这有可能是数据可以代码都在同一个段里(121f)

上图中用 debug d指令查看了121f:0263地址内容,看到了这块内存保存字符串“invalied password!”。

到这里,就开始有点接近目标了,这里就是校验密码失败后退出程序的地方,再往上一点代码,应该就是密码校验的地方,在被那就有可能找到密码存放的地方。

这时就需要一点一点对121f:01f3(b409 mov ah,09)前面的地址进行反汇编,由于intel x86 cpu的指令不是定长的,因此选定的反汇编起始地址不对可能会得到不正确的指令。

用debug反汇编 121f:01ce处的代码

用 u 命令一直往前反汇编,在 121f:01ce 这个地方可以看到调用了dos 21h的07号功能,这段代码是读取键盘输入的内容到寄存器al,然后将al的内容和 0xf0 这16进制数进行xor运算结果保存到al,最后把寄存器al的内容保存到寄存器 si 指向的内存地址。

这样看来,密码可能与 f0 做过xor运算,因此只需要找到内存中的密码,用xor就能还原出密码来。

继续往下反汇编

用debug反汇编121f:01e3处的代码

这里可以看到,这是一段字符串比较的代码,具体的细节可以查 intel 汇编语言,这里大概的意思就是把内存 0107 (没有指明段地址,默认还是121f)装载到寄存器cl,repz cmpsb 会根据 cx(ch被xor指令设为0) 的值作为循环的次数比较si 和 di 指向的内存,每比较一个字符串,就si、di寄存器就加1,cx寄存器就减1。

mov si,0108
mov di,028d

毫无疑问,这里此时si和di保存的就是键盘输入的口令和程序预设密码的内存地址,至于预设密码的地址保存在si还是di,这可以往前反汇编查看键盘输入的口令保存在位置,这样可以反推出密码存放的地址。

在这程序里,从代码中可以看到,si初始化为0108 (没有指明段地址,默认还是121f),而0107保存了要比较的字符串的长度,因此有理由判断密码可能保存在 121f:0108 这个地方。

于是执行debug 的d指令查看 121f:0107

用debug查看内存

这里可以看到0107处的内存是04,后面跟着4个c2,这里就可以比较肯定的判断,密码长度是4,密码的内容是4个c2,当然这密码是和前面提到 0xf0 进行xor运算的结果。

现在,就开始尝试还原密码用计算器或者上网随便找个进制转换的网页进行进制转换。

换算的结果是

f0的二进制表示为11110000

c2对应的二进制是11000010

把 11000010 和 11110000做 xor 运算得到

11110000
11000010   xor
00110010

得到的结果是 00110010,转换为16进制是0x32,通过查asc码表得到对应的字符是’2’,就是说密码是”2222″。

退出debug,再次执行readme.exe,输入密码 2222,最后密码校验通过,显示出里面的内容。

密码校验通过后显示的内容

至此,这个来自上世纪的密码就被破解了。

从文件的内容看光盘里面存放是软盘镜像img格式文件,这有可能当时电脑店用的光盘,那时还没有电脑城,可每个城市会有一两家电脑店,日常除了卖电脑给单位外,还会卖一些软件拷贝给用户,那时光驱还没普及,也没有u盘和移动硬盘,拷贝软件一般用软盘,所以这些软件一般是用软盘镜像img格式存放在光盘里,用户根据目录选好要拷贝的软件后,店员就会用hd-copy这个软件从光盘上拷贝到用户的软盘上,我还记得当时是10元拷贝一张软盘。

为什么这些软件的软盘镜像文件会放在光盘上呢?这是因为当时电脑的硬盘空间极少有超过1gb的,通常的都是几百mb,甚至一些电脑是没有硬盘的,而一张光盘有600多mb,一家电脑店通常会有十几张这样的光盘,里面有操作系统,汉字系统,字处理软件,工具软件,游戏也是极少的,即使有游戏也是很简陋也不好玩的游戏,好玩的游戏要等光驱开始普及的时候才看到。

从列出的软件来看,这张光盘应该比较后期的了,而这个密码可能是印在光盘的封面上的,也或许是这种光盘最初是在电脑店之间流通,加上密码可能是为了保护光盘的制作人的利益。

到光驱开始普及后,在市面上也能卖到这种装满软件光盘,但逐渐的里面的软件就是直接放在目录里面,不用做成软盘镜像格式了,因为方便用户就直接在光盘运行软件的安装程序。

最后说一下,这个 readme.exe 其实不是用汇编语言开发,至少不是全部是用汇编开发的,在一开始在linux下用strings检查文件的时候看到了 “borland” “turbo-c”的字样,borland的c编译器在当时很出名,可以生成很精简的高质量的机器码,因此用debug跟踪下来比较顺利。