第86章 Oracle的.SYM文件

在程序崩溃的时候,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所示的界面。

..\tu\8601.tif

图86.1 使用Hiew打开整个文件

参照其他.SYM文件,可知这种格式的文件头(及文件尾部)都有OSYM字样。据此判断,这个字符串可能是某种文件签名。

大体来说,这种文件的格式是“OSYM + 某些二进制数据 + 以0做结束符的字符串 + OSYM”。很明显,这些文件中的字符串应当是函数名和全局变量名。

如图86.2所示,字符串OSYM的位置较为固定。

..\tu\8602.tif

图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所示。

..\tu\8603.tif

图86.3 Binary block

这个文件的模式逐渐清晰了起来。为了便于理解,我在图中添加了几条分割线,如图86.4所示。

..\tu\8604.tif

图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所示。

..\tu\8605.tif

图86.5 RDBMS for Win64的.SYM文件(示例)

可见,数据表的所有元素都是64位数据,字符串偏移量也不例外。此外,大概是为了区别不同的操作系统,文件的签名改成了OSYMAM64。

如需让IDA自动加载.SYM文件中的函数名,可参考我的样本程序:https://github.com/dennis714/ porg/blob/master/oracle_sym.c