在程序崩溃的时候,Oracle RDBMS会把大量信息写到日志文件(log)里。日志文件会记录数据栈的使用情况,如下所示。
----- Call Stack Trace -----
calling call entry argument values in hex
location type point (? means dubious value)
-------------------- -------- ---------------------- -------------------
_kqvrow() 00000000
_opifch2()+2729 CALLptr 00000000 23D4B914 E47F264 1F19AE2
EB1C8A8 1
_kpoal8()+2832 CALLrel _opifch2() 89 5 EB1CC74
_opiodr()+1248 CALLreg 00000000 5E 1C EB1F0A0
_ttcpip()+1051 CALLreg 00000000 5E 1C EB1F0A0 0
_opitsk()+1404 CALL??? 00000000 C96C040 5E EB1F0A0 0 EB1ED30
EB1F1CC 53E52E 0 EB1F1F8
_opiino()+980 CALLrel _opitsk() 00
_opiodr()+1248 CALLreg 00000000 3C 4 EB1FBF4
_opidrv()+1201 CALLrel _opiodr() 3C 4 EB1FBF4 0
_sou2o()+55 CALLrel _opidrv() 3C 4 EB1FBF4
_opimai_real()+124 CALLrel _sou2o() EB1FC04 3C 4 EB1FBF4
_opimai()+125 CALLrel _opimai_real() 2 EB1FC2C
_OracleThreadStart@ CALLrel _opimai() 2 EB1FF6C 7C88A7F4 EB1FC34 0
4()+830 EB1FD04
77E6481C CALLreg 00000000 E41FF9C 0 0 E41FF9C 0 EB1FFC4
00000000 CALL??? 00000000
既然是编译器生成的程序,那么Oracle的可执行程序里必定会有调试信息、带有符号(symbol)信息的映射文件、或者是相似的信息。
在Windows NT版的Oracle RDBMS中,其可执行文件里存在着与.SYM文件有关的符号信息。可惜,官方不会公开.SYM文件的文件格式。固然Oracle可以使用纯文本文件,但是如此一来还要对其进行多次转换,性能必然大打折扣。
我们从最短的文件orawtc8.sym入手,试着分析这种格式的文件。Oracle 8.1.7的动态库文件orawtc8.dll之中,存在着这个.SYM文件的符号信息。对于oracle数据库的程序来说,版本越旧、功能模块的文件就越小。
使用Hiew打开上述文件,可以看到如图86.1所示的界面。
图86.1 使用Hiew打开整个文件
参照其他.SYM文件,可知这种格式的文件头(及文件尾部)都有OSYM字样。据此判断,这个字符串可能是某种文件签名。
大体来说,这种文件的格式是“OSYM + 某些二进制数据 + 以0做结束符的字符串 + OSYM”。很明显,这些文件中的字符串应当是函数名和全局变量名。
如图86.2所示,字符串OSYM的位置较为固定。
图86.2 OSYM signature and text strings
接下来,我把这个文件中的整个字符串部分(不包含尾部的OSYM签名字符串)复制了出来,并单独存储为一个文件strings_block。然后使用UNIX的strings和wc工具统计它字符串的数量:
strings strings_block | wc -l
66
可见它包含66个文本字符串。我们先把这个数字记下来。
通常来说,无论它是字符串、还是其他什么类型的数据,数据的总数往往会出现在二进制文件的其他部分。这次的分析过程再次印证了这个规律,我们可以在文件的开始部分、OSYM之后看到66(0x42):
$ hexdump -C orawtc8.sym
00000000 4f 53 59 4d 42 00 00 00 00 10 00 10 80 10 00 10 |OSYMB...........|
00000010 f0 10 00 10 50 11 00 10 60 11 00 10 c0 11 00 10 |....P...`.......|
00000020 d0 11 00 10 70 13 00 10 40 15 00 10 50 15 00 10 |....p...@...P...|
00000030 60 15 00 10 80 15 00 10 a0 15 00 10 a6 15 00 10 |`...............|
....
当然,0x42不是一个byte型数据,很可能是个以小端字节序存储的32位数据。正因如此,0x42之后排列着3个以上的零字节。
判断它是32位数据的依据是什么?Oracle RDBMS的符号文件可能非常大。以版本号为10.2.0.4的Oracle主程序为例,它的oracle.sym包含有0x3A38E(即238478)个符号。16位的数据类型不足以表达这个数字。
分析过其他.SYM文件之后,我更加确定了上述猜测:在32位的OSYM签名之后的数据,就是反映文本字符串数量的数据。
这也是多数二进制文件的常规格式:文件头通常包含程序签名和文件中的某种信息。
接下来,我们分析一下文件中的二进制部分。我把文件中第8字节(字符串计数器之后)到字符串之间的内容存储为另外一个文件(binary_block)。然后再使用Hiew打开这个新文件,如图86.3所示。
图86.3 Binary block
这个文件的模式逐渐清晰了起来。为了便于理解,我在图中添加了几条分割线,如图86.4所示。
图86.4 Binary block
多数的hex编辑器每行都显示16个字节,Hiew也不例外。所以,在Hiew的窗口里,每行信息对应着4个32位数据。
文件中的数据凸显了它的这种特征:在地址0x104之前的数据都是0x1000xxx形式的数据(请注意小端字节序),数据都以0x10、0x00字节开头;以0x108开始的数据,都是0x0000xxxx型的数据,都以两个零字节开头。
我们把这些数据整理为32位的数组,代码如下所示。
指令清单86.1 第一列是地址
$ od -v -t x4 binary_block
0000000 10001000 10001080 100010f0 10001150
0000020 10001160 100011c0 100011d0 10001370
0000040 10001540 10001550 10001560 10001580
0000060 100015a0 100015a6 100015ac 100015b2
0000100 100015b8 100015be 100015c4 100015ca
0000120 100015d0 100015e0 100016b0 10001760
0000140 10001766 1000176c 10001780 100017b0
0000160 100017d0 100017e0 10001810 10001816
0000200 10002000 10002004 10002008 1000200c
0000220 10002010 10002014 10002018 1000201c
0000240 10002020 10002024 10002028 1000202c
0000260 10002030 10002034 10002038 1000203c
0000300 10002040 10002044 10002048 1000204c
0000320 10002050 100020d0 100020e4 100020f8
0000340 1000210c 10002120 10003000 10003004
0000360 10003008 1000300c 10003098 1000309c
0000400 100030a0 100030a4 00000000 00000008
0000420 00000012 0000001b 00000025 0000002e
0000440 00000038 00000040 00000048 00000051
0000460 0000005a 00000064 0000006e 0000007a
0000500 00000088 00000096 000000a4 000000ae
0000520 000000b6 000000c0 000000d2 000000e2
0000540 000000f0 00000107 00000110 00000116
0000560 00000121 0000012a 00000132 0000013a
0000600 00000146 00000153 00000170 00000186
0000620 000001a9 000001c1 000001de 000001ed
0000640 000001fb 00000207 0000021b 0000022a
0000660 0000023d 0000024e 00000269 00000277
0000700 00000287 00000297 000002b6 000002ca
0000720 000002dc 000002f0 00000304 00000321
0000740 0000033e 0000035d 0000037a 00000395
0000760 000003ae 000003b6 000003be 000003c6
0001000 000003ce 000003dc 000003e9 000003f8
0001020
这里有132个值,是66×2的阵列。字符串的总量正好是66。那么,到底是每个字符串符号对应了2个32位数据,还是说这2个32位数据完全就是两个互不相干数组呢?我们继续分析。
以0x1000开头的值可能是某种地址。毕竟.SYM文件是为.DLL文件服务的,而且Win32 DLL文件的默认基址是0x10000000,代码的起始地址通常是0x10001000。
使用IDA工具打开orawtc8.dll文件,可以看到它的基址不是默认地址。尽管如此,我们可以看到它的第一个函数的对应代码为:
.text:60351000 sub_60351000 proc near
.text:60351000
.text:60351000 arg_0 = dword ptr 8
.text:60351000 arg_4 = dword ptr 0Ch
.text:60351000 arg_8 = dword ptr 10h
.text:60351000
.text:60351000 push ebp
.text:60351001 mov ebp, esp
.text:60351003 mov eax, dword_60353014
.text:60351008 cmp eax, 0FFFFFFFFh
.text:6035100B jnz short loc_6035104F
.text:6035100D mov ecx, hModule
.text:60351013 xor eax, eax
.text:60351015 cmp ecx, 0FFFFFFFFh
.text:60351018 mov dword_60353014, eax
.text:6035101D jnz short loc_60351031
.text:6035101F call sub_603510F0
.text:60351024 mov ecx, eax
.text:60351026 mov eax, dword_60353014
.text:6035102B mov hModule, ecx
.text:60351031
.text:60351031 loc_60351031: ; CODE XREF: sub_60351000+1D
.text:60351031 test ecx, ecx
.text:60351033 jbe short loc_6035104F
.text:60351035 push offset ProcName ; "ax_reg"
.text:6035103A push ecx ; hModule
.text:6035103B call ds:GetProcAddress
...
喔,我们好像见过字符串“ax_reg”!它不就是在.SYM文件的字符串区里的第一个字符串嘛!可见,这个函数的名字应该就是“ax_reg”。
上述DLL文件的第二个函数是:
.text:60351080 sub_60351080 proc near
.text:60351080
.text:60351080 arg_0 = dword ptr 8
.text:60351080 arg_4 = dword ptr 0Ch
.text:60351080
.text:60351080 push ebp
.text:60351081 mov ebp, esp
.text:60351083 mov eax, dword_60353018
.text:60351088 cmp eax, 0FFFFFFFFh
.text:6035108B jnz short loc_603510CF
.text:6035108D mov ecx, hModule
.text:60351093 xor eax, eax
.text:60351095 cmp ecx, 0FFFFFFFFh
.text:60351098 mov dword_60353018, eax
.text:6035109D jnz short loc_603510B1
.text:6035109F call sub_603510F0
.text:603510A4 mov ecx, eax
.text:603510A6 mov eax, dword_60353018
.text:603510AB mov hModule, ecx
.text:603510B1
.text:603510B1 loc_603510B1: ; CODE XREF: sub_60351080+1D
.text:603510B1 test ecx, ecx
.text:603510B3 jbe short loc_603510CF
.text:603510B5 push offset aAx_unreg ; "ax_unreg"
.text:603510BA push ecx ; hModule
.text:603510BB call ds:GetProcAddress
...
“ax_unreg”是字符串区域里的第二个字符串。第二个函数的起始地址是0x60351080,而在SYM文件里二进制区域的第二个数值正是10001080。据此推测,文件里的这个值应该就是相对地址,只不过,这个相对地址的基地址不是默认的DLL基址罢了。
简短截说,在.SYM文件中那个66×2的数据里,前半部分66个数值是DLL文件里的函数地址。它们也可能是函数里某个标签的相对地址。那么,由0x0000开头的、余下的66个值表达的是什么信息呢?这些数据的取值区间是[0,0x3f8]。它不像是位域的值,只是某种递增序列。关键问题是:每个值的最后一个数之间没有什么明确关系,它也不像是某种地址信息——地址的值应该是4、8或0x10的整数倍。
不妨直接问问您自己:如果您是研发人员,还要在这个文件里写什么数据?即便是瞎猜,也会猜得八九不离十:目前还缺少文本字符串(函数名)在文件里的地址信息。简单验证可知,的确如此,这些数值与字符串的第一个字母的地址存在对应关系。
大功告成。
此外,我还写了一段把.SYM文件中的函数名加载到IDA脚本的程序,以便.idc脚本文件自动解析函数的函数名:
#include <stdio.h>
#include <stdint.h>
#include <io.h>
#include <assert.h>
#include <malloc.h>
#include <fcntl.h>
#include <string.h>
int main (intargc, char *argv[])
{
uint32_t sig, cnt, offset;
uint32_t *d1, *d2;
int h, i, remain, file_len;
char *d3;
uint32_t array_size_in_bytes;
assert (argv[1]); // file name
assert (argv[2]); // additional offset (if needed)
// additional offset
assert (sscanf (argv[2], "%X", &offset)==1);
// get file length
assert ((h=open (argv[1], _O_RDONLY | _O_BINARY, 0))!=-1);
assert ((file_len=lseek (h, 0, SEEK_END))!=-1);
assert (lseek (h, 0, SEEK_SET)!=-1);
// read signature
assert (read (h, &sig, 4)==4);
// read count
assert (read (h, &cnt, 4)==4);
assert (sig==0x4D59534F); // OSYM
// skip timedatestamp (for 11g)
//_lseek (h, 4, 1);
array_size_in_bytes=cnt*sizeof(uint32_t);
// load symbol addresses array
d1=(uint32_t*)malloc (array_size_in_bytes);
assert (d1);
assert (read (h, d1, array_size_in_bytes) == array_size_in_bytes);
// load string offsets array
d2=(uint32_t*)malloc (array_size_in_bytes);
assert (d2);
assert (read (h, d2, array_size_in_bytes) ==array_size_in_bytes);
// calculate strings block size
remain=file_len-(8+4)-(cnt*8);
// load strings block
assert (d3=(char*)malloc (remain));
assert (read (h, d3, remain)==remain);
printf ("#include <idc.idc>\n\n");
printf ("static main() {\n"};
for (i=0; i<cnt; i++)
printf ("\tMakeName(0x%08X, \"%s\");\n", offset + d1[i], &d3[d2[i]]);
printf (")\n");
close (h);
free (d1); free (d2); free (d3);
};
使用这个脚本以后,我们可以看到:
#include <idc.idc>
static main() {
MakeName(0x60351000, "_ax_reg");
MakeName(0x60351080, "_ax_unreg");
MakeName(0x603510F0, "_loaddll");
MakeName(0x60351150, "_wtcsrin0");
MakeName(0x60351160, "_wtcsrin");
MakeName(0x603511C0, "_wtcsrfre");
MakeName(0x603511D0, "_wtclkm");
MakeName(0x60351370, "_wtcstu");
...
}
如需下载本章用到的oracle文件,请访问:http://beginners.re/examples/oracle/SYM/
。
此外,我们来研究一下Win64下的64位oracle RDBMS。64位程序的指针肯定就是64位数据了吧!这种情况下,8字节数据的数据特征就更为明显了。如图86.5所示。
图86.5 RDBMS for Win64的.SYM文件(示例)
可见,数据表的所有元素都是64位数据,字符串偏移量也不例外。此外,大概是为了区别不同的操作系统,文件的签名改成了OSYMAM64。
如需让IDA自动加载.SYM文件中的函数名,可参考我的样本程序:https://github.com/dennis714/ porg/blob/master/oracle_sym.c
。