해킹, 알고보면 너무나 단순한 그 진실(1) - #버퍼 오버플로우
코덕질/해탈내역들 2010/03/01 11:05
웹에서 사용하는 흔하면서도 간단한 해킹 기법 중 하나를 꼽자면 쿼리 기반의 SQL 인젝션이라면, 바이너리(즉 일반 어플리케이션을 일컫습니다)에 사용할 수 있는 해킹기법은 다름아닌 버퍼 오버플로우 공격입니다.
버퍼 오버플로우란, 간단히 말해서 특정 변수 이상의 "초과분 데이터"를 넣을 시 발생하는 현상인데, 간단히 말해서 그냥 "버퍼가 흘러넘침"과 일맥상통한다고 말할 수 있으려나요.. -_-b.
C++ 프로그래밍을 해 본 사람들에게만 와닿겠지만, char buf[255]라고 선언해 놓고서는 문자열을 500개, 1000개 넣는 꼴인데, 물론 Debug 모드에서는 스택의 메모리도 서로 엄격하게 구분(보호)되어 있어 실질적인 오버플로우를 경험하기 전에 먼저 디버거 창이 뜨겠지만(-_-;), 이런 방법을 쓰면 확인해볼 수 있습니다.
코딩을 해 보시고, for 구문 앞뒤로 BP(브레이크 포인트)를 잡아 보세요.
이런 모양이 될 텐데, 이제 [F5]키를 눌러 Watch 창에서 변수의 변화를 확인해 봅시다.
int test2에는 직접적인 메모리 값을 수정하는 코드가 없었다는 것을 명심하면서..!
우와, test2를 직접 수정하지 않았음에도 불구하고 메모리 값이 바뀌어 있는 게 눈에 들어오네요. (vc 2005 기준으로 아쉽게도 문자열 오버플로우는 막아버리더랍니다.)
소스를 보시면, 구조체 배열 10개를 선언해 놓고서는 100번째 구조체 포인터까지 데이터를 채우도록 하고 있습니다. 즉, 11번째 ~ 100번째 배열을 채울 때에는 사실 다른 변수의 메모리값을 채우고 있었다는 이야기가 되는 거죠[구조체 프로그래밍 할때 의외로 이쪽 부분에서 삽질(?) 많이 하게 만듭니다]. 생각해보면 컴퓨터 메모리의 구조가 0x00000000 ~ 0xffffffff까지의 1차원 구조로 되어 있고, 프로그램의 각종 변수와 데이터들은 이 주소 위에 모두 포인터 형식으로 배치되어 있으니, 어찌보면 당연한 이야기지만 이것을 악용한다면 어떨까요? 실례를 들자면, 문자열 버퍼에 문자열을 초과하는 데이터를 넣어서 버퍼 오버플로우를 발생시키는데, 보통 버퍼 오버플로우가 발생하면 프로그램에서는 버퍼 오버플로우를 처리하는 예외 함수를 실행시키도록 하고 있습니다. 그런데 해커가 버퍼 오버플로우를 적절히 이용하여 의도적으로 예외 함수를 호출할 포인터를 자신이 악용할 코드가 있는 포인터로 슬쩍 덮어씌워버린다면? 이제 그 컴퓨터는 바이바이~가 되는 겁니다 (...)
다시 생각해보면, 버퍼 오버플로우시 왜 프로그램이 얼어버리는지의 근본적 이유나 그따위 버퍼 오버플로우 때문에 왜 그렇게 윈도우 업데이트를 해야 하는지에 대해서, 대충 감 잡히지 않나요?
버퍼 오버플로우란, 간단히 말해서 특정 변수 이상의 "초과분 데이터"를 넣을 시 발생하는 현상인데, 간단히 말해서 그냥 "버퍼가 흘러넘침"과 일맥상통한다고 말할 수 있으려나요.. -_-b.
C++ 프로그래밍을 해 본 사람들에게만 와닿겠지만, char buf[255]라고 선언해 놓고서는 문자열을 500개, 1000개 넣는 꼴인데, 물론 Debug 모드에서는 스택의 메모리도 서로 엄격하게 구분(보호)되어 있어 실질적인 오버플로우를 경험하기 전에 먼저 디버거 창이 뜨겠지만(-_-;), 이런 방법을 쓰면 확인해볼 수 있습니다.
#include "stdio.h"
#include "string.h"
struct data {
int a,b,c;
} test[10];
int test2;
int main()
{
for (int i=0; i<100; i++)
test[i].a = test[i].b = test[i].c = 10;
return 0;
}
코딩을 해 보시고, for 구문 앞뒤로 BP(브레이크 포인트)를 잡아 보세요.
이런 모양이 될 텐데, 이제 [F5]키를 눌러 Watch 창에서 변수의 변화를 확인해 봅시다.
int test2에는 직접적인 메모리 값을 수정하는 코드가 없었다는 것을 명심하면서..!
우와, test2를 직접 수정하지 않았음에도 불구하고 메모리 값이 바뀌어 있는 게 눈에 들어오네요. (vc 2005 기준으로 아쉽게도 문자열 오버플로우는 막아버리더랍니다.)
소스를 보시면, 구조체 배열 10개를 선언해 놓고서는 100번째 구조체 포인터까지 데이터를 채우도록 하고 있습니다. 즉, 11번째 ~ 100번째 배열을 채울 때에는 사실 다른 변수의 메모리값을 채우고 있었다는 이야기가 되는 거죠[구조체 프로그래밍 할때 의외로 이쪽 부분에서 삽질(?) 많이 하게 만듭니다]. 생각해보면 컴퓨터 메모리의 구조가 0x00000000 ~ 0xffffffff까지의 1차원 구조로 되어 있고, 프로그램의 각종 변수와 데이터들은 이 주소 위에 모두 포인터 형식으로 배치되어 있으니, 어찌보면 당연한 이야기지만 이것을 악용한다면 어떨까요? 실례를 들자면, 문자열 버퍼에 문자열을 초과하는 데이터를 넣어서 버퍼 오버플로우를 발생시키는데, 보통 버퍼 오버플로우가 발생하면 프로그램에서는 버퍼 오버플로우를 처리하는 예외 함수를 실행시키도록 하고 있습니다. 그런데 해커가 버퍼 오버플로우를 적절히 이용하여 의도적으로 예외 함수를 호출할 포인터를 자신이 악용할 코드가 있는 포인터로 슬쩍 덮어씌워버린다면? 이제 그 컴퓨터는 바이바이~가 되는 겁니다 (...)
다시 생각해보면, 버퍼 오버플로우시 왜 프로그램이 얼어버리는지의 근본적 이유나 그따위 버퍼 오버플로우 때문에 왜 그렇게 윈도우 업데이트를 해야 하는지에 대해서, 대충 감 잡히지 않나요?
cf. 프로그램의 IAT 임포트 테이블을 덮어씌워서 다른 의도된 함수를 실행시키는 "API 후킹"이 있는데, 이는 '합법적(?)'인 방법이니 후킹인 셈이고, (경우에 따라) 사용자 정의 함수를 바꿔치기 할 수 없는 단점이 있다.
cf2. 이런 버퍼 오버플로우 말고도 WriteMemory를 이용하여 메모리 값을 바꿔도 된다고 말한다면? 얼추 맞는 답~ 이긴 한데, 메모리의 유동적인 특성을 고려할 때 일일이 그 변수를 찾아서 바이너리를 덤프할수도 없는 노릇이니 이 버퍼 오버플로우를 이용한 공격 방법이 사실 "비교적(..)" 쉬운 편이기는 하다. 또한 공격하는 입장에서 관리자 권한을 얻을 수 있는 유력한 방법이기도 하고.
cf3. 최근 <에이콘>사의 <리버싱>책을 읽고 있다 약간 난이도가 있어서 어셈블리와 리버싱 툴에 대한 어느 정도의 사전적 지식을 요구하는데, 악성코드나 인증 기법에 대한 디스어셈블리를 분석한 것들을 흥미롭게 읽고 있다. 이 포스팅도 이 책의 일부 부분을 읽고 생각난 것들을 죽~ 적어 본 건데, 의문점!! 이 기법을 이용하여 의도한 함수를 실행시킨다고 할 때 Windows NT 커널 이후에는 타 프로그램의 메모리에는 엑세스 할 수 없지 않나? 그렇다면 어떻게 자신이 원하는 함수를 실행하도록 유도하는 거지? 아는 사람.. 손?
cf2. 이런 버퍼 오버플로우 말고도 WriteMemory를 이용하여 메모리 값을 바꿔도 된다고 말한다면? 얼추 맞는 답~ 이긴 한데, 메모리의 유동적인 특성을 고려할 때 일일이 그 변수를 찾아서 바이너리를 덤프할수도 없는 노릇이니 이 버퍼 오버플로우를 이용한 공격 방법이 사실 "비교적(..)" 쉬운 편이기는 하다. 또한 공격하는 입장에서 관리자 권한을 얻을 수 있는 유력한 방법이기도 하고.
cf3. 최근 <에이콘>사의 <리버싱>책을 읽고 있다 약간 난이도가 있어서 어셈블리와 리버싱 툴에 대한 어느 정도의 사전적 지식을 요구하는데, 악성코드나 인증 기법에 대한 디스어셈블리를 분석한 것들을 흥미롭게 읽고 있다. 이 포스팅도 이 책의 일부 부분을 읽고 생각난 것들을 죽~ 적어 본 건데, 의문점!! 이 기법을 이용하여 의도한 함수를 실행시킨다고 할 때 Windows NT 커널 이후에는 타 프로그램의 메모리에는 엑세스 할 수 없지 않나? 그렇다면 어떻게 자신이 원하는 함수를 실행하도록 유도하는 거지? 아는 사람.. 손?


댓글을 달아 주세요
해킹은 크게,
루트 획득, 실행, 백도어 삽입, 흔적 제거
정도로 나눌 수 있습니다.
이 중 BOF는 루트 획득과 실행에서 주로 쓰이는 방법이죠.
해커는 일단 루트 즉 관리자 권한을 획득해야합니다.
개념만 말하면, 관리자 권한으로 실행되는 녀석의 취약점(BOF 등)을 이용해서 관리자 권한으로 실행되는 동안 '나의 코드'가 실행되도록 하면 되는 거죠.
BOF는 특히, '나의 코드'를 손쉽게 넣어주는 마법과도 같은 녀석입니다.
물론 쉽다는 건 상대적인 거고.. 이거 이용하려면 머리 빠져요.
분석 열심히 해서 '올바른 크기와 거리(오프셋)'를 알아내 적당한 크기의 '나의 코드'를 만들어내고 그걸 집어넣는 겁니다.
이후 돌려버리면 땡.
굳이 실행되도록 애 쓰는게 아니라 자연스레 '나의 코드'로 실행이 넘어갑니다.
그렇게 계산해서 버퍼에 코드를 집어넣으니까요.
우힠.. 결국 메모리 덤프로구만요 ㅠ_ㅠ.
그쪽은 역시 아직 손 댈 엄두도 안 납니다 (..)
원래 컴퓨터란 녀석은 생각치도 않게 허술한 구석이 있더라구요. ^^;
(이건 Windows만 그런 게 아니라 Linux나 다른 OS도 마찬가지 같습니다.)
그래서 저는 최선의 방어책은 개인이 사용하는 컴퓨터를 자주 포맷하고 포트도 최대한 적게 열어서 아예 들어올 구멍을 만들지 않은 것이라 생각합니다~ 네트워크에서 무슨 짓을 할 수 있든지 권한은 physical admin이 가장 많다니깐요. ^^ 그런 터라 현재 이용중인 서버 호스팅이 늘 불안하네요. 거긴 포맷하기도 어렵고 저 또한 늘 원격접속을 해야 하니까요.
... 나 프로그래밍 첨부터 다시배워야되나...
자...잠깐, 이건 어떻게 봐야 단순한건가요 대가리가 돌인 저는 전혀 이해가 불가능...orz
음... 전 모르겠습니다..ㅠ.ㅠ
그러니까 그걸 막는게 우리가 할 일이지이요 <-
SQL 인젝션 같은건 코드 짤때 조금만 신경써주면 되는부분인데 참... "나 뚫어 주시오" 하는 사이트들이 많더군요 -_-a
그나저나 (윈도가 아니라 리눅스입니다) 저걸 컴파일 해서 실행하면 왠지 쉘에서 오류뱉고 끝날것 같은 기분이 드는데 아닌가요?
핫, 안녕하세요? 초면이네요... (이름 없던 사람...)
저도 이쪽 분야에 장래희망이 있는데, 프로그래밍 열심히 해야 할것 같네요... ㄷㄷㄷ
에... 그러니까 실행시킬 함수나 코드를 컴파일 해서 기계어 코드를 바로 버퍼에다가 써버리면 되지요...
굳이 다른 프로그램쪽 메모리를 건드릴 필요 없이.
일반적으로는 스택포인터를 수정하는 방법으로 어택을 하더근영.;;;
그런데 제가 알기로는 그 수정한 포인터의 값이 다른 프로그램의 메모리 어드레스를 참조할 수 없다고 알고 있어서요 ;; 그렇다면 프로그램 내부에서 메모리 액세스를 해야 할텐데 말입니다... 그러면 메모리값을 수정해야 하는 것이 아닌가요? 아니면 오버플로우 공격 할때 목적 코드도 함께 덮어씌우는 건가요?
아니면 오버플로우 공격 할때 목적 코드도 함께 덮어씌우는 건가요? -> 빙고.
일반론 적인 설명으로..;;
f'(x) 내에서 f(x) 펑션이 호출되면
(메모리 맵)
f(x)Buf[0]
f(x)Buf[1]
...
f(x)Buf[N-1]
f(x)Buf[N]
f(x)StackPTR
f'(x)Buf[0]
f'(x)Buf[1]
...
f'(x)Buf[N-1]
f'(x)Buf[N]
f'(x)StackPTR
처럼 Stack메모리가 호출되게 되는데,
f(x) 펑션이 Return 할때 StackPTR로 점프하게 되어 있습니다.
뭐 그럼 간단하지요.
Buf[0~N] 까지 뭔가 쓰레기값을 쭉쭉 채운후에
그 다음 바이트 (StackPTR 에 해당하는 주소) 에다가 바이너리 코드로 JMP 다음 바이트
그리고 그 다음 바이트 부터는 원하는 코드의 목적코드들....
이렇게 펑션에 입력을 넣어버리면 만사땡큐...-_- 가 되어버리지요...
데이타 사이즈가 사이즈 인 만큼 일단 루트권한부터 먼저 획득하려 하지 않을까요우?
버퍼를 오버플로우해서 메모리에있는 기존 코드를 없애버리고, 원하는 코드를 넣는 거죠.즉 정상 코드는 함수 밖으로 돌아와야 하는데, 버퍼 오버플로우가 일어나서 돌려주는 부분이 날아가 버리고 새롭게 조작된 명령이 들어간 부분이 실행되게 됩니다.
본래는 접근이 불가하지만 버퍼가 넘치면서 코드 영역을 침범하게 되는거죠.
돌아오는 주소를 바꾸는 방법도 있습니다. 이 명령을 마치고 주소 02feff로 돌아와야 하는데 해커가 거기다 다른 코드를 심는 겁니다. 주소는 어셈블러 nop(no operation) 를 사용해서 맞춥니다. 이걸 잔뜩 심어놓으면 어디에 떨어지든 nop 을 거쳐서 해커가 원하는 코드가 실행되게 되죠. 보통은 쉘코드로 쉘을 뛰웁니다
디버그 API 를 이용하면 다른프로세스의 메모리 부분에 접근 가능합니다..
대표적인게 CreateRemoteThread 라는게 있네요;;