Return oriented programming 들어보셨나요? 아마 최근 아이폰 SMS DB 해킹기법이 Return oriented programming의 잘된 예라고 했던 듯 합니다. (기사가 기억이 잘..)
일단 이것인가 아시는 분들을 읽을 필요없는 글이고, 이것이 무엇인지 전혀 모르는 분을 위한 초간단 개요를 하나 적을까 합니다.
일단 Return oriented programming을 알려면 return to libc를 아는 것이 필요합니다. return to libc란 해킹 테크닉중의 하나로 library로 리턴하는 방법을 말하며, 상세한 내용은 pharack, smashing the stack for fun and profit이란 글에 자세히 나와 있습니다. 하지만 여기서는 간단하게만 알아보겠습니다.
다음과 같은 잘못된 코드가 있다고 해보죠.
int main(int argc, char** argv) { char buf[255]; strcpy(buf, argv[1]); }
이 코드가 잘못된 이유는 사용자의 입력을 지나치게 신뢰한나머지 buffer overflow가 가능하다는 점입니다. 그냥 buffer overflow만 되면 모르겠는데, 만약 이 코드를 root사용자의 권한으로 실행할 수 있도록 setuid 가 되어있다고 하면, 공격자 X는 다음과 같이 이 코드를 공격할 수 있습니다.
1. 일단 “/bin/sh”를 실행하는 C코드를 하나 짭니다. (system 함수 호출)
2. 걔를 disassemble합니다. 그럼 16진수 코드가 나오죠.
3. 위 프로그램의 인자로 약간의 패딩(최소한 오버플로우시키고 싶으니까 255글자 이상이겠죠)과 함께 아까 disassemble한 16진수 코드를 넣습니다.
4. 그러면 위 프로그램은 root의 권한으로 돌다가 /bin/sh를 실행.
5. 결국 공격자는 쉘을 따내게 됩니다.
이게 가능한 이유는 buf라는 데이터가 저장되는 주소에서 계속 오버플로우해서 가다 보면 main이라는 함수가 종료되는 스택 반환 주소가 나오기 때문입니다. 다시 말해, 공격자는 이렇게 메모리 레이아웃을 고칩니다.
buf[255] | /bin/sh/ | system 주소
그러면 프로그램은 종료하려고 하다가 system 주소로 RET하는데, 이 때 인자로 /bin/sh를 넘기게 됩니다. (아시다시피 function call의 인자는 스택을 통해 넘깁니다.) 결국 root권한으로 쉘을 내주게 되는 것이죠.
그러나 이것이 최근에는 다양한 방법으로 막히게 되는데, 그 중 하나는 변수의 전달을 어렵게 하는 보호 방법입니다. 앞서 보시면 system에 제가 ‘/bin/sh’를 넘겨야하는데, 이처럼 넘겨주는 변수가 메모리에 써있으면 안되고 레지스터에 들어있어야 하도록 강제하는 것입니다.
두번째는 데이터 영역과 코드 영역을 명확히 구분해버리는 것입니다. 기존에는 메모리를 overflow하면서 쓰고(write), 그 영역이 동시에 실행(execute)될 수 있었습니다만, 이제는 쓸수도 있으면서 실행도 가능한 메모리를 없애버린것입니다.
세번째는 코드 사이닝으로, 못믿을 놈이 넣은 코드는 실행안한다는 것입니다.
그외에도 여러가지 방어 기법이 있습니다. 예를들어 스택의 반환 주소를 랜덤화한다던가, 내가 호출할 system이란 펑션을 아예 라이브러리에서 지워버린다던가, 아니면 system이란 함수의 라이브러리내 위치 주소를 랜덤화한다던가, 스택을 overwrite하게 되면 곧바로 프로그램이 죽어버리게 막아놓는다던가.
Return oriented programming은 공격자가 인자와 함수를 정해서 호출하는 것이 아니라, 그냥 시스템에 존재하는, 이미 코드 사이닝이 된 코드를 호출해버리겠다는 것입니다. 예를들어 가상의 시스템내 라이브러리가 이렇게 이미 생겨있다고 해보겠습니다.
funcA()
….
copy ‘/bin/sh’ to argument. // (1)
RET
funcB()
copy ‘ls’ to argument.
system 호출 // (2)
RET
그럼 공격자는 funcA()와 funcB()를 조합합니다. 그래서 앞에서처럼 직접적으로 system을 호출하지 않고 코드를 1로 점프시킵니다. 그게 반환되어 오겠죠? 이미 copy다음엔 RET가 있으니까. 그러면 곧바로 2를 호출합니다. 이렇게 되면 결국 쉘을 따냅니다.
물론 시스템 라이브러리에서 이렇게 필요한 코드를 찾는다는 것은 고역이 아닐 수 없겠죠. 그래서 연구자들은 아예 라이브러리에서 유용한 code fragment를 뽑아내서 또다시 2차 라이브러리화하고 있습니다.