I have been using NixOS on all my devices, so the following blog is about dealing with user-level and kernel-level pwn challenges on NixOS through fish scripts and nix configurations.

What’s 「lianpwn」

Important

经常遇到师傅来问我 WP 里的「lianpwn」是什么,其实就是一些基于 pwncli 或者 pwntools 的简单包装,具体代码位于:nix-config 仓库中。更新:现已保存到 pypi 中,可以直接 pip install lianpwn 使用

I often get asked what ‘lianpwn’ in my WP is. It’s actually just some simple wrappers based on pwncli or pwntools. The specific code can be found in the nix-config repository. Update: You can use pip install lianpwn

User-level Pwn

I have a python module named lianpwn, which is based on pwncli and pwntools. There’re a few lambdas and helper classes defined in it:

from pwncli import *
 
lg_inf = lambda s: print("\033[1m\033[33m[*] %s\033[0m" % (s))
lg_err = lambda s: print("\033[1m\033[31m[x] %s\033[0m" % (s))
lg_suc = lambda s: print("\033[1m\033[32m[+] %s\033[0m" % (s))
i2b = lambda c: str(c).encode()
lg = lambda s_name, s_val: print("\033[1;31;40m %s --> 0x%x \033[0m" % (s_name, s_val))
debugB = lambda: input("\033[1m\033[33m[ATTACH ME]\033[0m")
 
 
def lg_dict(data):
    for key, value in data.items():
        lg(key, value)
 
 
def debugPID(io):
    try:
        lg("io.pid", io.pid)
        input()
    except Exception as e:
        lg_err(e)
        pass
 
 
# strfmt
class strFmt:
    def __init__(self):
        self.current_n = 0
 
    def leak_by_fmt(
        self,
        count,
        elf_idx=-1,
        libc_idx=-1,
        stack_idx=-1,
        separater=b".",
        new_line=True,
        identify=b"^",
    ):
        payload = identify + (b"%p" + separater) * count
        if new_line:
            payload += b"\n"
        s(payload)
        ru(identify)
        res = {}
        for i in range(count):
            temp_res = ru(separater, drop=True)
            if b"nil" in temp_res:
                continue
            temp_res = int(temp_res, 16)
            lg("temp_res", temp_res)
            if i == elf_idx:
                res["elf"] = temp_res
                lg_suc("addr_in_elf", temp_res)
            elif i == libc_idx:
                res["libc"] = temp_res
                lg_suc("addr_in_libc", temp_res)
            elif i == stack_idx:
                res["stack"] = temp_res
                lg_suc("addr_in_stack", temp_res)
        return res
 
    def generate_hn_payload(self, distance, hn_data):
        hn_data = hn_data & 0xFFFF
        offset = (distance // 8) + 6
        if hn_data > self.current_n:
            temp = hn_data - self.current_n
        elif hn_data < self.current_n:
            temp = 0x10000 - self.current_n + hn_data
        elif hn_data == self.current_n:
            return b"%" + i2b(offset) + b"hn"
        self.current_n = hn_data
        return b"%" + i2b(temp) + b"c%" + i2b(offset) + b"$hn"
 
    def generate_hhn_payload(self, distance, hhn_data):
        hhn_data = hhn_data & 0xFF
        offset = (distance // 8) + 6
        if hhn_data > self.current_n:
            temp = hhn_data - self.current_n
        elif hhn_data < self.current_n:
            temp = 0x100 - self.current_n + hhn_data
        elif hhn_data == self.current_n:
            return b"%" + i2b(offset) + b"hhn"
        self.current_n = hhn_data
        return b"%" + i2b(temp) + b"c%" + i2b(offset) + b"$hhn"

After that, here comes the simple template:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#   expBy : @eastXueLian
#   Debug : ./exp.py debug  ./pwn -t -b b+0xabcd
#   Remote: ./exp.py remote ./pwn ip:port
 
from lianpwn import *
from pwncli import *
 
cli_script()
set_remote_libc("libc.so.6")
 
io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc
 
 
ia()

Other Dynamic Libraries

It often suffers when coming to missing libs in NixOS, but I’ve got several solutions.

The easiest way is to use nix-shell:

with import <nixpkgs> { };
stdenv.mkDerivation {
  name = "fhs";
  buildInputs = with pkgs; [
    pkg-config
    # glibc.static
    zlib.static
    libffi
    libtool
    ghc
    gcc
    ocaml
    libseccomp
    liburing
  ];
}

However, the above way can only help compile c code with certain libs, such as libseccomp. The ultimate solution is to get such so from docker and copy to local environment. After that, patch it to any glibc version you want with patchelf. I also have a light tool called patch4pwn.

 cat ./Dockerfile
FROM ubuntu:20.04
 
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    make \
    libc6-dev
 
WORKDIR /app
 
 cat ./rundocker.fish
#!/usr/bin/env bash
 
docker build -t ubuntu-gcc .
docker run -it --rm -v $(pwd):/app ubuntu-gcc

Kernel-level Pwn

Here’s my kernel pwn template:

 ls
build.sh  exp.c  initgdb.sh
 cat ./build.sh
#!/usr/bin/env bash
 
set -e
 
echo $buildPhase
eval $buildPhase
cp ./exp ../rootfs/exp
cd ../rootfs
find . -print0 | cpio --null -ov --format=newc >../rootfs.cpio
cd ..
./run.sh
 
 cat exp.c
// author: @eastXueLian
// usage : eval $buildPhase
// You can refer to my nix configuration for detailed information.
 
#include "libLian.h"
 
extern size_t user_cs, user_ss, user_rflags, user_sp;
 
int main() {
    save_status();
 
    get_shell();
    return 0;
}
 
 cat ./initgdb.sh
#!/usr/bin/env bash
 
sudo -E pwndbg ./vmlinux.bin -ex "set architecture i386:x86-64" \
        -ex "target remote localhost:1234" \
        -ex "add-symbol-file ./rootfs/vuln.ko $1" \
        -ex "c"

Upload

It’s always suffering to upload our compiled elfs to the remote server. The following script is still improving:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#   expBy : @eastXueLian
#   Remote: ./exp.py remote ip:port -nl
 
import subprocess
from lianpwn import *
from base64 import b64encode, b64decode
from pwncli import *
 
cli_script()
 
io: tube = gift.io
 
commands = []
 
lg_inf("compiling exp.c")
if subprocess.run("musl-gcc -static -o exp.bin exp.c", shell=True).returncode:
    lg_err("compile error")
lg_suc("compile finished")
 
exp_data_list = []
SPLIT_LENGTH = 0x100
with open("./exp.bin", "rb") as f_exp:
    exp_data = b64encode(f_exp.read()).decode()
lg_inf("Data length: " + str(len(exp_data)))
for i in range(len(exp_data) // SPLIT_LENGTH):
    exp_data_list.append(exp_data[i * SPLIT_LENGTH : (i + 1) * SPLIT_LENGTH])
if not len(exp_data) % SPLIT_LENGTH:
    exp_data_list.append(exp_data[(len(exp_data) // SPLIT_LENGTH) :])
 
 
#  commands.append("cd rwdir; touch ./exp.b64")
#  for i in exp_data_list:
#  commands.append("echo -n '" + i + "'>> ./exp.b64")
#  commands.append("base64 -d ./exp.b64 > ./exp; chmod +x ./exp; ./exp")
#  commands.append("cat ./flag")
 
for i in commands:
    sl(i)
 
lg_suc(str(len(commands)) + " commands sent.")
ia()

Compilation

It’s simple to start a compilation environment:

{ pkgs ? import <nixpkgs> { } }:
 
pkgs.stdenv.mkDerivation {
  name = "linux-kernel-build";
  nativeBuildInputs = with pkgs; [
    getopt
    flex
    bison
    gcc
    gnumake
    bc
    pkg-config
    binutils
    perl
  ];
  buildInputs = with pkgs; [ elfutils ncurses openssl zlib ];
}

When compiling loadable kernel modules (LKM, with .ko as filename extension), the first step is to fetch linux source code from https://github.com/gregkh/linux/tags (easier to locate certain linux version).

After that, write c code with following template:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("eastXueLian");
 
static int __init YOURNAME_init(void) {
    return 0;
}
 
static void __exit YOURNAME_exit(void) {}
 
module_init(YOURNAME_init);
module_exit(YOURNAME_exit);

Makefile is also required for LKMs:

obj-m += exp.o
 
all:
	make -C /home/eastxuelian/compiling/linux-6.8-rc4 M=$(PWD) modules
 
clean:
	make -C /home/eastxuelian/compiling/linux-6.8-rc4 M=$(PWD) clean

New Methods for Packaging Kernel Pwn

Actually we have got good pwn templates for a long period of time, but when reproducing a kernel challenge from bi0sCTF-2024 [1]. I found a brand new configuration for accelerating kernel pwn debugging. If you are challenge setters, you can also refer to this blog to improve challengers’ pwning experience.

Using .img Instead of .cpio

.img files (.ext3 in the mentioned case from bi0sCTF-2024) are much easier to deal with, though their size might be larger than .cpio files.

Provided with rootfs.img, challengers can perform real-time operations on it through following commands:

mkdir ./rootfs
sudo mount -o loop rootfs.img ./rootfs/

The challengers can use following commands to create such image files:

# Create plain image
dd if=/dev/zero of=rootfs.img bs=1M count=1024
mkfs.ext3 rootfs.img
 
# mount and alter it
mkdir ./rootfs
sudo mount -o loop rootfs.img ./rootfs/
 
# eject image
sudo umount ./rootfs/

Attaching Exploit to Qemu

Instead of copying files to rootfs directory, the palindromatic challenge from bi0sCTF-2024 provides a new way:

run.sh

#!/bin/sh
qemu-system-x86_64 \
    \ # ...
    -drive file=rootfs.ext3,format=raw \
    -drive file=exploit,format=raw \
    \ # ...

init

[ -e /dev/sdb ] && cat /dev/sdb >/bin/pwn
chmod 755 /bin/pwn

Through the above configuration, we can enter pwn in qemu and run the exploit locally.


References

[1] bi0sCTF-2024 - palindromatic . K1R4 [2] pwn-scripts . eastXueLian