1. 개념
BROP: 소스코드 및 바이너리가 주어지지 않은 상태에서 프로세스의 상태 또는 출력내용으로 공격을 수행하는 기법
= ROP Chain + Brute Force
무작위 대입 과정이 있어서 시간이 오래걸림..
이 기법을 이해하기 앞서 다음 두 가지 가젯에 대해 이해해야 함
STOP Gadget : main() 함수 또는 취약한 함수를 다시 실행하는 주소 (_start 주소일 수도 있고 정말 함수의 시작주소 일 수도 있으나 결과적으로 복귀주소를 이 가젯으로 변경하면 프로세스는 처음 실행했을 때의 출력값을 보인다.)
BROP Gadget : 보통 libc_csu_init 함수에 있는 pop 6개 + ret 가젯을 의미하는데, 이처럼 레지스터를 많이 쓸 수 있는 희소한 가젯을 일컫는다. 이 가젯을 찾는 이유는 주변에 0x9 그리고 0x7 오프셋으로 pop rdi ; ret 가젯과 pop rsi ; ret 가젯을 찾을 수 있기 때문이다. 두 가젯은 모두 system() 함수에서 첫번째 인자로 /bin/sh 를 넣기 위해 사용된다.
2. 공격 순서
정말 아~~무것도 주어지지 않았기 때문에 메모리 릭부터 가젯 구성에 필요한 주소까지 전부 알아내야 한다.
단계를 나름 나눠봤는데 9단계나 된다;; (무엇?)
(1) 스택 오버플로우 크기 확인
복귀주소를 변경하기 때문에 당연히 스택에서 오버플로가 날 것을 예상할 수 있다. 간혹 CTF 에서 이 크기를 알려준다고 한다.
파이썬 스크립트로 brute forcing 해서 프로세스가 죽거나 기존의 출력결과와 다른 출력값을 낸다면 스택 오버플로를 의심할 수 있다.
(2) STOP Gadget 찾기
오버플로우 크기를 알고 있기 때문에 "A" * SIZE + CodeAddress 를 페이로드로 공격했을 때, 처음 출력값이 나오면 CodeAddress 가 STOP Gadget 이 된다.
** Code 영역을 알아야 하니 checksec 같은 툴을 써서 No PIE 인지 확인하면 될 것
** 코드 세그먼트 크기가 0x1000 이므로, 보통 오프셋은 0 ~ 0x1000 이다.
당연히 Code Base Address 부터 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림
(3) BROP Gadget 찾기
pop 6개 + ret 가젯을 찾는 과정이기 때문에 페이로드는 "A" * SIZE + CodeAddress + PARAM * 6 + STOP_GADGET 이 된다. PARAM * 6 은 더미로 채우는데 32비트면 4바이트 * 6 = 24 바이트 일 것이고 64비트면 8바이트 * 6 = 48바이트 가 된다.
공격했을 때 처음 출력내용이 그대로 나오면 CodeAddress 는 BROP Gadget 이 된다.
(pop 6개 + ret 이 성공했으니 처음 출력내용이 나오는거니까)
** pwntools 에 p32() 랑 p64() 쓰면 편하게 계산가능하다.
이 가젯을 찾는 이유는 근처에 pop rdi ; ret (offset: 0x9) 과 pop rsi ; ret (0x7) 가젯이 있어서라 했다.
따라서 여기서 얻은 CodeAddress + 0x9 또는 CodeAddress + 0x7 주소를 실제로 사용한다.
당연히 Code Base Address 부터 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림
(4) Printable Function 주소 찾기
출력함수는 C 언어에서 printf 와 puts 함수가 있는데 둘의 차이는 출력했을 때 개행 문자 "\n"가 출력되는지이다.
printf 일 경우 위에서 설명한 가젯 2개를 모두 사용 --> pop rdi ; ret (BROP Gadget +0x9) 과 pop rsi ; ret (+0x7)
puts 일 경우 하나만 사용 --> pop rdi ; ret (BROP Gadget +0x9)
보통 puts 함수를 쓸거라 생각하고 brute forcing 을 한다.
페이로드는 "A" * SIZE + POP_RDI_GADGET + CodeBase + CodeAddress 이다.
CodeBase 는 코드 영역의 시작주소이고, CodeAddress는 Base 부터 오프셋 범위(0 ~ 0x1000) 내로 1씩 증가시킨다. 위 페이로드로 공격했을 때 출력결과가 "\x7fELF" 문자열을 포함한다면 CodeAddress 는 puts@plt 가 된다.
** 리눅스 실행파일은 ELF 구조라 무조건 코드영역의 첫 4바이트는 "\x7fELF"이다. 그래서 CodeBase를 인자로 주고 puts 인지 아닌지 알아낸다.
당연히 Code Base Address 부터 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림
(5) 메모리 덤프
자 이제 puts@plt 를 알아냈으니 출력함수를 사용할 수 있다. 인자로 메모리 영역만 넣어주면 바이너리 정보를 제대로 얻어낼 수 있는 거다.
코드 영역만 필요하니 CodeBase ~ CodeBase + 0x1000 범위를 출력해서 덤프 파일을 만든다.
페이로드는 "A" * SIZE + POP_RDI_GADGET + CodeAddress + PUTS_PLT 가 된다.
** 계속 출력하다보면 프로세스가 처음 출력하는 문자열이 있는데 그 부분 전까지만 포함해야 한다.
보통 파이썬 코드로 data = response[:response.index(처음출력결과)] 라고 작성한다. 문자열의 index 함수는 발견못하면 -1 이라 끝까지 넣기 때문이다. 아무튼 저런식으로 data 를 계속 쌓아서 한번에 파일 출력을 해준다.
** 덤프 파일을 생성하면 리버싱 툴써서 분석한다. radare 를 많이 쓴다.
당연히 CodeAddress 는 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림
(6) Printable Function 의 GOT 주소 찾기
radare 로 덤프한 메모리를 다음과 같은 명령어로 분석을 진행한다.
$ r2 -B <CodeBase> <DumpFileName>
>> pd 10 @ <puts@plt>
검색하면 plt 영역의 코드가 나오는데, 세그먼트 내에 오프셋으로 jmp 할 주소를 계산하기 때문에 실제 GOT 주소를 찾을 수 있다.
(7) 메모리 릭 (leak libc address)
puts@got.plt 주소를 인자로 puts@plt 를 호출한다. 이는 libc 영역의 주소를 릭해서 오프셋 계산을 하기 위함이다.
페이로드는 "A" * SIZE + POP_RDI_GADGET + PUTS_GOT + PUTS_PLT 이다.
출력값은 당연히 puts 의 시작주소일 것이다. 이 주소의 끝 3자리 숫자를 가지고 libc 를 탐색한다.
ASLR 걸려있으면 여기서부터는 아래 단계들이 원자적으로 수행되어야 한다.
(8) libc 탐색 후 오프셋 찾기
필요한 도구는 libc-database 와 LibcSearcher 이다. 사용하기전에 어떤 라이브러리인지 파악해야 한다.
둘다 설치해서 사용법대로 system() 함수와 /bin/sh 문자열의 오프셋을 알아낼 수 있다.
릭한 주소 그대로 써도 되긴 하는데, puts 와 system 그리고 /bin/sh 의 오프셋을 알아낸 다음 익스를 짜는게 덜 귀찮다. (적어도 나는 그랬다.)
보통 LibcSearcher 모듈을 쓸 때 다음과 같이 쓴다.
from LibcSearcher import *
addr_puts_libc = 0x6f690 # 라이브러리에 따라 오프셋이 바뀔 수 있음.
lib = LibcSearcher('puts', addr_puts_libc)
libcBase = addr_puts_libc - lib.dump('puts')
system_addr = libcBase + lib.dump('system')
binsh_addr = libcBase + lib.dump('str_bin_sh')
print 'libc base : ' + hex(libcBase)
print 'system : ' + hex(system_addr)
print 'binsh : ' + hex(binsh_addr)
(9) 최종 익스플로잇
여기는 그냥 ROP 기법대로 하면 된다. 위 과정에서 알아낸 오프셋을 기반으로 libc base 에 더해서 실제 주소를 계산해서 페이로드를 짜면 된다.
페이로드는 "A" * SIZE + POP_RDI_GADGET + STR_BINSH_ADDR + SYSTEM_ADDR 이다.
Reference
- https://www.lazenca.net/pages/viewpage.action?pageId=16810286
- https://blog.h3x0r.kr/2018-12-30-brop/
위 자료는 Write-Up 이 있어서 참고하면 된다. 사용한 문제는 HCTF 2018 과 CodeGate 2018 예선문제이다.
(다른 문제 찾으면 추가예정)
'Security > System' 카테고리의 다른 글
CTF Summary (0) | 2020.02.07 |
---|---|
[Heap Overflow] House Of Orange (0) | 2019.07.06 |
linux system call table (0) | 2019.07.03 |
[Linux] Lazy Binding (0) | 2019.06.30 |
ROP Gadget Dictionary (0) | 2019.06.30 |