본문 바로가기

Let's Study/Basic System Hacking Technique

4. Return to Library (RTL)


system_hacking4.pdf





Shayete입니다. 4번째 시간에는 최신환경의 우분투에서 Return to library 기법을 학습하겠습니다.





Return to Libc 기법은 말 그대로, 라이브러리의 함수로 리턴해서 그 함수를 실행할 수 있습니다. 가령, 우리들이 임의로 짠 바이너리에 system() 함수가 없어도 라이브러리의 system()을 호출해서 그 바이너리에서 쓸 수 있지요.


이 기법을 알기 전에 리눅스의 plt, got가 어떤건지 이해하시면 좋습니다.



1. PLT & GOT




plt는 procedure linkage table이라고, 외부라이브러리에서 함수를 가져다 쓸 때에 이 프로시져 테이블에서 함수의 실제주소( got ) 를 참조합니다. plt는 어떤 프로시져들(함수들)이 있는지 나열되어있는 테이블이라고 생각하시면 편합니다.


그리고 got는 실제 함수들의 주소를 담고 있는 테이블입니다. 여기에서 라이브러리에 있는 함수들의 주소로 넘어가 함수를 실행합니다.






처음 함수를 실행하기 전과 실행한 후를 나타낸 모습입니다. 지금은 이렇게 복잡한 과정을 알 필요가 없고 외부 라이브러리에서 함수를 참조할 때 plt에서 got로 넘어가고 got에는 실제 라이브러리의 함수주소가 들어있다는 것만 아시면 됩니다.

그리고 got overwrite라는 기법이 이 plt, got의 동작과정에서 떠오른 기법입니다. got가 실제 함수의 주소를 가리켜서 그곳을 실행하기 때문에 got에 우리가 원하는 주소로 덮어버리면 특정 함수가 실행되지 않고 우리가 원하는 주소가 실행되겠지요.


plt, got 동작과정의 세세한 내용은 추후에 포스팅할 Return to Dynamic Linker를 학습할 때 필요합니다.




그림에 보이듯이 plt가 써져있는 건 외부라이브러리에서 참조한 함수들이고 녹색 동그라미로 쳐진 함수들은 사용자가 바이너리 내에 임의로 만든 함수들입니다. 사용자가 임의로 만든 함수들은 plt 테이블에 들어가지 않습니다.




실제로 외부라이브러리에서 참조하는 함수가 실행되기 전, 실행된 후의 got를 확인해보겠습니다. 외부라이브러리에서 참조하는, 예를 들어서 printf를 보겠습니다. 


printf가 한번도 불러지지 않았을 때의 plt 모습입니다.

 

printf의 plt주소 0x8048310으로 넘어가면 명령어가 jmp DWORD PTR ds:0x804a00c 이렇게 되어있는데 이 주소는 printf의 got주소를 가리키고 있습니다. got에 어떤 값이 들어있는지 x/wx 0x804a00c로 확인해보면 0x8048316이 들어있는 것을 확인할 수 있는데 이 주소는 plt + 6의 주소입니다. 이 주소가 들어있는 이유는 위의 plt, got 동작과정을 보시면 됩니다. 중요한 건 이게 아니고 다음 printf가 실행되었을 때 got에는 어떤 값이 들어있는지 확인해보도록 하겠습니다.




printf got를 확인해보면 0xb7e63280이 들어있는 걸 확인할 수 있습니다. 이걸 p printf로 확인해보면 printf의 실제주소인 것을 확인하실 수 있습니다.

위 그림들을  봤을 때 plt에서 특정 과정을 거쳐 got에 실제 함수의 주소가 들어가는 걸 확인할 수 있습니다.


하나 더 유의해야 할 건 plt에 리턴될 때에 스택의 [esp]와 [esp+4]에 각각 함수 인자1, 인자2가 들어가있는 걸 확인하실 수 있는데 다음에 포스팅 할 Return Oriented Programming을 학습할 때 알아야 할 지식입니다.




plt, got 동작과정을 간단하게 표현한 그림입니다. plt, got 지식은 이정도만 알아두시면 됩니다.



2. Return to Library



 이제 rtl에 대해 공부해봅시다. rtl은 공유 라이브러리에 있는 함수의 주소를 이용해서 바이너리에 존재하지 않는 함수를 사용할 수 있습니다. 예를 들어 아주 간단하게 scanf로 입력받아서 printf만 출력해주는 함수가 있는 바이너리가 있을 때에 공유라이브러리의 system 함수를 불러와서 쉘을 얻을 수 있지요. 이 기법은 메모리 보호기법인 DEP를 우회합니다. 쉘코드 실행이 아닌 직접 공유라이브러리의 함수를 호출함으로써 쉘코드가 필요없게 되요. 근데 여기서 DEP와 NX bit가 뭘까요.




윈도우에서는 DEP라고 하고 리눅스에서는 NX bit라고도 표현하지요. 둘다 결국은 똑같은 말입니다. 특정 메모리에서 쉘코드 실행을 막아주는 보호기법입니다. 예를 들어 스택에 쉘코드를 넣고 리턴어드레스를 쉘코드로 돌려도 실행되지 않고 세그멘테이션 오류가 뜹니다. 





아주 간단하게 예를 들어봅니다. 코드 달랑 11줄 짜고 이 코드로 system("/bin/sh")를 실행해보겠습니다. 실습하실 때에 왠만하면 리눅스 업데이트를 하지 말아주세요. 지금 코드를 보면 아주 간단합니다. argv[1]에 128바이트만큼 값을 받을 수 있고 그 값을 buf에 복사해서 printf하는 코드입니다. 저기 buf 배열에서 오버플로우를 일으켜 libc_start_main 대신 system을 실행하는 과정을 보여드리겠습니다.




우리가 눈여겨봐야하는 스택 상태입니다. buf 배열이 128바이트이니 128만큼 채우고 sfp 4바이트를 채운 후 리턴어드레스를 libc.so.6의 system() 주소로 덮습니다. 그 후 dummy 4 바이트, 인자값 4바이트를 채워 시스템 함수를 호출합니다. 여기서 dummy[4] 에는 무슨값이 들어갈까요. 보통 스택에는 함수를 연속적으로 호출할 때에 리턴어드레스, 인자값들, sfp, 리턴어드레스2, 인자값들2, sfp2, 리턴어드레스3, ,,, 이렇게 쌓여서 실행이 됩니다.


그리고 특정 함수가 호출되면 스택의 맨 위에는 특정 함수가 끝나고 돌아갈 리턴어드레스, 그 밑은 특정함수의 인자값들이 쌓여있습니다. 이 구조를 이용해 함수를 연속적으로 호출이 가능한데,



그림으로 보여드리자면



이런 느낌이지요. return address 1로 특정 함수를 콜했다면 스택의 맨 위는 pop pop ret 주소가 리턴어드레스가 되고 parameter 1, parameter 2가 각각 특정 함수의 인자값들이 됩니다. 특정함수가 끝나고 나면 pop pop ret이 실행되어 인자값 2개를 정리해주고 return address 2를 가리켜 return address 2를 실행할 수 있게 됩니다. 


말이 길어졌는데 dummy[4]의 존재이유는 그 다음 리턴어드레스를 가리키고 있는 용도입니다. 근데 사람들이 보통 dummy값에 쓰레기값 4바이트를 채우는 이유는 그 다음에 리턴할 주소가 없기 때문입니다. (모르고 쓰시는 분들도 많겠지요.) 간단하게 예를 들어볼까요.


buf 128byte + sfp + return + "AAAA" + parameter


이렇게 호출하면 리턴어드레스를 호출하고 다음 eip가 0x41414141 을 가리키고 있을 겁니다. 하지만 함수 하나만 호출할 분들은 이런 오류는 무시해주셔도 됩니다. 





위 그림은 바이너리에서 printf, system 간의 거리와 바이너리에서 참조하는 libc.so.6 라이브러리에 실제로 들어가서 printf, system 의 거리를 재는 모습입니다. 이건 libc.so.6 라이브러리에서 우리가 흔히 쓰는 함수들이 정의되어 있다는 것을 보여드리기 위해 찍었습니다.




최신 리눅스 환경에서 라이브러리에 ASLR 기법이 걸려있어서 주소값이 매번 바뀌는 걸 볼 수 있습니다. 이게 무슨 뜻이냐면 system() 함수를 호출하고 싶어도 매번 주소가 바뀌어서 정확하게 호출하기가 어렵다는 뜻입니다. 이걸 명령어를 이용해 풀어줍니다.




ulimit -s unlimited를 이용하면 스택이 더이상 증가하지 못할 정도로 확장되어 랜덤으로 매핑이 되는걸 방지할 수 있습니다.




p system 으로 확인해보면 시스템함수도 매번 고정되어있음을 확인할 수 있습니다. 시스템 함수 주소도 얻었으니 함수의 인자로 쓰일("/bin/sh") 파라미터 주소를 구해야 하는데 주소가 고정되어있고 뭔가 값들이 쌓여있는 코드영역, 0x8048000을 확인해보면 아스키코드로 쓸 수 있는 값이 보입니다. 0x34도 있고 0x44도 있네요. 이 외에 다른 값들도 사용가능하지만 간단하게 쓸 수 있는 문자값을 이용하도록 합니다.

0x44는 "D" 로 표현할 수 있습니다. 우리는 이걸 system 의 인자로 사용하도록 하겠습니다. "D"는 /bin/sh가 아닌데 어떻게 사용하냐구요?





./D 라는 실행파일에 /bin/sh를 카피해서 쉘이 실행되도록 만듭니다. 그 후 환경변수 PATH에 현재위치를 등록함으로써 "D" 만 눌러도 쉘이 실행되게 만듭니다. 여기까지 쭉 읽으셨으면 쉘이 어떻게 실행되는지 감이 잡히실거에요.


페이로드를 볼까요. 


buf 128byte + sfp 4byte + 0x4007b190 (system) + dummy 4byte + 0x44가 들어있는 주소.


이렇게 하면 system("D") 가 실행되어 현재 경로의 D가 실행됩니다. D가 실행되면 쉘이 떠지겠지요.



여기까지 rtl의 원리를 알아보았습니다. 모두 열심히 공부하세요. 화이팅!