setuid 함수로 level 13 계정의 uid로 바꿔주고 있고, gets 함수로 입력을 받는다. 입력을 받는 str의 크기는 256바이트 이지만, 입력값에서 제한을 하고 있지 않기 때문에 bof 취약점이 발생할 수 있다. 정상적인 입력값을 받는다면 문자열을 입력받고, 입력값을 그대로 출력해주는 프로그램이다.
파일 목록을 보면 attackme라는 파일이 있었고, 그 파일을 tmp 디렉토리로 복사하여(권한문제 때문) gdb로 main 함수를 확인하며 다음과 같다.
str의 크기는 256이지만 264 만큼의 크기를 확보하고 있기에 str 256바이트에 dummy 8바이트가 들어가는 것 같다. str의 시작점에서 ret까지의 거리는 str의 크기 256 + dummy(8) + SFP(4) = 268이다. 전 문제 처럼 환경변수에 쉘 코드를 넣고, RET에 환경변수의 주소를 넣어 level 13 계정의 쉘을 딸 수 있을 것 같다.
find 명령어의 size 옵션을 활용해서 2700 용량을 가진 파일을 찾을 수 있었다. 크기 뒤에 b, c, k, w를 붙여 확인해본 결과 2700은 바이트 단위임을 알 수 있었다.
찾은 found.txt 파일을 열어보니 다음과 같이 level9의 shadow 파일임을 알 수 있었다.
파일의 내용을 존 더 리퍼라는 패스워드 크랙 툴을 이용하여 크랙하면 다음과 같은 비밀번호를 얻을 수 있다.
Level 9
문제의 hint파일의 내용은 다음과 같다.
bof파일을 통해 level10의 권한을 얻어야 하는것 같고, buf2의 앞 두글자가 go와 같으면 시스템 함수를 실행시켜 주는 것 같다. 입력값은 buf로 받고 있는데, buf의 크기는 10인데, 입력값 제한을 40으로 하고 있어서 bof가 발생한다.
gdb로 main함수를 보면 다음과 같다.
입력값은 ebp - 40에 들어가고 있고, strncmp로 비교하는 값은 ebp-24 에 들어가고 있다. ebp - 40이 buf이고, ebp-24가 buf2인 것이다. 메모리 공간이 16만큼 차이가 나므로 프로그램을 실행하고 입력할 때 16개만큼 다른 값으로 채우고, go를 넣으면 buf2에 go가 들어가서 조건문을 만족시킬 수 있다.
Level 10
시스템에서 프로세스의 메모리는 그 프로세스만 접근이 가능하지만, 공유 메모리를 사용하게 되면 다른 프로세스에서도 메모리에 접근할 수 있다. 공유메모리의 key_t값이 주어졌기 때문에 이 키 값의 공유메모리를 사용하는 프로그램을 만들어서 대화 내용이 있는 메모리에 접근할 수 있을 것 같다.
다음과 같이 C코드를 작성하여 위 힌트의 대화방과 같은 키값을 사용하여 메모리를 공유하는 프로그램을 만들어 줬다.
공유메모리를 생성하려면 sys/shm.h 헤더파일과 sys/ipc.h헤더파일을 사용해야 한다. shmget 함수로 7530 키를 가진 공유 메모리를 생성할 수 있고 shmat 함수로 공유메모리 프로세스에 접근할 수 있다. 메모리 주소를 반환하며 SHM_RDONLY옵션을 사용하면 읽기 전용이다. 메모리에 저장된 값을 출력하면 다음과 같은 내용이 나와 level11의 비밀번호를 알 수 있다.
Level 11
hint파일의 내용은 다음과 같다.
파일 목록을 보면 attackme 라는 파일이 있는 것을 확인할 수 있다.
힌트 파일의 내용이 attackme 파일의 소스코드 인 것 같다. 인자로 문자열을 받고 있고 인자로 받은 문자열을 str에 저장하고, str을 출력하는 프로그램이다.
문제 파일을 gdb로 실행하면 권한에 의한 오류가 발생하기 때문에 파일을 복사하여 복사한 파일을 분석해야 한다.
gdb로 main 함수를 보면 다음과 같다.
ebp - 264를 사용하는 것으로 보아 256크기의 str 변수에 8바이트의 dummy가 들어가는 것 같다. 쉘 코드를 str에 넣고, str에서 bof를 발생시켜 ret까지 덮은 후에 ret에 str의 주소를 넣어 쉘 코드를 실행시킬 수 있을것 같아서 시도해보니 str의 주소가 프로그램을 시작할때마다 무작위로 바뀌는 aslr 보호 기법이 적용되어 있어서 불가능할 것 같다.
다른 방법을 찾아보다 환경변수는 리눅스 시스템에 저장되어 있기 때문에 한번 설정하면 주소가 바뀌지 않아서 환경 변수에 쉘 코드를 넣고 ret에 환경변수의 주소를 덮으면 쉘을 딸 수 있다고 한다.
hint 파일을 읽어보면 "level2 권한의 setuid가 걸린 파일을 찾는다." 라고 쓰여 있다.
파일을 찾는것은 find 명령어를 통해 찾을 수 있으며 -user 옵션을 통해 파일 소유자를, -perm 옵션은 권한을 의미하며 4000 권한은 setuid가 설정되어 있음을 의미한다. 따라서 다음과 같은 명령어로 힌트로 주어진 파일을 찾을 수 있다.
$ find / -user level2 -perm -4000 2>/dev/null
2>/dev/null은 표준에러(2)가 발생하면 /dev/null(리눅스의 휴지통)으로 넣어준다는 의미이다.
명령어를 실행하면 다음과 같은 결과가 나온다.
/bin/ExecuteMe 파일을 실행하면 level2 권한으로 명령어를 실행시켜준다고 나오고, /bin/bash 명령을 입력하면 level2의 쉘을 딸 수 있다.
Level 2
hint 파일을 읽어보면 텍스트 파일 편집 중 쉘의 명령을 실행시킬 수 있다고 한다.
level3 권한으로 setuid가 걸려있는 파일을 확인해보면 다음과 같다.
editor을 실행시키면 vim 텍스트 에디터가 실행된다.
vim 텍스트 에디터에서는 ! 뒤에 명령어를 사용하면 쉘 명령어를 사용할 수 있다.
! id로 level3 사용자 권한으로 명령어를 실행한다는 것을 알 수 있다.
! my-pass로 level3의 비밀번호를 확인할 수 있고 ! /bin/bash로 쉘을 딸 수 있다.
Level 3
hint 파일을 읽어보면 다음과 같다.
C 코드를 읽어보면 인자의 갯수가 2가 아니면 if문이 실행되면서 printf함수로 출력하고 프로그램을 종료한다. cmd라는 배열에 dig @와 argv[1] 인자로 준 값, 이후 문자열이 들어가고 있고 시스템 함수로 cmd 배열의 문자열을 실행한다. autodig 프로그램을 실행하면서 인자로 명령어를 넣어줘야 하는데 이미 앞에서 dig 명령어가 사용되고 있기 때문에 동시에 여러 명령어를 사용하려면 세미클론을 사용하여 인자를 전달하면 된다.
autodig 프로그램은 find명령어로 찾으면 다음과 같은 곳에 있다.
autodig를 실행하고 인자로 ";/bin/bash;"를 넣어주면 level4의 쉘을 딸 수 있고, my-pass 명령어로 비밀번호를 확인할 수 있다.
Level 4
hint 파일을 보면 "누군가 /etc/xinetd.d/에 백도어를 심어놓았다.!" 라고 한다.
해당 경로의 파일 목록을 보면 다음과 같이 backdoor파일을 확인할 수 있다.
backdoor파일의 내용을 보면 다음과 같다.
finger 이라는 서비스를 통해 level5의 권한으로 /home/level4/tmp/backdoor이라는 서버 파일을 실행한다.
finger 명령어는 리눅스에서 사용자의 계정정보를 확인하는 명령어이다. finger을 실행하면 backdoor 파일의 내용으로 인해 /home/level4/tmp/backdoor 파일이 실행될 것 같다.
tmp 폴더로 이동하여 backdoor 파일을 만들어야 한다.
시스템함수로 my-pass 명령을 실행하는 c 코드를 작성하고 gcc로 컴파일 하고 finger명령어를 실행하면 비밀번호를 알 수 있다.
Level 5
hint 파일을 읽어보면 다음과 같다.
힌트의 level5 파일은 level6 계정 권한으로 setuid가 걸려있음을 확인할 수 있다.
level5를 실행하고 /tmp 디렉토리를 확인해보면 level5.tmp라는 임시파일이 없다. 임시파일을 만들고 삭제시켜주는것 같다.
생성되는 임시파일을 이용하여 권한상승을 하는 레이스 컨디션 공격 기법이 있다고 한다.
레이스 컨디션(Race Condition)은 한정된 자원을 동시에 이용하려는 여러 프로세스가 자원의 이용을 위해 경쟁을 벌이는 현상이라고 한다. 레이스 컨디션을 이용한 공격은 취약 프로그램이 생성하는 임시 파일의 이름을 파악하고, 그 임시파일과 같은 이름의 파일을 생성히고 이 파일에 심볼릭 링크를 생성한 후 원본 파일을 지운다. 원본 파일이 지워진 상태에서 취약한 프로그램이 심볼릭 링크를 건 파일과 같은 파일을 생성할 때를 기다렸다가 심볼릭 링크를 이용해 파일의 내용을 변경한다.
이 문제의 경우 취약한 파일은 level5 이고, 임시파일의 이름은 level5.tmp가 된다.
tmp 디렉토리에 level5.tmp에 심볼릭 링크로 연결할 파일을 하나 만들고 심볼릭 링크로 연결했다.
이후 level5를 실행하면 심볼릭링크로 연결된 level5에 level5.tmp의 내용이 저장되고 /tmp/level5파일의 내용을 확인하면 다음 레벨의 비밀번호를 확인할 수 있다.
Level 6
level6 계정으로 접속하면 다음과 같은 힌트가 나오고, 텔넷 접속 서비스가 나온다.
번호를 입력하면 ssh 연결이 끊기고, ctrl + c로 현재 프로그램을 종료하려고 하면 ctrl + c를 사용할 수 없다고 한다.
다시 level6 계정으로 접속해서 힌트만 보이는 화면에서 ctrl + c를 누르면 현재 프로그램이 강제 종료되고 프롬프트가 나온다. 파일 목록을 확인해보면 password 파일이 있고 password파일을 확인해보면 level7계정의 비밀번호를 알아낼 수 있다.
Level 7
hint 파일을 보면 다음과 같은 내용이 나온다.
level7 을 실행해보면 wrong.txt가 존재하지 않는다는 메시지가 나오면서 문제를 더이상 진행할 수 없다.
union 키워드 뒤에 공용체 이름을 지정하고, 중괄호 안에 변수를 선언한다. 닫는 중괄호 뒤에는 반드시 세미클론을 붙여줘야 한다. 공용체는 보통 main함수 밖에 정의한다.
정의한 공용체를 사용하려면 공용체 변수를 선언해야 한다. 공용체 이름 앞에는 union 키워드를 붙이고, 공용체 이름과 공용체 변수 이름을 지정하여 선언한다. sizeof 연산자로 공용체 크기를 구하면 가장 큰 자료형의 크기가 공용체의 전체 크기로 나온다. 위 코드의 경우 char 8개 크기인 배열로 8바이트 이다.
공용체의 멤버에 접근할 때는 점을 사용한다. 공용체는 멤버중 가장 큰 자료형의 공간을 공유하기 때문에 어느 한 멤버에 값을 저장하면 나머지 멤버의 값은 사용할 수 없는 상태가 된다. 공용체의 멤버는 한 번에 하나씩 값을 쓰면 정상적으로 사용할 수 있다.
실무에서는 공용체에 값을 저장할 때 어떤 멤버를 사용할 지 미리 정해두고 꺼낼때도 정해둔 멤버에서 값을 꺼내는 식으로 사용한다. 여러 멤버에 동시에 접근하지 않는 경우 메모리 레이아웃에 멤버를 모아둘 때 사용한다. 임베디드 시스템이나 커널 모드 디바이스 드라이버 등에서 주로 사용하며 보통은 거의 쓰지 않는다.
d1의 멤버중 가장 큰 자료형인 num2에 0x12345678을 할당하고 다른 멤버들을 출력해보면 d1.num2는 저장한 숫자가 그대로 나오지만, 다른 멤버는 숫자중 일부만 나온다. 공용체는 값을 저장하는 공간은 공유하지만 가져올 때는 해당 자료형의 크기만큼만 가져오기 때문이다. 우리가 사용하는 CPU는 리틀 엔디언 방식으로 값을 메모리에 저장한다. 리틀 엔디언은 숫자를 1바이트씩 쪼개서 낮은 자릿수가 앞에 오도록 한다. 따라서 0x12345678을 리틀 엔디언 방식으로 메모리에 저장하면 78 56 34 12가 된다. 공용체는 앞에서부터 자료형의 크기만큼 값을 가져오므로 d1.num1은 앞의 2바이트 56 78을 가져오고, d1.c1은 앞의 1바이트인 78만 가져온다.
공용체도 구조체처럼 typedef로 별칭을 지정하고 별칭으로 선언하거나, 익명 공용체를 정의할 수 있다. 또한 다음과 같이 정의하는 동시에 변수를 선언할 수 있다.
malloc함수로 메모리를 할당할 때 크기를 알아야 하므로 sizeof(union Box)처럼 공용체의 크기를 구할 수 있다. 공용체 포인터도 멤버에 접근할 때는 화살표 연산자를 사용한다. strcpy함수로 공용체 포인터의 멤버에 접근한 뒤 문자열을 복사하고, 공용체 포인터도 멤버중 가장 큰 자료형의 공간을 공유함을 출력값을 통해 확인할 수 있다. 할당한 메모리는 사용이 끝났으면 꼭 free로 해제해줘야 한다.
54.4 퀴즈
정답은 long long int 자료형의 크기인 8 이다.
정답은 d이다.
정답은 0x1111 이다.
54.5 연습문제 : 정수 데이터 공용체 정의하기
정답은 다음 코드와 같다.
// 1.
union Data{
char c1;
short num1;
};
// 2.
union Data
// 3.
d1.num1 = 0x5678;
0x5678 은 2바이트 이므로 short, 0x78은 1바이트이므로 char로 선언하고, 리틀 엔디언으로 값이 저장되기 때문에 0x5678을 num1멤버에 넣으면 위와 같은 결과가 나온다.
54.6 연습문제 : 공용체 포인터 사용하기
화살표 연산자를 사용했으므로 포인터를 선언하여 메모리를 할당해야 하기 때문에 정답은 union Data *d1 = malloc(sizeof(union Data)); 이다.
54.7 심사문제 : 정수 데이터 공용체 정의하기
정답은 다음 코드와 같다.
union Data{
char c1;
int num1;
};
공용체의 크기가 4가 나오므로 4바이트 크기를 갖는 자료형이 사용되야 한다.
54.8 심사문제 : 공용체 포인터 사용하기
정답은 다음 코드와 같다.
union Data *d1 = malloc(sizeof(union Data));
d1->num2 = 0x11111111;
화살표 연산자가 사용됬기 때문에 포인터를 선언한 후 메모리를 할당해야 한다. 공용체는 가장 큰 자료형의 공간을 공유하므로 가장 큰 멤버에만 0x111111111을 넣으면 된다.
Unit 55. 구조체와 공용체 활용하기
구조체 안에 구조체와 공용체가 들어갈 수 있고, 반대로 공용체 안에 구조체와 공용체가 들어갈 수도 있다.
구조체를 멤버로 가지려면 구조체 안에서 구조체 변수를 선언하면 된다. 위 코드에서는 Person 구조체가 Phone 구조체를 멤버로 가지고 있다. 멤버에 접근할 때는 점을 사용하여 가지고 있는 구조체에 계층적으로 접근하면 된다. 위 코드와 같이 점을 두 번 사용하면 구조체의 areacode와 num 멤버에 접근할 수 있다.
구조체 안에 구조체를 정의할 때는 무조건 안에 들어가는 구조체를 먼저 선언해줘야 한다.
만약 Phone구조체를 다른 곳에서는 쓰지 않고 특정 구조체 안에서만 쓴다면 다음과 같이 구조체 안에 구조체를 정의하는게 더 편리하다. 이때 안에 정의하는 구조체는 정의하고 반드시 변수를 선언해 줘야 한다.
struct Person {
char name[20];
int age;
struct Phone {
int areacode;
unsigned long long num;
} phone;
};
구조체 변수를 선언하며 안에 있는 구조체까지 초기화 하려면 중괄호 안에 중괄호를 사용하여 다음과 같이 초기화 하면 된다.
struct 구조체이름 변수이름 = { 값1, 값2, { 값3, 값4 } };
55.2 구조체 안의 구조체 멤버에 메모리 할당하기
다음은 구조체 안에 구조체 멤버가 변수로 있는 상태에서 메모리를 할당하여 사용하는 방법이다.
Person 구조체에 메모리를 할당하면 각 구조체의 멤버에 접근하려면 p1은 포인터이기 때문에 화살표 연산자를 사용하여 멤버에 접근하고, phone은 포인터가 아닌 일반 변수이므로 점을 사용하여 멤버에 접근한다. 구조체 포인터 사용이 끝나면 free함수로 할당된 메모리를 해제해야 한다.
Person 구조체 안에는 구조체 포인터를 멤버로 갖고 있다. 먼저 바깥 구조체의 포인터에 메모리를 할당하고 멤버로 있는 구조체 포인터에 메모리를 할당해야 한다. 각 멤버에 접근하려면 p1은 포인터이고, 구조체 멤버 phone도 포인터이므로 화살표 연산자로 접근하면 된다. 구조체 포인터 사용이 끝나면 메모리를 해제해야 하는데, 안쪽에 있는 멤버부터 메모리를 해제해야 한다. 만약 바깥에 있는 구조체를 먼제 해제하게 되면 데이터가 사라져서 멤버에 더이상 접근할 수 없다. 멤버 포인터에 저장된 주소도 알 수 없기 때문에 해제도 할 수 없다. 그래서 ㅇㄴ쪽의 구조체 먼제 해제해야 한다.
제일 안에 있는 x, y, z는 float로 선언되어 있는 익명 구조체 이다. 익명 공용체가 x, y, z익명 구조체오 배열 v를 감싸고 있다. float x, y, z는 변수 3개이고, float v[3]도 배열의 요소가 3개이므로 자료형도 같고, 개수도 같으므로 같은 공간을 차지한다. 공용체로 묶어주면 x, y, z와 v[3]은 같은 공간을 공유하게 된다.
v는 배열이기 때문에 인덱스로 접근하여 값을 할당할 수 있다. 공용체에 넣었으므로 값은 같은 공간에 있기 때문에 x,y,z 멤버로도 접근할 수 있다.
55.4 퀴즈
정답은 d 이다.
정답은 c 이다.
정답은 b이다.
정답은 d 이다.
55.5 연습문제 : 게임 캐릭터 구조체 만들기
정답은 struct Stats stats 이다.
55.6 연습문제 : 게임 캐릭터 구조체 사용하기
화살표 연산자로 접근하고 있기 때문에 포인터로 만들어서 메모리를 할당시켜줘야 하므로 정답은 struct Champion *lux = malloc(sizeof(struct Champion)); 이다.
55.7 연습문제 : 장치 옵션 구조체 만들기
정답은 다음 코드와 같다.
union {
unsigned long long option;
struct {
unsigned char boot[4];
unsinged char interrupt[2];
unsigned char bus[2];
};
};
사용하는 공간이 총 8 바이트이기 때문에 공간을 공유하는 공용체를 8바이트로 만들면 코드에서 저장한 값들을 다 저장할 수 있다.
컴퓨터는 CPU가 메모리에 접근할 때 32비트는 4바이트 단위, 64비트는 8바이트 단위로 접근한다. 만약 32비트 CPU에서 4바이트보다 작은 데이터에 접근하게 되면 내부적으로 시프트 연산이 발생하여 효율이 떨어지기 때문에 C컴파일러에서 CPU가 메모리의 데이터에 효율적으로 접근할 수 있도록 구조체를 일정한 크기로 정렬한다. 구조체 크기가 15나 17바이트가 되면 접근 효율이 떨어지기 떼문에 2, 4, 8, 16바이트 단위로 정렬한다.
구조체의 멤버를 정렬하면 안되는 경우도 있다. 사진파일의 경우 정렬하게 되면 사진을 저장할 때 마다 조금씩 깨질 수 있다. 또한 네트워크로 데이터를 전송할 경우 몇 바이트씩 어떤 순서로 보낼지 규약을 정해놓았는데, 이때 정렬이 발생하면 정해놓은 규약에서 벗어나 받는 쪽에서 데이터를 알아볼 수 없게 된다.
51.1 구조체 크기 알아보기
구조체 전체 크기는 sizeof 연산자를 사용하여 알 수 있다. sizeof 연산자는 다음과 같이 사용할 수 있다.
sizeof(struct 구조체)
sizeof(구조체별칭)
sizeof(구조체변수)
sizeof 구조체변수
다음은 가상의 네트워크 구조체 PacketHeader를 정의하여 멤버의 크기와 구조체의 크기를 구한 것이다.
구조체의 크기를 구하려면 sizeof 연산자 안에 변수나 struct 키워드와 구조체 이름을 넣어주면 된다. PacketHeader 구조체 안에는 1바이트 크기의 char 변수와 4바이트 크기의 int 변수가 들어있어 전체 크기는 5가 나와야 할 것 같지만 8이 나왔다. C에서 구조체들을 정렬할 때는 멤버중에서 가장 큰 자료형 크기의 배수로 정렬한다. 위 구조체에서는 int가 가장 큰 자료형이므로 느끼는 4바이트를 기준으로 정렬하여 구조체 전체 크기는 8바이트가 되었다. 1바이트 크기인 char flags 뒤에는 4바이트를 맞추기 위해 남는 공간에 3바이트가 더 들어가게 되는데 이렇게 구조체를 정렬할 때 남는 공간을 채우는 것을 패딩이라고 한다.
다음 코드를 이용하여 구조체를 정렬한 뒤 멤버의 위치가 위 그림처럼 되는지 확인할 수 있다. 구조체에서 멤버의 위치(offset)를 구할 때는 offsetof 매크로를 사용하여 구할 수 있다. offsetof매크로는 stddef.h 헤더파일에 정의되어 있다.
구조체를 정의할 때 위 아래로 #pragma pack(push, 1)과 #pragma pack(pop)을 넣었다. pack을 1로 설정하면 1바이트 단위로 정렬하여 남는 공간 없이 자료형 크기 그대로 메모리에 올라간다. #pragma pack(push, 1)을 한 번 사용하면 이후 모든 구조체에 영향을 주므로 정렬 한 뒤 #pragma pack(pop)를 사용하여 설정을 이전 상태로 되돌린다. 만약gcc버전이 4.0미만이면 __attribute__((aligned(1), packed))를 사용한다. offsetof로 멤버의 위치를 확인하면 다음과 같다.
구조체도 변수를 선언하거나 메모리를 할당하게 되면 메모리 공간을 차지하므로 메모리 관련 함수를 사용할 수 있다.
52.1 구조체와 메모리를 간단하게 0으로 설정하기
구조체의 모든 멤버를 0으로 만들기 위해 각 멤버에 접근하여 0을 저장하는 것은 매우 번거로운 작업이다. 따라서 {0, }과 같이 중괄호를 사용하여 모두 0으로 초기화 할 수 있다. 이 방법은 변수에만 사용할 수 있고, malloc 함수로 할당한 메모리에는 사용할 수 없다.
struct 구조체이름 변수이름 = { 0, };
구조체 변수나 메모리의 내용에 한꺼번에 값을 설정하려면 다음과 같이 memset함수를 사용하면 된다.
memset함수로 구조체 변수의 값을 설정할 때는 &p1과 같이 주소연산자 &을 사용하야 메모리 주소를 구해서 넣어줘야 한다. 그리고 설정할 값과 크기를 넣어준다. 위 코드에서는 구조체의 내용을 0으로 설정하고 Point2D구조체 크기만큼 설정했다. 그리고 각 멤버를 출력해보면 모두 설정한 0이 나온다. 다음과 같이 malloc함수로 설정한 동적 메모리에도 값을 설정할 수 있다.
memset 함수로 메모리를 설정할 때 구조체가 포인터 변수이기 때문에 &을 사용하지 않고 그대로 넣어준다. 설정할 값과 크기를 넣어주고 각 멤버를 출력해보면 모두 0이 나온다.
52.2 구조체와 메모리 복사하기
내용이 같은 구조체를 만들거나 이미 생성하여 값을 저장한 구조체나 메모리를 다른 곳에 복사할 경우 memcpy 함수를 사용하여 메모리의 내용을 다른 곳에 복사할 수 있다. string.h 헤더파일에 선언되어 있다. memcpy(목적지포인터, 원본포인터, 크기); 의 형태로 사용한다.
구조체 변수 p1, p2를 선언하고 p1의 멤버에만 값을 저장한 상태에서 memcpy 함수를 사용하여 p1의 내용을 p2에 복사하였다. memcpy함수를 사용할 때는 구조체 변수에 주소 연산자를 사용하여 변수의 메모리 주소를 넣고, 크기는 구조체의 크기를 넣어준다. 목적지 포인터와 원본포인터는 앞쪽이 목적지 포인터이고, 뒷쪽이 원본 포인터이다. p2의 각 멤버를 출력해보면 p1의 멤버에 저장했던 값이 나온다. 다음은 malloc함수로 할당한 동적 메모리끼리 복사하는 방법이다.
구조체 변수를 선언할 때 대괄호 안에 크기를 넣어주면 배열로 선언할 수 있다. 배열에서 각 요소에 접근하려면 배열 뒤에 대괄호를 사용하며 대괄호 안에 인덱스를 지정해주면 된다. 이 상태에서 멤버에 접근하려면 점을 사용한다. p[0].x 구조체 배열의 첫 번째 요소의 멤버 x에 접근한다는 뜻이다. 구조체 배열을 선언하는 동시에 초기화하려면 다음과 같이 중괄호 안에 중괄호를 사용하면 된다.
구조체 포인터를 선언하고 사용하려면 메모리를 할당해야 한다. 배열의 요소 개수 만큼 반복하면서 각 요소에 구조체 크기 만큼 메모리를 할당해야 한다. 구조체 포인터 배열에는 포인터가 들어있으므로 요소 개수를 구하려면 구조체 포인터 배열의 전체 크기에서 구조체 포인터의 크기로 나눠주면 된다. sizeof(struct Point2D *)는 구조체 포인터의 크기이다. 멤버에 접근할 때는 배열안에 들어있는 요소가 포인터이므로 화살표 연산자를 사용하여 멤버에 접근해야 한다. 구조체 배열의 사용이 끝나면 배열 크기만큼 반복하며 각 요소에 할당된 동적 메모리를 해제해야 한다.
PLT는 Procedure Linkage Table의 약자로 외부 프로시저를 연결해주는 테이블이다. PLT를 통해 다른 라이브러리에 있는 프로시저를 호출하여 사용할 수 있다.
GOT은 Global Offset Table의 약자로 PLT가 참조하는 테이블로 프로시저들의 주소가 들어 있다.
함수를 호출한다는 것은 PLT를 호출한다는 의미이며 GOT으로 점프한다.
GOT에는 함수의 실제 주소가 있다. GOT에 실제 함수 주소가 있다면 GOT을 바로 호출하지 왜 PLT를 거치는 것일까?
이 이유를 알기 위해서는 링커(Linker)를 알아야 한다.
printf라는 함수를 사용하려면 소스 안에는 printf를 호출하는 코드가 있고, include 한 헤더파일 안에는 printf에 대한 선언이 있다.
소스 파일을 실행파일로 만들기 위해서는 컴파일 과정을 거쳐야 한다. 컴파일을 하면 오브젝트 파일이 생성된다. 오브젝트 파일은 printf 함수의 구현 코드를 모르기 때문에(선언이 다른 헤더 파일에서 되어있기 때문) 실행은 할 수 없다. 오브젝트 파일을 실행 가능하게 만드려면 printf의 실행 코드를 찾아서 오브젝트 파일과 연결시켜야 한다. printf의 실행코드는 printf의 구현 코드를 컴파일한 오브젝트 파일이며 이러한 오브젝트 파일들이 모여있는것을 라이브러리라 한다. 이렇게 소스 코드를 컴파일한 오브젝트 파일과 필요한 라이브러리를 연결시키는 작업을 링킹 이라고 한다.
링크를 하는 방법에는 Static과 Dynamic 방식이 있다. Static Link 방식은 파일 생성시 라이브러리 내용을 포함한 실행 파일을 만든다. gcc에서 -static 옵션을 적용하여 컴파일하면 Static Link 방식으로 컴파일 할 수 있다. Static Link 방식은 실행파일 안에 모든 코드가 포함되서 라이브러리 연동 과정이 따로 필요 없고, 필요한 라이브러리를 따로 관리 하지 않아도 되는 장점이 있지만, 파일 크기가 커지고, 동일한 라이브러리를 사용하더라도 해당 하이브러리를 사용하는 모든 프로그램들의 라이브러리의 내용을 모두 매핑시켜야 한다는 단점이 있다.
Dynamic Link방식은 공유 라이브러리를 사용하여 라이브러리 하나를 메모리 공간에 매핑하고 여러 프로그램에서 공유하여 사용한다. 실행파일안에 라이브러리 코드가 없으므로 파일 크기가 비교적 작아지고, 실행시에도 적은 메모리를 차지한다. 또한 라이브러리를 따로 업데이트 할 수 있기 때문에 유연한 방식이다. 하지만 실행파일이 라이브러리에 의존해야 하기 때문에 라이브러리가 없으면 실행할 수 없다.
링크 방식은 file 명령어를 통해 확인할 수 있다.
Dynamic Link 방식으로 컴파일 할 때 PLT와 GOT를 사용하게 된다. Dynamic Link 방식으로 컴파일 하면 라이브러리가 프로그램 외부에 있어 함수의 주소를 알아오는 과정이 필요하다. 프로그램이 만들어지면 함수를 호출할 때 PLT를 참조한다. PLT는 GOT으로 점프하고, GOT에는 실제 함수의 주소가 쓰여 있어 이 함수를 호출한다.
이때 첫 호출이냐 아니냐에 따라 동작이 조금 달라진다.
두 번째 호출일 경우 GOT에 실제 함수의 주소가 쓰여있지만, 첫 번째 호출이면 GOT에 실제 함수의 주소가 쓰여있지 않다. 첫 번째 호출시는 Linker가 dl_resolve라는 함수를 사용하여 필요한 함수의 주소를 알아오고 GOT에 그 주소를 써준 후 해당 함수를 호출한다.
SFP는 Saved Frame Pointer 의 약자로 실행될 때 이전의 EBP값을 가지고 있다. EBP는 현재 스택의 가장 바닥을 가리키는 포인터로 새로운 함수가 호출되면 EBP 레지스터 값이 지금까지 사용하던 스택 꼭대기의 위에 위치하게 되며, 새로운 스택이 시작된다. EBP는 새로운 함수가 호출되거나 현재 실행중인 함수가 종료되어 리턴될 때 마다 값이 달라진다. 현재 함수가 끝나면 이전 함수의 EBP가 필요하게 되는데, 이 이전 함수의 EBP를 저장하는 공간이 SFP이다.
SFP는 32bit에서는 4byte, 64bit에서는 8byte이다.
RET는 pop eip 와 jmp eip를 수행하는데 다음 수행할 명령을 eip에 넣고 그 주소로 가서 실행하는 것이다. 따라서 버퍼 오버플로우에서 RET에 함수 주소나 쉘코드를 덮어쓰면 그 함수나 코드가 실행되는 것이다.