만족

[정보보안] 코드 보안: 계정과 권한, 프로그램 실행 구조 본문

[정보보안] 코드 보안: 계정과 권한, 프로그램 실행 구조

기타 CS 이론/IT Security Satisfaction 2021. 10. 17. 19:04

Cucoo's Egg

뻐꾸기는 직접 새끼를 기르지 않고 다른 새의 둥지에 알을 옮겨 그 새가 키우게 한다.

 

그것처럼 다른 시스템에 프로그램(뻐꾸기 알)을 옮겨두고 추후 실행되었을 때 공격자가 원하는 동작을 하게 하는 공격 방법이다.

 

공격자는 대상 시스템의 Shell 권한 취득을 목적으로 한다.

 

공격자는 Shell 권한을 취득하기 위해,

Shell을 실행시키는 코드를 작성하고 대상 시스템의 취약점이 있는 프로그램을 찾아낸 후,

그 프로그램에 강제로 오류를 일으켜 시스템이 공격자의 프로그램을 실행시키게 만든다.

 

어떻게 이것이 가능한가?

 

하나씩 알아보자.

 

리눅스/유닉스의 계정

먼저 리눅스/유닉스 시스템에서 사용하는 주요 보안 개념에 대해 알아보자.

 

리눅스에서는 사용자 타입이 "일반 사용자/루트 사용자" 두 가지가 존재한다.

 

 이 계정 정보들은 /etc/passwd 파일에서 확인할 수 있다.

[사용자계정명]:[해시된패스워드]:[사용자번호]:[그룹번호]:[사용자명]:[홈 디렉터리]:[쉘 위치] 로 표현되며

root:x:0:0:root:/root/:/bin/bash의 의미는 아래와 같다.

 

사용자계정명(로그인 시 사용하는 이름)이 root이고

해시된패스워드는 x이며

사용자번호가 0이고, 그룹번호는 0이며

사용자의 이름(쉘에서 표시되는 이름)은 root이고

접속 시 홈 디렉터리는 /root이고

배시 사용 시 /bin/bash가 실행된다.

 

사용자 번호는 root가 0, 일반 사용자는 500번부터 고유하게 지정된다.

 

리눅스/유닉스의 권한

ls -al

위 명령어로 파일 리스트와 각 파일에 해당하는 권한 정보, 소유자 정보 등을 함께 볼 수 있다.

 

첫 번째 열에서는 파일 권한을 나타낸다.

d/rwx/rwx/rwx 로 나누어 볼 수 있으며,

첫 번째 인수는 디렉터리 여부를 나타내고(파일일 경우 -, 디렉터리면 d)

2,3,4번째 인수는 각각 소유자 권한, 그룹 권한, 일반 권한(소유자가 아니며 같은 그룹에도 속해 있지 않는 유저)을 말한다.

 

r: 읽기, w: 쓰기, x: 실행을 의미하며

만약 rwxr-x----이라면, 소유자는 읽기, 쓰기, 실행이 가능하고, 

소유자와 같은 그룹의 사용자는 실행만, 나머지 사용자들은 아예 사용할 수 없는 상태가 된다.

 

두 번째는 해당 파일에 연결된 링크(바로가기)의 갯수를 의미한다.

 

세 번째는 해당 파일을 생성한 사용자이고,

네 번째는 해당 파일 생성자의 그룹 또는 사용자를 나타낸다.

 

chmod 명령어를 이용해 파일 권한을 수정할 수도 있다.

chmod [NEW_MODE] [TARGET_FILE]

MODE는 위의 rwxrwxrwx를 각 3개의 비트를 8진수로 나타낸다.

rwx----wx는 rwx/rwx/rwx로 구분해 비트로 나타내면 111/000/011이고

이를 8진수로 나타내면 703이 된다.

 

chmod 703 your_file.txt 처럼 사용하여 권한을 수정할 수 있다.

 

리눅스/유닉스의 권한 상승

일부 명령어들은 실행 시 반드시 관리자 권한이 필요하지만, 일반 사용자도 사용할 수 있어야 한다.

이를 위해 SetUid라는 기능이 있으며,

파일 권한에 SetUid 플래그가 있을 경우 접속 계정에 관계없이 파일 실행 중에는 파일의 소유자 권한으로 동작하게 된다.

 

SetUid플래그가 설정된 파일 권한은 소유자 권한 부분의 rwx 부분에서 x 자리에 s가 들어간다.

 

chmod에서 SetUid를 지정하려면 chmod 4755 [FILE] 처럼 앞에 4를 추가로 붙이면 SetUid가 설정된다.

 

리눅스/유닉스의 권한 상승:  공격

공격자는 이 SetUid가 설정된 파일을 찾는다.

find / -user root -perm +4000

=> 소유주가 root이고 권한이 4000 이상인(SetUid가 설정된)파일을 /(루트 디렉터리)에서부터 찾는다.

 

예를 들어, root가 편의성을 위해 루트 계정 없이도 /bin/bash에 접근할 수 있게 setUid를 설정해 두었다면

공격자는 find 명령어로 그 파일을 찾아 일반 사용자 권한으로 실행시켜 root권한으로 /bin/bash를 사용할 수 있게 된다.

 

버퍼 오버플로우 공격

정해진 버퍼보다 더 큰 크기의 데이터를 집어넣어 프로그램의 흐름을 마음대로 바꿔버릴 수도 있다.

 

이를 버퍼 오버플로우 공격이라 하는데,

버퍼 오버플로우에 대해 알아보기 전에 먼저 프로그램 실행 단계에 대해 알아보자.

 

프로그램 실행 시 메모리 구조

코드 (0x...00)
데이터(리터럴 문자열 등)
힙 (↓힙은 주소가 높은 쪽으로 자람)
 
스택 (스택은 주소가 낮은 쪽으로 자람) (0x...FF)

힙에는 동적 할당된 변수(malloc, free)등으로 자라고 줄어든다.

 

스택은 매개변수, 지역변수, 함수호출 등으로 자라고 줄어든다.

 

프로그램 실행 시 메모리 구조: 예제

int function(int a, int b){
  a= a+ b;
  return a;
}

void main(){
  int c;
  c= function(1,2);
}

위 코드를 어셈블리 코드로 변환한 것의 일부는 아래와 같다.

먼저 main에 해당하는 함수 내용이다.

 

1의 pushl %ebp는 스택 베이스 포인터를 스택에 넣는다. (함수가 종료된 후 원래의 스택 프레임으로 복원하기 위함)

=> 스택 베이스 포인터와 스택 탑 포인터를 통해 현재 함수의 스택 프레임 범위를 알 수 있다

2의 movl %esp, %ebp 는 스택 탑 포인터를 스택 베이스 포인터로 옮겨서 스택 프레임을 재지정할 준비를 한다

=> 1,2 으로 스택 프레임 범위가 재지정되었다. 이 과정은 함수 호출 시마다 반드시 발생한다.

 

3의 subl $4, %esp는 스택 탑 포인터를 4만큼 감소시킨다.

 

4,5의 pushl $2, pushl $1은 스택에 2와 1을 넣는다.

이 과정에서 스택 탑 포인터를 각각 4만큼, 총 8만큼 감소시킨다 (스택은 0쪽으로 자란다는 것을 주의하자).

 

현재 베이스 포인터(ebp)의 값이 x라고 하면, 스택 포인터(esp)의 값은 x-4-4-4-4= x-16가 된다.

 

이제 call function이 실행되어 function 라벨을 가진 함수로 점프한다.

 

이때 function 실행이 완료된 후 다음 라인인 addl을 실행하기 위해 addl ... 의 주소를 스택에 추가한다.

 

이제 스택 포인터의 값은 x-16-4= x-20이 된다.

 

function의 7,8번에서도 main과 마찬가지로 스택 프레임을 재지정하는 동작을 한다.

 

8번 라인까지 실행되면 현재 ebp는 ebp=esp=x-20-4= x-24이 된다.

 

9에서는 movl 12(%ebp), %eax 하고 있는데,

12(%ebp)는 %ebp+12를 의미한다.

 

%ebp+12에는 뭐가 있을까?

바로 4번에서 한 pushl $2로 스택에 추가된 2가 들어있다.

=> 2를 %eax로 옮겨온다.

 

다음 라인인 10에서는 %eax와 8(%ebp)= %ebp+8을 더해서 %ebp+8위치에 저장하는 동작을 한다.

%ebp+8에는 5번에서 한 pushl $1로 스택에 추가된 1이 들어있다.

 

따라서 1+2의 결과인 3을 그 위치에 넣는다

(%ebp+8에 저장되어 있던 1이 3으로 바뀜)

 

11번 라인에서 1+2결과가 저장된 8(%ebp)를 %edx로 옮긴다.

 

12번 라인에서는 1+2결과가 저장된 %edx에 있는 값을 %eax에 옮긴다.

 

13번 라인에서 L2로 점프한다.

 

L2에서는 함수를 떠난다.

 

떠난다는 것은 ebp를 호출 전 스택 프레임으로 되돌리는 것을 말한다.

 

그것을 위해 우리는 함수가 호출되자마자 이전 스택 베이스 포인터(ebp)를 스택에 넣었다(7번).

 

ret하면 스택 맨 위에 있는 데이터를 pop하고 그 값을 ebp로 변경하고

한번 더 pop해서 main함수의 call function 다음 라인인 addl의 주소를 가져와 그곳으로 점프한다.

 

esp의 값은 현재 x-16이 되었다.

 

다시 main으로 돌아와서,

 

addl에서 $8, %esp를 하고 있다.

이는 %esp의 값을 8만큼 증가시킨다(스택의 크기를 줄인다).

=> 이제 스택 포인터는 x-16+8= x-8 위치를 가리킨다.

 

다음 라인인 movl %eax, %eax는 의미없는 동작이므로 생략한다.

 

mov %eax, -4(%ebp)에서

%eax에 저장되어 있던 1+2의 결과인 3이 -4(%ebp) 위치로 이동한다.

=> %ebp-4는 우리가 3에서 실행한 subl $4, %esp로 확보한 스택 프레임의 비워진 공간(int c)이다.

 

이후 main도 function과 마찬가지로 leave, ret을 통해 프로그램이 종료되지만 동일한 동작이므로 생략한다.

 

프로그램 실행 구조: 취약점 파악

위에서 프로그램의 함수가 실행되는 모습을 봤다.

 

눈여겨 볼 것은 함수가 끝나면 스택에서 pop을 두 번 진행해서 두 번째 pop에서 꺼낸 위치로 점프한다는 점이다.

 

우리는 이 값을 원하는 값으로 바꿔 임의의 위치로 점프하도록 할 것이다.

 

이것이 어떻게 가능한가?

침범하면 안되는 영역(ret해서 점프할 명령의 주소값)에 침범해 값을 덮어씌울 것이다.

void main(){
  //ret이 선언되었으므로 스택에 빈 공간이 하나 생긴다
  int *ret;
  //현재 esp(스택 포인터)에서 2칸만큼(8)이동했을 때의 주소를 ret에 할당한다
  //ret은 main이 종료되고 ret했을 때 점프할 명령어의 주소를 담고 있는 위치를 가리킨다
  ret= (int*)&ret+2;
}

이 코드에서 ret은 main이 종료되었을 때(ret) 점프할 위치를 담고 있는 포인터이다.

 

이 값을 조작함으로써 원하는 곳으로 프로그램의 흐름을 변경시킬 수 있다.

 

shell은 기계어 코드로, shell을 실행시키는 내용을 담고 있다.

void main(){
  //ret이 선언되었으므로 스택에 빈 공간이 하나 생긴다
  int *ret;
  //현재 esp(스택 포인터)에서 2칸만큼(8)이동했을 때의 주소를 ret에 할당한다
  //ret은 main이 종료되고 ret했을 때 점프할 명령어의 주소를 담고 있는 위치를 가리킨다
  ret= (int*)&ret+2;
  
  *ret= (int*)shell;
}

*ret= (int*)shell; 하면 main의 ret에서 shell코드로 점프하게 되는 것이다.

 

이 원리를 이용해 버퍼 오버플로우 공격을 통해 실행 흐름을 제어할 수 있게 된다.

 

버퍼 오버플로우 공격의 실제 용례는 다음에 알아보도록 하자.



Comments