字符串操作下的Pwn

什么是字符串

C风格的字符串是指由一个连续的字符序列组成,并且以一个空字符(null)作为结束。一个指向字符串的指针实际上就是指向该字符串的起始。字符串长度指的是空字符之前的字节数。

常见操作错误

无边界字符串复制

我们常见的缓冲区溢出一半就会发生在这里。

无边际字符串复制,实际上是读取数据,复制到一个定长定缓冲区中。如我们常见定栈溢出:

1
2
3
4
5
int mian(){
char buf[32];
puts("Enter your Name:\n");
gets(buf);
}

这就是常见的无边界字符串复制,gets这个位置是一个很明显的栈溢出点。除了gets ,还有strcpy(),strcat()等等函数。

差一错误

其实差一错误,是我们通常所说等off-by-one。。看几个例子:
my_gets 函数导致了一个off-by-one漏洞,原因是for循环的边界没有控制好导致写入多执行了一次,这也被称为栅栏错误

1
2
3
4
5
6
7
8
9
10
int my_gets(char *ptr,int size)
{
int i;
for(i=0;i<=size;i++)
{
ptr[i]=getchar();
}
return i;

}

my_gets 函数导致了一个off-by-one漏洞,原因是for循环的边界没有控制好导致写入多执行了一次,这也被称为栅栏错误
off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括

  • 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节。
  • 字符串操作不合适

    空结尾错误

    这种又属于 off-by-one 的一个分支称为 NULL byte off-by-one
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int main(void)
    {
    char buffer[40]="";
    void *chunk1;
    chunk1=malloc(24);
    puts("Get Input");
    gets(buffer);
    if(strlen(buffer)==24)
    {
    strcpy(chunk1,buffer);
    }
    return 0;

    }

程序乍看上去没有任何问题(不考虑栈溢出),可能很多人在实际的代码中也是这样写的。 但是 strlen 和 strcpy 的行为不一致却导致了off-by-one 的发生。 strlen 是我们很熟悉的计算 ascii 字符串长度的函数,这个函数在计算字符串长度时是不把结束符 \x00 计算在内的,但是 strcpy 在复制字符串时会拷贝结束符 \x00。这就导致了我们向chunk1中写入了25个字节.

off-one-by 在堆上的利用

一般来说,单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制 ptmalloc 验证的松散性,基于Linux堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。 此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based)的off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况。

demo

Asis CTF 2016的一道题目,考察点是null byte off-by-one

分析

题目漏洞主要是在create中myread里,read函数可以发现对于边界的考虑是不当的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
signed __int64 __fastcall myread_9F5(_BYTE *ptr_buf, int size)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( size <= 0 )
return 0LL;
buf = ptr_buf;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == 10 )
break;
++buf;
if ( i == size )
break;
}
*buf = 0;
return 0LL;

利用

保护开启状况如下:

1
2
3
4
5
6
7
➜  off-one-by checksec b00ks
[*] '/media/psf/Home/MyCTF/ctf-wiki/off-one-by/b00ks'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

为了方便调试,我将 系统随机化关闭,那么之后程序基址将保持不变。