最近接触了一个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
程序由多个SEGMENT组成,具体结构如下:
注1:用IDA打开目标EXEà”CTRL+E”à选择”入口点”,此时将停在”启动代码”处.
注2:按编译链接时的顺序每个OBJ文件对应一个SEGMENT
上图中的”BAS源文件”对应若干个SEGMENT(取决于有多少个OBJ文件),而每个SEGMENT都是如下的一个结构:
这个”SegHdr”是个管理结构,范围是当前段(SEGMENT),大小为30H.
该结构包含了对应的BAS文件的文件名,各个数据段的偏移等.
每个BAS文件在链接后对应着程序中的一个SEGMENT,段头的前10个字节就是该文件的文件名.这些SEGMENT是按编译顺序排放的.之所以这里会是10个字节因为每个名字都有一个”bl”前缀.
在所有的OBJ合并到EXE中之后,这些段名是不见的,它们都变成了同一个段中的不同偏移位置.
每一个SEGMENT的真正代码都是从30H处开始,大致结构如下:
由上图可以看出,所有写在SUB/FUNCTION中的函数都会被这些JMP指令路过.
而那些没写在任何SUB/FUNCTION块中的代码将被执行.也就是QBasic中没一个像C语言那样的main()函数.
关于结构方面的东西介绍完了,下面再来看一下跟逆向有实质性关联的东西吧.
QBasic函数的参数是从左向右压栈的.由本函数负责清理.
参数默认情况下是通过引用传递的,所以在函数内部修改参数值是会影响到外面的.
如:CALL TestSub(1, 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 说明.
这个可以说是整个逆向过程中最重要的一步了,如果库函数无法识别工作量将会扩大N倍.
但由于IDA本身没有带QBasic的符号,所以在开工之前需要把QB安装目录中的库文件做成
SIG符号文件.记得保留那些中间(PAT)文件,防止IDA不能自动识别时用来手动识别.
具体的制作方法可以去网上搜下教程,这里就不讨论了.
在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"
………………….
(限于篇幅有限,就不贴完整的了)
QBasic中READ和DATA是相对出现的,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数据.
1)这里特别需要注意的就是QBasic中变量在使用前可以不用声明,因此如果出现如下代码,编译器也不会报错,但结果就大不一样了:
var10 = 1: var12 = 2: var14 = 3: var16 = 4
CALL TestSub(var10, var13, var14, var16)
这里由于手误将”var12”写成了”var13”编译器是发现不了的.
2)所有函数在调用之前都应该声明,否则会被当成数组或变量.
至此都已经差不多了,剩下的都是些体力活了~
在此期间收到了很多的朋友的帮助与技术支持,特别要感谢Pete’s的burger2227与qb_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