关于某些环境下第二次调用disp_str产生乱码的问题解析
前言 标题中的”某些环境“包括笔者和一些同学的64位系统。 笔者验证时数次得到不符合预期的结果,于是数次推翻之前的结论,一开始以为这仅仅是一个与开关PIC选项有关的问题,后来发现可能是开关PIE的问题,再去实验发现同样的选项在不同编译器版本下行为也不同,可能是不同编译器的行为不同。 本文中关于PIC、PIE的分析比较粗糙,并且笔者的相关知识也很浅薄,可能会有很多错误,希望读者包涵并指正。 问题描述 在进行orange chapter5的第i个实验时,第二个disp_str的输出会变成乱码。原本笔者以为是对输出字符串的修改导致了错误,但经过许多尝试发现即使不做任何修改,只要编译一下原始的代码,第二次disp_str的输出就是乱码。更诡异的是原本附件中的a.img的运行结果是正常的,那么这有可能是编译环境不同,导致出现了问题。 图中可以看到最后一行输出是乱码。 问题溯源 为了方便查看,笔者只将start.c编译到start.o,其中第一次调用disp_str的部分是这样的: 比较有意思的是__x86_get_pc_thunk_bx函数,它的内容是这样的: public __x86_get_pc_thunk_bx __x86_get_pc_thunk_bx proc near ; __unwind { mov ebx, [esp+0] retn ; } 这个函数跟软件安全课程中介绍的病毒获取自身代码段位置的代码一样,是一种实现PIE(position-independent Executable,位置无关可执行文件),因为call指令相当于push ip; jmp。下一句紧接着执行mov ebx, [esp+0];retn就会将刚压进栈的ip的值赋给ebx。此时ebx的值是add ebx, (offset _GLOBAL_OFFSET_TABLE)的地址。所以下一句的add就会让ebx的值变为_GLOBAL_OFFSET_TABLE的值了,后续的字符串都是相对此时的ebx寻址的。 从lea指令开始为第一次调用disp_str布置参数,将[ebx]+aCstartBegins-_GLOBAL_OFFSET_TABLE_也就是aCstartBegins的实际地址压入栈中。 这段代码这么做是因为aCstartBegins和_GLOBAL_OFFSET_TABLE_的相对偏移是固定的,但程序本身的装载地址会发生变化,为了实现位置无关需要运行时获取代码所在的地址。 于是第一次调用十分正常,在disp_str中下断点,在bochs中调试: 看到正常的ebx的值应该是0x32ff4,此时aCstartBegins的地址是0x31000。 继续到第二次调用disp_str,用调试器或者静态分析都可以看到,正确的字符串aCstartEnds地址应该是aCstartBegins+0x2a=0x3102a。 ebx的值变成了0x32fa0=0x32ff4-0x54,导致disp_str显示字符串的地址变成了0x30fd6=0x3102a-0x54。 既然ebx的值改动了,浏览start.o的反汇编结果也没有发现ebx被改动,并且两次disp_str调用紧邻着也会输出乱码,那就只有可能是disp_str中本身修改了ebx. disp_str: push ebp mov ebp, esp xchg bx, bx # magic_break for debug mov esi, [ebp + 8] ; pszInfo mov edi, [disp_pos] mov ah, 0Fh .1: lodsb test al, al jz .2 cmp al, 0Ah jnz .3 push eax mov eax, edi mov bl, 160 ; <--------------修改bl div bl and eax, 0FFh inc eax mov bl, 160 ; <--------------修改bl mul bl mov edi, eax pop eax jmp .1 .3: mov [gs:edi], ax add edi, 2 jmp .1 .2: mov [disp_pos], edi pop ebp ret 发现代码中两次修改ebx的值,都是将bl(ebx低8位)改为160,也就是16进制的0xa0. 再根据先前的ebx从0x32ff4变成0x32fa0,正好是低8位从0xf4变成了0xa0,出错过程完全分析清楚。 ...