Een tijdje geleden was er een
interessant topic over buffer overflows en exploits.
Nou denk ik dat er best wel een aantal mensen zijn die hier over kunnen meepraten, dus heb ik een soort van proposal gemaakt over een toekomstige 'feature' in een compiler die straks geport gaat worden naar een specifiek OS toe (de mijne
). Wat ik een beetje van plan was, is om te kijken of het uberhaupt zo moeilijk is om via een compiler al een heleboel exploits buiten de deur te houden en naar mijn mening kan dat best.
Daarom heb ik dus een proposal gemaakt hierover en aangezien der best wel wat fouten her en der in zitten en er over heel wat dingen nog gediscussieerd kunnen worden gooi ik em maar bij deze online..
Proposition "safe mode" option in CybOS compilers
-------------------------------------------------
Especially in the times of persistent connections between many computers and users, the need for tense security is rising every day. Not only the need for better software is needed, but also the need for better tools to create these software is a very important part of the security-measurements that should be taken. For instance, if a application like a java engine has a serious exploit, the software running on it can be 100% secure, but will still hold the exploits from the underlying application. This works it's way right down to the hardware levels of the computer system.
After spending some time in the world of exploits and protection against them, I noticed that almost all exploits are resulting from bad programming, wether or not intentionally. A simple program that doesn't check the size of incoming data is a very good candidate for a "buffer overflow"-exploit.
A "buffer overflow"-exploit is simply a case of overflowing the input buffer in a way, it changes the normal execution of the program into the execution of code which is passed in the buffer (which overflowed). With the right factors, it's not very hard to create your own code which is run by the remote machine by "dropping the code inside an egg" (ie: eggdropping the code into a buffer-overflow).
In todays protected kernel environments where all programs run in heavily protected area's so they cannot harm other program or either the kernel itself, it's not very logical that the OS would allow the programs to hurt themselves. Afterall, the propositions I will make aren't very hard to implement, yet somehow, nobody actually implemented it inside there compilers.
JayTaph
The theory behind "safe mode"
-----------------------------
Before you can actually read this chapter, it is highly recommended that you know your intel 386+ PMODE theory.
How does a normal program (or process) execute in a protected kernel environment:
1. The kernel allocate memory to store the process.
2. The kernel initialises a task (including TSS) for the scheduler.
3. The kernel creates a LDT for the new process.
4. The kernel copies the pages it needs into the memory and sets the process as runnable.
5. The scheduler will run the process when appropriate.
The catch in this "initialision"-flow is the creation of the LDT. A standard LDT consists of 2 descriptors:
1. code (readonly) segment, base <0>, limit <codelimit>+<datalimit>+<stacklimit>
2. data (r/w) segment, base <0>, limit <codelimit>+<datalimit>+<stacklimit>
The stack of the program will be placed at the top of the datalimit. For instance, the selectors upon entry of the process could be:
(code (.text) size 10000)
(data (.data) size 10000)
( (.bss ) size 10000)
(stack (.stack) size 10000)
CS: 000Fh (index 8 + ring 3 + LDT) (limit = 10000)
DS: 0017h (index 16 + ring 3 + LDT) (limit = 50000)
SS: 0017h (index 16 + ring 3 + LDT) (limit = 50000)
SP: 050000h
Two questions pops into mind when viewing the descriptors and selectors:
1. Why is the base and limit of the code the same as the base of the data?
Not sure. It's very possible that it doesn't have to be that way. Data doesn't need to interfere with the code, only the code can interfere with the data. When placing the code inside a protected area of the memory, there is no possibility for anything to overflow data in order to run code. However, it isn't possible anymore to run self-modifying code in the general way.
2. Why is the stack in the same descriptor as the data?
Most languages uses the stack to pass variables from one function to another, and to declare temporary values used in the function:
In this code-snippet, there are 3 variables: global_data which is located in the .data segment. the param1 which is located on the stack and i which is also located on the stack.
When entering test_function, the BasePointer (EBP) will store the current stackpointer (ESP). After that, room for the temporary data will be reserved (in this case, 4 bytes for the integer "i". Everything BELOW EBP are the temporary vars while everything above points to the parameter list of the function.
The line "i = param1", which copies a variable to another variable actually copies 2 variables on the stack ( mov eax, [ebp+4]; mov [ebp-8], eax), while the other line "i = global_data" copies from the data-segment to the stack: mov eax, [global_data]; mov [ebp-8], eax). As you might know, ebp and esp uses SS and normal data references are done in DS.
There should be no need for the stack and data to be the same memory area, also, there should be no need for the code to be in the same area as the data or stack.
Buffer overflows
----------------
Standard overflows are created by modifing the return address of a function by changing it into a own function which execve's a shell.
1. Pfunction
A Pfunction is a protected function. This means the function gets another stack than the original one from the program. This means the original stack which holds the return address and the saved stackpointer, while the 'stackcopy' holds the data and the temporary vars. Exploits which rely on modifying the EIP don't work anymore since there is no EIP to modify
A normal stack will hold the following items inside a function:
RETURN ADDRESS (EIP)
STACKPOINTER (EBP)
TMP [3]
TMP [2]
TMP [1]
I
So when we overwrite TMP[5], we actually are overwriting the EIP value. When we change this into the addres of another function, lets say: tmp[5]=&exploit, we could do some serious damage (in case of stack-data can also be executed by the program).
A Pfunction does something else with the stack:
1. 'CALL' to the Pfunction by placing EIP on the stack and loading the new EIP.
2. Save original EBP and copy the current ESP into EBP
3. No, create a new stack-descriptor, the base of the stack is ESP+8 and the limit is the 'precalculated' size of the stack by the program (parameter-size + variable-size).
4. Load the new stack selector and load the ESP on the start of the global vars:
if we have 1 int and 1 char on the paramlist, we load ESP with 8. This is because we should only load 5 (4 bytes for the int, 1 byte for the char), but the stack is DWORD aligned, so 8 bytes it is..
if we have 1 integer as temporary var (4 bytes), the limit of the descriptor should be 12 (8 param, 2 var)
dword [SS:ESP-8] points to the integer,
byte [SS:ESP-4] points to the char,
dword [SS:ESP+0] points to the temporary integer
The footer of a function should do:
1. Delete stack descriptor.
2. Restore original SS and ESP
3. Restore EBP
4. RET to the EIP
Pre:
1. No more manipulation of the stack.
Con:
1. Creating a selector on the fly PER function is very slow and therefor should only be done by critical functions like input-function. There isn't really any reason to use a Pfunction on simple calculation functions or functions which are run very often.
Pfunctions can be created by the using the keyword __PROTECTED__
char * __PROTECTED__ GetInput (void);
2. Pchar
A Pchar is basicly the same as a Pfunction, only it is data type, not a function. When using for instance: PChar tmp[10], the char array is setup as a fresh descriptor which has a base &tmp and a limit 10 (and not 12). References to a PCHAR are always based from offset 0 and the data segment (possible the ES, GS or FS can be used).
Flow of defining a PCHAR:
The DJGPP GCC compiler output of "gcc -S test.c" returns test.S:
Now we change the code so it uses Pchar:
When using a Pchar, the compiler allocated memory and defines the descriptors trough the normal system calls:
System call interrupt = 0x80
Allocate Memory:
AH = 20h
AL = Granularity (0 = ECX is in bytes, 1 = ECX is in 4K pages)
ECX = size of allocation
Return:
EAX = size of allocation (depending on granularity)
EBX = base address of the allocation
Allocate Descriptor
AH = 40h
AL = Granularity (0 = ECX is in bytes, 1 = ECX is in 4K pages)
EBX = base address
ECX = limit
Return:
AX = selector
Free Descriptor
AH = 41h
BX = Selector
The (future) CCC (CybOS C Compiler) output of "ccc -S test.c" returns test.S:
This program should not cause a overflow, but a segmentation fault. In this example, it's pretty obviously that it will overflow, but most of the times it wont because of many external factors. So in critical processes, it's better that the program shuts itself down after finding a "error", rather then letting the program continue so it can exploit itself.
Also, notice that in this example, the real delay is in the fact that in every recursion FS is loaded. A smart compiler could notice how many Pchars are used in a recursion, and when only 3 are found, FS, GS and ES could be loaded BEFORE entering the loop so you will have no penalty when using Pchars. But bare in mind: Pchars are mostly used when receiving user-data. These functions will idle a lot so you will notice no delay in that case.
Limitations of the LDT
----------------------
A local descriptor table can only store 8192 selectors. With the exception of 3 selectors (text, data, stack) this leaves 8189 selectors for Pchars and PFunction. This should be more than enough for a single program, but even when it's not, there is always the possibility to switch to another LDT. Off course, even the GDT supports 8192 descriptors (including TSS, including global descriptors) so Pchars entries will eventually exhaust. I think the changes on that are very slim.
interessant topic over buffer overflows en exploits.
Nou denk ik dat er best wel een aantal mensen zijn die hier over kunnen meepraten, dus heb ik een soort van proposal gemaakt over een toekomstige 'feature' in een compiler die straks geport gaat worden naar een specifiek OS toe (de mijne
Daarom heb ik dus een proposal gemaakt hierover en aangezien der best wel wat fouten her en der in zitten en er over heel wat dingen nog gediscussieerd kunnen worden gooi ik em maar bij deze online..
Proposition "safe mode" option in CybOS compilers
-------------------------------------------------
Especially in the times of persistent connections between many computers and users, the need for tense security is rising every day. Not only the need for better software is needed, but also the need for better tools to create these software is a very important part of the security-measurements that should be taken. For instance, if a application like a java engine has a serious exploit, the software running on it can be 100% secure, but will still hold the exploits from the underlying application. This works it's way right down to the hardware levels of the computer system.
After spending some time in the world of exploits and protection against them, I noticed that almost all exploits are resulting from bad programming, wether or not intentionally. A simple program that doesn't check the size of incoming data is a very good candidate for a "buffer overflow"-exploit.
A "buffer overflow"-exploit is simply a case of overflowing the input buffer in a way, it changes the normal execution of the program into the execution of code which is passed in the buffer (which overflowed). With the right factors, it's not very hard to create your own code which is run by the remote machine by "dropping the code inside an egg" (ie: eggdropping the code into a buffer-overflow).
In todays protected kernel environments where all programs run in heavily protected area's so they cannot harm other program or either the kernel itself, it's not very logical that the OS would allow the programs to hurt themselves. Afterall, the propositions I will make aren't very hard to implement, yet somehow, nobody actually implemented it inside there compilers.
JayTaph
The theory behind "safe mode"
-----------------------------
Before you can actually read this chapter, it is highly recommended that you know your intel 386+ PMODE theory.
How does a normal program (or process) execute in a protected kernel environment:
1. The kernel allocate memory to store the process.
2. The kernel initialises a task (including TSS) for the scheduler.
3. The kernel creates a LDT for the new process.
4. The kernel copies the pages it needs into the memory and sets the process as runnable.
5. The scheduler will run the process when appropriate.
The catch in this "initialision"-flow is the creation of the LDT. A standard LDT consists of 2 descriptors:
1. code (readonly) segment, base <0>, limit <codelimit>+<datalimit>+<stacklimit>
2. data (r/w) segment, base <0>, limit <codelimit>+<datalimit>+<stacklimit>
The stack of the program will be placed at the top of the datalimit. For instance, the selectors upon entry of the process could be:
(code (.text) size 10000)
(data (.data) size 10000)
( (.bss ) size 10000)
(stack (.stack) size 10000)
CS: 000Fh (index 8 + ring 3 + LDT) (limit = 10000)
DS: 0017h (index 16 + ring 3 + LDT) (limit = 50000)
SS: 0017h (index 16 + ring 3 + LDT) (limit = 50000)
SP: 050000h
Two questions pops into mind when viewing the descriptors and selectors:
1. Why is the base and limit of the code the same as the base of the data?
Not sure. It's very possible that it doesn't have to be that way. Data doesn't need to interfere with the code, only the code can interfere with the data. When placing the code inside a protected area of the memory, there is no possibility for anything to overflow data in order to run code. However, it isn't possible anymore to run self-modifying code in the general way.
2. Why is the stack in the same descriptor as the data?
Most languages uses the stack to pass variables from one function to another, and to declare temporary values used in the function:
code:
1
2
3
4
5
6
7
8
| int global_data = 0;
void test_function (int param1) {
int i;
i = param1;
i = global_data;
} |
In this code-snippet, there are 3 variables: global_data which is located in the .data segment. the param1 which is located on the stack and i which is also located on the stack.
When entering test_function, the BasePointer (EBP) will store the current stackpointer (ESP). After that, room for the temporary data will be reserved (in this case, 4 bytes for the integer "i". Everything BELOW EBP are the temporary vars while everything above points to the parameter list of the function.
The line "i = param1", which copies a variable to another variable actually copies 2 variables on the stack ( mov eax, [ebp+4]; mov [ebp-8], eax), while the other line "i = global_data" copies from the data-segment to the stack: mov eax, [global_data]; mov [ebp-8], eax). As you might know, ebp and esp uses SS and normal data references are done in DS.
There should be no need for the stack and data to be the same memory area, also, there should be no need for the code to be in the same area as the data or stack.
Buffer overflows
----------------
Standard overflows are created by modifing the return address of a function by changing it into a own function which execve's a shell.
1. Pfunction
A Pfunction is a protected function. This means the function gets another stack than the original one from the program. This means the original stack which holds the return address and the saved stackpointer, while the 'stackcopy' holds the data and the temporary vars. Exploits which rely on modifying the EIP don't work anymore since there is no EIP to modify
A normal stack will hold the following items inside a function:
code:
1
2
3
4
5
6
7
| int blaat (void)
{
int tmp[3];
int i;
return 0;
} |
RETURN ADDRESS (EIP)
STACKPOINTER (EBP)
TMP [3]
TMP [2]
TMP [1]
I
So when we overwrite TMP[5], we actually are overwriting the EIP value. When we change this into the addres of another function, lets say: tmp[5]=&exploit, we could do some serious damage (in case of stack-data can also be executed by the program).
A Pfunction does something else with the stack:
1. 'CALL' to the Pfunction by placing EIP on the stack and loading the new EIP.
2. Save original EBP and copy the current ESP into EBP
3. No, create a new stack-descriptor, the base of the stack is ESP+8 and the limit is the 'precalculated' size of the stack by the program (parameter-size + variable-size).
4. Load the new stack selector and load the ESP on the start of the global vars:
if we have 1 int and 1 char on the paramlist, we load ESP with 8. This is because we should only load 5 (4 bytes for the int, 1 byte for the char), but the stack is DWORD aligned, so 8 bytes it is..
if we have 1 integer as temporary var (4 bytes), the limit of the descriptor should be 12 (8 param, 2 var)
dword [SS:ESP-8] points to the integer,
byte [SS:ESP-4] points to the char,
dword [SS:ESP+0] points to the temporary integer
The footer of a function should do:
1. Delete stack descriptor.
2. Restore original SS and ESP
3. Restore EBP
4. RET to the EIP
Pre:
1. No more manipulation of the stack.
Con:
1. Creating a selector on the fly PER function is very slow and therefor should only be done by critical functions like input-function. There isn't really any reason to use a Pfunction on simple calculation functions or functions which are run very often.
Pfunctions can be created by the using the keyword __PROTECTED__
char * __PROTECTED__ GetInput (void);
2. Pchar
A Pchar is basicly the same as a Pfunction, only it is data type, not a function. When using for instance: PChar tmp[10], the char array is setup as a fresh descriptor which has a base &tmp and a limit 10 (and not 12). References to a PCHAR are always based from offset 0 and the data segment (possible the ES, GS or FS can be used).
Flow of defining a PCHAR:
code:
1
2
3
4
5
6
7
8
9
10
| void main (void)
{
int i;
char tmp[10];
for (i=0; i!=11; i++)
{
tmp[i]='A'; // Uh Oh
}
} |
The DJGPP GCC compiler output of "gcc -S test.c" returns test.S:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| .file "test.c"
gcc2_compiled.:
___gnu_compiled_c:
.text
.p2align 2
.globl _main
_main:
pushl %ebp ; Setup stack frame
movl %esp,%ebp
subl $24,%esp ; Reserve mem
nop
movl $0,-4(%ebp) ; -4(ebp)=i i=0;
.p2align 4,,7
L3:
cmpl $11,-4(%ebp) ; i==11?
jne L6 ; No .. jmp L6
jmp L4 ; Otherwise done
.p2align 4,,7
L6:
leal -16(%ebp),%eax ; eax = -16(ebp) = base of TMP
movl -4(%ebp),%edx ; edx = i
movb $65,(%edx,%eax) ; mov 'A' eax+edx (tmp[i])
L5:
incl -4(%ebp) ; increase i
jmp L3 ; jmp to the 'for'-loop
.p2align 4,,7
L4:
L2:
movl %ebp,%esp ; Restore stackspace
popl %ebp ; Restore EBP
ret ; return (to stub/end of program) |
Now we change the code so it uses Pchar:
code:
1
2
3
4
5
6
7
8
9
10
| void main (void)
{
int i;
pchar tmp[10];
for (i=0; i!=11; i++)
{
tmp[i]='A'; // Uh Oh
}
} |
When using a Pchar, the compiler allocated memory and defines the descriptors trough the normal system calls:
System call interrupt = 0x80
Allocate Memory:
AH = 20h
AL = Granularity (0 = ECX is in bytes, 1 = ECX is in 4K pages)
ECX = size of allocation
Return:
EAX = size of allocation (depending on granularity)
EBX = base address of the allocation
Allocate Descriptor
AH = 40h
AL = Granularity (0 = ECX is in bytes, 1 = ECX is in 4K pages)
EBX = base address
ECX = limit
Return:
AX = selector
Free Descriptor
AH = 41h
BX = Selector
The (future) CCC (CybOS C Compiler) output of "ccc -S test.c" returns test.S:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| .file "test.c"
gcc2_compiled.:
___gnu_compiled_c:
.text
.p2align 2
.globl _main
_main:
pushl %ebp ; Setup stack frame
movl %esp,%ebp
subl $24,%esp ; Reserve mem (why is it always 24?)
nop
(*) movl $10, %ecx ; Size of allocation
(*) movl $2000, %eax ; Service Nr and granularity
(*) int $0x80
; EBX already filled
(*) movl $10, %ecx ; Size of allocation
(*) movl $4000, %eax ; Service Nr and granularity
(*) int $0x80
(*) movw %ax, (_main.tmp_slctr) ; Place selector into global data defined by the compiler
movl $0,-4(%ebp) ; -4(ebp)=i i=0;
.p2align 4,,7
L3:
cmpl $11,-4(%ebp) ; i==11?
jne L6 ; No .. jmp L6
jmp L4 ; Otherwise done
.p2align 4,,7
L6:
(*) movw %fs, (_main.tmp_slctr) ; Load FS with selector
movl -4(%ebp),%edx ; edx = i
(*) movb $65,fs:(%edx) ; mov 'A' into tmp[i]
L5:
incl -4(%ebp) ; increase i
jmp L3 ; jmp to the 'for'-loop
.p2align 4,,7
L4:
L2:
(*) movw (_main.tmp_slctr), %bx ; Load selector into BX
(*) movl $4100, %eax ; Service Nr
(*) int $0x80 ; Remove descriptor
movl %ebp,%esp ; Restore stackspace
popl %ebp ; Restore EBP
ret ; return (to stub/end of program) |
This program should not cause a overflow, but a segmentation fault. In this example, it's pretty obviously that it will overflow, but most of the times it wont because of many external factors. So in critical processes, it's better that the program shuts itself down after finding a "error", rather then letting the program continue so it can exploit itself.
Also, notice that in this example, the real delay is in the fact that in every recursion FS is loaded. A smart compiler could notice how many Pchars are used in a recursion, and when only 3 are found, FS, GS and ES could be loaded BEFORE entering the loop so you will have no penalty when using Pchars. But bare in mind: Pchars are mostly used when receiving user-data. These functions will idle a lot so you will notice no delay in that case.
Limitations of the LDT
----------------------
A local descriptor table can only store 8192 selectors. With the exception of 3 selectors (text, data, stack) this leaves 8189 selectors for Pchars and PFunction. This should be more than enough for a single program, but even when it's not, there is always the possibility to switch to another LDT. Off course, even the GDT supports 8192 descriptors (including TSS, including global descriptors) so Pchars entries will eventually exhaust. I think the changes on that are very slim.
Yo dawg, I heard you like posts so I posted below your post so you can post again.