One_Blog

Return Oriented Programming - ROP 완벽이해 본문

시스템해킹

Return Oriented Programming - ROP 완벽이해

0xOne 2022. 10. 31. 23:04
728x90

오늘은 오랜만에 시스템 해킹에 관련한 글을 써보도록 하겠습니다.

 

오늘 알아볼 공격 기법은 ROP,

Return Oriented Programming

입니다.

우선 ROP를 이해하기 위해 알아야 하는 사전 지식에 관해 짚고 넘어가겠습니다.

 

PLT & GOT

Return to Libc

함수 호출 규약

 

이것들에 대해 알고있다면, 바로 ROP를 시작해도 될 것입니다.

참고로 제가 설명하는 ROP는 64bit으로 설명됩니다.

 

 

 

시작

 

ROP

ROP란 무엇일까요?

ROP를 정말 간단하게 설명하자면, ret에 가젯과 함수의 주소를 연속적으로 연결해

공격자가 원하는 실행흐름을 프로그래밍 하는 것을 의미합니다.

 

하지만 이렇게 설명하면, 처음 ROP를 접하는 사람 입장에서 

" 뭔소리야 ㅅㅂ"

소리가 나올 수 밖에 없습니다.

 

그 어떤 프로세스던, 해당 프로세스에 main함수가 존재하는 한,main함수의 끝에는 ret라는 어셈블리어가 존재합니다.

 

ret은 현재 실행되고 있는 함수가 종료된 뒤 프로그램의 실행 흐름을 어디로 옮길지 결정하는어셈블리어 입니다.일반적으로 프로세스에선, 대략적으로 함수의 흐름이

 

START -> LIBC_START_MAIN -> MAIN -> LIBC_START_MAIN -> START

 

이런식으로 이루어집니다.START함수로 시작해서 다른 함수를 거쳐 결국엔 START 함수로 끝이 나는 거죠.

 

그렇다면, 지금까지 배운 걸로 생각했을 때 Main함수에서 ret이 실행되면 어떤 함수로프로그램의 실행 흐름이 옮겨질까요?

정답은 LIBC_START_MAIN함수입니다.

그런데 만약, 우리가 여기서 ret의 반환주소를 건드릴 수 있다면,

어떻게 될까요?

 

우리가 자유자재로 프로그램의 실행흐름을 다룰 수 있게 될 것 입니다.

일반적으로 이 ret의 반환주소를 우리가 원하는 주소로 덮어서

프로그램의 실행 흐름을 옮기는 이것을 우리는 

Ret Overwrite 공격이라고 부릅니다.

 

그러나 우리는 이것보다 좀 더 고차원적인 공격을 할 것입니다.

 

ROP와의 차이점은 무엇일까요?

그 차이점에 대해 설명해드리겠습니다.

 

Ret overwrite는 단순히 main함수의 ret반환 주소를 다른 주소로 덮는 것으로 끝났습니다.

그리고 그 함수의 경우에는 이미 사용자가 만들어놓은 함수인 경우가 많죠.

인자값이 필요 없는.

ex) system("/bin/sh");

이런 함수 말입니다.

 

하지만 우리는 사용자가 이전에 호출한 적 없는 함수를 호출할 것입니다.

우리는 이것을 위해 가젯이라는 것을 활용해야 합니다.

가젯이란 ret으로 끝나는 어셈블리어 코드 조각을 의미합니다.

 

하나의 프로세스 안에는 엄청나게 많은 어셈블리 코드가 존재하죠?

이렇게 하나의 함수의 일부만 보더라도,

하나의 프로세스는 엄청나게 많은 어셈블리어가 들어있음을 알 수 있습니다.

그리고 이 어셈블리어의 흐름 중에는,

pop rdi;

ret;

과 같이 ret으로 끝나는 연속된 어셈블리어 코드도 있을 것입니다.

가젯은 이런 것을 뜻합니다.

특정 레지스터를 호출하거나, 특정 값을 불러온 후,

ret으로 끝나는 프로세스 내부의 어셈블리 코드 조각.

 

우리는 ROP를 위해 이를 활용해야 합니다.

64bit를 기준으로, 함수는 호출될 때 인자값을 레지스터에서 참조합니다.

https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#linux-system-call-table 

 

Chromium OS Docs - Linux System Call Table

Linux System Call Table These are the system call numbers (NR) and their corresponding symbolic names. These vary significantly across architectures/ABIs, both in mappings and in actual name. This is a quick reference for people debugging things (e.g. secc

chromium.googlesource.com

(관련 자료)

Read함수만 보더라도, rdi, rsi, rdx 레지스터로부터 값을 참조하죠.

ex) read(0, buf,0x100)같은 형식으로 함수를 호출할 때,

인자값은 rdi = 0, rsi = buf의 주소 , rdx = 0x100 이런식으로 이루어짐.

 

우리가 공격을 할 때, read함수를 사용하기 위해 ret주소를 read함수의 주소로 덮어

호출했는데 만약 rdi, rsi, rdx레지스터에 이상한 값이 들어있다면 어떻게 될까요?

실행이야 되겠지만, 우리는 우리가 원하는 흐름을 만들기는 어려울 것입니다.

이때 가젯을 활용하는 겁니다.

 

함수를 호출하기 전, 우리가 원하는 인자값을 제대로 전달해 주기 위해 가젯을 쓰는 것이죠.

예시로 read(0, buf, 0x100)의 형태로 함수를 호출한다고 쳐 봅시다.

우선 rdi 레지스터에 0이라는 값이 들어가야겠죠?

그래서 우리는 rdi에 값을 넣을 수 있는

pop rdi;

ret;

으로 이루어진 가젯을 사용할 겁니다.

그러면 익스플로잇 코드는

p64(pop_rdi + ret 가젯의 주소) + p64(0)

이런식으로 될 것입니다.

그럼 여기서  rdi는 인자값 세팅이 완료되었으니, 

이제 남은 rsi, rdx에 값을 전달해야 겠죠?

그러기 위해 이전에 사용했던 pop rdi, ret에서 ret을

pop rsi;

ret

으로 이루어진 가젯의 주소로 덮는 것입니다.

나머지 rdx도 값을 전달해 주기 위해,

pop rsi, ret에서 ret에 rdx를 호출하는 가젯 주소를 덮어주면 되겠죠.

그렇게 인자값을 다 전달하고 나면, 

마지막에 가젯으로 쓴 pop rdx, ret에서 ret을 read함수의 주소로 덮으면?

인자값으로 (0, buf, 0x100)를 가져가는 read함수가 호출되는 것이죠.

이렇게 가젯의 ret을 이용하여 공격자가 원하는 프로그램 흐름을 만들어내기에,

해당 기법이 Return Oriented Programming이라고 불리는 것입니다.

 

그런데, 우리가 이렇게 공격을 할 때, 가젯이 항상 저렇게

이상적인 형태로 존재하는 것은 아닙니다. 

특정 레지스터를 호출하는 가젯이 없을수도 있고

아니면 가젯은 있는데 원하는 레지스터 뿐만 아니라 다른 레지스터들도 호출하는 가젯이 있을 수 있죠.

 

원하는 레지스터 뿐만 아니라 다른 레지스터들도 호출하는 경우에는,

그냥 해당 가젯을 사용해도 됩니다.

어차피 우리가 실행하려는 함수는 그 다른 레지스터를 참조하지 않을테니까요.

 

그런데 만약, 특정 레지스터를 호출하는 가젯 자체가 없다면?

가젯을 library에서 참조하면 됩니다.

예시에서 들었던 read함수만 해도, 보통 rdx 레지스터를 호출하는 가젯이 없기에 

우리는 보통 library에서 값을 참조하곤 합니다.

일반적으로 작성된 C언어는 대부분 

Libc 라이브러리를 호출하기에 , 보통

Libc 라이브러리에서 가젯을 찾아 사용합니다.

 

자 그럼 함수에 인자값을 전달하는 방법을 알았으니,

우리가 원하는 함수를 호출하는 방법을 알아야겠죠?

보통 우리가 함수를 호출할 때, 우리는 해당 함수를

라이브러리 라는 곳에서 참조를 합니다.

 

이 정도는 ROP를 읽는 사람이라면 다들 알고 있겠죠?

그런데 이 함수를 참조할 때, 라이브러리에서 해당 함수만을 

참조하는 게 아닌 해당 라이브러리에 있는 모든 함수를 가져옵니다.

 

쉽게 말해 라이브러리 전체가 프로세스 메모리에 통째로 매핑이 되는 거죠.

그래서 보통 프로세스 메모리엔 system함수가 함께 매핑됩니다.

"/bin/sh"이라는 문자열도 함께요.

 

우리가 공격에 쉽게 사용할 수 있는 system이라는 함수가 프로세스 메모리에

적재되는 것을 알았으니, 이제 그 함수의 주소를 구해야겠죠?

보통 라이브러리 안에서 데이터들간의 거리는 고정되어 있습니다.

버전에 따라 조금씩 달라지곤 하지만, 보통 다 정해져있다는 거죠.

 

예시로, Ubuntu GLIBC 2.27-3ubuntu1.2에서 

read 함수와 system 함수 사이의 거리는 항상 0xc0ca0입니다.

 

함수의 주소를 구하는 건

https://learn.dreamhack.io/84#7

 

로그인 | Dreamhack

 

dreamhack.io

해당 강의를 참고해주세요. 설명이 너무 길어지기 때문에..

 

오늘은 이렇게 ROP에 대해 알아보았습니다.

실제 익스까진 아니더라도, 다들 ROP의 원리까지는 이해하셨으면 좋겠네요.

글은 여기서 마치겠습니다.

잘못된 정보나 오타 있으면 댓글로 지적 바랍니다.

읽어주셔서 감사합니다.