CTF/시스템해킹

[Dreamhack] Bypass NX & ASLR

dDong2 2022. 10. 25. 22:18

 

✔️ Bypass NX & ASLR

 

저번 시간 배운 것들을 보호하기 위해서 공격자가 메모리에 임의 버퍼의 주소를

알기 어렵게 하고, 메모리 영역에서 불필요한 실행 권한을 제거하는 보호 기법을

추가로 도입하기 위해서 Address Space Layout Randomization(ASLR),

No-eXecute(NX)을 개발했다.

 

ASLR은 바이너리가 실행될 때마다 스택과 힙과 공유 라이브러리 등을

임의의 주소에 할당하는 보호 기법이다.

해당 보호 기법은 커널에서 지원하는 보호 기법이며,

cat /proc/sys/kernel/randomize_va_space 명령어를 통해 확인할 수 있다.

리눅스에서 해당 명령어는 0, 1 또는 2의 값을 가진다.

 

1) No ASLR - 0 : ASLR을 적용하지 않는다.

2) Conservative Randomization - 1 : 스택, 힙, 라이브러리, vdso 등을 의미한다.

3) Conservative Randomization + brk - 2 : 1의 영역과 brk로 할당한 영역이다.

 

 

해당 프로그램은 메모리의 주소를 출력하는 코드이다.

 

 

해당 코드를 컴파일하고 실행시켜보면 메인 함수 주소를 제외하고

모두 변경된느 것을 확인할 수 있다. 즉, 실행할 때마다 다른 영역들의 주소는 변경된다는 것이다.

ASLR이 적용되면, 라이브러리는 임의 주소에 매핑되는 것은 맞지만,

라이브러리 파일을 그대로 매핑하는 것이므로 매핑된 주소로부터

라이브러리의 다른 심볼들까지의 거리(Offset)은 항상 같다.

 

NX는 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을

분리하는 보호 기법으로 NX가 적용된 바이너리는 실행될 때 각 메모리에

필요한 권한만을 부여받게 된다. 이러한 NX를 인텔에서는 XD(eXectue Disable),

윈도우는 DEP(Data Execution Prevention), ARM에서는 XN(eXecute Never)라고

칭하고 있다.

 

 

지난 시간에 만든 r2s.c를 재 컴파일하고 확인해보면

NX가 NX enabled로 활성화 된 것을 확인할 수 있고,

 

 

지난 번에 사용한 exploit 코드가 막힌 것을 확인할 수 있다.

이러한 NX와 ASLR을 우회하기 위해서는 Return-to-Libc라는 RTL 공격기법과

Ruturn Oriented Programming이라는 ROP가 존재한다.

 

 

✔️ Library - Static Link vs Dynamic Link

 

도서관처럼 원하는 정보와 기능을 획득하여 사용하는 라이브러리 개념이 존재한다.

라이브러리는 컴퓨터 시스템에서 프로그램들이 함수나 변수를 공유해서 사용할 수 있게한다.

예를 들어서 printf, scanf, strlen 등 많은 C 프로그래머들이 코드를 작성하면서 사용하는 함수도

자주 사용되는 함수들을 묶어놓은 라이브러리 파일 내 코드라고 할 수 있다.

 

이러한 과정에서 링크(Link)라는 개념은 컴파일의 마지막 단계로 알려져있다.

프로그램에서 어떤 라이브러리 함수를 사용하면 호출된 함수와 실제 라이브러리 함수가

링크 과정에서 연결되게 된다.

 

 

다음과 같은 코드를 컴파일했을 때, puts는 stdio.h 헤더 파일에 있기 때문에

심볼로는 기록되지만 해당 심볼에 대한 자세한 내용은 기록되어 있지 않다.

이러한 심볼과 관련된 정보들을 찾아서 최종 실행 파일에 기록하는 것이

링크 과정에서 하는 일 중 하나이다.

 

 

완전히 컴파일하고 나서는 libc에서 해당 심볼을 탐색하여 링크한 것을 확인할 수 있다.

gcc는 소스 코드를 컴파일할 때 표준 라이브러리의 라이브러리 파일들을 모두 탐색한다.

 

 

다음과 같이 표준 라이브러리 경로도 확인이 가능하다.

이러한 라이브러리는 크게 동적 라이브러리와 정적 라이브러리로 구분되는데,

동적 라이브러리를 링크하는 것을 동적 링크(Dynamic Link), 정적 라이브러리를

링크하는 것을 정적 링크(Static Link)라고 부른다.

 

동적 링크는 동적 링크된 바이너리를 실행하면 동적 라이브러리가

프로세스 메모리에 매핑되고 실행 중에 라이브러리 함수를 호출하면

매핑된 라이브러리에서 호출할 함수 주소를 찾아 함수를 실행한다.

 

정적 링크는 바이너리에 정적 라이브러리의 필요한 모든 함수를 포함하여

자신의 함수를 호출하는 것처럼 호출할 수 있지만, 여러 바이너리에서 라이브러리를 사용하면

라이브러리의 복제가 여러 번 이루어지므로 용량을 낭비할 수도 있다.

 

 

각각의 용량을 비교해보면, static이 synamic보다 100배 더 가까운 용량을

차지하는 것을 확인할 수 있다. 이러한 static은 puts가 있는 주소를 직접 호출하지만,

dynamic에서는 puts의 plt 주소를 호출하여 실행 흐름을 옮기게 된다.

조금 더 코드를 실행시켜보면 dl_runtime_reslov_xsavec라는 함수가 실행된다.

해당 함수에서 puts 주소가 구해지고, GOT에 주사가 써진다.

 

PLT와 GOT는 동적 링크된 바이너리에서 라이브러리 함수의 주소를 찾고,

기록할 때 사용되는 중요한 테이블입니다. 그런데, PLT에서 GOT를 참조하여

실행 흐름을 옮길 때, GOT의 값을 검증하지 않는다는 보안상의 약점이 있다.

 

따라서 앞의 예에서 GOT에 저장된 puts의 주소를 공격자가 임의로 변경할 수 있으면,

두 번째로 puts가 호출될 때 공격자가 원하는 코드가 실행되게할 수 있다.

 

🔰 퀴즈

1) X

2) X

3) X

4) O

 

 

✔️ Return To Library (함께)

 

NX로 인해서 공격자는 버퍼에 주입한 셸 코드를 실행하기 어려워졌지만,

스택 버퍼 오버플로우 취약점으로 반환 주소를 덮는 것은 가능해서

공격자들은 실행 권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격 기법을

고안했다. 일반적으로 실행 권한이 있는 메모리 영역은 바이너리의 코드 영역과

바이너리가 참조하는 라이브러리의 코드 영역이다.

예로 libc에는 system, execve 등 프로세스 실행과 관련된 함수들이 구현되어 있다.

 

 

해당 실습 프로그램은 다음과 같이 구성되어있고

 

 

카나리도 존재하고, NX 적용되어 있는 것을 확인할 수 있다.

셸 코드를 실행하기 위한 /bin/sh를 binsh 변수에 담아준 것을 볼 수 있다.

위에서 본 것처럼 PLT와 GOT는 라이브러리 함수의 참조를 위해 사용하는 테이블이기에

PLT에 system을 추가하기 위해서 코드 작성이 되어있다.

현재 PIE는 No PIE 상태여서 ASLR이 걸려있어도 PLT의 주소는 고정되므로

라이브러리의 베이스 주소를 몰라도 라이브러리 함수를 실행할 수 있다.

이 공격 기법을 Return to PLT라고 한다.

 

밑에 출력문부터 입력까지 총 두번의 오버플로우를 통해 카나리를 우회하고

반환주소를 덮을 수 있도록 작성된 코드이다.

 

카나리를 우회하기 위해서 첫 번째 입력에서 적절한 길이의 데이터를

입력하여 카나리를 구할 수 있다. 그리고 rdi 값을 /bin/sh 주소로 설정하여

셸 획득을 진행할 수 있는데, /bin/sh를 변수에 담아준 주소를 알고

system 함수의 PLT 주소를 획득해 system 함수를 호출한다.

해당 함수를 호출하여 셸을 실행하기 위해서는 리턴 가젯을 활용해야 한다.

 

리턴 가젯(Return gadget)은 ret로 끝나는 어셈블리 코드 조각을 의미한다.

pop rdi가 ret을 의미하게 되는데 이러한 리턴 가젯은 반환 주소를 덮는

공격의 유연성을 높여 익스플로잇에 필요한 조건을 만족할 수 있도록 돕는다.

 

 

다음과 같이 카나리 값과 함께 elf 정보가 출력된다.

그리고나서 ROPgadget 명령어를 통해 리턴 가젯을 찾을 수 있다.

해당 ROPgadget이 설치되어있는데 명령어를 찾을 수 없다고 해서

다음 명령어들을 수행해주었다.

 

pip3 list
sudo pip3 uninstall ROPgadget
sudo apt install python3-pip
sudo -H python3 -m pip install ROPgadget

 

 

정상적으로 설치된 것을 확인할 수 있다.

 

 

해당 프로그램에서의 리턴 가젯은 400853이라는 주소가 나왔다.

 

 

그리고나서 /bin/sh 주소를 찾아보면 0x400874라는 주소를 확인할 수 있다.

또한, pwndbg를 통해서 plt 주소도 확인할 수 있다.

 

 

순서대로 찾은 것들을 하나씩 페이로드에 작성해주었다.

ELF 헤더에 기록된 정보를 찾기 위해서 https://learn.dreamhack.io/59#6

이 곳을 참고해서 plt 주소를 찾아 저장해주었다.

앞선 셸 주소와 리턴 가젯 주소를 기록해주었다.

그리고 나서 그 다음이 문제였는데, system 함수로 rip가 이동할 때

스택은 system 함수 내부에 있는 movaps 명령어때문에 0x10단위로

정렬되어있어야 한다고 했다. 그래서 아무 의미없는 가젯을

system 함수 전에 추가해주어야 한다는 설명이 있었다.

해당 가젯을 추가하지 않거나 잘못된 바이트 만큼의 가젯을 추가하면

다음과 같이 제대로 작동하지 않는다. 

 

 

원인을 찾기위해서 우선 pop rdi와 관련된 가젯 주소를 찾아보았다.

 

 

ret와 관련된 상당히 많은 가젯이 나오는 것을 확인할 수 있는데,

우리가 확인했던 400853도 나오는 것을 확인할 수 있다. 그리고 여기 해당 가젯과 가깝게

기본 ret 주소를 밑에서 확인할 수 있는데, 해당 지점만큼의 no-op을 채워주면 될 것이라고 생각했다.

 

 

main에서 프로그램을 실행하다보면 /bin/sh가 system 함수에 의해서 실행되는 것을

확인할 수 있고 해당 함수에 의해 명령어가 실행되는 것을 확인할 수 있다.

 

 

정상적으로 실행되는 것을 확인할 수 있다.

해당 process로 로컬에서 실행하는 부분을 remote하여 문제와 연결해주었다.

 

 

정상적으로 flag를 획득할 수 있다.

 

 

✔️ Return Oriented Programming (함께)

 

저번 시간에 편의를 위해서 PLT에 system 함수를 포함하였지만,

실제 바이너리에서는 system 함수가 PLT에 포함될 가능성은 거의 없다.

그래서 ASLR이 걸린 환경에서 system 함수를 사용하려면 프로세스에

libc가 매핑된 주소를 찾고, 그 주소로부터 system 함수의 오프셋을 이용해

함수의 주소를 계산해야 한다.

 

 

해당 실습 코드는 다음과 같다. ROP 페이로드는 리턴 가젯으로 구상되는데,

ret 단위로 여러 코드가 연쇄적으로 실행되는 것을 ROP chain이라고 부른다.

이번 예제에서는 NX를 적용하여 컴파일하고 ROP를 이용한

GOT Overwrite를 실습해본다.

 

 

ASLR, 카나리, NX가 적용되어 있는 것을 확인할 수 있다.

나머지 사항은 이전 코드와 같으며 이번에는 system 함수를 호출하지 않아서

PLT에 등록되지 않고 /bin/sh 문자열도 데이터 섹션에 기록하지 않는다.

따라서 함수 주소를 직접 구하고, 셸 문자열을 사용할 방법을 생각해야 한다.

 

 

이전과 같은 방법으로 카나리를 구할 수 있다.

그 다음은 system 함수의 주소를 계산해야 하는데, 같은 libc 안에서

두 데이터 사이의 거리는 항상 같기 때문에 libc가 매핑된 영역의 임의 주소를

구하게 된다면 다른 데이터의 주소를 모두 계산할 수 있다.

 

 

그 다음은 /bin/sh가 이전처럼 저장되어있지 않기 때문에

search를 통해 검색한 것 중 libc에 포함된 문자열을 참조하여

버퍼에 해당 문자열을 입력하여 사용할 수 있다.

 

system 함수와 /bin/sh 문자열의 주소를 알게되면 저번처럼 가젯을

이용하여 호출할 수 있지만 system 함수의 주소를 알았을 때는 이미

페이로드 전송 이후이기 때문에 다시 main 함수로 돌아가서

버퍼 오버플로우를 일으켜야하는데 이러한 공격 패턴은

ret2main이라고 부르며 GOT Overwrite 기법을 이용해볼 수 있다.

 

 

pop rdi 리턴 가젯은 4007f3인 것을 확인할 수 있고,

pop rsi에 해당하는 리턴 가젯은 4007f1인 것을 확인할 수 있다.

여기서 pop rsi에 해당하는 부분은 read 함수가 rax로 0x00을 받고,

순서대로 rdi, rsi, rdx를 받기 때문에 해당 값도 저장해준다.

그런데 우리는 rdx 값을 모르고 rdx와 관련된 가젯은 바이너리에서

찾기 어렵다. 해당 가젯을 찾기 위해서는 libc 코드 가젯이나

libc_csu_init 가젯을 통해 확인할 수 있다.

 

 

다음 명령어를 수행하여 pop rdx ; ret에 대한 값이

0x001b96인 것을 확인할 수 있다. 해당 실습에서는 read 함수의 GOT를 읽고

rdx 값이 설정되기 때문에 해당 rdx_addr에 관해서는 추가하지 않아도 된다.

 

익스플로잇 코드를 천천히 확인해보면, 아까전에 작성한 카나리 우회이후에

plt와 got 주소를 저장하기 위해서 elf를 통해서 주소를 참조해온다.

그리고 아까 구해준 rdi와 rsi 주소를 저장하고 페이로드를 작성해준다.

 

페이로드는 buf에서 canary까지의 공간만큼에 0x40을 채워주게 되는데,

buf가 0x30만큼의 크기를 할당받고 있고, dummy 공간인 8만큼에 해당 영역을 차지한

다음에 카나리 값을 다음에 대입하여준다. 그리고나서 rbp 8바이트 만큼의 영역에

다시 더미값을 채워주게 되고 이후 반한될 ret 주소를 덮어씌워서 셸 코드를 실행한다.

 

페이로드의 순서는 puts, read, /bin/sh 순서인데,

p64비트 리틀 엔디안 패킹을 통해서 담아준 변수를 담아주어 더하게 된다.

read 함수의 got를 읽기 위해서 pop_rdi에 read_got를 더해주는데,

puts(read@got)를 읽게 되고 puts_plt로 해당 내용을 호출하게 된다.

 

그 다음은 read 함수를 호출하게 위해서 pop_rdi에 0을 채워넣음으로써

read(0, , ) 상태로 존재하게 되고 그다음 read@got와 0을 차례대로 담아준다.

마지막으로 read_plt로 해당 내용을 호출하게 된다.

 

마지막으로 read 함수에 /bin/sh 문자열을 담아주기 위해서

read 함수의 첫번째 인자 값으로 /bin/sh가 담겨있는 곳을 담아주어

read_plt로 해당 함수를 호출한다.

 

payload 전송 이후에 생긴 read 주소를 u64 언패킹한 6자리에 00 2개를 더해서

libc base 주소를 구한다. 위에서 배운 것처럼 libc가 매핑된 영역의 주소를 구하기 위해서

read 심볼을 가리키는 주소 만큼을 뺀 나머지가 기존 주소를 가리키게 되고

해당 주소에서 system 심볼을 가리키는 주소를 더해주면 system 함수를

호출할 수 있게 된다. 그리고 해당 시스템에 /bin/sh 문자열을 추가하여 실행하면

셸을 실행시킬 수 있게 된다.

 

 

정상적으로 명령을 수행할 수 있고, remote로

문제 접속 정보를 입력하여 flag를 획득해보자.

 

근데.... 문제가 있었다...

 

 

몇번을 시도해도 EOF 에러가 났고, 댓글에서 힌트를 발견했다.

사용하는 libc 버전이 달라서 그럴수도 있고 여러가지 의심할 수 있는 정황들이 있었다.

그래서 https://libc.rip/ 여기 사이트를 이용해서 삽질에서 벗어날 수 있었다.

 

내가 마지막으로 시도한 익스플로잇의 read 주소를 대입해서 넣어봤고,

 

 

다음과 같이 여러가지 결과가 나오는 libc_main_ret하고 110~ 정도의 차이가 나는게

libc6_2.27에서 2가지 버전이 존재했고 둘다 같은 값을 가지고 있어서

해당 offset 값을 참고하여 익스플로잇 코드를 수정해주었다.

 

 

그 사이에 접속 정보도 만료되어서 접속 정보를 바꾸고 익스플로잇을 시도했다.

 

 

정상적으로 출력되는 것을 확인할 수 있었다.

해당 환경에서 사용하는 libc offset 값이 로컬에서 참조하는 값과

달라서 difference 값이 달라서 계속해서 EOF 에러가 발생했던 것 같다.

 

 

✔️ Return Oriented Programming (혼자)

 

문제 정보는 다음과 같다.

 

이 문제는 서버에서 작동하고 있는 서비스(basic_rop_x64)의 바이너리와 소스 코드가 주어집니다.
Return Oriented Programming 공격 기법을 통해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.

 

일단은 basic_rop_x64 파일을 확인해보았다.

 

 

카나리는 없고 NX가 적용되어있는 상태이다.

 

 

코드를 살펴보면 다음과 같은데, read와 write 함수를 사용하는 것을

볼 수 있고 해당 과정에서 버퍼 오버플로우가 사용될 수 있다.

 

 

앞선 실습에서 사용했던 리턴 가젯들이 해당 파일에도 존재하는 것을 확인할 수 있다.

실제로 저 코드에 맞게 해당 접속 정보에서 실행해보면

 

 

입력받은 값을 전부 출력하지 못하고 69개의 문자가 출력되는 것을 확인할 수 있다.

 

 

그리고 메인함수는 0x4007ba에서 시작되는 것을 확인할 수 있고,

그 외에도 write와 read 함수의 주소를 확인할 수 있다.

 

 

해당 문제에서 주어지는 libc 파일을 분석하기 위해서 system을 출력해보았는데

system에서 libc_base 주소까지 빠지는 차이값이 0x45390이였다.

그래서 해당 내용을 libc.rip 사이트에서 돌려보았더니

 

 

이러한 정보가 출력되었고 write와 read 등을 확인할 수 있고 puts도 확인할 수 있다.

 

 

plt 정보는 다음과 같았다. 여기서 puts 함수를 사용해보자.

페이로드는 입력 값이 buf크기인 40만큼에 sfp 8바이트 값만큼을 더해 0x48 만큼을 넣어주고

리턴 가젯인 poprdi를 넣고 read_got, puts_plt, main 순서대로 넣고 페이로드를 보낸다.

 

 

반환되는 read 주소에 libc base 주소를 알기위해서 libc 심볼의 read를 뺀 주소값에

libc 심볼의 system과 위에서 확인한 bin_sh 주소를 담아준 다음에

다시 해당 과정을 수행하여 buf+sfp8을 더미로 채운다음에

ret 반환주소에 system("/bin/sh")처럼 실행하여 셸 코드를 수행할 수 있다.

 

 

성공적으로 획득할 수 있다.

 

그리고 다음 x86문제도 동일했다.

 

이 문제는 서버에서 작동하고 있는 서비스(basic_rop_x86)의 바이너리와 소스 코드가 주어집니다.
Return Oriented Programming 공격 기법을 통해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.

 

문제와 코드 자체가 동일하지만,

 

Arch:     i386-32-little

 

아키텍쳐가 다르고 32비트 운영체제인 것을 확인할 수 있다.

 

 

그래서 가젯 또한 뒤에 ebp 형태로 오기 때문에 총 9개가 검색되는 것을 확인할 수 있다.여기서 0x0804868b가 pop ebp ; ret 인것을 확인할 수 있다.

 

 

plt는 다음과 같이 확인되었다.

 

 

해당 프로그램을 disass main 하여 메인 함수 부분을 보게되면,

ret 를 덮기 위해서는 0x40 크기인 buf와 dummy+ebp 전부 덮어야한다. (0x48)

그리고 32비트 환경에서 라이브러리 주소는 0xf7로 시작하는 점을 이용해서 read 함수 주소를

받을 수가 있는데 gdb 내에서 vmmap 명령어를 활용해 확인이 가능하다.

 

그리고 libc 데이터베이스에서 계속 print 후 계산하는 방법 말고도 다른 방법이 있나

찾아보다가 strings 명령어를 활용해서 libc 파일 내부에서 찾는 방법을 찾게 되었다.

(참고: https://holinder4s.tistory.com/72)

 

 

그렇게해서 /bin/sh에 해당하는 offset 값 0x15902b를 구할 수 있었다. 

그리고 위에서 확인한 push ebp의 시작부분 0x80485d9 메인 주소도 확인할 수 있다.

 

 

위에 64에서 했던 것과 동일하지만 32비트 운영체제에서는

함수호출규약이 조금 다르기 때문에 반대로

puts plt의 주소값이 먼저 페이로드에 저장되게 하고

새로운 페이로드에서 system에 ebp값과 binsh값이 감싸지는 형태의 chain을 구성해야한다.

 

 

성공적으로 플래그를 획득할 수 있다.

 

 

 

화이팅 💪