Hope, peace, joy and love. Four bytes for the four candles of advent. Four days and it’s Christmas!
Connect using nc ctf.csd.lol 2024
.
Category
Points
Author
Binary exploitation
125
kolmus
1 | ![]() | 12/21 5:50 pm |
2 | ![]() | 12/22 4:29 pm |
3 | ![]() | 12/23 7:28 am |
4 | ![]() | 12/23 12:21 pm |
5 | ![]() | 12/23 2:55 pm |
6 | ![]() | 12/23 4:00 pm |
7 | ![]() | 12/25 12:41 am |
8 | ![]() | 12/25 3:46 am |
9 | ![]() | 12/25 3:06 pm |
10 | ![]() | 12/26 1:26 am |
[Challenge Author] Kolmus's write-up was selected as the best write-up submitted for this challenge.
View this write-up on GitHubLet's start by reverse engineering the provided binary.
The main function seems to be mmap()-ing one page of memory to address 0x13370000 as read-write-execute ((PROT_WRITE | PROT_READ | PROT_EXEC) == 7
). It then reads 4 bytes from stdin into that buffer and appends 0xc3
and a null byte. 0xc3
is the opcode for ret
. The first 3 bytes are then checked to see if they are 0x0f
or 0xcd
, which are the first byte of the opcode for syscall
and int
respectively. If those bytes are not present, the program will call the buffer as shellcode.
Since 4 bytes are hardly enough to do anything by themselves, we need to find a way to execute more shellcode. Maybe we can use some of the values that are already present in the registers/memory to our advantage.
After patching the binary to use the provided libc and ld (I use pwninit for this), debugging the program using gdb and breaking at the start of the shellcode (4 nops in this case), we can see that the registers are as follows:
Our goal will be to somehow make another read
syscall to read more shellcode into the buffer. For that we need the registers to be as follows:
rax = 0 rdi = 0 rsi = 0x13370000 to 0x13371000 rdx = more than 4
We also need to make sure execution eventually ends up back at the newly read shellcode. As you can see, all the registers are already set up to work for a read syscall, the problem now is making the syscall. Since we can't use syscall
or int
directly, we need to find a place in memory that contains a syscall and is reachable from the current position by using only 4 bytes worth of instructions. One intersting value I noticed is in rcx
. Disassembling the function it is part of (libc's read() wrapper) reveals the following:
We're only one instruction off from a syscall! Changing cl
to 0x5f
will make rcx
point to a syscall instruction that's part of libc's read()
. This also means it will likely not crash if we go there. mov cl, 0x5f
takes up 2 bytes, leaving us with 2 more. jmp rcx
is also 2 bytes, but remember that we also still need to bring execution back to the shellcode at 0x13370000. There is one more thing we can benefit from: the stack combined with the terminating ret
. As you know, ret
pops the return address from the stack and jumps to it. Libc's read() also returns. The idea is to use 2 push
instructions to put the address of the shellcode on the stack (push rdx
), and rcx
on top of it (push rcx
). Then, the ret
will jump to the read syscall pointed to by rcx
(allowing us to input as much shellcode as we like) and after libc's read() finishes, it will jump back to the shellcode. For the second stage, I simply use pwntools' shellcraft to pop a shell.
#!/usr/bin/env python3 from pwn import * exe = ELF("./4bytes_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe context.terminal = ['cmd.exe', '/c', 'start', 'cmd.exe', '/c', 'wsl.exe'] ADDR = 'ctf.csd.lol' PORT = 2024 def debug(p): gdb.attach(p) input('debugger started. press enter to continue ...') def conn(): if args.REMOTE: p = remote(ADDR, PORT) else: p = process([exe.path]) return p p = conn() payload = asm(''' mov cl, 0x5f push rdx push rcx ''' + shellcraft.amd64.linux.sh()) #debug(p) p.sendafter(b':', payload) p.interactive() # cat flag.txt # csd{4LL_1_W4n7_4_CHR15TmaS_iS_4tuN3_4_L1F3}
Flag: csd{4LL_1_W4n7_4_CHR15TmaS_iS_4tuN3_4_L1F3}