0x00 泄露程序基址
通过_dl_rtld_libname
_dl_rtld_libname
是一个存在ld.so 里的结构体
glibc2.27:
glibc2.30:
程序基址与_dl_rtld_libname
指针的偏移量和2.27不同为 offset=270
即
读取该地址上面的内容可以泄露出程序基地址,该地址为主程序加载 ld 文件时存储 ld 文件名的地址
_dl_rtld_libname
和 libc 基地址的偏移是不固定的,不同的 ld 文件有不同的偏移,需要手测或者爆破
程序基地址为0x55e4fceaf238 - 0x238 == 0x55e4fceaf000
这个地址的偏移一般离程序基地址来说是 0x238,但是不一定我patch了不同版本的glibc 2.23 这个偏移都不一样
patch了2.30的glibc 这个偏移也不一样
About | stdin && stdout && stderr
bss 段上的stdin,stdout,stderr
有些时候stdin,stdout,stderr 指针是存在于bss 段上的如下
因为设置了setvbuf 的原因(PIE 关闭 PIE 打开的时候情况是一样的)
若没有设置setvbuf,就不会在bss段上,但是不设置setvbuf远程打的就没回显;
有些时候stdin,stdout,stderr 又不再bss 段上,并不是很清楚原因
关于close(1) 即关闭stdout
有些时候出题会关闭标准输出,当你拿到shell 也没办法打印flag,或者只有orw,且close(1);
对于可以拿到shell的情况:
使用exec命令 exec 1>&0
对于orw情况:
orw 如西湖论剑noleakfmt bss 段无stdout,stdin 指针,但是栈上存的有IO_FILE 结构体指针
我们想办法抬栈修改,IO_FILE 结构体
(抬栈可以调用start 函数中的libc_start_main,在libc_start_main 中有 rsp=rsp-0x90 的操作)
成功打印条件是输出管道能用有两种方式
1.把stdout的IO_FILE 指针,指向stderr结构体,即用错误输出的输出管道
即原本 stdout_pointer == stdout_addr 修改为stdout_pointer === stderr_addr
此时我们用stdout 就是用的 stderr 的IO_FILE ,通过标准错误管道输出的。
2.修改stdout 结构体的参数fileno=2(原本等于1),使得输出当作标准错误输出(stderr)来输出
修改了fileno参数后,打印是会通过stderr的管道输出。
通过修改IO结构体泄露地址
如果存在任意地址写 或者 可以劫持stdout 就可以实现修改stdout 结构体实现libc 泄露
- 通过修改 puts函数工作过程中stdout 结构体中的 _IO_write_base ,来达到泄露libc地址信息的目的。
puts函数在源码中是由 _IO_puts实现的,而 _IO_puts 函数内部会调用 _IO_sputn,结果会执行 _IO_new_file_xsputn,最终会执行 _IO_overflow
_IO_puts - > _IO_sputn -> _IO_new_file_xsputn
_flag的构造满足的条件:
_flags = 0xfbad0000 //初始magic value
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800
一般 方法
覆盖前:
覆盖后
把_IO_read_ptr 、end、base 都覆盖为00 然后覆盖 _IO_write_base 最低位覆盖位\x00
若不能劫持stdout或不能直接修改stdout 结构体
只有通过爆破libc,可以通过main_arena 或其他临近IO_FILE 结构体的libc,使用部分覆盖低位的方式爆破
stdout结构体地址,然后和上面一样修改flag等标志,泄露出libc 地址
下面是例子 可以看见main_arena 地址只有后5位与,libc_stdout不同
而1000是一个分页 ,所以stdout 最、低3位 760是写死了,不管libc_base 怎么变化,stdout libc低三位一定是
760,若所以与main_arena 地址只相差 ce 两位不同(最多相差3位)
爆破成功概率为:1/16*16 或 1/16 * 16 * 16 , 成功概率还行
0x01 任意地址执行:
修改 .fini_array
若有两次任意地址写(对写的大小字节有一定要求)
可以修改.fini_array
的值为任意地址,不过.fini_array
只能用一次
具体例子:pwnable 3x17 由于wp很多,题目不详细说明
exit类
__libc_atexit
调用方法:
elf.sym['__elf_set___libc_atexit_element__IO_cleanup__']
程序是静态编译的时候,可以修改__libc_atexit
来实现任意地址跳转
这个可以无限次使用,在 ida 里也能找到它
利用 _dl_fini 执行函数
几个地方都可以利用
函数执行顺序如下: exit
→ _run_exit_handlers
→ __call_tls_dtors
→ call _dl_fini
以下是也许可以利用的点:
0x7f236a931b3e <_dl_fini+126> call qword ptr [rip + 0x216404] <0x7f236a921c90>
0x7f236a931d6e <_dl_fini+686> call qword ptr [rip + 0x2161dc] <0x7f236a921ca0>
0x7f236a931df3 <_dl_fini+819> call qword ptr [r12 + rdx*8] <0x400925>
这里记得曾经有一道题,r12是可控的,而rdx又刚好为 0 ,就可以造成跳转
0x7f6c23257e13 <_dl_fini+851> call rax <0x4009d4>` rax 指向 fini 会调用其数组内的函数
例子
具体的利用在 exit 函数的_dl_fini
函数里:
0x7f22d9fdca02 <_dl_fini+98> lea rdi, [rip + 0x217f5f] <0x7f22da1f4968>
0x7f22d9fdca09 <_dl_fini+105> call qword ptr [rip + 0x218551] <0x7f22d9c2a440>
rdi: 0x7f22da1f4968 (_rtld_global+2312) ← 0x68732f6e69622f /* '/bin/sh' */
rsi: 0x0
rdx: 0x7f22d9fdc9a0 (_dl_fini) ← push rbp
rcx: 0x1
call qword ptr [rip + 0x218551]
是_rtld_global
上面的地址,rdi 也是用的_rtld_global
上面的地址
不同的 ld 对应的_rtld_global
不一样,需要手调爆破
例题: De1ctf 2019 pwn unprintable
abort
_IO_flush_all_lockp 就是修改vtable 一种打法,利用的是 _IO_OVERFLOW
高版本IO中exit利用
exit 中程序会结束,会调用很多IO中的库函数来完成程序结束的过程
因为高版本glibc IO FILE 的库函数有改动,所以exit的利用方式不一样
在2.23及以下
一般攻击FILE结构体就是劫持IO函数的_chain字段为我们伪造的IO_FILE_plus
,然后修改vtable表中的io_str_overflow为system
。
在高版本libc下(要配合heap是使用不是单纯的任意地址执行)
如libc2.31下也依然是利用io_str_overflow
这个函数,但io_str_overflow
函数的实现发生了变化,在_IO_str_overflow
中有malloc 和 free 还有memcpy 我只需要伪造IO_FILE结构体,执行正确的程序流就行
IO_str_overflow利用:
int _IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF)//fp->flags==0 不满足条件 进入else
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)//注意这里new_size 是经过计算才能申请到tcache对应size 的chunk
return EOF;
new_buf = malloc (new_size);//这里会新申请一个chunk 可以把tcache 中free_hook申请到
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);//把之前的buffer上的内容复制
free (old_buf);//这里有free 可以触发free hook
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
有两种利用方式
- 1.old_buf 上布置system, malloc 申请到free_hook,memcpy把布置system地址复制到free hook
然后old_buf 起始可以布置“/bin/sh”,就变成的最经典的free_hook=system + free(binsh_addr)
- 2.也可以利用
_IO_str_overflow
实现srop
这里看_IO_str_overflow
的汇编在+52 有个把rdi+0x28 赋给rdx操作的
而自从glibc2.29开始,setcontext中的gadget索引由rdi变为了rdx,需要先控制rdx的值才能够进行后续的srop
而_IO_str_overflow
中的这条汇编正好可以将rdx进行赋值,且来源还是rdi,rdi正好是FILE结构体的首地址
只需要将fp+0x28设置为我们可以控制的地址就可以进行srop,且这条语句是在malloc之前执行的
所以利用方法就是:首先将malloc_hook设置为setcontext+61,然后触发_IO_str_overflow
,
事先在我们伪造的FILE结构体中设置好相应的数据,从而将rdx赋值为我们可以控制的地址
接着_IO_str_overflow
调用malloc触发setcontext,进行srop。
触发malloc条件如下
fp->_flags=0;//if (fp->_flags & _IO_USER_BUF) return EOF;/* not allowed to enlarge */
fp->_IO_write_ptr=srop_addr
/*
0x0 _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x00007ffff7e2f0dd <+61>: mov rsp,QWORD PTR [rdx+0xa0]
*/
new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100);
memcpy (new_buf, fp->_IO_buf_base, (fp)->_IO_buf_end - (fp)->_IO_buf_base);
free (fp->_IO_buf_base);
_IO_file_overflow
通过任意地址写修改vtable上_IO_file_overflow指针,需要两次任意地址改,利用比较方便,只需要如通过printf 等函数,
触发刷新IO,即可触发_IO_file_overflow函数
我们只需要把 _IO_file_jumps中 _IO_file_overflow的地址改为system,把stdout等IO_FILE 结构体的flags为改为
“/bin/sh”
当call _IO_file_overflow 就<=> call sysem("/bin/sh")
总结:
1.修改_IO_file_jumps 的 _IO_file_overflow指针为system
2.修改stdout的flags=“/bin/sh\x00”
想办法刷新IO流触发,call _IO_file_overflow即可
修改 _stack_chk_fail 内部函数的 .got.plt 表
该函数内部函数按调用顺序排列如下:
strchrnul 函数
mempcpy 函数
strlen_ifunc 函数
got 表内有可改函数就可以改为 main 函数的地址来进行无限循环
0x02 打印flag
flag文件已经读入
__stack_chk_fail
glibc2.26 以下
如果在程序中flag文件已经被读入,如果只是打印flag 可以利用stack smash 直接不用管canary
因为栈检查失败,程序就会执行 __stack_chk_fail 函数,报错信息打印 argv[0] 指针,argv[0]
指向栈上存储的文件名信息,我们直接把argv[0] 指向flag地址,即可借助报错信息打印flag
malloc_printerr
若 __stack_chk_fail 使用不了,我们还可以利用malloc_printter
触发
在 glibc_malloc 时检测到错误的时候,会调用 malloc_printer函数
static void malloc_printerr(const char *str) {
__libc_message(do_abort, "%s\n", str);
__builtin_unreachable();
}
会调用 __libc_message来执行 abort 函数,如下:
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
/* Avoid using this arena in future. We do not attempt to synchronize this
with anything else because we minimally want to ensure that __libc_message
gets its resources safely without stumbling on the current corruption. */
if (ar_ptr)
set_arena_corrupt (ar_ptr);
if ((action & 5) == 5)
__libc_message ((action & 2) ? (do_abort | do_backtrace) : do_message,
"%s\n", str);
else if (action & 1)
{
char buf[2 * sizeof (uintptr_t) + 1];
buf[sizeof (buf) - 1] = '\0';
char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
while (cp > buf)
*--cp = '0';
__libc_message ((action & 2) ? (do_abort | do_backtrace) : do_message,
"*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);//这里一样可以利用来打印flag
}
else if (action & 2)
abort ();
}
flag文件未读入
未读入的情况一般有几种方法得到flag
知道flag文件名:
-
orw ,open read write
-
ora,open read alarm ,这里没有给打印函数,可以通过alarm 对读入flag ascii 码值进行计时,得到flag
-
or + asm cmp ,这里open + read 读入flag,然后利用程序自带汇编cmp gadget ,或写一段汇编代码注入,与输入 的flag进行比较类似测信道攻击
-
sendfile + open
sendfile(1,open("/flag",NULL),0,1000)
不知道flag文件名:
- getdents
利用getdents 系统调用,getdents可以读取目标目录下的所有文件名。
先open("dir_path",0x10000) 打开目标目录,
然后getdent(fd,buffer,count),读取打开目录中的文件名到buffer上,即可得到flag的文件名。