ctf101-Binary Exploitation

Binary Exploitation

Binaries, or executables, are machine code for a computer to execute. For the most part, the binaries that you will face in CTFs are Linux ELF files or the occasional windows executable. Binary Exploitation is a broad topic within Cyber Security which really comes down to finding a vulnerability in the program and exploiting it to gain control of a shell or modifying the program’s functions.

1. Registers

A register is a location within the processor that is able to store data, much like RAM. Unlike RAM however, accesses to registers are effectively instantaneous, whereas reads from main memory can take hundreds of CPU cycles to return.

Registers can hold any value, like addresses(pointers), results from mathematical operations, characters, etc.

Some registers are reserved(保留的) however, meaning they have a special purpose and are not “general purpose registers” (GPRs).

On x86, the only 2 reserved registers are rip and rsp which hold the address of the next instruction to execute and the address of the [stack](#2. The Stack) respectively.

On x86, the same register can have different sized accesses for backwards compatability(向后兼容),like rax,eax,ax,al,ah.

2. The Stack

In computer architecture, the stack is a hardware manifestation of the stack data structure (a Last In, First Out queue).

In x86, the stack is simply an area in RAM that was chosen to be the stack - there is no special hardware to store stack contents. The esp/rsp register holds the address in memory where the bottom of the stack resides. When something is pushed to the stack, esp decrements by 4 (or 8 on 64-bit x86), and the value that was pushed is stored at that location in memory. Likewise, when a pop instruction is executed, the value at esp is retrieved (i.e. esp is dereferenced), and esp is then incremented by 4 (or 8).

N.B. The stack “grows” down to lower memory addresses!

Conventionally, ebp/rbp contains the address of the top of the current stack frame, and so sometimes local variables are referenced as an offset relative to ebp rather than an offset to esp. A stack frame is essentially just the space used on the stack by a given function.

2.1 Uses

The stack is primarily used for a few things:

  • Storing function arguments
  • Storing local variables
  • Storing processor state between function calls

3. Calling Conventions

To be able to call functions, there needs to be an agreed-upon way to pass arguments. If a program is entirely self-contained in a binary, the compiler would be free to decide the calling convention. However in reality, shared libraries are used so that common code (e.g. libc) can be stored once and dynamically linked in to programs that need it, reducing program size.

In Linux binaries, there are really only two commonly used calling conventions: cdecl for 32-bit binaries, and SysV for 64-bit.

3.1 cdecl

In 32-bit binaries on Linux, function arguments are passed in on the stack in reverse order. A function like this:

3.2 SysV

For 64-bit binaries, function arguments are first passed in certain registers:

  1. RDI
  2. RSI
  3. RDX
  4. RCX
  5. R8
  6. R9

then any leftover arguments are pushed onto the stack in reverse order, as in cdecl.

3.3 other conventions

Any method of passing arguments could be used as long as the compiler is aware of what the convention is. As a result, there have been many calling conventions in the past that aren’t used frequently anymore.See Wikipedia for a comprehensive list.

4. Global Offset Table (GOT)

The Global Offset Table (or GOT) is a section inside of programs that holds addresses of functions that are dynamically linked. As mentioned in the page on [calling conventions](# 3. Calling Conventions), most programs don’t include every function they use to reduce binary size. Instead, common functions (like those in libc) are “linked” into the program so they can be saved once on disk and reused by every program.

Unless a program is marked [full RELRO](#7.4 Relocation Read-Only (RELRO)), the resolution of function to address in dynamic library is done lazily. All dynamic libraries are loaded into memory along with the main program at launch, however functions are not mapped to their actual code until they’re first called.

To avoid searching through shared libraries each time a function is called, the result of the lookup is saved into the GOT so future function calls “short circuit” straight to their implementation bypassing the dynamic resolver.

This has two important implications:

  1. The GOT contains pointers to libraries which move around due to [ASLR](#7.2 Address Space Layout Randomization (ASLR))
  2. The GOT is writable

These two facts will become very useful to use in [Return Oriented Programming](#6. Return Oriented Programming (ROP))

4.1 PLT

Before a functions address has been resolved, the GOT points to an entry in the Procedure Linkage Table (PLT). This is a small “stub” function which is responsible for calling the dynamic linker with (effectively) the name of the function that should be resolved.

5. Buffers

A Buffer Overflow is a vulnerability in which data can be written which exceeds the allocated space, allowing an attacker to overwrite other data.

5.1 Buffer Overflow

The simplest and most common buffer overflow is one where the buffer is on the stack. Let’s look at an example.

#include <stdio.h>

int main() {
    int secret = 0xdeadbeef;
    char name[100] = {0};
    read(0, name, 0x100);
    if (secret == 0x1337) {
        puts("Wow! Here's a secret.");
    } else {
        puts("I guess you're not cool enough to see my secret");
    }
}

There’s a tiny mistake in this program which will allow us to see the secret. name is decimal 100 bytes, however we’re reading in hex 100 bytes (=256 decimal bytes)! Let’s see how we can use this to our advantage.

If the compiler chose to layout the stack like this:

        0xffff006c: 0xf7f7f7f7  // Saved EIP
        0xffff0068: 0xffff0100  // Saved EBP
        0xffff0064: 0xdeadbeef  // secret
...
        0xffff0004: 0x0
ESP ->  0xffff0000: 0x0         // name

let’s look at what happens when we read in 0x100 bytes of 'A’s.

The first decimal 100 bytes are saved properly:

        0xffff006c: 0xf7f7f7f7  // Saved EIP
        0xffff0068: 0xffff0100  // Saved EBP
        0xffff0064: 0xdeadbeef  // secret
...
        0xffff0004: 0x41414141
ESP ->  0xffff0000: 0x41414141  // name

However when the 101st byte is read in, we see an issue:

        0xffff006c: 0xf7f7f7f7  // Saved EIP
        0xffff0068: 0xffff0100  // Saved EBP
        0xffff0064: 0xdeadbe41  // secret
...
        0xffff0004: 0x41414141
ESP ->  0xffff0000: 0x41414141  // name

The least significant byte of secret has been overwritten! If we follow the next 3 bytes to be read in, we’ll see the entirety of secret is “clobbered” with our 'A’s.

The remaining 152 bytes would continue clobbering values up the stack.

Passing an impossible check

How can we use this to pass the seemingly impossible check in the original program? Well, if we carefully line up our input so that the bytes that overwrite secret happen to be the bytes that represent 0x1337 in little-endian, we’ll see the secret message.

A small Python one-liner will work nicely: python -c "print 'A'*100 + '\x37\x13\x00\x00'"

This will fill the name buffer with 100 'A’s, then overwrite secret with the 32-bit little-endian encoding of 0x1337.

Going one step further

As discussed on [the stack](#2. The Stack) page, the instruction that the current function should jump to when it is done is also saved on the stack (denoted as “Saved EIP” in the above stack diagrams). If we can overwrite this, we can control where the program jumps after main finishes running, giving us the ability to control what the program does entirely.

Usually, the end objective in binary exploitation is to get a shell (often called “popping a shell”) on the remote computer. The shell provides us with an easy way to run anything we want on the target computer.

Say there happens to be a nice function that does this defined somewhere else in the program that we normally can’t get to:

void give_shell() {
    system("/bin/sh");
}

Well with our buffer overflow knowledge, now we can! All we have to do is overwrite the saved EIP on the stack to the address where give_shell is. Then, when main returns, it will pop that address off of the stack and jump to it, running give_shell, and giving us our shell.

Assuming give_shell is at 0x08048fd0, we could use something like this: python -c "print 'A'*108 + '\xd0\x8f\x04\x08'"

We send 108 'A’s to overwrite the 100 bytes that is allocated for name, the 4 bytes for secret, and the 4 bytes for the saved EBP. Then we simply send the little-endian form of give_shell's address, and we would get a shell!

This idea is extended on in [Return Oriented Programming](#6. Return Oriented Programming (ROP))

6. Return Oriented Programming (ROP)

Return Oriented Programming (or ROP) is the idea of chaining together small snippets of assembly with stack control to cause the program to do more complex things.

As we saw in [buffer overflows](#5. Buffers), having stack control can be very powerful since it allows us to overwrite saved instruction pointers, giving us control over what the program does next. Most programs don’t have a convenient give_shell function however, so we need to find a way to manually invoke system or another exec function to get us our shell.

32 bit

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

char name[32];

int main() {
    printf("What's your name? ");
    read(0, name, 32);

    printf("Hi %s\n", name);

    printf("The time is currently ");
    system("/bin/date");

    char echo[100];
    printf("What do you want me to echo back? ");
    read(0, echo, 1000);
    puts(echo);

    return 0;
}

We obviously have a stack buffer overflow on the echo variable which can give us EIP control when main returns. But we don’t have a give_shell function! So what can we do?

We can call system with an argument we control! Since arguments are passed in on the stack in 32-bit Linux programs (see [calling conventions](#3. Calling Conventions)), if we have stack control, we have argument control.

When main returns, we want our stack to look like something had normally called system. Recall what is on the stack after a function has been called:

        ...                                 // More arguments
        0xffff0008: 0x00000002              // Argument 2
        0xffff0004: 0x00000001              // Argument 1
ESP ->  0xffff0000: 0x080484d0              // Return address

So main's stack frame needs to look like this:

        0xffff0008: 0xdeadbeef              // system argument 1
        0xffff0004: 0xdeadbeef              // return address for system
ESP ->  0xffff0000: 0x08048450              // return address for main (system's PLT entry)

Then when main returns, it will jump into system's [PLT](#4.1 PLT) entry and the stack will appear just like system had been called normally for the first time.

Note: we don’t care about the return address system will return to because we will have already gotten our shell by then!

Arguments

This is a good start, but we need to pass an argument to system for anything to happen. As mentioned in the page on [ASLR](#7.2 Address Space Layout Randomization (ASLR)), the stack and dynamic libraries “move around” each time a program is run, which means we can’t easily use data on the stack or a string in libc for our argument. In this case however, we have a very convenient nameglobal which will be at a known location in the binary (in the BSS segment).

Putting it together

Our exploit will need to do the following:

  1. Enter “sh” or another command to run as name
  2. Fill the stack with
    1. Garbage up to the saved EIP
    2. The address of system's PLT entry
    3. A fake return address for system to jump to when it’s done
    4. The address of the name global to act as the first argument to system

64 bit

In 64-bit binaries we have to work a bit harder to pass arguments to functions. The basic idea of overwriting the saved RIP is the same, but as discussed in [calling conventions](#3. Calling Conventions), arguments are passed in registers in 64-bit programs. In the case of running system, this means we will need to find a way to control the RDI register.

To do this, we’ll use small snippets of assembly in the binary, called “gadgets.” These gadgets usually pop one or more registers off of the stack, and then call ret, which allows us to chain them together by making a large fake call stack.

For example, if we needed control of both RDI and RSI, we might find two gadgets in our program that look like this (using a tool like rp++ or ROPgadget):

0x400c01: pop rdi; ret
0x400c03: pop rsi; pop r15; ret

We can setup a fake call stack with these gadets to sequentially execute them, poping values we control into registers, and then end with a jump to system.

Example

    	0xffff0028: 0x400d00            // where we want the rsi gadget's ret to jump to now that rdi and rsi are controlled
        0xffff0020: 0x1337beef          // value we want in r15 (probably garbage)
        0xffff0018: 0x1337beef          // value we want in rsi
        0xffff0010: 0x400c03            // address that the rdi gadget's ret will return to - the pop rsi gadget
        0xffff0008: 0xdeadbeef          // value to be popped into rdi
RSP ->  0xffff0000: 0x400c01            // address of rdi gadget

Stepping through this one instruction at a time, main returns, jumping to our pop rdi gadget:

RIP = 0x400c01 (pop rdi)
RDI = UNKNOWN
RSI = UNKNOWN

        0xffff0028: 0x400d00            // where we want the rsi gadget's ret to jump to now that rdi and rsi are controlled
        0xffff0020: 0x1337beef          // value we want in r15 (probably garbage)
        0xffff0018: 0x1337beef          // value we want in rsi
        0xffff0010: 0x400c03            // address that the rdi gadget's ret will return to - the pop rsi gadget
RSP ->  0xffff0008: 0xdeadbeef          // value to be popped into rdi

pop rdi is then executed, popping the top of the stack into RDI:

RIP = 0x400c02 (ret)
RDI = 0xdeadbeef
RSI = UNKNOWN

        0xffff0028: 0x400d00            // where we want the rsi gadget's ret to jump to now that rdi and rsi are controlled
        0xffff0020: 0x1337beef          // value we want in r15 (probably garbage)
        0xffff0018: 0x1337beef          // value we want in rsi
RSP ->  0xffff0010: 0x400c03            // address that the rdi gadget's ret will return to - the pop rsi gadget

The RDI gadget then rets into our RSI gadget:

RIP = 0x400c03 (pop rsi)
RDI = 0xdeadbeef
RSI = UNKNOWN

        0xffff0028: 0x400d00            // where we want the rsi gadget's ret to jump to now that rdi and rsi are controlled
        0xffff0020: 0x1337beef          // value we want in r15 (probably garbage)
RSP ->  0xffff0018: 0x1337beef          // value we want in rsi

RSI and R15 are popped:

RIP = 0x400c05 (ret)
RDI = 0xdeadbeef
RSI = 0x1337beef

RSP ->  0xffff0028: 0x400d00            // where we want the rsi gadget's ret to jump to now that rdi and rsi are controlled

And finally, the RSI gadget rets, jumping to whatever function we want, but now with RDI and RSI set to values we control.

7. Binary Security

Binary Security is using tools and methods in order to secure programs from being manipulated and exploited. This tools are not infallible, but when used together and implemented properly, they can raise the difficulty of exploitation greatly.

7.1 No eXecute (NX)

The No eXecute or the NX bit (also known as Data Execution Prevention or DEP) marks certain areas of the program as not executable, meaning that stored input or data cannot be executed as code. This is significant because it prevents attackers from being able to jump to custom shellcode that they’ve stored on the stack or in a global variable.

7.2 Address Space Layout Randomization (ASLR)

Address Space Layout Randomization (or ASLR) is the randomization of the place in memory where the program, shared libraries, the stack, and the heap are. This makes can make it harder for an attacker to exploit a service, as knowledge about where the stack, heap, or libc can’t be re-used between program launches. This is a partially effective way of preventing an attacker from jumping to, for example, libc without a leak.

Typically, only the stack, heap, and shared libraries are ASLR enabled. It is still somewhat rare for the main program to have ASLR enabled, though it is being seen more frequently and is slowly becoming the default.

7.3 Stack Canaries

Stack Canaries are a secret value placed on the stack which changes every time the program is started. Prior to a function return, the stack canary is checked and if it appears to be modified, the program exits immeadiately.

Bypassing Stack Canaries

Stack Canaries seem like a clear cut way to mitigate any stack smashing as it is fairly impossible to just guess a random 64-bit value. However, leaking the address and bruteforcing the canary are two methods which would allow us to get through the canary check.

Stack Canary Leaking

If we can read the data in the stack canary, we can send it back to the program later because the canary stays the same throughout execution. However Linux makes this slightly tricky by making the first byte of the stack canary a NULL, meaning that string functions will stop when they hit it. A method around this would be to partially overwrite and then put the NULL back or find a way to leak bytes at an arbitrary stack offset.

A few situations where you might be able to leak a canary:

  • User-controlled format string
  • User-controlled length of an output
    • “Hey, can you send me 1000000 bytes? thx!”

Bruteforcing a Stack Canary

The canary is determined when the program starts up for the first time which means that if the program forks, it keeps the same stack cookie in the child process. This means that if the input that can overwrite the canary is sent to the child, we can use whether it crashes as an oracle and brute-force 1 byte at a time!

This method can be used on fork-and-accept servers where connections are spun off to child processes, but only under certain conditions such as when the input accepted by the program does not append a NULL byte (read or recv).


Fill the buffer N Bytes + 0x00 results in no crash

Buffer (N Bytes) 00 ?? ?? ?? ?? ?? ?? ?? RBP RIP

Fill the buffer N Bytes + 0x00 + 0x00 results in a crash

N Bytes + 0x00 + 0x01 results in a crash

N Bytes + 0x00 + 0x02 results in a crash

N Bytes + 0x00 + 0x51 results in no crash

Repeat this bruteforcing process for 6 more bytes…


7.4 Relocation Read-Only (RELRO)

Relocation Read-Only (or RELRO) is a security measure which makes some binary sections read-only.

There are two RELRO “modes”: partial and full.

7.4.1 Partial RELRO

Partial RELRO is the default setting in GCC, and nearly all binaries you will see have at least partial RELRO.

From an attackers point-of-view, partial RELRO makes almost no difference, other than it forces the GOT to come before the BSS in memory, eliminating the risk of a [buffer overflows](#5.1 Buffer Overflow) on a global variable overwriting GOT entries.

7.4.2 Full RELRO

Full RELRO makes the entire GOT read-only which removes the ability to perform a “GOT overwrite” attack, where the GOT address of a function is overwritten with the location of another function or a ROP gadget an attacker wants to run.

Full RELRO is not a default compiler setting as it can greatly increase program startup time since all symbols must be resolved before the program is started. In large programs with thousands of symbols that need to be linked, this could cause a noticable delay in startup time.

8. The Heap

The heap is a place in memory which a program can use to dynamically create objects. Creating objects on the heap has some advantages compared to using the stack:

  • Heap allocations can be dynamically sized
  • Heap allocations “persist” when a function returns

There are also some disadvantages however:

  • Heap allocations can be slower
  • Heap allocations must be manually cleaned up

8.1 Heap Exploitation

8.1.1 Overflow

Much like a stack buffer overflow, a heap overflow is a vulnerability where more data than can fit in the allocated buffer is read in. This could lead to heap metadata corruption, or corruption of other heap objects, which could in turn provide new attack surface.

8.1.2 Use After Free (UAF)

Once free is called on an allocation, the allocator is free to re-allocate that chunk of memory in future calls to malloc if it so chooses. However if the program author isn’t careful and uses the freed object later on, the contents may be corrupt (or even attacker controlled). This is called a use after free or UAF.

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

typedef struct string {
    unsigned length;
    char *data;
} string;

int main() {
    struct string* s = malloc(sizeof(string));
    puts("Length:");
    scanf("%u", &s->length);
    s->data = malloc(s->length + 1);
    memset(s->data, 0, s->length + 1);
    puts("Data:");
    read(0, s->data, s->length);

    free(s->data);
    free(s);

    char *s2 = malloc(16);
    memset(s2, 0, 16);
    puts("More data:");
    read(0, s2, 15);

    // Now using s again, a UAF

    puts(s->data);

    return 0;
}

8.1.3 Advanced Heap Exploitation

Not only can the heap be exploited by the data in allocations, but exploits can also use the underlying mechanisms in malloc, free, etc. to exploit a program. This is beyond the scope of CTF 101, but here are a few recommended resources:

9. Format String Vulnerability

A format string vulnerability is a bug where user input is passed as the format argument to printf, scanf, or another function in that family.

The format argument has many different specifies which could allow an attacker to leak data if they control the format argument to printf. Since printf and similar are variadic functions, they will continue popping data off of the stack according to the format.

For example, if we can make the format argument “%x.%x.%x.%x”, printf will pop off four stack values and print them in hexadecimal, potentially leaking sensitive information.

printf can also index to an arbitrary “argument” with the following syntax: “%n$x” (where n is the decimal index of the argument you want).

While these bugs are powerful, they’re very rare nowadays, as all modern compilers warn when printf is called with a non-constant string.

example

#include <stdio.h>
#include <unistd.h>

int main() {
    int secret_num = 0x8badf00d;

    char name[64] = {0};
    read(0, name, 64);
    printf("Hello ");
    printf(name);
    printf("! You'll never get my secret!\n");
    return 0;
}

Due to how GCC decided to lay out the stack, secret_num is actually at a lower address on the stack than name, so we only have to go to the 7th “argument” in printf to leak the secret:

$ ./fmt_string
%7$llx
Hello 8badf00d3ea43eef
! You'll never get my secret!

猜你喜欢

转载自blog.csdn.net/Ga4ra/article/details/91953332
101
ctf