R 20: Dangling Pointers and Memory Leaks (35 pts)

What you need

Purpose

To compare pointer vulnerabilities in C and Rust.

Heap Overflow in C

Execute this command to create a new C program named heapo.c:
nano heapo.c
Enter this code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
   char* name1 = (char *) malloc(20);
   char* name2 = (char *) malloc(20);

   printf("Enter name1: ");
   scanf("%s", name1);
   printf("Enter name2: ");
   scanf("%s", name2);

   printf("\n");
   printf("name1 address: %p ; name2 address: %p\n\n", name1, name2);

   printf("name1: \"%s\"\n\n", name1);
   printf("name2: \"%s\"\n\n", name2);
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile the program and run it:

gcc -o heapo heapo.c
./heapo
Enter this name1, which is 20 characters long:
AAAAAAAAAAAAAAAAAAAA
Enter this name2, which is 20 characters long:
BBBBBBBBBBBBBBBBBBBB
The program works as expected, printing out the correct values of the names, as shown below:

Execute this command to run the program again:

./heapo
Enter this name1, which is 40 characters long:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter this name2, which is 40 characters long:
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
The names overwrite the heap metadata and turn into bizarre nonsense, as shown below:

This is very cruel to the developer. No warning or error message appears. C allows an attacker to craft input that will write to memory locations that were not allocated for that purpose.

Heap Overflow in Rust

Execute this command to create a new Rust program named rheap.rs:
nano rheap.rs
Enter this code:
fn main() {
    let mut name1 = Box::new(String::new());
    let mut name2 = Box::new(String::new());

    println!("name1 address: {:p}. name2 address: {:p}.", &name1, &name2);
    println!("name1 contains {}.  name2 contains {}. ", name1, name2);
    
    println!("Enter name1: ");
    let _num = std::io::stdin().read_line(&mut name1).unwrap();
    println!("Enter name2: ");
    let _num = std::io::stdin().read_line(&mut name2).unwrap();
    
    println!("name1 address: {:p}. name2 address: {:p}.", &name1, &name2);
    println!("name1 contains {}.  name2 contains {}. ", name1, name2);
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile the program and run it:

rustc -g rheap.rs
./rheap
Enter this name1, which is 20 characters long:
AAAAAAAAAAAAAAAAAAAA
Enter this name2, which is 20 characters long:
BBBBBBBBBBBBBBBBBBBB
The program works as expected, printing out the correct values of the names, as shown below:

Execute this command to run the program again:

./rheap
Enter this name1, which is 40 characters long:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter this name2, which is 40 characters long:
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
The program works as expected, printing out the correct values of the names, as shown below. There is no overflow.

Using gdb to Examine the Stored Strings

Execute these commands to debug the Rust executable and list its source code:
gdb -q rheap
list 1,15
The debugger starts, and shows the Rust source code, as shown below.

Execute these commands to set a breakpoint and start running the program:

break 14
run
Enter this name1, which is 20 characters long:
AAAAAAAAAAAAAAAAAAAA
Enter this name2, which is 20 characters long:
BBBBBBBBBBBBBBBBBBBB
The program halts at the breakpoint.

Execute these commands to examine the strings:

print name1
print name2
The debugger shows the two pointers, which are 0x20 = 32 bytes apart, as shown below.

To see more information about how the strings are stored, execute these commands:

print *name1
print *name2
As shown below, the actual strings are also stored at addresses which are 0x20 = 32 bytes apart.

Execute these commands to run the program again:

run
y
Enter this name1, which is 40 characters long:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter this name2, which is 40 characters long:
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
The program halts at the breakpoint.

Execute these commands to examine the strings:

print name1
print name2
print *name1
print *name2
As shown below, the actual strings are now stored at addresses which are 0x40 = 64 bytes apart.

Rust has automatically reallocated space so no overflow occurs.

R 20.1: File Information (10 pts)

Execute these commands:
q
y
file rheap
The flag is covered by a green rectangle in the image below.

Dangling Pointer in C

Execute this command to create a new C program named dangp.c:
nano dangp.c
Enter this code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
   char* name1 = (char *) malloc(20);
   
   printf("Enter name1: ");
   scanf("%s", name1);
   printf("name1 address: %p ; value %s\n", name1, name1);

   free(name1);
   
   char* name2 = (char *) malloc(20);

   printf("Enter name2: ");
   scanf("%s", name2);
   printf("name2 address: %p ; value %s\n", name2, name2);
   printf("name1 address: %p ; value %s\n", name1, name1);
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile the program and run it:

gcc -o dangp dangp.c
./dangp
Enter this name1:
A
Enter this name2:
B
Because of the errant "free" command, name1 and name2 both end up pointing to the same address, so the value entered into name2 overwrites name1.

This happens because the "free" command does not erase the "name1" pointer. It remains "dangling"--available for use, even though the memory it points to is no longer properly reserved and may be re-used for a different purpose.

Like other memory corruption vulnerabilities, this is a serious security flaw and responsible for many current exploits.

Heap Pointers in Rust

Execute this command to create a new Rust program named rheapp.rs:
nano rheapp.rs
In Rust, there is no "free" command. Instead, it automatically frees heap objects when they "fall out of scope".

To see how that works, enter this code:

fn main() {
	{
        let mut name1 = Box::new(String::new());

        println!("Enter name1: ");
        let _num = std::io::stdin().read_line(&mut name1).unwrap();
        println!("name1 address: {:p} ; contents {}.", &name1, name1);
	}

    let mut name2 = Box::new(String::new());

    println!("Enter name2: ");
    let _num = std::io::stdin().read_line(&mut name2).unwrap();

    println!("name2 address: {:p} ; contents {}.", &name2, name2);
    println!("name1 contents {}.", name1);
}
Save the file with Ctrl+X, Y, Enter.

Execute this command to compile the program:

rustc rheapp.rs
The compiler rejects the code with an error message, as shown below.

It won't allow a developer to make the mistake of using "name1" after it is no longer allocated.

R 20.2: Error (5 pts)

The flag is covered by a green rectangle in the image above.

Using free() Correctly in C

Execute this command to create a new C program named cnoleak.c:
nano cnoleak.c
Enter this code:
#include <stdio.h>
#include <stdlib.h>

void noleak()
{
   char * p = malloc(1000);
   free(p);
   return;
}

int main()
{
   int i, j;

   for (i=0; i<10; i++) {
   	  for (j=0; j<1000; j++) noleak();
      printf("Press Enter to Continue\n");
      getchar();
   }
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile the program and run it:

gcc -o cnoleak cnoleak.c
./cnoleak
Open a second SSH window and execute this command:
watch "pmap $(pgrep leak)"
The total memory consumed by the "cnoleak" process is shown at the bottom of the list and updated every 2 seconds, as shown below.

In the first SSH window, you see a "Press Enter to Continue" message.

Press Enter a few times, watching the other window. The total memory usage does not change, as shown below.

This is because the memory allocated with alloc() is freed each time with free().

Press Ctrl+C in both windows to terminate the running processes.

Memory Leak in C

Execute this command to create a new C program named cleak.c:
nano cleak.c
Enter this code:
#include <stdio.h>
#include <stdlib.h>

void leak()
{
   char * p = malloc(1000);
   return;
}

int main()
{
   int i, j;

   for (i=0; i<10; i++) {
   	  for (j=0; j<1000; j++) leak();
      printf("Press Enter to Continue\n");
      getchar();
   }
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile the program and run it:

gcc -o cleak cleak.c
./cleak
In the second SSH window, execute this command:
watch "pmap $(pgrep leak)"
In the first SSH window, you see a "Press Enter to Continue" message.

Press Enter a few times, watching the other window. The total memory usage increases each time, as shown below.

This is a memory leak. Memory is allocated but never freed, so the memory usage keeps increasing.

Press Ctrl+C in both windows to terminate the running processes.

Using Box Properly in Rust

Execute this command to create a new Rust program named rnoleak.rs:
nano rnoleak.rs
In Rust, there is no "free" command. Instead, it automatically frees heap objects when they "fall out of scope".

To see how that works, enter this code:

use std::io::{stdin, Read};

fn noleak() {
	let _a = Box::new([0.0f64; 1000]);
}

fn main() {
   for _i in 1..11 {
       for _j in 1..1001 {
       	   noleak();
       }
       println!("Press Enter to Continue:");
       stdin().read(&mut [0]).unwrap();
   }
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile and run the program:

rustc rnoleak.rs
./rnoleak
In the second SSH window, execute this command:
watch "pmap $(pgrep leak)"
In the first SSH window, you see a "Press Enter to Continue" message.

Press Enter a few times, watching the other window. The list of memory segments is longer than it was for the C program, but the total memory usage is constant, as shown below.

There is no memory leak, because Rust automatically frees the memory each time the function returns.

Press Ctrl+C in both windows to terminate the running processes.

Memory Leak in Rust

Execute this command to create a new Rust program named rleak.rs:
nano rleak.rs
Rust's "forget" function can cause memory leaks, because it drops a value without running its "destructor", which would have freed its memory.

To see how that works, enter this code:

use std::io::{stdin, Read};
use std::mem;

fn leak() {
	let a = Box::new([0.0f64; 1000]);
	mem::forget(a);
}

fn main() {
   for _i in 1..11 {
       for _j in 1..1001 {
       	   leak();
       }
       println!("Press Enter to Continue:");
       stdin().read(&mut [0]).unwrap();
   }
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile and run the program:

rustc rleak.rs
./rleak
In the second SSH window, execute this command:
watch "pmap $(pgrep leak)"
In the first SSH window, you see a "Press Enter to Continue" message.

Press Enter a few times, watching the other window. The total memory usage increases each time, is constant, as shown below.

Press Ctrl+C in both windows to terminate the running processes.

R 20.3: Stat (10 pts)

Execute this command:
stat rleak
The flag is covered by a green rectangle in the image below.

Duplicate Pointers in C

C allows two pointers to point to the same memory address. To see that, execute this command to create a new C program named dupp.c:
nano dupp.c
Enter this code:
#include <stdio.h>
#include <stdlib.h>

int main()
{
   int * ptr1;
   ptr1 = (int*)malloc(sizeof(int));
   int * ptr2 = ptr1;

   *ptr1 = 1;
   printf("ptr1 points to address %p and contains %d\n", ptr1, *ptr1);
   printf("ptr2 points to address %p and contains %d\n\n", ptr2, *ptr2);

   printf("Changing *ptr1 to 2.\n");
   *ptr1 = 2;
   printf("ptr1 points to address %p and contains %d\n", ptr1, *ptr1);
   printf("ptr2 points to address %p and contains %d\n", ptr2, *ptr2);
}
Save the file with Ctrl+X, Y, Enter.

Execute these commands to compile the program and run it:

gcc -o dupp dupp.c
./dupp
As shown below, changing *ptr1 also changes *ptr2. That is confusing and can easily cause developers to make errors, such as double-free or dangling pointers.

Copying and Ownership

Execute this command to create a new Rust program named copy.rs:
nano copy.rs
Enter this code:
fn main() {
    let a = 1;
    println!("a is stored at address {:p} and contains {}", &a, a);
    
    let b = a;
    println!("b is stored at address {:p} and contains {}", &b, b);
    println!("a is stored at address {:p} and contains {}\n", &a, a);
    println!("Changing b to 2\n");

    let b = 2;
    println!("b is stored at address {:p} and contains {}", &b, b);
    println!("a is stored at address {:p} and contains {}", &a, a);
}
Save the file with Ctrl+X, Y, Enter.

Execute this command to compile and run the program:

rustc copy.rs
./copy
Rust creates a new object for b with a different address, and copies the contents of a into it.

This happens because an integer on the stack is a "primitive object" that consumes few resources.

Ownership in Rust

What happens with more expensive resources, such as items on the heap?

Rusts avoids duplicate pointers with "ownership".

Execute this command to create a new Rust program named own.rs:

nano own.rs
Enter this code:
fn main() {
    let a = Box::from(1i8);
    println!("a is stored at address {:p} and contains {}", &a, a);
    
    let b = a;
    println!("b is stored at address {:p} and contains {}", &b, b);
    println!("a is stored at address {:p} and contains {}", &a, a);
}
Save the file with Ctrl+X, Y, Enter.

Execute this command to compile the program:

rustc own.rs
Rust rejects this code with a "value borrowed here after move" error, as shown below.

The "let a" statement allocates some memory and assigns it to a, so a owns it.

The "let b = a" statement transfers ownership of that memory to b, and a can no longer use it.

So the attempt to print a fails.

R 20.4: Error (10 pts)

The flag is covered by a green rectangle in the image above

Sources

Rust Tutorial
Debugging Rust
Dependencies
Macro cmd_lib::run_cmd
The Cargo Book
Fearless Security: Memory Safety

Posted 5-30-2020