안 쓰던 블로그

32bit ROP 본문

CTF/Pwnable

32bit ROP

proqk 2020. 11. 10. 23:41
반응형

ROP란

ROP(Return Oriented Programming)란 공격자가 실행 공간 보호(NX-bit)ASLR, ASCII Armor나 코드 서명(Code signing)과 같은 보안 방어가 있는 상태에서 코드를 실행할 수 있게 하는 기술이다. 취약한 프로그램 내부에 있는 기계어 코드들의 섹션(가젯: Gadget)을 이용하여 공격 시에 특정 명령을 실행시키는 식으로 수행한다. ROP는 RTL, RTL Chainging Calls, GOT overwrite의 3가지 기법으로 이루어져 있다.


보호기법

NX-bit(Not Excutable, DEP)

아까 ROP란 실행 공간 보호(NX-bit)같은 보안 방어가 있는 상태에서 코드를 실행하여 공격한다고 할 때 등장한 보호기법. NX-bit는 가상 메모리의 최소 단위인 페이지에 기존에 읽고 쓰는 권한 외에도 실행 권한까지 체크하여 메모리 공격으로부터 시스템을 보호하는 기법이다. 다르게 말하면 메모리 페이지가 write와 excutable권한을 동시에 갖지 못하게 설정한다는 말이다. 이것이 적용되어 있으면 공격자는 스택, 힙, 데이터 섹션에서 공격 코드를 실행하지 못한다.

 

예를 들어 30만큼의 버퍼 BUF[30]가 있고, 버퍼오버플로우 공격을 한다고 가정한다. 현재 메모리에는 BUF[30], SFP, RET 순으로 있을 것이다. 버퍼오버플로우는 버퍼(스택 공간)에 쉘코드+아무값으로 30만큼 채운 뒤 4바이트짜리 sfp를 덮고, 남은 RET 자리에 쉘 코드 시작 주소를 넣는다. 그러면 오버플로우를 이용하여 RET 주소를 쉘 코드 시작 주소로 덮어씌우게 되었고, 실행되면 쉘 코드가 실행되어 exploit이 된다. 하지만 이런 상황에서 NX-bit가 설정되어 있다면, 오버플로우를 해서 RET 주소를 덮어씌웠어도 스택 공간에 있는 쉘 코드를 실행시킬 권한이 없어서 exploit이 불가능하다.

 

ASLR(Address Space Layout Randomization)

ASLR은 프로세스 내에서 맵핑되는 파일들이 호출 및 실행되는 주소를 랜덤화하는 기법이다. 주소는 힙, 스택, PEB, TEB의 위치 상에서 랜덤으로 정한다. 이 보호 기법이 적용되어 있으면 버퍼오버플로우 취약점으로 공격을 시도했더라도 쉘 코드를 실행시킬 정확한 주소를 알지 못하므로 공격이 실패하게 된다.

 

ASCII Armor

공유 라이브러리 영역의 첫 주소에 0x00 인 NULL값을 포함시켜서 strcpy() 같은 string을 다루는 함수를 사용할 때 이를 문자열의 끝으로 인식해서 제대로 동작하지 않도록 하게 하는 기법이다. 일반적으로 공격 코드는 아스키 문자열의 형태로 전달되며 '0'을 문자열의 끝으로 인식하므로 가능한 기법이다. 이 보호 기법이 적용되어 있으면 일반적인 RTL 공격 기법으로는 RET을 덮어쓸 수 없다. 우회 방법으로는 Return to PLT가 있으며, libc에 return 하는 것이 아니라 바이너리에 로드된 PLT 함수들을 사용하는 방법이다.


ROP를 이루는 세 가지 기법

1. RTL(Return to Libc=Library)

RTL이란 Return address영역을 공유 라이브러리 함수의 주소로 변경하여 해당 함수를 호출시키는 기법이다. 더 쉽게 말하면 리턴 값을 라이브러리가 있는 곳으로 바꿔버린다는 의미이다. 어떻게 하냐면 보통 문제에서 배열을 주는데, 배열 크기만큼의 버퍼 공간을 전부 채우면 saved ebp, saved eip, return값, 인자 순으로 나온다. eip 부분에 원하는 함수의 주소를 넣어주고 함수가 끝난 후에 해야 할 return 값을 받고 인자를 받는다.

이 기법을 이용해 NX-bit를 우회할 수 있다.

 

참고: Cdecl 호출 규약

32bit 시스템에서는 Cdecl 호출 규약을 사용한다. Cdecl 호출 규약의 특징으로는 함수의 인자 값을 오른쪽에서 왼쪽 순서로 스택에 저장하는 것이 있다. 또한 함수의 Return 값을 EAX 레지스터에 저장하며, 사용된 스택 정리는 해당 함수를 호출한 함수가 정리한다.

ret = f(a,b,c);

다음과 같은 코드가 있으면 어셈블리 코드 형태는 다음과 같다

push c
push b
push a
call f
mov ret, eax

오른쪽에서 왼쪽 순서로 3개의 인자 값을 push로 스택에 저장한다. 함수 호출 후에 반환된 값은 EAX 레지스터에 저장되며 그 값을 ret변수에 저장한다.

 

2. RTL Chaining Calls

여러 함수를 연계하여 호출한다. RTL에서는 하나의 함수만 호출했지만 RTL Chaining에서는 여러 함수를 연계하여 호출한다. 구체적으로는  RTL에서는 return address가 들어가는 곳에 4바이트 더미를 넣었는데, 같은 위치에 pop ret같은 명령어 주소를 넣어 스택 포인터를 다음으로 위치시키는 방법이다.

아까 RTL에서는 버퍼+ebp+eip+return값+인자 형태였는데

RTL Chaining에서는 버퍼+ebp+eip+리턴값+"0"+공간주소(bss)+넣을 양+원하는 함수+리턴값+인자 이다

예를 들어 return값에 pop을 세 번 하는 코드의 주소값을 넣으면 "0", 공간주소, 넣을 양 부분이 pop되면서 빠지고, 원하는 함수가 실행되게 된다.

 

3. GOT overwrite

dynamic방식 컴파일은 공유 라이브러리를 사용한다. 라이브러리를 메모리 공간에 맵핑하고 여러 프로그램에서 그 라이브러리 파일을 공유해서 쓰는 방식이다. dynamic 방식에서 함수를 호출하면 PLT를 참조하게 된다. PLT는 GOT를 참조하는데, GOT에는 라이브러리의 실제 함수 주소가 쓰여 있어서 그 주소의 함수를 호출하는 식이다. 일단 PLT GOT에 대해서 자세하게 알아본다.

 

PLT(Procedure Linkage Table)

함수들이 나열되어 있는 테이블. 다른 파일 안에 구현한 함수를 연결시켜 준다. 사용자가 만든 함수는 같은 파일이므로 따로 연결할 필요가 없어 PLT를 참조하지 않지만, main()처럼 외부 라이브러리에서 함수를 가져와 사용해야 할 때 PLT를 참조한다. gdb에서 info 함수이름 명령어를 쳤을 때 @plt로 표시되어 있는 함수들이 외부 라이브러리 함수이다.

 

GOT(Global Offset Table)

함수들의 실제 주소를 담고 있는 테이블. 프로그램 실행 후 libc.so 내의 함수들의 실제 주소를 담고 있으며, 라이브러리에서 함수를 호출할 때 PLT가 GOT를 참조하여 해당 주소로 점프한다.

 

즉, 외부 함수가 호출되는 과정은 pinrtf사용->printf@PLT->printf@GOT->printf호출 순이다.

이 때 GOT 주소에 다른 함수의 주소를 덮어씌우게 되면 PLT는 잘못된 주소값을 실행하게 되고, 이런 공격 방식이 GOT overwrite이다.

 


그럼 결국 ROP란?

앞서 다룬 보호 기법 등이 걸려 있어서 BOF, RTL과 같은 공격을 수행하지 못하는 경우 함수를 실행하기 위해 여러 가지를 조합하여 실행시키는 것이다. '여러 가지'란 ROP를 이루는 세 가지 기법의 그것들이 된다.

ROP를 설명한 다른 글들을 보면 ROP를 하기 위해서는 read의 PLT, GOT 주소 등등이 필요하다.. 는 식으로 먼저 막 찾는데, PLT와 GOT을 찾아서 GOT overwrite를 하기 위함이었다. GOT overwrite를 하여 공격한 함수의 GOT에 공격자가 원하는 system함수(보통 /bin/sh)의 주소를 넣어서 쉘을 가져오고 이것저것 하면 된다.


가젯(Gadget)

가젯은 ROP공격을 할 때 쓰이는 바이트 조각들을 의미한다. 가젯들을 찾아 재조합하여 공격에 필요한 주소를 만든다. 처음엔 가젯이라는 말이 잘 와닿지 않았는데, 의미 없는 여러 조각들을 모아서 의미를 부여한다는 느낌이라고 하면 될 것같다. 예를 들어 시스템 함수 주소가 0x00102030일 때 그냥 호출을 하면 0x00인 NULL바이트 때문에 실행이 불가능하므로, 메모리 속에서 0x00 0x10 0x20 0x30값을 가지는 주소를 찾아 조합하여 시스템 함수 주소를 만들어낸다.

 

여러 개의 함수를 호출하기 위해 사용될 때는 Gadgets라고 부른다

 

호출 하는 함수의 인자가 없을 경우에는 "ret" 가젯이 사용된다

호출 하는 함수의 인자가 1개인 경우에는 "pop; ret" 가젯이 사용된다

호출 하는 함수의 인자가 2개인 경우에는 "pop; pop; ret" 가젯이 사용된다

호출 하는 함수의 인자가 3개인 경우에는 "pop; pop; pop; ret" 가젯이 사용된다

 

이 가젯들은 ESP 레지스터의 값을 증가시키는 역할을 한다. 다시 말하면 RTL에 의해 호출되는 함수에 전달되는 인자 값이 저장된 영역을 지나 다음 함수가 호출될 수 있도록 하는 역할이다. 참고로 32bit 바이너리에서는 pop 명령어의 피연산자 값은 중요하지 않다


예시 문제 풀이 32bit ROP

ropasaurusrex라는 문제 파일이다. 처음에 실행하면 아무 값이나 입력받고 WIN을 출력한다

 

checksec을 해 보면 NX가 걸려있는 것을 확인할 수 있다.

 

main에서는 FUN_080483f4()를 실행하고 

WIN이라는 문자열을 출력하고 끝난다

 

FUN_080483f4()에서는 136만큼의 버퍼가 있는데 0x100 즉, 256만큼의 입력을 받는다. 입력값이 더 크므로 버퍼를 오버해서 넣을 수 있고, 이 부분이 취약점이다.

현재 버퍼 모양은 buf[136]+sfp[4]+ret[4]인데 리턴주소 ret를 덮으려면 버퍼 크기인 136+4바이트를 넣고 아무 주소를 넣는다

 

136 이상을 입력하면 오버플로우가 발생하여 segment fault 메시지가 뜨는 것을 확인한다

 

여기까지 알게 된 사실을 정리하면 다음과 같다

일단 프로그램에서 read와 write 함수가 쓰였다. 이를 이용하여 ROP를 해야 한다

GOT overwrite를 하여서 read함수의 GOT에 /bin/sh의 주소를 넣는다. 그러면 read가 실행되면서 쉘이 실행된다

이를 위해 필요한 것들은

1) read의 PLT, GOT 주소

2) write의 PLT, GOT 주소

3) pop pop pop ret(pppr)의 주소

4) system의 offset

5) /bin/sh를 덮어씌울 공간인 bss의 주소

이다

 

1. read와 write의 PLT, GOT 주소 얻기

gdb에서 간단히 찾을 수 있다

첫번째로 jmp를 해 주는 부분이 PLT이고 jmp되는 부분이 GOT이다

 

read PLT: 0x0804832c

read GOT: 0x804961c

write PLT: 0x0804830c

write GOT: 0x8049614

 

 

2. pppr의 주소 얻기

objdump명령어로 pppr의 주소를 찾을 수 있다

pppr의 주소: 0x80484b6

3. bss 주소 얻기

/bin/sh을 넣어주기 위한 bss주소도 objdump로 구할 수 있다. 사진 상에서 왼쪽에서 2번째 값이 주소이다

bss주소: 0x08049628

 

4. system의 offset 구하기

마지막으로 system과 read함수 사이의 offset을 구해야 한다. 그냥 gdb로 들어가면 system의 주소가 나오지 않고, 프로그램을 bof시켜서 세그먼트 오류를 일으키면 생기는 core파일에서 얻을 수 있다

core dumped가 뜨면 core파일이 생성된다

gdb로 들어가서 read-system값을 구한다

system offset: 0xacf30

 

반응형

'CTF > Pwnable' 카테고리의 다른 글

64bit ROP  (0) 2020.12.09
64it ROP  (0) 2020.11.28
32bit와 64bit - x86과 x64 비교  (0) 2020.10.13
레지스터  (0) 2020.10.13
스택과 SFP와 RET  (0) 2020.10.13
Comments