문제분석
코드 분석
1. bleh[80] 선언
2. 최고관리자 권한으로 변경 후, fgets()로 사용자에게 입력값을 받고 79바이트 만큼 bleh[80]에 저장
3. bleh[]을 출력
분석한 내용을 보면 fgets()로 입력 시 bleh[]에 79바이트 만큼만 저장하도록 지정되어있다.
bleh[80]는 80바이트이고 79바이트만 저장할 수 있도록 되어있기 때문에, 이전 문제들 처럼 BOF를 하는 것은 불가능하다.
그렇다면 어떻게 상위 계정의 셸을 얻어낼 수 있을까?
이번 문제에서는 BOF와는 다른 취약점이 존재한다.
코드를 다시 한번 봐보자.
printf()를 사용하는 방식이 좀 이상하다는 것을 알 수 있다.
보통 printf() 사용시에는 printf("%d",num); 처럼 기본적으로 포맷스트링을 인자로 넣어서 사용한다.
개념정리
○ 포맷스트링
%d,%c,%s,%n,%h,%x,%p 등과 같이 자료형을 지정해주는 지정자를 말한다.
그러나 level20 문제의 소스에서는 포맷스트링을 사용하지 않고 인자로 bleh를 넣어주고있다.
따라서 이러한 잘못된 printf() 사용방식으로 인해 포맷스트링 버그가 발생할 수 있다!
포맷스트링 버그 발생 시, 메모리의 내용을 출력하거나 원하는 주소에 인젝션을 하는등 여러가지 버그가 발생 할 수 있다.
○ FSB 대표적인 포맷스트링 (32bit 운영체제 기준)
1) %x : %x 가 가리키는 4 바이트 만큼의 메모리 값을 출력한다. (주로 %x 대신에 %p를 사용하는 경우가 많다.)
2) %[N]d, %[N]c : %d 또는 %c가 가리키는 메모리 영역의 값을 N바이트 만큼의 크기로 출력한다.
3) %n : %n 앞에서 나온 바이트 수의 총합 값을 %n 포맷스트링이 가리키는 값을 주소로 하는 공간에 넣는다.
이때, 값을 넣는 단위는 4바이트이다.
4) %hn : %n과 기본적인 기능은 똑같지만 값을 넣는 단위가 2바이트이다.
위 내용은 각 포맷스트링들이 포맷스트링 버그가 발생할 경우의 역할들을 나열한 것이다.
포맷스트링 버그를 이용하여 셸권한을 얻기 위해서는 RET의 주소를 알아야한다.
(포맷스트링 버그를 이용해 RET에 셸코드 주소를 덮어씌워야한다.)
필자는 소스를 직접 컴파일하여 SFP 값과 RET 주소의 차이를 이용하는 방법을 사용하려했다. 그러나 ASLR 기법이 적용되어있어서 attackme의 메모리 주소가 프로그램을 실행할때마다 바뀌었다.
○ ASLR
BOF, RTL과 같은 메모리 공격기법으로부터 메모리를 보호하기 위해 만들어진 방법이다.
메모리내의 스택, 힙, 라이브러리등의 주소를 랜덤한 영역에 배치하여, 공격에 필요한 Target address를 예측하기 어렵게 만드는 역할을 한다.
이러한 ASLR 때문에 level20 문제에서는 RET의 주소를 알아낼 수 있는 방법이 없다.
그렇다면 어떻게 해야할까?
RET 대신에 생성자와 소멸자를 이용하면 된다!
○ 생성자, 소멸자
C언어 프로그램의 경우 생성자, 소멸자와 관련된 섹션인 .ctors, .dtors를 생성한다.
.ctors는 main()이 시작되기 전에 호출되며, .dtors는 main()이 종료될때 호출된다.
소멸자 .dtors를 이용하여 해당 영역에 셸코드의 주소를 덮어씌운다면, main() 종료시에 .dtors가 호출될때 셸코드의 주소가 호출 될 것이고 결국 셸코드가 실행될 것이다.
.dtors의 주소를 구해보도록하자.
힌트의 소스를 tmp 폴더에 복사하고 컴파일하여 확인해봤다.
(원본 attackme를 대상으로 objdump를 하면 심볼 테이블 정보가 안나온다.)
확인 결과, 위와 같이 나왔다.
.dtors 관련 섹션으로는 __DTOR_LIST__ , __DTOR_END__ 가 있다.
이 중 _DTOR_END__는 main() 종료 시, .dtors가 호출되고 마지막에 return 되는 주소이다.
따라서 _DTOR_END__에 셸코드 주소를 덮어씌우면 main() 종료 시에 최종적으로 셸코드가 실행될 것이다!
( _DTOR_END__의 주소 : 0x08049598 )
다음으로 셸코드를 메모리에 적재하여 해당 주소를 구해보자.
필요한 주소들을 구했으니 본격적으로 포맷스트링 버그를 활용해보자.
○ FSB 포맷스트링
1) %x : %x 가 가리키는 4 바이트 만큼의 메모리 값을 출력한다.
2) %[N]d : %d가 가리키는 메모리 영역의 값을 N 바이트 만큼의 크기로 출력한다.
3) %n : %n 앞에서 나온 바이트 수의 총합 값을 %n 포맷스트링이 가리키는 값을 주소로 하는 공간에 넣는다.
이때, 값을 넣는 단위는 4바이트이다.
4) %hn : %n과 기본적인 기능은 똑같지만 값을 넣는 단위가 2바이트이다.
위에 있는 포맷스트링들을 이용하여 포맷스트링 버그를 발생시키는 페이로드를 작성해보자.
(해당 페이로드의 목적은 셸코드의 주소를 _DTOR_END__에 덮어씌우기 위한 페이로드이다.)
앞으로 설명하는 동안 포맷스트링이 가리킨다는 표현을 사용할 것이다.
무슨말인지부터 이해해보도록하자.
"AAAA%x%x%x%x%n"
위와 같은 입력값을 입력한다고 가정해보자.
그때의 출력값은 위 사진이라고 가정한다면, 각각의 포맷스트링은 어떤 값을 가리키고 있는 것일까?
위와 같이 메모리의 내용을 4바이트 단위씩 순서대로 가리키게 된다.
이러한 방식의 포맷스트링 버그를 이용한 공격이 포맷스트링 공격이다.
이제 본격적으로 페이로드 작성하여 포맷스트링 공격을 시도해보자.
페이로드 작성이 어렵기 때문에 한단계씩 천천히 만들어보겠다.
페이로드
첫번째로 페이로드는 %n 앞에 %d가 먼저 나와야한다.
왜냐하면 %d를 통해 원하는 값을 표현하고 %n을 통해 원하는 값을 _DTOR_END__에 넣어야하기 때문이다.
(이 말이 이해가 안된다면 위에 설명한 포맷스트링 설명을 다시보고오길 바란다.)
1) [_DTOR_END__의 주소]%[셸코드의 주소]d%n
이제 %d를 통해 덮어씌울 값을 표현해야한다.
우리가 덮어씌울 값은 셸코드의 주소이고, 이것을 10진수로 변환해보자.
0xbffff2c2 == 3221222082
2) [_DTOR_END__의 주소]%3221222082d%n
다음으로 해당 셸코드의 주소값을 덮어쓸 주소인 _DTOR_END__ 주소를 %n이 가리키게 해야한다.
따라서 %d 앞에 해당 주소를 넣어준다.
3) \x98\x95\x04\x08%3221222082d%n
이렇게만 하면 완성같지만... 아쉽게도 %n이 [_DTOR_END__의 주소]를 가리키지 않는다.
%n이 [_DTOR_END__의 주소]를 정확하게 가리키게 하기 위해서는 앞에 있는 포맷스트링인 %d가 무엇을 가리키는지 부터 확인해봐야한다.
따라서 아까 복사한 attackme를 통해 %x 포맷스트링으로 메모리 내용을 출력하여 확인해봤다.
입력값으로 "AAAA%x%x%x%x"를 준 결과이다.
%x를 4번 주었을 때 처음에 넣어던 값인 AAAA가 출력되는 것을 확인할 수 있다.
(AAAA를 아스키코드 값으로 41이다.)
이 말인 즉슨, 우리가 입력하는 처음 4바이트 값은 네 번째 포맷스트링부터 가리키게 된다는 것을 의미한다.
따라서 %n이 [_DTOR_END__의 주소]를 가리키게 하기 위해서는 %n 앞에 최소 3개의 포맷스트링이 있어야한다.
더미 포맷스트링으로 %x를 페이로드에 추가해 표현해보자.
4) \x98\x95\x04\x08%x%x%3221222082d%n
마지막으로 한가지 남았다.
%n은 앞에서 표현된 바이트 수의 값을 %n이 가리키는 주소에 덮어씌운다고 하였다.
우리는 앞에서 셸코드의 주소인 0xbffff2c2 == 3221222082 로 미리 변환하여 %d로 표현했었다.
그러나 이후에 페이로드가 추가되면서 %d 앞에 12 바이트만큼 표현되어버렸다.
따라서 3221222082-12(4+4+4) = 3221222070으로 값을 바꿔줘야한다.
페이로드를 완성하면 다음과 같다.
5) \x98\x95\x04\x08%x%x%3221222070d%n
최종적으로 페이로드를 완성하였다!
이제 직접 실행해보자.
segmentation fault가 떴다.
왜 이런 결과가 나왔을까?
보통 x86 시스템에서는 int 형 숫자 표현에 제한이 있고 3221222070과 같은 너무 큰 숫자는 범위를 초과하기 때문에 인식 할 수 없다.
그러나 우리는 %d를 통해 너무 큰 숫자를 표현하려했기 때문에 에러가 발생한 것이다!
따라서 %d를 통해 표현할 셸코드 주소의 값을 절반으로 쪼개서 2바이트씩 따로 삽입해줘야한다.
0xbffff2c2를 두 가지로 쪼개어 각각 10진수로 표현해보자.
1) 0xbfff == 0x1bfff == 114687
2) 0xf2c2 == 62146
1)번에서 0xbfff 앞에 1을 붙인 이유는 스택에서 bf로 시작하는 주소 값은 음수를 의미하기 때문에, 양수인 0xbfff를 표현하기 위해 1을 앞에 붙여서 10진수 값을 계산했다.
아까의 페이로드를 수정하여 다시 작성해보자.
6) [\x98\x95\x04\x08][dummy 4바이트][\x98\x95\x04\x08+2]%x%x%[셸코드 주소 0xf2c2]d%hn%[셸코드 주소 0xbfff]d%hn
5번 페이로드에 비해 좀 더 복잡해진 것을 확인 할 수 있다.
먼저 셸코드의 주소를 4바이트가 아닌 2바이트씩 나눠서 표현하기 때문에 %n 대신에 %hn을 사용하였다.
그리고 2바이트씩 나눈 것을 각각의 주소에 넣어줘야 하기 때문에 [_DTOR_END__의 주소] 뒤에 [_DTOR_END__의 주소+2] 도 추가하였다.
그 사이에 [dummy 4바이트]가 추가된 이유는 dummy 값을 넣지 않으면 2번째 %hn이 [_DTOR_END__의 주소+2]를 가리키지 않기 때문이다.
페이로드를 완성해보면 다음과 같다.
7) \x98\x95\x04\x08AAAA\x9a\x95\x04\x08%x%x%62146d%hn%114687d%hn
마지막으로 아까와 마찬가지로 페이로드에 여러 바이트들을 추가해줬기 때문에 미리 구한 %d의 숫자를 바꿔줘야한다.
(1) 62146 → 62146 - 20(4+4+4+4+4) = 62126
(2) 114687 → 114687 - 62146 = 52541
바꿔서 페이로드를 완성하면 다음과 같다.
8) \x98\x95\x04\x08AAAA\x9a\x95\x04\x08%x%x%62126d%hn%52541d%hn
완성한 페이로드를 실행해보자!
결과는?
ㅠㅠ
필자는 여기서 삽질을 많이했다.
삽질 끝에 문제점을 찾아냈다.
9) \x98\x95\x04\x08AAAA\x9a\x95\x04\x08%8x%8x%62118d%hn%52541d%hn
문제점을 찾아 수정한 페이로드는 위와 같다.
변화한 점을 말하자면 %x 대신에 %8x를 사용하였고 해당 바이트 만큼 %d의 숫자도 바꿔주었다.
(1) 62146 → 62146 - 20(4+4+4+8+8) = 62118
(2) 114687 → 114687 - 62146 = 52541
%x 사용시 바이트 값이 4바이트로 고정된다고 생각했던 필자의 실수였다.
%x를 사용할 경우에는 %x로 출력되는 메모리 값에 따라 해당 바이트 값이 바뀐다.
따라서 4바이트가 아니라 ?바이트 값만큼으로 인식된 것이다.
이렇다보니 뒤에 %d에 넣은 값과의 합산 결과가 [셸코드의 주소]가 아닌 이상한 주소 값이 나왔을 것이고 [_DTOR_END__의 주소]에 이상한 값이 들어갔을것이니 당연히 결과가 segmentation fault가 나온 것이다!
따라서 해당 바이트 값을 고정시키기 위해 %x 대신에 %8x를 사용해야한다. (%x를 8바이트로 고정)
사실 이와 같은 이슈 때문에 FSB 페이로드 구성 시, %x보다는 %p를 많이 사용한다.
exploit
완성한 페이로드로 다시 한번 도전해보자!
결과는?
ftz 클리어!!