Skip to content

Day6 Exploit 1

Contents

함수의 시작(스택 메모리 공간 생성하기)

  • 함수가 호출 될 때 이전 함수의 스택 베이스 주소를 복원하기 위해서 rbp 를 push 해서 스택에 저장해둔다. 위와 같은 경우 main 함수가 add 함수를 호출하고 있는데 add 함수가 스택을 사용하기 전에 main 함수에서 사용하던 스택 베이스 주소, 즉 rbp 에 있던 값을 스택에 저장해서 main 함수의 스택을 복원해야 할 때를 대비한다.

    push rbp        ; 이전에 있던 함수의 rbp 를 스택에 저장
    mov rbp, rsp    ; 스택의 베이스 주소를 스택의 탑 주소와 같게 만듦. 이 시점에서 스택의 크기는 최소 단위 8 바이트가 됨
    sub rsp, 0x60   ; 스택의 크기를 0x60 만큼 늘려줌 
    

    stack-return

  • 그래서 일반적인 함수는 위와 같은 어셈블리 코드로 시작된다.

함수의 끝 (스택 메모리 공간 정리하기)

  • 함수가 일을 다 마치고 원래의 함수로 되돌아 가야 할 때는 위와 같은 어셈블리 코드로 늘려 놓은 스택을 되돌린 후 이전 함수의 rbp 를 복원한다. 이를 이해 다음과 같은 명령어를 사용한다.

    leave
    ret
    
  • 늘려진 스택을 줄이고 원래 함수의 스택 베이스 주소를 복원하는 용도로 leave 명령어가 사용된다. leave 명령어는 다음의 코드와 같은 기능을 한다.

    mov rsp, rbp    ; rsp 에 rbp 를 대입해서 늘어난 스택을 다시 크기가 최소 단위인 8 바이트 스택으로 만든다. 
    pop rbp         ; pop 명령으로 그곳에 남아있던 원래의 함수의 스택 베이스 주소를 rbp 에 복원시킨다. 
    

Exploit (1)

지금까지 했던 리버싱의 의미

지금까지 했던 리버싱은 비밀번호를 요구하는 프로그램을 해킹하는 것이었다. 입력한 값의 비밀번호를 판정하는 분기를 역추적하여 분기를 결정하는 조건을 이해하고 그 분기를 조작하는 것인데, 실제 해킹에서도 해커의 목표에 따른 분기점을 역공학으로 분석한 후 분기를 조작하는 것이다.

리눅스 특수 권한

https://eunguru.tistory.com/115

특수 권한 파일의 의미

해커가 서버에 접근하는 것은 웹 취약점이나 어플리케이션 취약점을 통해 접근하는 것이지만 정작 서버에 명령을 내리고 싶어도 권한이 없으면 명령을 내릴 수 없다. 그래서 권한 상승이 필요한데 그 권한 상승의 방법 중 하나가 setuid 가 설정된 파일을 사용하여 명령을 내리는 것이다. 다음 그림을 보자.

program

컴퓨터가 프로그램을 실행할 때는 다음과 같이 실행한 프로그램이 현재 디렉토리에 있는지 확인한다. 만약 없다면 환경 변수의 PATH 의 경로에 있는 디렉토리를 모두 뒤져서 프로그램이 있는지 확인한다.

결국 프로그램이 있다면 이제 실행한 사용자에게 실행권한이 있는지 확인한다. 만약 실행권한이 없다면 프로그램에 setuid 특수권한이 설정되어 있는지 확인한다. 특수권한이 없다면 프로그램 실행은 최종적으로 기각되고 특수권한이 있다면 그 권한으로 프로그램이 실행된다.

해커 입장에서는 이미 권한이 있는 프로그램을 해킹해봤자 아무런 의미가 없다. 자기 자신의 금고를 자기가 따봤자 아무런 의미가 없는 것과 마찬가지이다. 해커는 권한이 상승된 프로그램을 해킹하고 프로그램의 실행흐름을 조작하여 자신이 원하는 어셈블리어들을 실행하길 원한다. 위에서 현금 인출기를 해킹한 예시로 따지면 "현금 인출기 권한을 가진 프로그램" 을 해킹해야지 프로그램의 실행 흐름을 "무차별적으로 돈을 인출하는 어셈블리어를 실행"하는 곳으로 바꿔 버릴 수 있는 것이다.

특수 권한 파일 검색

그러므로 어떤 시스템에 침투하는 것을 성공했다면 먼저 특수 권한이 설정된 프로그램을 찾아보자. 다음 명령어로 특수권한이 설정된 프로그램을 찾을 수 있다.

  • setuid 비트 : find /tmp/test -perm -4000 -ls

  • setgid 비트 : find /tmp/test -perm -2000 -ls

  • sticky 비트 : find /tmp/test -perm -1000 -ls

프로그램의 흐름을 조작하는 가장 기초적인 방법

해킹에 대한 정의를 일단은 리버싱을 통해 프로그램의 작동방식 을 알아낸 후 그 작동방식을 조작하는 것이라고 내리자.

바로 그 작동방식을 조작하는 가장 기초적인 방법 중 하나가 Buffer Overflow(BOF) 이다. 한 마디로 권한이 없는 프로그램을 마음대로 다룰 수 있다는 것인데 BOF 로 어떻게 그것이 가능한지 알아보자.

프로세스가 메모리에 할당될 때 위와 같이 메모리 구조가 형성된다. 만약 버퍼에 입력을 받는 프로그램일 경우 사용자가 버퍼에 입력할 데이터의 길이를 반드시 체크해야 한다. 그렇지 않으면 어떤 사용자가 버퍼에 데이터를 마음대로 넣어서 스택 맨 위에 있는 프로그램의 리턴 주소값 또한 덮어 쓸 수 있 기 때문이다. 리턴 주소를 조작한다는 의미는 프로그램의 흐름을 바꾼다는 뜻이고, 프로그램의 흐름을 맘대로 바꿀 수 있다는 의미는 해킹 할 수 있다는 의미이다.

image

위 그림처럼 사용자가 입력하는 데이터의 길이를 검사하지 않을 경우 사용자가 A 를 한도끝도 없이 입력할 수 있다. 그러면 컴퓨터는 정직하게 그대로 스택에 덮어쓰는데 이 경우 리턴 주소값이 A 의 아스키 코드 값인 0x41 로 덮어쓰여져서 0x4141414141414141 이 될 것이다.


Buffer Overflow

bof1 설명

그런 다음 bof1 유저로 접속합니다. 비밀번호는 bof1 입니다.

그러면 위와 같이 실행파일 bof1 과 소스코드 bof1.c, 그리고 bof2 의 비밀번호가 저장된 파일 bof2.pw 가 있습니다.

그런데 bof2.pw 는 권한이 없어 읽을 수 없습니다.

하지만 고맙게도 bof1 실행파일이 bof2 권한으로 실행되기 때문에 이 프로그램을 잘만 이용하면 bof2.pw 을 읽을 수 있을 것 같습니다.

bof1 의 소스코드를 살펴보니 위와 같습니다. innocent 변수를 KEY 와 비교하고 있는데 KEY0x61616161 로 고정되어 있습니다. innocent 변수를 0x61616161 로 만들기만 하면 프로그램 권한인 bof2 로 쉘이 실행되서 bof2.pw 를 읽을 수 있습니다.

그런데 소스코드에서 gets 함수를 사용하고 있습니다. 대표적으로 gets 함수strcpy 함수는 입력받은 값을 버퍼에 저장할 때 입력 데이터의 길이를 검증하지 않고 단순히 입력 데이터를 스택에 복사하기만 합니다. 그렇기 때문에 정해진 버퍼의 길이를 초과해서 데이터를 전달해도 데이터가 그대로 스택에 복사된다. 이 프로그램은 buf 에 데이터를 gest 함수로 받아 저장하기 때문에 buf 에 저장될 수 있는 한계보다 더 큰 데이터를 입력하여 innocent 변수를 조작할 수 있습니다.

그런데 데이터를 얼마만큼 초과시켜야 buf 의 경계를 넘어서서 innocent 의 값을 조작할 수 있을까요?

buf 와 innocent 사이의 거리

다음과 같이 gets 까지 가서 buf 의 주소값을 확인한다

gets

innocentKEY 를 비교하는 cmp 명령어까지 가서 innocent 의 주소값을 확인한다.

innocent

innocent[rbp - 4] 이다.

innocent_addr

그러면 이제 두 개의 주소값의 차이값을 구한다. 그 차이값이 140 이다.

distance

bufinnocent 와의 거리가 140 인 것을 알았으니 데이터를 얼마나 초과해서 전달해야 innocent 를 덮어쓸 수 있는지 알게 되었다.

innocent 변수 조작하기

프로그램이 입력값을 표준입력으로부터 받는 경우이기 때문에 파이프 | 를 통해서 조작된 입력값을 전달할 수 있다. 보통의 경우 다음과 같이 입력값이 전달된다.

echo twice | ./bof

따라서 innocent 를 덮어쓰기 위해서는 쓰레기 값 140 바이트를 전달한 후 이후에 어떤 값을 입력하면 innocent 변수가 덮어씌워지게 된다.

python -c "print 'x'*140 + 'aaaa'" | ./bof

그런데 innocent 변수를 덮어썼을 때 쉘이 실행되기 때문에 입력 형태를 조금 바꾸어야 한다.

파이프 | 와 입력 스트림

표준 입력을 받는 프로그램은 키보드로부터의 입력이 끝날 때까지 입력을 받고 입력 스트림이 끝났을 때 프로그램을 종료한다. 예를 들어서 bash 라는 쉘 프로그램은 표준 입력(키보드)로부터 입력을 받는데 단일 실행을 했을 때 명시적으로 프로그램을 종료 시켜야지 프로그램이 끝난다. 왜냐하면 일반적으로 표준 입력 스트림은 종료되지 않기 때문이다. 하지만 파이프 | 를 통해 입력 스트림을 표준 입력이 아닌 다른 프로그램의 출력으로 리다이렉트 시키면 다른 프로그램이 끝났을 때 입력 스트림도 종료되기 때문에 프로그램이 종료된다.

echo ls | bash

위의 명령어를 실행해보면 bash 쉘은 원래 실행 되었을 때 입력이 계속되는 한 종료되지 않지만 위와 같이 파이프 | 를 통해서 입력 스트림을 표준 입력에서 echo ls 라는 프로그램의 출력으로 리다이렉트 시킨다면 echo ls 이 종료되었을 때 함께 종료된다. 왜냐하면 입력 스트림도 종료되기 때문이다. 따라서 파이프 | 로 입력을 전달받은 쉘이 종료되지 않기 위하여 입력받은 데이터를 출력해주는 역할을 하는 cat 명령어를 함께 사용해야 한다. 다음의 명령어를 실행해보자.

(echo ls;cat) | bash

그렇기 때문에 만약 innocent 변수를 덮어 썼을 때 루트 권한의 쉘이 얻어진다면 먼저 출력 프로그램과 cat 을 세미콜론과 괄호로 묶고 하나의 프로그램 처럼 여겨지게 해야 한다. 그러면 출력 프로그램이 종료되어도 cat 은 종료되지 않고 출력을 입력으로 리다이렉트 시켜주기 때문에 입력 스트림이 종료되지 않는다. 그래서 다음과 같이 innocent 를 덮어쓰면 된다.

(python -c "print 'x'*140 + 'aaaa'";cat) | ./bof

bof2 권한으로 쉘이 열리면 bof2.pw 를 읽으면 된다.

만약 메인함수 인자로 입력을 받는다면?

  • 이 경우는 main 함수에서 argv[1] 의 값을 가져와서 strcpy 함수를 사용하여 buf 에 복사하는 경우이다. 보통 다음과 같이 입력값이 전달된다.

    ./bof aaaaaaaaaa

  • 그래서 인자에 쓰레기 값 140 바이트를 전달하면 innocent 변수 직전까지 닿을 것이고 그 140 바이트 이후에 어떤 값을 입력하면 innocent 변수를 덮어쓸 수 있게 된다.

    ./bof `python -c "print 'a'*140 + 'xxxx'"`
    

bof2 ~ bof4 풀어보기

  • bof2 의 비밀번호를 알아내었으니 bof2 유저로 접속하고 bof3 의 비밀번호를 파헤쳐보세요.

  • 그리고 bof4 까지 풀어보세요.

  • 정답은 각각의 소스코드에 주석처리 되어 있습니다.


과제

HW.md 파일에 따라 과제를 하시면 됩니다. (발표를 하며 설명을 할 수 있어야 합니다)