Dreamhack/Dreamhack Wargame (Challenge)

[159] IT 비전공자 [dreamhack] p_rho문제풀기

imaginefuture-1 2025. 2. 18. 09:23

 

 

? 슬로베니아어요?

 

???
아 알고리즘 문제구나 ㅋㅋㅋㅋㅋㅋㅋㅋ근데 왜 pwanble..?

 


 

 

해설과 함께 이해하며 풀어보았다

https://velog.io/@rlajunwon/Dreamhack-prho

 

[Dreamhack] p_rho

main함수감사하게도 win함수가 존재한다간단하게 idx를 입력받고, 다음 반복문때 buf\[idx]의 값을 넣어주는 프로그램이다bof는 안 터질것 같았고, 다른 취약점이 존재할 것 같았다보면 바로 보인다Ou

velog.io


 

소스코드 분석

더보기

1. 프로그램 분석

이 프로그램은 반복문을 돌면서 숫자를 입력받고, 이를 배열(buf)에 저장하는 구조입니다.

  • idx (인덱스)를 입력받고,
  • buf[idx] = 사용자가 입력한 값;
  • 다시 idx = buf[idx];를 통해 idx가 업데이트됩니다.

이 과정을 무한히 반복하는 코드예요.


2. 버퍼 오버플로우 (BOF)는 터지지 않음

버퍼 오버플로우(BOF)는 배열 크기보다 더 많은 데이터를 넣을 때 발생하는 취약점이에요.
이 코드에서는 입력 크기를 직접 제한하지 않지만, 스택에서 실행되지 않기 때문에 전형적인 BOF는 발생하지 않습니다.

→ 즉, 스택을 덮어쓰는 공격(스택 오버플로우)은 어렵습니다.


3. 다른 취약점 발견!

하지만 다른 취약점이 존재합니다.
보면 바로 Out of Bounds(배열 범위를 벗어나는 취약점, OOB) 가 보입니다.

💡 OOB(Out of Bound) 취약점이란?
배열에서 정상적인 범위를 벗어난 위치를 읽거나 쓰는 것을 의미합니다.
예를 들어, 배열의 크기가 10인데 buf[100] 같은 잘못된 접근이 가능하다면 문제가 됩니다.

이 코드에서는 idx를 마음대로 조작할 수 있으므로,
→ buf의 정상적인 범위를 벗어난 주소에 접근할 수 있습니다!


4. 공격 시나리오 (어떻게 해킹할 수 있을까?)

💡 목표:
코드에서 win() 함수가 존재한다고 했습니다!
우리는 win() 함수를 실행하고 싶어요.

💡 방법:

  1. Negative Index (음수 인덱스) 활용
    • buf 배열의 주소는 0x404080입니다.
    • 이 프로그램은 idx를 음수로 입력해도 받아줍니다.
    • 즉, buf[-1], buf[-2] 처럼 이상한 위치도 덮어쓸 수 있습니다!
  2. printf 함수를 덮어쓰기 (aaw, Arbitrary Address Write)
    • 프로그램의 반복문에서는 printf() 함수를 호출합니다.
    • 우리가 printf()의 실행 주소를 덮어쓰면, printf() 대신 win() 함수를 실행하도록 조작할 수 있습니다.
  3. GDB로 분석하면…
    • buf의 주소는 0x404080 (힙 영역)
    • printf()의 실행 주소를 찾아서, 그 위치에 win() 함수의 주소를 덮어쓰기!

5. 최종 공격 방법

Negative Index(OOB)를 이용해서 printf()의 주소를 찾아 덮어쓴다.
win() 함수의 주소를 입력해서 덮어쓴다.
프로그램이 다음 printf()를 실행할 때, win()이 실행된다! 🎉


💡 결론

이 취약점은 Out of Bounds (배열 범위 벗어나기) + 임의 주소 쓰기(aaw, Arbitrary Address Write) 기법을 활용한 공격이에요!
즉, 스택 오버플로우 없이도, 배열의 특정 주소를 조작해서 실행 흐름을 변경할 수 있는 취약점입니다.

이제 실제 GDB를 사용해서 printf()의 주소를 찾아보면, 직접 공격을 시도할 수도 있어요! 🚀

win함수란?

더보기

💡 win() 함수란?

win() 함수는 개발자가 문제를 만들 때 의도적으로 숨겨놓은 함수일 가능성이 큽니다.
즉, 소스코드에서는 보이지 않지만 바이너리 파일 안에는 존재하는 함수일 수 있습니다.


📌 win() 함수의 역할

  1. 보통 CTF(해킹 대회) 문제에서 특정한 목표를 달성하면 실행되는 함수입니다.
  2. system("/bin/sh") 같은 쉘을 실행하는 코드일 가능성이 높습니다.
  3. 또는 특정 메시지를 출력하고 플래그(flag)를 제공하는 역할일 수도 있습니다.
  4. 개발자가 의도적으로 정답 루트를 만들기 위해 추가한 "숨겨진 비밀 함수"일 가능성이 큽니다.

💡 win() 함수 찾는 방법

소스코드에는 보이지 않지만, 바이너리 파일 내에는 존재할 수 있습니다.
이를 확인하는 방법은 여러 가지가 있습니다.


1️⃣ gdb로 확인하는 방법

바이너리를 gdb로 분석해서 win() 함수가 있는지 확인할 수 있습니다.

gdb ./prob
(gdb) info functions

📌 win()이라는 이름의 함수가 리스트에 있는지 확인하세요!

만약 win()이 존재하면:

0x401234  win

이런 식으로 주소가 보일 거예요.


2️⃣ objdump로 확인하는 방법

objdump 명령어를 사용해서 바이너리 내부의 함수 목록을 볼 수도 있습니다.

objdump -t ./prob | grep win

결과가 나온다면, win() 함수가 존재하는 것입니다!


3️⃣ strings로 확인하는 방법

바이너리 내부의 문자열을 검색해서 win 관련 문구가 있는지 찾아볼 수도 있습니다.

strings ./prob | grep win

여기서 win 관련 메시지가 보인다면, 어떤 역할을 하는 함수인지 유추할 수 있습니다.


💡 win()이 있다면, 어떻게 활용할까?

만약 win() 함수가 system("/bin/sh")을 실행하는 함수라면?
우리는 이 함수로 코드 실행 권한을 얻을 수 있습니다! 🚀

즉,
프로그램 실행 흐름을 win()으로 바꾸면 우리가 원하는 코드를 실행할 수 있다!

이제 해야 할 일은?

  1. win() 함수의 주소를 찾기 (gdb나 objdump 활용)
  2. 취약점을 이용해서 실행 흐름을 win()으로 바꾸기!

💡 결론

win() 함수는 개발자가 일부러 숨겨놓은 비밀 함수일 가능성이 높습니다.
이 함수가 하는 역할을 찾고, 실행하도록 조작하면 문제를 풀 수 있을 거예요! 🚀


용어 정리

 

OOB는 Out of Bounds 즉, 배열 범위를 벗어나는 취약점

BOF는 Bufer Over Flow 즉, 넘쳤다. 배열크기보다 더 많은 데이터를 넣을때 발생하는 취약점

Win()는 즉 개발자가 문제를 만들 때 의도적으로 숨겨놓은 함수

 


OOB 일상 예시

더보기

💡 OOB(Out of Bounds, 범위를 벗어난 접근) 일상 예시

OOB(Out of Bound)란 배열(리스트)의 정상적인 범위를 벗어난 위치에 접근하는 것을 의미합니다.
쉽게 말하면 없는 번호를 호출하거나, 존재하지 않는 공간에 데이터를 넣는 것과 같아요.


📌 일상 속 OOB 예시

1️⃣ 엘리베이터 층수 예시 🚪

  • 엘리베이터에는 1층 ~ 10층 버튼만 있는데,
  • 실수로 "13층"을 누르면? 🚨 → 잘못된 범위 접근 (OOB 오류 발생!)
  • 정상적인 층 범위를 벗어나서 오작동하거나 오류가 날 수 있음

2️⃣ 편의점 계산대 예시 💳

  • 편의점에서 **상품 목록(배열)**이 있음
    1. 삼각김밥
    2. 컵라면
    3. 초코우유
    
  • 하지만 계산할 때 점원이 "5번 초코칩 쿠키"를 입력
  • 그런데 실제로 5번 상품이 없으면? 🚨 → "상품 없음" 오류 발생!

3️⃣ 전화번호부 예시 📞

  • 스마트폰 연락처에서 1번 ~ 100번까지 저장된 연락처가 있음
  • 근데 갑자기 "101번 친구한테 전화 걸기"를 누르면?
  • 📢 "연락처 없음" 오류 발생 🚨 → 메모리에 없는 데이터를 불러오려고 시도했기 때문

4️⃣ 학교 출석부 예시 📚

  • 선생님이 1번~30번 학생 출석 체크
  • 그런데 40번 학생 부르면?
  • 📢 "40번 학생 없음" 🚨 → 리스트에 없는 번호를 호출한 것 (Out of Bounds)

5️⃣ TV 채널 예시 📺

  • 우리 집 TV는 1번~100번 채널까지만 지원
  • 그런데 리모컨으로 "150번 채널" 입력하면?
  • 🚨 "채널 없음" 오류 발생 → OOB! 정상적인 채널 번호가 아님

📌 OOB의 문제점

잘못된 데이터를 불러오거나, 오류가 발생함
특정 상황에서는 프로그램이 충돌(크래시)할 수도 있음
해커가 이를 이용하면 메모리를 조작할 수 있음 (해킹 취약점 발생!)


💡 OOB와 해킹의 관계

일상에서 OOB는 단순한 실수로 끝나지만,
컴퓨터 프로그램에서 OOB를 악용하면 보안 문제가 발생할 수 있음

  1. OOB로 중요한 정보(비밀번호, 주소 등) 탈취
  2. OOB를 이용해 프로그램을 조작하고 해킹 가능
  3. 메모리 영역을 조작해서 win() 함수 실행 가능!

🔥 결론

💡 "OOB는 원래 접근하면 안 되는 곳을 잘못 접근하는 것"
💡 "TV 채널, 엘리베이터 층수, 출석부처럼 리스트에 없는 데이터를 불러오면 OOB 발생"
💡 "해커들은 이를 이용해 보안 취약점을 공격함" 🚨

이제 OOB 개념이 확실히 이해됐을 거예요! 🚀

BOF 일상 예시

더보기

💡 BOF (버퍼 오버플로우, Buffer Overflow) 일상적인 예시

BOF(버퍼 오버플로우)는 정해진 공간(버퍼)보다 더 많은 데이터를 넣어서 넘쳐버리는 상황을 의미합니다.
쉽게 말하면 컵에 물을 너무 많이 부어서 넘치는 것과 같아요! ☕💦


📌 BOF(버퍼 오버플로우) 일상 예시

1️⃣ 컵에 물 따르기 예시 ☕

  • 300ml짜리 컵이 있음
  • 물을 500ml 부으면? 🚨 컵이 넘쳐서 흘러버림
  • 정해진 용량(버퍼)보다 더 많은 데이터를 넣어서 문제가 발생하는 상황!

2️⃣ 노트 필기 예시 📖

  • 공책 한 페이지에 10줄만 적을 수 있음
  • 그런데 20줄을 적으면?
  • 📢 "글씨가 공책 바깥으로 삐져나옴!" 🚨 (메모리 침범)

3️⃣ 엘리베이터 최대 탑승 인원 초과 🚪

  • 엘리베이터 최대 정원 10명
  • 그런데 15명이 타면? 🚨 과부하 & 경고음
  • 정해진 크기(버퍼)보다 더 많은 데이터를 넣으면 문제 발생

4️⃣ 비밀번호 입력칸 예시 🔑

  • 로그인할 때 최대 8자리 비밀번호 입력 가능
  • 그런데 "1234567890123" (13자리) 입력하면? 🚨
  • → 프로그램이 제대로 작동하지 않거나, 해킹 취약점 발생 가능!

5️⃣ 자동차 트렁크에 짐 싣기 🚗

  • 트렁크 용량이 50kg
  • 그런데 100kg 짐을 넣으면? 🚨 트렁크 고장!
  • 버퍼 크기보다 더 많은 데이터를 넣으면 시스템이 망가짐

💡 BOF가 위험한 이유?

  1. 프로그램이 비정상적으로 종료될 수 있음
  2. 메모리 구조를 바꾸고, 프로그램 실행 흐름을 조작할 수 있음
  3. 해커가 이를 악용하면 임의의 코드를 실행 가능! (Exploit 가능)

🔥 결론

💡 "BOF는 원래 정해진 공간보다 더 많은 데이터를 넣어서 넘쳐버리는 것"
💡 "컵에 물을 너무 많이 따르거나, 엘리베이터 정원을 초과하는 것처럼 생각하면 이해하기 쉬움"
💡 "해킹에서는 BOF를 악용해서 프로그램을 조작하고, 원하는 코드를 실행할 수 있음" 🚀

이제 BOF 개념이 확실히 이해됐을 거예요! 😎🔥

 

보호기법 일상 예시

더보기

💡 보호 기법(Exploit Mitigation) 일상 예시

프로그램이 해킹당하지 않도록 보호하는 여러 가지 기법들이 있습니다.
이런 보호 기법을 일상적인 예시로 쉽게 설명해볼게요! 🚀


🔥 주요 보호 기법 & 일상적인 비유 🔥

1️⃣ Stack Canary 🦜 (스택 카나리)

💡 "음식에 독이 들었는지 확인하는 왕"

  • 옛날 왕들은 음식이 안전한지 확인하기 위해 **음식을 먼저 먹는 하인(카나리)**을 둠.
  • 만약 하인이 죽으면? 음식에 독이 들었다는 걸 알아차리고 왕은 안 먹음!
  • BOF(버퍼 오버플로우) 공격을 방지하기 위해, 스택에 랜덤한 값(Canary)을 넣어놓고 변조 여부를 체크하는 원리!

📌 즉, 해커가 스택을 조작하려고 하면 Canary 값이 변하기 때문에, 프로그램이 이를 감지하고 종료함!

🔹 일상 예시:
음식 독 감별사 → 스택 카나리 (변조 감지!)
은행 금고 경보 장치 → 해킹 감지 후 즉시 종료!


2️⃣ NX (No eXecute) 🚫 (스택 실행 방지)

💡 "시험 볼 때 커닝 방지!"

  • 시험 중에 선생님이 "책상 위에 책 펼쳐놓으면 안 됩니다!" 라고 함.
  • 만약 학생이 책을 펼쳐서 커닝하려 하면? 선생님이 바로 적발해서 시험 무효 처리!
  • NX 보호 기법은 메모리의 특정 영역(스택 등)에서 코드를 실행하는 것을 막아서 해킹을 방지하는 원리!

📌 즉, 해커가 쉘코드를 넣어도 실행이 안 됨!

🔹 일상 예시:
시험 중 커닝 금지 → NX (스택에서 코드 실행 금지!)
엘리베이터 버튼이 특정 층에서는 작동안 함 → 메모리에서 실행 금지!


3️⃣ RELRO (Relocation Read-Only) 🔒 (GOT 보호)

💡 "ATM 기기의 고정된 계좌 연결!"

  • ATM 기기가 은행 서버와 연결되는 고정된 주소를 사용하도록 설정됨.
  • 만약 해커가 ATM이 연결된 은행 주소를 바꾸려 하면? 🚨 "읽기 전용(Read-Only)"이기 때문에 변경 불가능!
  • RELRO는 Global Offset Table(GOT)을 Read-Only로 만들어 해커가 악성 코드 주소로 바꾸는 것을 막음!

📌 즉, 해커가 GOT를 변조하는 걸 막아서 공격을 방어함!

🔹 일상 예시:
ATM → 고정된 계좌 연결 (변경 불가!)
USB의 "읽기 전용" 모드 → RELRO (데이터 변조 불가!)


4️⃣ PIE (Position Independent Executable) 🏠 (코드 위치 랜덤화)

💡 "집 주소를 매일 바꾸기!"

  • 해커가 도둑질을 하려고 매일 밤 특정 집(프로그램 코드 위치)을 털러 감.
  • 하지만 집 주소가 매일 바뀌면? 도둑이 특정 집을 찾을 수 없음!
  • PIE는 실행 파일의 메모리 주소를 매번 랜덤하게 바꿔서 해킹을 어렵게 만드는 보호 기법!

📌 즉, 프로그램의 코드 위치가 매번 바뀌기 때문에 ROP 공격이 어려워짐!

🔹 일상 예시:
집 주소 매일 바꾸기 → PIE (코드 위치 랜덤화!)
매일 자리 배치 바꾸기 → 프로그램 실행할 때마다 주소 변경!


5️⃣ ASLR (Address Space Layout Randomization) 🎲 (메모리 주소 랜덤화)

💡 "미로 게임에서 출구 위치를 매번 바꾸기!"

  • 미로 게임을 하는데, 출구가 항상 같은 위치에 있으면? 쉽게 탈출 가능!
  • 하지만 출구 위치가 매번 랜덤하게 바뀌면? 탈출이 어려워짐!
  • ASLR은 프로그램의 메모리 주소(스택, 힙, 라이브러리 등)를 매번 랜덤하게 바꿔서 해킹을 어렵게 만듦!

📌 즉, 해커가 특정 주소를 노려서 공격하는 걸 어렵게 만듦!

🔹 일상 예시:
미로 출구 랜덤 배치 → ASLR (메모리 주소 랜덤화!)
랜덤 좌석 배치 → 특정 주소를 기억하기 어렵게 만듦!


📌 보호 기법 정리

보호 기법 설명 일상 비유
Stack Canary 🦜 스택 변조 감지 독 감별사 (독이 들면 죽음)
NX (No eXecute) 🚫 스택에서 코드 실행 방지 시험 중 커닝 금지
RELRO (Read-Only GOT) 🔒 중요한 주소 변조 방지 ATM의 고정된 계좌 연결
PIE (코드 위치 랜덤화) 🏠 실행 파일의 코드 위치 변경 집 주소를 매일 바꾸기
ASLR (메모리 주소 랜덤화) 🎲 스택, 힙, 라이브러리 주소 랜덤화 미로 출구 위치를 매번 변경

💡 결론

🔹 "해킹을 방지하는 보호 기법들은, 일상에서도 흔히 볼 수 있는 보안 방식과 비슷하다!"
🔹 "Canary, NX, RELRO, PIE, ASLR을 이해하면 해킹과 보안 개념이 더 쉽게 와닿을 것!"

💡 이제 보호 기법이 더 친숙하게 느껴지죠? 😆🔥 🚀


프로그램을 실행하니 VAL..VAL...무한으로 입력들어간다
보호기법은 사진과 같다. NO PIE 이기에 고정되어있는 주소를 사용한다!
DOCKERFILE을 통해 환경설정을 환인해보니, FLAG파일은 /home/pwn/flag에 있을 것으로 보인다

Dockerfile 설명

더보기

이 Dockerfile은 socat을 사용하여 특정 바이너리 (./prob)를 실행하는 컨테이너 환경을 구축합니다. 각 줄을 하나씩 살펴볼게요.


1. 기본 이미지 설정

FROM ubuntu:24.04@sha256:c920ba4cfca05503764b785c16b76d43c83a6df8d1ab107e7e6610000d94315c
  • ubuntu:24.04 이미지를 기반으로 컨테이너를 생성합니다.
  • 특정 sha256 해시를 사용하여 정확한 이미지 버전을 고정합니다. (보안 및 일관성 유지 목적)

2. 필수 패키지 설치

RUN apt-get update && apt-get install -y socat adduser
  • apt-get update : 패키지 목록을 업데이트합니다.
  • apt-get install -y socat adduser : socat과 adduser 패키지를 설치합니다.
    • socat: 네트워크 소켓과 프로세스를 연결하는 도구 (서버 역할을 수행)
    • adduser: 새로운 사용자 추가를 위한 명령어

3. 사용자 추가

RUN adduser pwn
  • pwn이라는 새 사용자를 생성합니다.
  • 보안상의 이유로 루트 계정 대신 pwn 사용자로 실행하기 위한 조치입니다.

4. 작업 디렉토리 설정

WORKDIR /home/pwn
  • WORKDIR을 /home/pwn으로 설정하여 이후의 명령들이 해당 디렉토리에서 실행되도록 합니다.

5. 파일 복사

COPY ./flag ./flag
COPY ./deploy/prob ./prob
  • ./flag 파일을 컨테이너 내 /home/pwn/flag로 복사합니다.
  • ./deploy/prob 파일을 컨테이너 내 /home/pwn/prob로 복사합니다.

6. 파일 권한 설정

RUN chmod 644 ./flag 
RUN chmod 755 ./prob
  • chmod 644 ./flag : flag 파일의 권한을 rw-r--r--로 설정
    • 소유자(pwn)는 읽기(r), 쓰기(w), 다른 유저는 읽기(r)만 가능
    • 보안상 플래그 파일이 실행되거나 수정되지 않도록 설정
  • chmod 755 ./prob : prob 파일의 권한을 rwxr-xr-x로 설정
    • 모든 사용자가 실행 가능 (x)

7. 사용자 변경

USER pwn
  • 컨테이너에서 실행되는 모든 명령을 pwn 사용자 계정으로 수행하도록 변경
  • 루트 권한을 제한하여 보안을 강화

8. 포트 개방

EXPOSE 8080
  • 컨테이너의 8080번 포트를 외부에서 접근할 수 있도록 개방
  • 단순히 포트를 노출할 뿐, -p 8080:8080 등의 옵션이 필요할 수도 있음

9. 실행 명령어

CMD socat TCP-LISTEN:8080,reuseaddr,fork EXEC:./prob,stderr
  • socat을 사용하여 네트워크 연결을 ./prob 프로그램과 연결
  • 옵션 설명:
    • TCP-LISTEN:8080 : 8080 포트에서 TCP 연결을 대기
    • reuseaddr : 재사용 가능한 주소 설정 (바로 재실행 가능)
    • fork : 연결이 들어올 때마다 새로운 프로세스를 생성하여 실행
    • EXEC:./prob,stderr : 들어온 요청을 ./prob 실행 파일과 연결하고, stderr(표준 오류)도 출력

결론

  • 이 Dockerfile은 리눅스 기반 바이너리 문제(prob)를 제공하는 서버를 구축합니다.
  • socat을 이용해 8080 포트에서 연결을 받아 ./prob 실행 파일과 연결합니다.
  • pwn이라는 별도의 유저를 생성하여 실행하는 등 보안적으로 신경을 쓴 구조입니다.

이제 docker build와 docker run을 하면 문제 서버가 실행될 거예요. 🚀

소스코드다,for ;;로 무한 루프이다.

 

왜 이소스코드는 OOB 취약점이 발생할까?

더보기

💡 주어진 소스코드 분석 (왜 OOB 취약점이 발생하는가?)

int __fastcall __noreturn main(int argc, const char **argv, const char **envp) 
{
  __int64 v3; // [rsp+8h] [rbp-18h] BYREF
  __int64 i; // [rsp+10h] [rbp-10h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  
  for ( i = 0LL; ; i = buf[i] ) // 🔥 반복문 (OOB 발생 가능!)
  {
    printf("val: ");
    __isoc99_scanf("%lu", &v3);
    buf[i] = v3; // 🔥 OOB 발생 지점
  }
}

1️⃣ OOB(Out of Bounds) 취약점이 발생하는 이유

💡 주요 원인: buf[i] = v3;에서 i 값을 마음대로 조작할 수 있기 때문!

🔹 배열을 인덱싱할 때 범위 검사를 안 함!
🔹 i 값이 초기에는 0이지만, 반복문이 돌면서 buf[i]의 값을 따라감.
🔹 사용자가 i의 값을 조작해서 배열 범위를 벗어난 주소(buf[-1], buf[1000])에도 접근할 수 있음!

즉, i 값을 음수(-1, -2...)나 아주 큰 값(1000, 9999...)으로 조작하면 OOB 취약점이 발생! 🚨


2️⃣ OOB 취약점이 발생하는 코드 흐름

  1. 처음 실행될 때:
    • i = 0 → buf[0] = v3; (정상적으로 저장)
  2. 다음 반복문 실행 시:
    • i = buf[0] (사용자가 입력한 값)
    • 즉, 사용자가 buf[0]에 9999를 넣으면, i = 9999가 됨!
    • 그러면 다음 입력이 buf[9999] = v3;로 저장됨 → OOB 발생! 🚨
  3. 음수 인덱스를 입력할 수도 있음
    • 사용자가 buf[0] = -1을 넣으면?
    • i = -1이 되고, buf[-1]을 덮어쓰게 됨.
    • 메모리 보호가 없으면 GOT, 함수 포인터 등의 중요한 데이터를 덮어쓸 수 있음!

3️⃣ OOB를 악용해서 해킹하는 방법 (Exploit)

💡 이 코드에서는 win() 함수가 존재하므로, OOB를 활용해서 실행 흐름을 바꿀 수 있음!

📌 공격 시나리오

  1. i 값을 음수 또는 매우 큰 값으로 조작
    • buf[-1] 같은 중요한 메모리 주소를 덮어쓸 수 있음.
  2. printf() 함수의 실행 주소를 찾아서 덮어쓰기
    • printf()를 실행할 때, win() 주소로 실행되도록 변경!
  3. 다음번 printf() 실행 시 win() 함수가 실행됨 → 🚀 해킹 성공!

4️⃣ OOB 방어 방법

이런 취약점을 막기 위해서는 배열의 접근 범위를 검사해야 함!
📌 해결 방법

if (i < 0 || i >= BUF_SIZE) {
    printf("Out of bounds access!\n");
    exit(1);
}
buf[i] = v3;

이렇게 범위를 체크하면 OOB 발생을 방지할 수 있음!


🔥 결론

📌 이 코드에서는 buf[i] = v3;에서 i 값을 사용자가 조작할 수 있기 때문에 OOB 발생!
📌 i 값을 음수나 큰 값으로 설정하면 배열 밖의 메모리를 덮어쓸 수 있음 → 해킹 가능! 🚀
📌 이를 이용해 함수 실행 흐름을 조작하면 win() 함수 실행 가능! 😎🔥

WIN() 함수 소스코드다 /bin/sh를 실행하는 백도어 역할을 하는 함수로 보인다!!

win()함수 소스코드 설명

## **💡 `win()` 함수 분석 및 설명**

### **📌 코드 분석**
```c
unsigned __int64 win()
{
  char *argv[3]; // [rsp+0h] [rbp-20h] BYREF
  unsigned __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = __readfsqword(0x28u); // 스택 카나리 값 저장 (보안 체크)
  argv[0] = "/bin/sh";  // 첫 번째 인자: 실행할 프로그램 (쉘)
  argv[1] = 0LL;        // 두 번째 인자: NULL (인자가 없음)
  execve("/bin/sh", argv, 0LL); // `/bin/sh` 실행 (쉘을 띄움)
  return v2 - __readfsqword(0x28u); // 스택 카나리 체크 후 반환
}
```

---

## **1️⃣ `win()` 함수가 하는 일**
이 함수는 **쉘(`/bin/sh`)을 실행하는 코드**입니다.  
즉, **이 함수가 실행되면 공격자가 시스템에 접근할 수 있게 됩니다!** 🚨  

---

## **2️⃣ 코드 동작 원리**
1. **스택 카나리 저장**
   ```c
   v2 = __readfsqword(0x28u);
   ```
   - `__readfsqword(0x28u)`는 **FS 레지스터의 특정 위치에서 값을 읽음** (Canary 보호 기법)
   - 보안 체크를 위해 **현재 스택의 Canary 값을 저장**함.

2. **쉘 실행을 위한 명령어 설정**
   ```c
   argv[0] = "/bin/sh";  
   argv[1] = 0LL;
   ```
   - `argv[0]`에 **실행할 프로그램 경로(`/bin/sh`)**를 설정.  
   - `argv[1] = NULL` → 실행 인자 없음.

3. **execve()를 이용해 쉘 실행**
   ```c
   execve("/bin/sh", argv, 0LL);
   ```
   - `execve()`는 현재 프로세스를 **새로운 프로세스로 교체**하는 시스템 호출.
   - **즉, 현재 실행 중인 프로그램을 `/bin/sh`로 바꿔버림!**
   - 공격자가 **쉘을 직접 조작할 수 있는 환경을 제공함!** 😈🔥

4. **스택 카나리 체크**
   ```c
   return v2 - __readfsqword(0x28u);
   ```
   - 실행이 정상적으로 끝난다면, **스택 카나리 값이 변조되었는지 검사**.
   - Canary 값이 변경되었으면 **프로그램이 크래시(오류) 발생!**  

---

## **3️⃣ 이 함수가 존재하면 왜 위험한가?**
📌 **이 함수가 실행되면 공격자는 `/bin/sh` 쉘을 실행해서 시스템 명령어를 입력할 수 있음!**  
📌 **즉, 공격자가 `win()` 함수를 실행할 수만 있다면, 시스템을 해킹할 수 있음!**  

✅ **공격 목표:**  
- OOB(Out of Bound) 취약점을 이용해 프로그램의 실행 흐름을 **`win()` 함수로 변경**.
- 그러면 프로그램이 `win()`을 실행하면서 쉘을 띄우게 됨.
- 공격자는 쉘에서 **임의의 명령어를 실행하여 시스템을 제어 가능!** 😈🔥  

---

## **4️⃣ 해킹 과정 (Exploit)**
1️⃣ **OOB 취약점을 이용해서 특정 메모리 주소를 덮어쓰기**  
2️⃣ **GOT(전역 오프셋 테이블) 또는 `printf()` 등의 함수 포인터를 `win()` 주소로 변경**  
3️⃣ **프로그램이 `printf()` 실행 시, `win()`을 실행하도록 유도**  
4️⃣ **`win()` 실행 후 `/bin/sh`가 열리면 해킹 성공!** 🎉  

---

## **🔥 결론**
- `win()` 함수는 **`/bin/sh`를 실행하는 "백도어" 역할을 하는 함수**!  
- OOB 취약점을 이용해서 **프로그램 실행 흐름을 `win()`으로 바꾸면 해킹 가능!**  
- 즉, 이 문제는 **"win() 실행 = 해킹 성공"**이므로, 실행 흐름을 조작하는 것이 목표 🚀😎

 

 

buf의 주소가 보인다 0x404080
printf()의 주소는 0x404008

 

OOB 취약점을 이용해

buf에 잘못된 영역(oob) 위치에 win()주소를 넣는데, 사실

 printf()함수에 win()함수 주소를 넣어 printf()실행할때 실제로는 win이 실행하도록 조작하면 짜자잔 플래그가 나오겠죠?

 

예를들어

buf 엘베가 10층까지있는데, 13층을 눌러서, 거기 13층에 win함수 주소를 넣는거고, 그 win함수 주소가 사실은 printf 함수 주소를 실제로 넣는다는 얘기

더보기

💡 네! 거의 정확하게 이해하셨습니다!

일상적인 엘리베이터 예시를 이용해서 다시 정리해볼게요. 🚀


📌 OOB 공격을 이용한 GOT 오버라이드 (일상 비유)

🔹 일반적인 상황 (정상적인 프로그램 동작)

  • 엘리베이터(buf 배열)에는 1층~10층(정상적인 배열 인덱스)까지만 있음.
  • 버튼을 눌러서 원하는 층에 갈 수 있음.
  • printf() 함수는 5층에 있고, win() 함수는 13층에 있다고 가정하자.
buf[0] = 1층
buf[1] = 2층
buf[2] = 3층
...
buf[9] = 10층

🔹 해커의 공격 (OOB 취약점 악용)

📌 버튼을 13층(OOB)까지 입력하면?

  • 프로그램이 원래 허용하지 않은 메모리 영역을 조작할 수 있음.
  • 즉, buf[13] 위치(잘못된 영역)에 win() 함수의 주소를 넣을 수 있음!
  • 근데 사실 그 13층(buf[13])이 printf()의 GOT 테이블 주소였음!! 🚨

📌 공격 흐름 정리

💡 즉, 우리가 13층(OOB 위치)에 win()의 주소를 넣는다는 건...
💡 "13층이 사실은 printf()의 주소가 저장된 곳(GOT 테이블)이라는 뜻!"

📌 결과적으로 printf()를 실행할 때, win()이 실행되도록 변조하는 것! 😎🔥


📌 한 줄 요약

OOB 취약점을 이용해 printf()의 GOT를 덮어쓰기
printf()의 원래 주소 대신 win() 함수의 주소를 저장
프로그램이 printf()를 실행할 때, 실제로는 win()이 실행됨
결국 win()에서 /bin/sh을 실행 → 해킹 성공! 🚀🎉

이제 OOB + GOT Overwrite 공격의 개념을 완벽하게 이해하신 거예요! 😆🔥

 


해설 중 의문 두가지 발견

 


1. 근데 풀이에서는 갑자기 buf주소와 printf 함수 주소 차이를 계산하다..왜지?
그냥 13층 주소에 넣으면 되는거아니여?

더보기

💡 buf의 주소와 printf()의 주소 차이를 계산하는 이유?

네, 질문이 아주 날카롭네요! 🚀
단순히 "buf[13]에 win()의 주소를 바로 넣으면 되는 거 아니야?" 라고 생각할 수 있지만,
보통 그렇게 하면 제대로 원하는 주소를 덮어쓸 수 없는 경우가 많습니다!

이제 왜 주소 차이를 계산하는지를 설명해볼게요.


📌 1️⃣ "buf[13]에 win() 주소를 바로 넣으면 안 되나?"

💡 일반적인 메모리 구조

  • buf는 배열이므로, 특정한 규칙에 따라 메모리에 저장됨.
  • printf()의 GOT 테이블 주소는 메모리 어딘가에 따로 존재함.
  • buf[13]이 정확히 printf()의 GOT 주소 위치에 대응된다고 보장할 수 없음.

📌 즉, "13층에 버튼을 누른다고 꼭 내가 원하는 printf() 주소를 덮어쓰는 게 아닐 수도 있음!" 🚨
📌 메모리의 실제 배치를 고려해야 정확한 공격이 가능함!


📌 2️⃣ 메모리 주소 차이를 계산하는 이유

우리가 정확히 printf()의 GOT 주소를 덮어쓰려면,
"buf 배열이 시작되는 주소"와 "GOT 테이블의 printf() 주소" 간의 거리(바이트 차이)를 알아야 함!"

🔹 일상적인 비유

  • 엘리베이터 버튼을 누르는 것이 아니라, 직접 수동으로 층 수를 조작하는 상황을 상상해보자!
  • 우리가 13층을 가려고 하지만, 엘리베이터 시스템 내부에서 버튼 위치와 실제 층의 위치가 다를 수도 있음.
  • 따라서 "현재 내 위치(buf 시작 주소)" 와 "13층의 실제 위치(printf의 GOT 주소)" 간의 거리를 계산해서 조작해야 함.

📌 즉, 단순히 "13층에 주소를 넣는다"는 방식보다는, "실제 printf() GOT 주소를 정확히 겨냥해서 조작하는 것"이 더 확실함!


📌 3️⃣ 실제 공격 과정에서의 주소 계산

1️⃣ buf의 시작 주소를 확인 (0x404080 예시)
2️⃣ printf()의 GOT 주소를 확인 (0x404090 예시)
3️⃣ 두 주소의 차이를 계산 (0x404090 - 0x404080 = 16(0x10) 바이트 차이)
4️⃣ 이 차이를 활용해 buf[16/8] 위치에 win()의 주소를 넣음!


📌 4️⃣ 결론

✅ "그냥 13층에 주소를 넣으면 안 되는 이유"
→ buf[13]이 printf()의 GOT 테이블 주소와 정확히 일치한다는 보장이 없음! 🚨

✅ "주소 차이를 계산하는 이유"
→ 정확한 GOT 테이블 주소를 찾아서 win() 주소로 덮어쓰기 위해!

✅ "메모리 구조를 이해해야 성공적인 익스플로잇 가능!"
→ 단순한 직관적 접근보다는 메모리의 실제 배치를 분석해서 조작하는 것이 중요! 🚀🔥

이제 진짜 해킹 개념을 완전히 이해하셨네요! 😎🔥


 


2. 해설에서는 8개씩 끊어서 넣어야한다는데 왜지?

### **💡 왜 8개씩 끊어서 넣어야 할까? (배열의 크기 단위와 정렬 문제)**  

---

## **📌 1️⃣ 프로그램이 데이터를 저장하는 방식 (메모리 정렬)**
컴퓨터가 데이터를 메모리에 저장할 때, **자료형(데이터 타입)**에 따라 저장 크기가 결정됩니다.

💡 **이 코드에서 `buf`는 `__int64` (8바이트 정수, `long long`)로 선언되었을 가능성이 큼!**  
즉, `buf[i]`의 단위는 **1바이트가 아니라 8바이트(64비트)** 라는 뜻입니다.

📌 **즉, buf 배열은 "8바이트 간격"으로 정렬되어 있음!**  

---

## **📌 2️⃣ 메모리에서 `buf` 배열이 저장되는 방식**
### **🔹 메모리에서 `buf` 배열이 차지하는 공간 예시**
(각 칸은 8바이트 크기)  

| 인덱스 (i) | 실제 메모리 주소 |  
|------------|----------------|  
| `buf[0]`   | `0x404080`      |  
| `buf[1]`   | `0x404088`      |  
| `buf[2]`   | `0x404090`      |  
| `buf[3]`   | `0x404098`      |  
| ...        | ...            |  
| `buf[15]`  | `0x404100`      |  

💡 **배열의 각 요소가 "8바이트 단위"로 증가함!**  
💡 **즉, `buf[i]`는 `0x404080 + (i * 8)`의 주소에 저장됨.**  

---

## **📌 3️⃣ "120만큼 떨어져 있다"는 의미**
### **❓ `buf`의 시작 주소에서 `printf()`의 GOT까지 거리가 120바이트라면?**  
📌 **120바이트를 이동해야 `printf()`의 GOT를 덮어쓸 수 있음!**  

---

## **📌 4️⃣ 왜 `idx = -15`를 넣어야 할까?**
💡 **`buf[i]`가 8바이트 단위이므로, 120바이트 차이를 만드려면 몇 개의 `buf` 칸을 이동해야 할까?**  

✅ **120바이트 ÷ 8바이트 = 15칸 이동해야 함!**  
✅ **즉, `buf[-15]`가 `printf()` GOT를 덮어쓸 위치가 됨!**  

📌 **그렇기 때문에 `idx = -15`를 넣어서 정확한 메모리 위치를 조작해야 함!** 🚀  

---

## **📌 5️⃣ 결론**
✅ **"왜 8개씩 끊어서 넣어야 하는가?"**  
→ `buf` 배열이 **8바이트(`__int64`) 단위로 정렬되어 있기 때문!**  

✅ **"120바이트 떨어져 있다고 왜 `-120`이 아니라 `-15`를 넣어야 하는가?"**  
→ `buf`의 각 요소는 8바이트 단위로 증가하므로, **120바이트 이동하려면 15칸(-15)을 이동해야 정확한 위치에 도달!**  

---

### **💡 핵심 요약**
📌 **배열이 8바이트 단위로 저장됨** → **배열의 인덱스(칸) 기준으로 이동해야 함**  
📌 **120바이트 떨어져 있다면, `120 ÷ 8 = 15칸(-15)` 이동해야 정확한 위치를 덮어쓸 수 있음**  
📌 **따라서 `idx = -15`를 사용해야 제대로 GOT를 덮어쓸 수 있음!** 🚀🔥  

이제 메모리 구조와 주소 계산이 완벽하게 이해되셨을 거예요! 😆🎉​


캬 이제 진짜 문제 풀러가보자

from pwn import *

# Dreamhack 서버 설정
host = "host1.dreamhack.games"
port = 21784

# 원격 서버에 연결
p = remote(host, port)

# ELF 로드
elf = ELF('./prob')

# 필요한 주소들
win_addr = elf.symbols['win']
printf_got = elf.got['printf']

# 정확한 offset 사용 (-15)
p.sendlineafter(b"val: ", str(-15))  # OOB 인덱스
p.sendlineafter(b"val: ", str(int(win_addr)))  # printf GOT를 win()으로 덮어쓰기

# 쉘 획득
p.interactive()

 


DH{26d70156c74deb1d205afd3883a184382337af3ece964d81899bb528e3dbfed2}$