본문 바로가기

5. 방학 활동/개념 정리

[2021.07.26] 포너블팀 나동빈-시스템해킹 강의 정리

# 어셈블리어로 Hello World 출력하기

nano는 대표적인 editor 프로그램. 어셈블리 소스코드를 생성해줌.

어셈블리어 입력

 

# 레지스터의 용도와 시스템 콜 이해하기

  • rax는 가장 중요한 레지스터중 하나. 시스템콜의 실질적인 번호를 가리키는 레지스터이자, 함수가 실행이 된 다음에 결과가 담기는 레지스터
  • rbx 메모리 주소를 지정할 때 사용
  • rcx 카운터 레지스터로서 반복문에서 많이 사용함
  • rdx 데이터 레지스터로서  연산을 수행할 때 rax와 함께 많이 사용
  • rax부터 rdx까지를 데이터 레지스터라고 많이 부름.

 

이 네 개는 포인터 레지스터라고 부름. 특정한 주소를 가리키는 레지스터 4개를 정의해 놓은 것.

  • rsi 메모리를 이동하거나 비교할 때 출발지 주소를 가리킴
  • rdi 메모리를 이동하거나 비교할 때 목적지 주소를 가리킴
  • rbp 함수의 파라미터나 변수의 주소를 가리킬 때 많이 사용
  • rsp 스택에 삽입 및 삭제 명령에 의해 변경되는 스택에서 가장 위에 있는 주소를 가리키는 레지스터
  • r8부터 r15까지는 일반적으로 함수의 매개변수로써 많이 사용됨.

시스템 콜

구글에 64bit system call table 이라고 치면 많은 자료들이 나옴.

  • rax에 0이 담겨있다면 system read 함수를 사용한다는 의미
  • 각각의 함수에는 기본적으로 매개변수가 따라옴 (%rdi 등)
  • 대표적으로 write같은 경우 rdi가 fd(파일 디스크립터)라는 역할을 한다는 것을 알 수 있음
  • rsi에는 write로 출력을 할 때 어떤 문자열을 출력할지 저장
  • rdx는 출력할 문자열의 길이

"Hello World"라는 문자열을 만들어 놓고, 그 문자열의 위치를 가리키는 포인터 msg를 하나의 변수로 마련한 다음,

text 코드 영역에 가장 첫번째로 실행되는 start라는 함수를 정의함.

  • rax에 1을 넣어 system write라는 system call을 불러옴.
  • rdi에 1을 넣어 어떤 문자열을 출력하겠다고 system call 함수에 매개변수로 넣어줌
  • rsi에 정확히 어떤 문자열을 출력할건지 msg라는 포인터 변수를 넣어줌
  • rdx에 "Hello World" 문자열을 출력할 수 있도록 12라는 길이를 넣어줌

이렇게 하나의 system call을 불러올 수 있는 준비를 마침.

결과적으로 syscall을 불러옴으로써 "Hellow World"가 콘솔창에 출력되는 것.

syscall 밑에 있는 것들은 무엇일까?

60은 프로그램을 종료하겠다는 의미.

rdi에는 error code를 넣어주면 되는데, rdi에 0을 넣음으로써 안전하게 종료할 수 있도록 마련해준 것.

즉, 첫번째 syscall은 "Hello World"를 출력해주는 것이고, 두번째 syscall은 프로그램을 종료하는 시스템콜

 

# 메모리 구조 이해하기

 

32bit 운영체제

하나의 프로그램이 실행돼서 프로세스가 띄워지면 실제로 메인 메모리 상에서 하나의 segment는 이러한 구조를 가짐

Stack

스택 영역 (후입선출)

함수, 지역변수 정보를 포함하고 있음

함수를 호출할 때마다 메모리 상에 있는 stack 영역에 쌓이게 됨.

이 부분에서 가장 취약점이 많이 노출되고, buffer overflow 공격들을 실행할 수 있음

Heap

동적으로 할당되는 변수의 데이터들이 위치하는 공간

malloc() 함수로 동적 할당을 할 수 있는데, 이걸로 할당된 모든 변수들은 Heap 공간에 정의됨

BSS

프로그램에서 사용될 변수들이 실제로 위치하는 영역

아직 초기화가 이루어지지 않은 변수들을 말함

Data

초기화가 이루어진 변수들

Text(Code)

실제로 우리가 작성한 소스코드가 들어가는 공간

코드는 시스템이 알아들을 수 있는 실질적인 명령어

모두 기계어 코드로써 컴파일러가 만들어놓은 코드라고 할 수 있음

프로그램을 실행하게 되면 어셈블리

 

 

# 스택 프레임(Stack Frame) 이해하기

sum.c

컴파일

 

더보기

* 메모리 보호기법 SSP(Stack Smashing Protector)

vi로 어셈블리코드 열어보기

 

프로그램이 실행되면 가장 먼저 main 함수가 실행되는데, main 함수에서 곧바로 sum이라는 함수가 실행됨. 매개변수로 1, 2를 넣어서 a+b 반환

 

main()

main 함수가 불러지면 가장 아래쪽에 RET(return address)가 들어가게 됨

 

RET

특정한 함수가 끝난 뒤에 돌아갈 장소를 의미

프로그램 실행했을 때 가장 먼저 실행되는 함수인 start() 함수가 main()을 불러오고,

main이 끝나게 되면 다시 main()을 불러온 위치로 돌아가게 되는데, 그 돌아가는 위치가 RET

 

RBP

스택이 시작하는 베이스 포인터를 의미

여기서부터 스택이 위쪽으로 차례대로 쌓일 것이라는 것을 알려주는 첫 번째 공간

 

sum()

sum() 호출 이후 stack frame에 데이터가 차곡차곡 쌓이게 됨

함수를 불러올 때 그 함수의 매개변수가 RET의 아래쪽에 들어가게 됨

위쪽의 RET는 sum() 함수를 불러온 뒤 다 실행하고 나서 돌아가는 위치를 말함

시간이 지나서 sum() 함수가 리턴을 하면 맨 위(버퍼)부터 하나씩 사라지게 됨

변수 c에는 위에서 실행한 부분에 대한 결과 값인 3이 담김

 

컴파일하여 sum을 실행해보면 값이 아무것도 안나옴

return 3이 마지막으로 수행되기 때문에 아무것도 출력되지 않는 것

 

3 출력 확인

결과적으로 이렇게 stack frame에 쌓였다가 다시 줄어드는 방식으로 마지막으론 RET를 반환하게 되는 것이 stack frame의 작동 원리

가장 중요한 부분이 RET인데, RET는 함수가 실행된 다음에 돌아가는 주소

이 RET를 바꿔서 특정 함수가 끝나고 돌아가는 위치를 해커가 임의로 조작함으로써 서버를 제어할 수 있음. 대표적인 공격이 버퍼 오버플로우

 

어셈블리 코드 분석하기

main() 함수를 보면 위에서 뭔가를 하다가 결과적으로 sum()이라는 함수를 불러오는 것을 볼 수 있음

 

sum() 함수 불러오기 전까지 확인

맨 처음 main() 함수가 불러와짐과 동시에 RET가 자리잡게 되는 거고,

그 위에 push를 했으니까 RBP가 들어감.

RBP=RSP (둘이 동일한 위치를 가리킴)

이 상태에서 RSP에서 16을 빼니까 16만큼 jump를 하게 됨

→ 16만큼 공간을 확보해서 이 안에서 어떠한 코드를 작성하겠다는 의미

프로그램 최적화를 위해 32bit인 ESI, EDI를 사용했지만, 작동하는 원리는 RSI RDI와 같다고 생각하면 됨

 

sum() 불러오기

RSP가 16만큼 공간을 확보했었음

sum()을 불러왔기 때문에 RET가 맨 처음에 들어가게 되고,

push RBP를 했기 때문에 그 위에 RBP가 들어감

RBP - 4의 위치에 EDI 값(1)을 넣음

RBP - 8의 위치에 ESI 값(2)을 넣음

RBP - 4의 위치, 즉 1을 EDX라는 레지스터에 넣음

EDX의 값을 EAX에 더해줌(2+1=3) → 결과적으로는 3이라는 값을 얻음

RBP를 다시 빼줌으로써 RET로 다시 돌아감(sum()을 불렀던 위치로)

 

EAX(RAX)같은 경우는 특정한 함수가 끝날 때 그 반환 값을 가지고 있는 레지스터를 가리킴

 

sum() 함수가 끝나면 다시 이런 상황이 됨

main()에서 sum()이 실행 후 그 뒤에 실행해야 할 때 스택 프레임

 

이 상태에서 EAX 값을 RBP-4에다가 둠

다시 3이라는 값을 EAX에다가 넣고, leave를 하고, 마지막으로 RET를 하여 최종적으로 3이라는 값이 반환되는 것

 

 

# 어셈블리어로 에코 프로그램 만들기

에코 프로그램: 내가 입력한 어떤 문자열을 그대로 재출력해주는 프로그램 → 기본적인 입출력 다루는 것에 의의를.

 

nano echo.s

둘이 같은 의미 (rax에 0을 넣는다는)

 

다 0으로 초기화해준다는 의미

 

64를 빼줌으로써 64만큼의 공간을 확보

 

rdi에 0을 넣어 읽을 수 있게 해줌

총 63개만큼 문자가 담길 수 있도록 systemcall을 한 것

 

1을 넣어주어 출력할 수 있도록 함

 

rax 60은 프로그램 종료를 의미

완성

 

 

 

# 어셈블리어 기본 문법

 

 

# 어셈블리어로 반복문 구현하기

loop.s

"A"라는 문자가 출력되게 하는 프로그램

 

반복문을 활용하여 "A"라는 문자를 여러번 출력하게 만들 것임

CMP 두개의 변수를 비교

CMP가 존재하면 je 등의 명령어가 나옴

r10과 100이 동일하면 done 함수로 이동하는 방식

inc는 1 증가하는 명령어

 

반복문을 작성할 때는 반드시 빠져나가는 조건을 만들어놔야 함

 

 

# 디버깅 시작하기(strace, pwndbg 활용법)

전에 만들었던 echo.s 활용

strace -ifx ./echo

프로그램을 실행하면서 내부적으로 어떤 시스템콜을 호출해서 출력이 이루어지는지 하나씩 보여줌

HelloWorld를 입력하면 앞에 아스키코드로 "HelloWorld"가 들어가고, 뒤에는 null 값이 넣어지는것을 알 수 있음

63바이트만큼 입력을 받기 때문에

 

gdb echo

b * _start

r

ni

 

# 어셈블리어로 별 피라미드 만들기(1)

0x0a는 줄바꿈을 의미

cl은 한바이트만 가져온다는 의미

 

pyramid.s

입력 받은것을 한글자라고 가정한것(1byte만 저장하는것)

 

16진수 0x30은 문자 '0'이므로 30을 빼주는 것

 

# 어셈블리어로 별 피라미드 만들기(2)

별 피라미드를 cpp로 코딩하기

#include <stdio.h>

int main(void){
	int n = 7,i,j;
	for(int i=0; i<n; i++){
		for(int j=0; j<=i; j++){
			printf("*");
		}
		printf("\n");
	}
	return 0;
}

피라미드는 이렇게 커졌다가 다시 작아져야 하는 형태가 되어야 함( > ) 이런 모양

 

그럼 코드도 이렇게 짜면 됨

#include <stdio.h>

int main(void){
	int n = 7,i,j;
	for(int i=0; i<n; i++){
		for(int j=0; j<=i; j++){
			printf("*");
		}
		printf("\n");
		}
	for(int i=n-1; i>0; i--){
		for(int j=0; j<=i; j++){
			printf("*");
			}
		printf("\n");
	}
	return 0;
}

이제 이걸 어셈블리어로 만들어 볼 것임.

 

일단 증가하는거 먼저 테스트해봤는데 정상작동 했음

section .data
        STAR db '*'
        EMPTY db 0x0a

section .text
        global _start

_start:
        mov rax, 1 ;write system call setting
        mov rdi, 1 ;default output mode
        mov rdx, 1 ;output length setting
        mov r10, 0 ;index
        mov r9, [rsp+16] ;find the entered strings

        cmp r9, 0 ; not input
        je _done ; program exit

        mov cl, [r9] ;only 1byte stored at cl
        movzx r9, cl ;string type cl -> stored r9
        sub r9, 0x30 ;index

        mov r8, r9
        xor r9, r9
        call _syscall

_small:
        cmp r10, r9
        je _up;
        mov rsi, STAR ;output star
        syscall ;output
        mov rax, 1 ; write system call setting
        inc r10 ; j 1 increase
        jmp _small ; output agian

_up:
        cmp r9, r8 ; i==n
        je _down ; down function execution
        mov rsi, EMPTY ; changing the line setting
        syscall ; output function execution
        mov rax, 1 ; output mode
        mov r10, 0
        add r9, 1;
        jmp _small
_down:
        cmp r9, 0 ; i==0
        je _done ;
        mov rsi, EMPTY ; changing the line setting
        syscall ;
        mov rax, 1 ; write system call
        mov r10, 0 ; j initialize
        sub r9, 1 ; i decrease 1
        jmp _big ; output again

_big:
        cmp r10, r9 ; i==j
        je _down
        mov rsi, STAR ; star output setting
        syscall
        mov rax, 1
        inc r10
        jmp _big

_done:
        mov rax, 60
        mov rdi, 0
        syscall

_syscall:
        syscall
        ret

intel 방식의 어셈블리어를 이용하여 별 피라미드를 출력할 수 있었음.