주요정보통신기밥 웹 취약 분석 평가 방법 두 번째 항목인 포맷 스트링(FS)이다.
버퍼 오버플로우도 그렇고 포맷 스트링도 그렇고 웹 보안 취약점으로 나오기도 하지만 실상은 시스템 보안 쪽에 가까운 느낌이 많이 난다. 처음 학습할 때도 시스템 해킹 과목에서 들었던 것 같고..
그리고 이 취약점이 앞서 공부했던 버퍼 오버플로우보다도 더욱 발생률이 저조하다,,,
오버플로우 공격은 오래 전부터 알려지기 시작하여 여러 형태로 발전하면서 굉장히 위협적인 공격으로 변모하였고 인식되고 있으나 포맷 스트링은 발견 이후로 많은 시간이 흘렀으나 크게 발전된 형태 없이 남아있다.
뭐, 그렇다고 공부 중요도가 떨어지겠네 하고 안하는 마인드로 가면 보안 측면에선 좋진 않을 것 같다. 방심할 수 있는 곳에서 항상 터지는게 보안 문제니까.. 포맷 스트링도 그렇고 오버플로우 공격도 그렇고 난이도도 상당한 데다가 발생하기라도 하면 크게 영향을 미칠 수 있는 공격이다.
서론이 길었는데 포맷 스트링이 무엇인지 알고 가보자. 포맷 스트링은 printf, scanf 등의 함수들로부터 다양한 형태로 값을 입력/출력할 수 있게 도와준다.
매개변수 | 형식 |
%d | 정수형 10진수 상수 |
%f | 실수형 상수 |
%c | 문자 값 |
%s | 문자 스트링 |
%x | 양의 정수(16진수) |
%n | 쓰인 총 바이트 수 |
위의 예시들이 대표적인 포맷 스트링들이다. 그런데 이 포맷 스트링을 인자로 사용하는 함수들이 포맷 스트링이 필요로 하는 인자의 개수와 실제로 받는 인자의 개수를 비교하지 않아서 문제가 생긴다. 이 포맷 스트링이 레지스터나 스택에서 그 값을 읽어오기 때문에 이러한 개수 검증이 없는 함수들을 이용하여 원하는 특정 위치를 읽어오거나 변조할 수 있는 공격을 포맷 스트링 버그 혹은 포맷 스트링 공격이라고 한다.
어 그런데 원하는 위치의 메모리를 읽어오는 것은 러프하게 이해를 해보겠는데 포맷 스트링으로 변조는 어떻게 하는거지?? 이는 %n이 %n을 받는 인자에 방금 쓰인 바이트 수를 저장해주기 때문에 이를 이용하여 변조를 할 수 있게 되는 것이다.
조금씩 실습해가면서 보면 이해가 쉬울 것 같다! 애초에 조금은 난해할 수는 있음 ㅠ.ㅠ
%n을 알아보기 위해 작성한 아주아주 조악한 코드이다. 여기서 9번 라인이 FSB 위험이 있는 코드다.
printf("%s\n", buf);
미리 결론부터 말하자면 위와 같이 입력 형태를 지정해주어야 안전하다. 그리고 저렇게 뒤에 &i를 붙일 필요가 없긴 하다. 단순히 %n을 알아보기 위한 코드라고 생각하면 된다.
처음 i의 값은 0으로 초기화했지만 사용자가 12345와 %n을 추가하여 입력해 주었더니 i의 값이 5가 되었다. 여기서 확인해야 할 두가지는 사용자가 %n을 추가하여 입력했더니 앞에 사용된 5 바이트를 i에 저장해준다 그리고 입력 형태를 지정하지 않고 사용하니 %n이 문자열이 아닌 포맷 스트링으로 인식하여 fsb 공격이 가능하구나 이렇게 알고 있으면 된다.
포맷 스트링으로 특정 위치의 메모리 읽어오기
포맷 스트링으로 하는 읽어오는 공격이다. 포맷 스트링을 이용하여 허용되지 않은 곳의 메모리를 참조하여 정보를 탈취하는 것을 예시로 들 수 있을 것 같다. 변조는 수행하지 않는 수동적 공격.
#include <stdio.h>
char *secret = "TOP SECRET!!";
int main(){
char *addr = secret;
char buf[0x100];
printf("input >>");
scanf("%s", buf);
printf(buf);
puts("");
return 0;
}
buf 함수로부터 secret 변수에 저장된 TOP SECRET을 읽어오는 것이 목적!
메인 함수를 디스어셈블링 한 모습이다. 어셈블리 코드를 보면 secret 변수가 0x601048 주소에 저장되어 있다.
그럼 0x601048 주소로 접근하여 FSB를 이용해 %s로 불러내면 secret 변수 안의 내용을 볼 수 있겠네???
printf(buf);에 브레이크를 건다. 여기에 브레이크를 거는 이유는 저 라인이 fsb 취약점이 있다. 저기에 우리가 입력할 공격 벡터가 삽입될 것이고 공격 벡터가 작동하더라도 만약 스택에 우리의 목표물이 있지 않다면 결과가 나오지 않을 것이기 때문에 printf가 실행되기 전 스택을 열어보기 위해 브레이크를 거는 것이다.
아오 눈 아파 스택 포인터(rsp)의 메모리를 50개 열었다. 여기서 601048 주소가 보이나?? 전혀 보이지 않는다.
그럼 메모리에 올라와있지 않으므로 secret 변수를 공격할 수가 없지만! secret 변수는 포인터 변수이다. 즉 다른 주소를 가리키고 있다는 소리
secret 변수의 내용은 0x4006f0에 저장이 되어 있던 것이다. 다시 위의 메모리를 보면 34번째에 0x00-04006f0 주소가 보이게 된다.
좋다. 이제 다 왔다. 그럼 스택 포인터를 기준으로 34번째에 있는 메모리를 참조하여 %s로 가져오면 나는 그 내용을 볼 수 있을 것 같다!! %[ ]$s 형태로 특정 위치에 있는 곳에 포맷 스트링을 사용할 수 있다. 34번째의 값이 필요하니까 %34$s를 삽입하면 된다.
???
Segfault가 발생하고 출력된 값은 무슨 이상한 주소가 나왔다. 원래는 여기서 값이 나왔어야 한다. 난 dreamhack에서 이렇게 공부했기 때문.
엥? %1$s 즉 그냥 첫 번째 위치인 %s 자리를 불러왔더니 오히려 TOP SECRET!!이 출력되었다. 이게 뭔가?
+(이거 공부하는데 얘 때문에 며칠 날림) 그냥 가볍게 보세요. 시스템 해킹으로 깊게 들어가지 않으실거면 크게 중요하진 않을 것 같습니다. 더 궁금하신 분들은 함수 호출 규약 참조 바람
첫 번째로 %34$s로 나는 스택에 저장되어 있던 secret이 참조하고 있는 곳을 호출하려 했지만 문제는 printf가 호출된 이후를 고려하지 않았던 것. printf(buf)에 브레이크 포인트를 걸었기 때문에 호출되기 전 스택 상황은 분명 34번째에 목표물이 있던 건 확실했으나 printf 이후 상황이 달라졌다. 리턴 주소가 push 되었고 그 아래에 레지스터 저장 영역과 추가로 패딩이 이루어져 $rsp의 위치가 달라지게 되었다.
사용하고 있는 운영체제는 64비트. 즉 x86-64에서는 함수 호출 규약에 따라 call 명령어를 만날 시에 8바이트 리턴 주소를 푸시한다. (32비트는 4바이트) 그 후에 PLT(dynamic linking 참조)나 자체 함수에서 추가로 푸시함.
그렇게 되어 main의 원래 rsp-8-8 > rsp-16된 주소가 우연하게 지역 버퍼인 buf의 rbp-0x110과 같았기 때문에 %1$s 자리에 출력이 될 수 있었던 것이다.
printf(buf) 다음 행인 puts에 브레이크 포인트를 걸고 printf 이후의 스택 상황을 본 모습이다. $rsp와 $rsp+16에 4006f0이 들어있어서 %1$s 혹은 %2$s로 정보를 유출시킬 수 있던 것이다. 빡세다....
그럼 만약 우연히 위치하지 않았고 스택에서 찾아볼 수 없었다면 %[n]$s로 read할 수 없었다는 것이 아닌가?
아마 맞을 것이다. 변조를 하지 않는다는 가정 하에서.
너무 글이 길어졌기에 변조를 통한 FSB는 다음 글에서 계속!!
갑자기 아주 기초적인 말이 하고 싶어졌는데 저기서 secret을 유출시키지 못했다 하더라도 FS 취약점이 없는 것은 아니다. 단지 secret 유출만 지금은 안되는 것일뿐 여전히 취약한 라인이다. 다음 글에서 설명할 %n이나 다른 취약점인 오버플로우 등을 이용하여 변조하여 충분히 접근이 가능하다.
'주요정보통신기반 웹 취약점 분석·평가 항목' 카테고리의 다른 글
버퍼 오버플로우(BO) (0) | 2025.04.17 |
---|---|
불충분한 인증/인가 취약점 (0) | 2024.11.27 |