혜랑's STORY

[2021 겨울 시스템 1주차 : 달고나 문서 정리(4-5)] 본문

2021 SISS 21기 활동/겨울방학 System

[2021 겨울 시스템 1주차 : 달고나 문서 정리(4-5)]

hyerang0125 2021. 1. 10. 16:21

#4 프로그램 구동 시 Segment에서는 어떤 일이?

 프로그램이 실행되어 프로세스가 메모리에 적재되고 메모리와 레지스터가 어떻게 동작하는지 알아보기 위하여 간단한 프로그램 예를 들어 보았다.

void function(int a, int b, int c){
	char buffer1[15];
    char buffer2[10];
}

void main(){
	function(1, 2, 3);
}

 실행 결과 이러한 결과가 나왔다.

simple.c 프로그램이 컴파일 되어 실제 메모리 상에 어느 위치에 존재할지 알아보기 위해 컴파일 한 뒤, gdb를 이용하여 어셈블리 코드와 메모리에 적재될 logical address를 살펴보도록 하자.

앞에 붙어 있는 주소가 logical address이다. 이 주소를 자세히 보면 function()함수가 아래에 자리잡고 main()함수는 위에 자리잡고 있음을 알 수 있다. 따라서 메모리 주소를 바탕으로 생성될 이 프로그램의 segment 모양은 다음과 같은 것이다.

simple.c 프로그램이 실행 될 때의 segment 모습

segment의 크기는 프로그램마다 다르기 때문에 최상위 메모리의 주소는 그림과 같이 구성되지 않을 수도 있다. simple.c는 전역변수를 지정하지 않았기 대문에 data segment에는 링크된 라이브러리의 전역변수 값만 들어있다.

#5 Buffer overflow의 이해

버퍼(buffer)란 시스템이 연산 작업을 하는데 있어 필요한 데이터를 일시적으로 메모리 상의 어디엔가 저장하는데 그 저장 공간을 말한다. 대부분의 프로그램에서는 바로 이러한 버퍼를 스텍에다 생성한다. 스텍은 함수 내에서 선언한 지역 변수가 저장되게 되고 함수가 끝나고 나면 반환된다.

buffer overflow는 미리 준비된 버퍼에 크기보다 큰 데이터를 쓸 때 발생하게 된다. 즉, 준비된 용량보다 큰 데이터를 쓰면 버퍼가 넘치게 되고 프로그램은 에러를 발생시키게 된다. 이전에 스텍에 저장되어 있던 데이터가 바뀌게 되기 때문이다. 따라서 buffer overflow 공격은 공격자가 메모리상의 임의의 위치에다 원하는 코드를 저장시켜 놓고 return address가 저장되어 있는 지점에 그 코드의 주소를 집어 넣음으로서 EIP에 공격자의 코드가 있는 곳의 주소가 들어가게 해 공격을 하는 방법이다.

공격자는 버퍼가 넘칠 때, 즉 버퍼에 데이터를 쓸 때 원하는 코드를 넣을 수가 있다. 물론 이 때는 정확한 return address가 저장되는 곳을 찾아 return address도 정확하게 조작해 줘야 한다.

Byte order

 데이터가 저장되는 순서가 바뀐 이유는 바이트 정렬 방식이다. 현존하는 시스템들은 두 가지의 바이트 순서를 가지는데 이는 big endian 방식과 little andian 방식이 있다. big endian 방식은 바이트 순서가 낮은 메모리 주소에서 높은 메모리 주소로 되고 littel endian 방식은 높은 메모리 주소에서 낮은 메모리 주소로 되어 있다. 

little endian은 더하거나 빼는 셈을 할 때 낮은 메모리 주소 영역의 변화는 수의 크기 변화에서 더 적기 때문에 저장 순서를 뒤집어 놓는다. 즉, little endian 시스템에 return address 값을 넣을 때는 바이트 순서를 뒤집어서 넣어주어야 한다.

함수가 리턴될 때 return address는 EIP에 들어가게 되고, EIP는 공격이 들어있는 명령을 수행하게 된다. 이것이 바로 buffer overflow를 이용한 공격 방법이다.

만날 수 있는 문제점 한 가지

return address 위의 버퍼 공간이 공격 코드를 넣을 만큼 충분하지 않다면 다른 공간을 찾아 보아야 한다. 그렇다면 return address 이전의 버퍼 공간을 활용하여야 한다. 그러나 이 공간도 부족 하다면 return address 부분만을 제외한 위아래 모든 공간을 활용하도록 코딩을 할 수 있을 것이고 그것도 안 된다면 또다시 다른 공간을 찾아보아야 한다.

쉘 코드 만들기

쉘 코드란 쉘(shell)을 실행시키는 코드이다. 쉘은 흔히 명령 해석기라고도 불리는데 사용자의 키보드 입력을 받아서 실행파일을 실행시키거나 커널에 어떠한 명령을 내릴 수 있는 대화통로이다. 쉑 코드는 바이너리 형태의 기계어 코드(혹은 opcode)이다. 

쉘 코드를 만들어야 하는 이유는 실행중인 프로세스에게 어떠한 동작을 하도록 코드를 넣어 그 실행 흐름을 조작할 것이기 때문에 역시 실행 가능한 명령어를 만들어야 하기 때문이다(컴퓨터가 2진수 명령어를 수행하기 때문)

쉘 실행 프로그램

우리가 쉘 상에서 쉘을 실행시키려면 '/bin/sh'라는 명령을 내리면 된다.

쉘을 실행시키기 위해서 execve()라는 함수를 사용했다. 이 함수는 바이너리 형태의 실행 파일이나 스크립트 파일을 실행시키는 함수이다. 이 함수는 세 개의 인자들이 모두 const char* 형 인자들을 요구하고 있고 첫번째 인자는 파일 이름, 두 번째 인자는 함께 넘겨줄 인자들의 포인터, 세 번째 인자는 환경인자들을 채워주었다.

Dynamic Link Library & Static Link Library

Dynamic Link Library는 동적 링크 라이브러리라고 해석되고 있으며, 프로그래머는 기계어 코드를 직접 구현할 필요가 없고 그냥 호출해서 사용하면 된다. 이러한 기능들을 라이브러리라고 하는 형태로 존재하고 있다.

그러나 운영체제의 버전과 libc의 버전에 따라 호출 형태나 링크 형태가 달라질 수 있는데 그 영향을 받지 않고 기계어 코드를 실행파일이 직접 가지고 있게 할 수 있는 방법이 Static Link Library이다. 다만 Dynamic Link Labrary 방식보다 실행파일의 크기는 커진다.

NULL의 제거

우리는 이 기계어 쉘 코드를 얻은 다음 이것을 문자열 형태로 전달할 것이다. 그러나 char형 배열로 전달하면 0의 값을 만나면 그것을 문자열 끝으로 인식하고, 0x00 뒤에 어떤 값이 있더라도 그 이후는 무시해버린다. 따라서 \x00인 기계어 코드가 생기지 않도록 만들어 주어야 한다.

다른 방법

쉘 코드를 저장할 변수를 int형으로 만들어 준다. 다만 유의할 점은 little endian 순서로 정렬해야 하며 int형이므로 4byte 단위로 만들어줘야 한다.