// Name: sbof_auth.c
// Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_auth(char *password) {
int auth = 0;
char temp[16];
strncpy(temp, password, strlen(password));
if(!strcmp(temp, "SECRET_PASSWORD"))
auth = 1;
return auth;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: ./sbof_auth ADMIN_PASSWORD\n");
exit(-1);
}
if (check_auth(argv[1]))
printf("Hello Admin!\n");
else
printf("Access Denied!\n");
}
위의 코드는 입력받은 문자열과 “SECRET_PASSWORD"
문자열을 비교하여 일치하면 1
, 불일치하면 0
을 리턴하는 check_auth
함수를 이용하여 결과에 따른 문자열을 출력한다.
check_auth
함수에는 버퍼 오버플로우가 발생할 수 있는데, 바로
strncpy(temp, password, strlen(password));
부분이다. temp
배열의 크기는 16 바이트로 선언이 되었는데, strcpy()
함수를 사용할 때, 입력받은 문자열의 길이인 strlen(password));
만큼 복사하여 temp
에 넣으므로 입력받은 문자열이 16바이트를 넘어선다면, temp
는 넘치게 된다.
auth는 temp의 버퍼 뒤에 존재하므로 temp에서 오버플로우를 발생시키면 auth의 값을 0이 아닌 임의의 값으로 변경할 수 있다. 그렇게 된다면, 실제 인증 여부와는 무관하게 if (check_auth(argv[1])
는 참이되어 printf("Hello Admin!\n");
이 실행된다.
위의 코드를 해석한 것을 기반으로 채워보면,
temp는 어떤 값이 들어가던 무관하므로 아무값으로 채우고, auth영역으로 넘어가는 순간 0이 아닌 임의의 값으로 auth를 변경하게 되므로
if (check_auth(argv[1])) printf("Hello Admin!\n");
가 실행된다.
공부를 하다보니, argv[1]
의 의미와 더불어 평소에는 생략해왔던 main
함수의 매개변수에 대한 의문이 생겼다.
대체 argv[1]
이 무엇이길래 scanf
와 같은 값을 입력받는 함수 없이 입력되는 값을 check_auth
함수로 넘기는 것 일까?
int argc
→ 메인 함수에 전달되는 정보의 갯수 char* argv[]
→ 메인 함수에 전달되는 실질적인 정보를 저장하는 문자열 배열argv[0]
은 프로그램의 실행 경로로 항상 고정argv[1]
부터 정보를 전달check_auth
함수는 auth
변수를 리턴하는 함수이다. auth = 0
일 때 값이 리턴된다면, “Access Denied!”
가 출력되어야 맞다고 나는 해석했다. 그런데 auth
를 다 0
으로 채워도 “Hello Admin”
이 뜬다.
→ 해결완료!
read
함수로 입력받는 것은 16진수 값이 아닌 문자열이다. 0
은 문자열로 입력받으면 아스키코드로 0x30
으로 인식되므로 내가 생각했던 NULL
값인 0x00
과는 다르다. 내가 원하는대로 NULL
값을 채우려면 아스키코드가 0x00인 문자열을 넣어주어야한다.
// Name: sbof_leak.c
// Compile: gcc -o sbof_leak sbof_leak.c -fno-stack-protector
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
char secret[16] = "secret message";
char barrier[4] = {};
char name[8] = {};
memset(barrier, 0, 4); // barrier 배열을 다 NULL로 채우는 함수 memset
printf("Your name: ");
read(0, name, 12);
printf("Your name is %s.", name);
}
위의 코드에서는 3개의 배열이 선언되어 있다.
우리가 메모리 릭을 통해서 얻어낼 내용을 담고 있는 secret
, 값을 입력 받는 name
, 그 사이에 위치한 널바이트로 채워진 배열 barrier
이다. 세 배열은 선언된 순서대로 스택에 쌓이게 된다.
그런데 위의 코드를 보면, read
함수에서 12바이트 만큼 읽어와서 name
배열에 채우게 되어있다. name
배열의 크기는 8바이트로 선언되어 있으므로 12바이트 만큼 읽어서 name
배열에 채우면 그 밑에 위치한 barrier
배열의 값까지 침범하게 된다. barrier
배열을 채우고 있는 널바이트를 모두 제거한다면, name
에 입력된 문자열의 끝을 인식하지 못할 것이고, name
배열의 값을 출력하면, name
배열부터 barrier
배열을 거쳐 secret
배열까지 저장된 값을 모두 밷어낼 것을 예상해 볼 수 있다.
위의 코드 해석을 기반으로 값을 채워보면, barrier
의 값을 널바이트가 아닌 값으로 채움으로써 저장된 secret
배열의 값까지 출력 시킬 수 있다. name
과 barrier
에 들어가는 값은 중요하지 않다. (내가 처음 보안 공부를 할 때, 왜 A로 채우지? 왜 B지? 와 같은 의문을 가졌었기에 혹시 몰라서 쓴다..ㅎㅎ)
// Name: sbof_ret_overwrite.c
// Compile: gcc -o sbof_ret_overwrite sbof_ret_overwrite.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char buf[8];
printf("Overwrite return address with 0x4141414141414141: ");
gets(buf);
return 0;
}
위의 코드를 보면, gets
함수가 buf
에 들어갈 문자열을 입력받을 때, buf
의 크기인 8바이트 이상으로 값을 받아 채운다면, 오버플로우가 일어날 것이다. 이때, main
함수의 반환 주소를 담는 공간에 내가 흐름을 바꾸길 원하는 공간의 주소를 담는다면, main
함수가 끝날 때, 내가 원하는 곳으로 실행 흐름이 바뀔 것이다.
위의 코드에서는 return address를 0x4141414141414141
로 채우라고 한다. 0x41
은 문자로 A
이다. (아스키코드표 참고) 그러므로 return 주소를 저장하는 ret
공간을 A
로 채우면, main
의 반환 주소는 우리가 원하는 대로 0x4141414141414141
이 된다.
buf
와 sfp
의 값은 무관하다.