DreamHack - System Hacking (진행중)

곽무경·2022년 7월 20일
0

System Hacking

목록 보기
26/27

리눅스 프로세스의 메모리 구조

  • 코드 세그먼트
    실행 가능한 기계 코드가 위치하는 영역, 읽기 권한과 실행 권한이 부여된다.
  • 데이터 세그먼트
    컴파일 시점에 값이 정해진 전역 변수/상수들이 위치하는 영역, 읽기 권한이 부여된다.
    • data 세그먼트 : 값이 변할 수 있는 데이터가 위치하는 영역
    • rodata 세그먼트 : 값이 변하지 않는 데이터가 위치하는 영역
  • BSS 세그먼트
    컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 영역, 읽기 권한이 부여된다.
    프로그램이 시작될 때 모두 0으로 초기화된다.
  • 스택 세그먼트
    함수의 인자, 지역 변수 등이 위치하는 영역, 읽기와 쓰기 권한이 부여된다.
    스택 프레임이라는 단위로 사용된다. 함수가 호출될 때 생성, 반환될 때 해제
  • 힙 세그먼트
    동적으로 할당된 데이터가 위치하는 영역, 읽기와 쓰기 권한이 부여된다.
    C언어의 malloc(), calloc() 등을 호출해서 할당받는 메모리가 이 세그먼트에 위치

컴퓨터 구조 (x86-64)

컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고, 이들을 구성하는 방법

  • 폰 노이만 구조
    • 중앙처리장치 : 프로그램의 연산을 처리, 시스템을 관리
      ALU, CU, Register
    • 기억장치 : 컴퓨터의 동작에 필요한 여러 데이터를 저장
      주기억장치(RAM), 보조기억장치(SSD, HDD)
    • 버스 : 컴퓨터-컴퓨터, 부품-부품 사이에 신호를 전송하는 통로
      데이터 버스, 주소 버스, 제어 버스, 랜선, 프로토콜, ...
  • 명령어 집합구조 - x86-64 아키텍처
    x64 아키텍처 : 인텔의 64비트 CPU 아키텍처 (CPU가 한번에 처리할 수 있는 데이터의 크기)
    • 범용 레지스터 : 다양한 용도로 사용되는 레지스터 - 64비트(32비트, ...)
      • rax(eax, ax, ah, al) : 함수의 반환값
      • rbx(ebx, bx, bh, bl)
      • rcx(ecx, cx, ch, cl) : 반복문의 반복 횟수, 각종 연산의 시행 횟수
      • rdx(edx, dx, dh, dl)
      • rsi(esi, si) : 데이터 이동 시 원본 포인터
      • rdi(edi, di) : 데이터 이동 시 목적지 포인터
      • rsp(esp, sp) : 사용중인 스택 위치 포인터
      • rbp(ebp, bp) : 스택 바닥 포인터
    • 세그먼트 레지스터 : cs, ss, ds, es, fs, gs
      fs에는 Stack Canary도 포함
    • 명령어 포인터 레지스터 : 어느 부분의 코드를 실행할 지 가리키는 포인터 (rip)
    • 플래그 레지스터 : 프로세스의 현재 상태를 저장하는 레지스터
      CF(Carry Flag), ZF(Zero Flag), SF(Sign Flag), OF(Overflow Flag)

x64 어셈블리 언어

명령어, 피연산자로 구성된다.
피연산자 : 상수, 레지스터, 메모리
메모리 피연산자는 []으로 둘러싸이고, 앞에 크기 지정자가 추가될 수 있다.
QWORD(8바이트), DWORD(4바이트), WORD(2바이트), BYTE(1바이트)
QWORD PTR [0x8048000] : 0x8048000의 데이터를 8바이트만큼 참조

  • 데이터 이동

    • mov : 어떤 값을 레지스터, 메모리에 옮기는 명령어
    • lea : 유효 주소를 저장
  • 산술 연산

    • add, sub, inc(++), dec(--)
  • 논리 연산 (비트 단위)

    • and, or, xor, not
  • 비교

    • cmp : 두 피연산자를 빼서 비교
    • test : 두 피연산자에 AND를 취해서 비교
  • 분기

    • jmp : rip 이동
    • je : jump if equal (직전에 비교한 결과가 같았다면)
    • jg : jump if greater
  • 스택

    • push : 스택 최상단에 데이터 저장
    • pop : 스택 최상단의 값을 꺼내서 대입
  • 프로시저

    • call : 함수 호출
    • leave : 스택프레임 정리
    • ret : 반환주소로 반환
  • 시스템 콜

    • syscall : 함수
      인자 순서 : rsi → rdi → rdx → rcx → r8 → r9 → 스택
    syscallraxrdirsirdx
    read0x00fdbufcount
    write0x01fdbufcount
    open0x02filenameflagmode
    close0x03fd
    evecve0x3bfilename??

GDB

  • start : 진입점부터 프로그램 분석
  • break(b) : 중단점 지정
  • continue(c) : 다음 중단점까지 실행
  • run(r) : 단순 실행(중단점이 없다면 프로그램 끝까지)
  • disassemble, u, disas, nearpc, pdisassemble : 함수 디스어셈블
  • ni : next instruction, 다음 명령어 한줄 실행
  • si : step into, 다음 명령어를 한줄 실행하되, 서브루틴 있다면 내부로 진입
  • finish : 서브루틴에 진입했을 때 함수 끝까지 한 번에 실행
  • x : 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩
    포맷 지정자 : o(8), x(16), d(10), i(instruction), s(string), ...
    크기 지정자 : b(byte), h(halfword), w(word), g(giant, 8 bytes)
  • tele : 메모리 덤프 기능
  • vmmap : 가상 메모리 레이아웃(매핑 영역까지)

pwntools

  • process : 로컬 바이너리를 대상으로 익스플로잇 (주로 디버깅)
  • remote : 원격 서버를 대상으로 익스플로잇 (실제 공격)
  • send : 데이터를 프로세스에 전송
    • send('A') : "A"를 전송
    • sendline('A') : "A\n" 를 전송
    • sendafter('hello', 'A') : 바이너리가 "hello"를 출력하면 "A" 전송
    • sendlineafter('hello', 'A') : 바이너리가 "hello"를 출력하면 "A\n" 전송
  • recv : 프로세스에서 데이터를 수신
    • recv(1024) : 최대 1024바이트까지 수신
    • recvline() : 개행 문자를 만날 때까지 수신
    • recvn(5) : 5바이트 수신
    • recvuntil('hello') : "hello" 까지 수신
    • recvall() : 전부 수신
  • p64 : packing (p32, p16, p8) 리틀 엔디언의 바이트 배열로 변경
  • u64 : unpacking (u32, u16, u8) 역의 과정
  • interactive : 셸을 획득했거나 특정 상황에 직접 입력을 주고 싶을때 사용
  • ELF : 바이너리의 ELF 헤더 정보 가져오기
e=ELF("./test")
e.plt['read'] / e.got['read'] / ...
  • context.log_level : 로깅 기능
context.log_level='error' : 에러만 출력
context.log_level='debug' : 오고 가는 모든 데이터 출력
context.log_level='info' : 비교적 중요한 정보들만 출력
  • context.arch : 아키텍처 지정
    amd64 (x86-64), i386(x86), arm(arm)
  • shellcraft : 셸코드 생성
  • asm : 셸 코드를 기계어로 어셈블

함수 호출 규약

함수의 호출 및 반환에 대한 약속

  • x86 : cedcl, stdcall, fastcall, thiscall
    • cdecl
      • 레지스터의 수가 적어 스택을 통해 인자를 전달 (마지막부터 첫 번째까지 거꾸로 전달)
      • 호출자가 스택을 정리
  • x86-64 : System V AMD64 ABI / MS ABI의 Calling Convention
    • System V AMD64 ABI의 Calling Convention
      • rdi, rsi, rdx, rcx, r8, r9, 스택을 이용하여 인자를 전달
      • 호출자가 스택을 정리

지역 변수가 선언되었더라도, 반환의 용도로만 사용되면 스택에 할당하지 않음

라이브러리

  • 컴퓨터 시스템에서 프로그램들이 함수나 변수를 공유해서 사용할 수 있게 함
    printf, scanf, malloc 등등의 C 함수들이 라이브러리에 포함
    리눅스에는 C의 표준 라이브러리인 libc가 탑재되어 있음

링크

  • 호출된 함수와 실제 라이브러리의 함수가 연결되는 과정
  • 동적 링크 (Dynamic Link)
    • 동적 라이브러리가 프로세스의 메모리가 매핑
      라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고,
      그 함수를 실행
  • 정적 링크 (Static Link)
    • 바이너리에 정적 라이브러리의 모든 함수가 포함 (바이너리의 용량이 매우 커짐)

PLT & GOT

  • 라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블
    라이브러리 함수를 호출하면, 함수의 이름을 바탕으로 심볼을 탐색하고
    해당 함수의 정의를 발견하면 그 주소로 $rip을 이동 (runtime_resolve)

  • 함수가 처음 호출되면 GOT에는 PLT 내부의 주소가 적혀 있는 상태
    첫 호출 과정에서 runtime_resolve가 실행되면서, 해당 함수의 주소가 GOT에 써짐
    이후에 그 함수를 호출하면 GOT에 써져 있는 주소를 바탕으로 함수가 바로 실행됨
  • PLT : 라이브러리의 심볼 주소
  • GOT : 바이너리에서 매핑된 함수 주소
  • 함수가 여러 번 호출될 때, GOT의 값을 검증하지 않으므로 보안상의 취약점이 존재
    → 어떤 함수의 GOT를 공격자가 원하는 코드가 실행되게끔 변경할 수 있음 (GOT Overwrite)

Exploit

익스플로잇 : 상대 시스템을 공격하는 것

1. Shellcode

셸코드 : 익스플로잇을 위해 제작된 어셈블리 코드 조각 (일반적으로 셸을 얻기 위함)

  • orw 셸코드 (open-read-write)
    파일을 열고, 읽고, 출력해주는 셸코드
  • execve 셸코드
    임의의 프로그램을 실행하는 코드
    /bin/sh 를 실행하면 셸을 획득할 수 있다.

2. Stack Buffer Overflow

스택의 버퍼에서 발생하는 오버플로우
버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터가 변조되어 문제가 발생할 수 있다.
scanf, read, gets 등에서 발생할 수 있다.

2-1. Stack Canary 우회

  • Brute Force (무차별 대입)
  • TLS 접근

2-2. Return to ShellCode

셸 코드와 Return Address Overwrite를 이용하여 셸 획득
버퍼에 셸 코드를 넣고, Return Address를 버퍼로 덮어쓰기

3. Return to Library (NX를 우회하는 공격 기법)

  • 실행 권한이 남아있는 코드 영역(바이너리의 코드 영역, 라이브러리의 코드 영역)으로
    반환 주소를 덮는 공격 기법
  • ASLR이 적용되어 있더라도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정되므로
    라이브러리의 베이스 주소를 몰라도 라이브러리 함수를 실행할 수 있음
  • Return gadget을 이용 (ret로 끝나는 어셈블리 코드 조각을 계속 연결)
  • 64비트의 경우 가젯-인자-함수 순으로 배치
  • 가젯은 ROPgadget을 이용하여 찾을 수 있음

Mitigation (Exploit에 대한 보호 기법)

1. Stack Canary

  • 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법
  • 변조가 확인되면 프로세스를 강제로 종료 (stack smashing detected)
  • x86-64 아키텍처에서 Stack Canary 값은 fs:0x28의 값
  • 카나리 값은 프로세스가 시작될 때 TLS에 전역 변수로 저장되고, 각 함수마다 이 값을 참조

2. NX & ASLR

  • ASLR
    • 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법
    • 커널에서 지원하는 보호 기법
      cat /proc/sys/kernel/randomize_va_space 로 적용 여부 확인 가능
      0(적용하지 않음), 1(스택, 힙, 라이브러리, vdso 등), 2(1 + brk로 할당한 영역)
      brk : 데이터 세그먼트의 영역을 확장시켜 주는 syscall
    • main 함수를 제외한 다른 영역의 주소들은 실행할 때마다 바뀜
    • 라이브러리 매핑 주소로부터 다른 심볼들까지의 오프셋은 항상 동일
  • NX
    • 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법
    • NX가 적용되면 코드 영역 외에는 실행 권한이 부여되지 않음
      → Return to Shellcode와 같은 공격 기법을 사용하지 못함(스택에 실행권한이 없음)
    • checksec을 이용해 적용 여부 확인 가능(컴파일러 옵션에 의해 적용)

0개의 댓글