宝塔服务器面板,一键全能部署及管理,送你10850元礼包,点我领取

一、前言

最近接触了一个QBasic编写的16位程序的逆向,该程序是运行在纯DOS环境下,虽然此环境已离我们远去,能接触到的机会不是太多,但为了防止有意外碰到的同学像我最开始那样走弯路,特此将笔记整理一下发出,希望对需要的同学有些帮助.由于时间仓促,研究不深文中难免有错误之处,欢迎各高手指教.

QBasic7.1是微软公司推出的一套基于Basic语言的集成开发环境,虽然在当时来说此开发环境功能已经非常强大,但相对于现如今我们这些用惯了Visual Studio, Eclipse等更加强大的人来说,那个环境就有点太老土了.因此我们得换一套更有效率的环境,我用的是SlickEdit 15.0.0.5,至于编译我写了一个简单的批处理来完成.

: 由于在不同的编译参数下可能会生成不同的程序结构,因此本文只讨论如下参数生成的程序

BC test.bas/V/D/O/FPi/G2/Ot/Lr/Fs/Zi/T/C:512;

其它的差别不大,同学们可以自己研究下 J

二、必要的结构

1: QBasic程序的结构

  程序由多个SEGMENT组成,具体结构如下:

逆向QBasic7.1笔记-风君子博客

 

1:IDA打开目标EXEàCTRL+Eà选择入口点,此时将停在启动代码.

2:按编译链接时的顺序每个OBJ文件对应一个SEGMENT

2: BAS源文件所在SEGMENT的结构:

上图中的BAS源文件对应若干个SEGMENT取决于有多少个OBJ文件),而每个SEGMENT都是如下的一个结构:

逆向QBasic7.1笔记-风君子博客

 

这个SegHdr是个管理结构,范围是当前段SEGMENT),大小为30H.

该结构包含了对应的BAS文件的文件名,各个数据段的偏移等.

逆向QBasic7.1笔记-风君子博客

 

每个BAS文件在链接后对应着程序中的一个SEGMENT,段头的前10个字节就是该文件的文件名.这些SEGMENT是按编译顺序排放的.之所以这里会是10个字节因为每个名字都有一个bl前缀.

在所有的OBJ合并到EXE中之后,这些段名是不见的,它们都变成了同一个段中的不同偏移位置.

3: 代码结构

每一个SEGMENT的真正代码都是从30H处开始,大致结构如下:

逆向QBasic7.1笔记-风君子博客

 

由上图可以看出,所有写在SUB/FUNCTION中的函数都会被这些JMP指令路过.

而那些没写在任何SUB/FUNCTION块中的代码将被执行.也就是QBasic中没一个像C语言那样的main)函数.

三、开始逆向

关于结构方面的东西介绍完了,下面再来看一下跟逆向有实质性关联的东西吧.

1: 参数传递与调用方式

QBasic函数的参数是从左向右压.由本函数负责清理.

参数默认情况下是通过引用传递的,所以在函数内部修改参数值是会影响到外面的.

:CALL TestSub1, 2, 3)将会生成如下代码

 mov word ptr [bp – 14h], 1

 mov word ptr [bp – 16h], 2

 mov word ptr [bp – 18h], 3

 lea ax, [bp-14h]

push ax

 lea ax, [bp-16h]

 push ax

 lea ax, [bp -18h]

 push ax

 call TestSub

 如果要进行传值调用需要在函数声明时添加BYVAL 说明.

2: 识别库函数

这个可以说是整个逆向过程中最重要的一步了,如果库函数无法识别工作量将会扩大N.

但由于IDA本身没有带QBasic的符号,所以在开工之前需要把QB安装目录中的库文件做成

SIG符号文件.记得保留那些中间PAT)文件,防止IDA不能自动识别时用来手动识别.

具体的制作方法可以去网上搜下教程,这里就不讨论了.

3: 库函数的转换

QBasic中库函数的名字一般都有个前缀B$以此来防止与正常的函数重名.因此即使完成了库函数识别也无法开始工作,还得做一步额外的工作:库函数到接口函数的转换.

这个工作是由编译器在编译时完成的,现在我们反过来操作难度有点大,但好在QBasic的库函数也不是很多,我用了一个比较土的方法:Google中搜索BAS文件以及相应的OBJ文件并加以人肉分析对比,这样便可以建立起一个对应表, :

B$GOSA============= GOSUB SubName

B$FCHR============= CHR$n)

B$SAS1============== FileName& = "xxxxxxx"

B$PEI2 ============== PRINT x%

B$PSSD============== PRINT "xxxxxxxx";

B$SSHL============== SHELL "xxxxx"

………………….

限于篇幅有限,就不贴完整的了)

4: 对于READ处理

QBasicREADDATA是相对出现的,DATA用来定义一个数据集,READ则从这些数据集中取出数据给变量赋值.:

DATA AAA, BBB, CCC, DDD, EEE

FOR I = 0 TO 4

  READ KKK$

  PRINT KKK$

NEXT I

与之对应的部分汇编代码如下:

…….

push   ds

push   offset kkk

xor     ax, ax

push   ax

call    B$RDSD ==== READ

push   offset kkk

call    B$PESD ==== PRINT

……..

这里, DATA去哪了呢?

….来到该代码所在的SegHdr部分,那个Off2指向的区域BC_DS)就包含了这些DATA数据.

5: 需要注意的地方

1)这里特别需要注意的就是QBasic中变量在使用前可以不用声明,因此如果出现如下代码,编译器也不会报错,但结果就大不一样了:

var10 = 1: var12 = 2: var14 = 3: var16 = 4

CALL TestSubvar10, var13, var14, var16)

这里由于手误将var12写成了var13编译器是发现不了的.

2)所有函数在调用之前都应该声明,否则会被当成数组或变量.

 

至此都已经差不多了,剩下的都是些体力活了~

四、致谢

在此期间收到了很多的朋友的帮助与技术支持,特别要感谢Petesburger2227qb_liu.

本文欢迎转载,请保证信息完整.如有不清楚的欢迎讨论.

thinkSJ 于南京)

五、参考资料

1: PC Magazine's BASIC Techniques and Utilities  by Ethan Winer

  http://www.ethanwiner.com

2:  Pete's QBASIC Site

  http://www.petesqbsite.com

3: QB CULT MAGAZINE

  http://qbcm.hybd.net