블로그 이미지
자유로운설탕

calendar

1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

Notice

2019. 8. 25. 17:34 보안

  이번 시간에는 리버싱(리버스 엔지니어링: Reverse Engineering)과 포렌식(Forensic)에 대해서 이야기 해보려 한다. 시작하기 전에 먼저 양해를 구하고 싶은 부분은 이 두 분야에 대한 실무를 해본적이 없기 때문에 그다지 자신이 없다. 다만 어떻게 이 두 분야를 바라봐야 할까에 대해서 간단한 예제들를 중심으로 이야기를 풀어보려 한다.



[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 하드닝(Hardening)
10. 보안 설계 문제
11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터


 


 


1. 들어가면서

  위의 두 개의 큰 주제를 하나의 파트로 묶은 이유는, 결과적으로 생각했을 때 두 분야의 연결점이 무척 많다고 생각하기 때문이다. 둘 중 한 쪽 분야에 대한 탄탄한 지식은 다른 분야를 접근 할때 허들을 많이 낮출테고, 일을 함에 있어서도 공통이 되는 지식 분야가 분리하기 힘들 정도로 많이 겹친다고 본다. 비유하자면 포렌식이 생태계과 생물들이 살아갔던 흔적에 주로 관심이 있다면, 리버싱은 생태계 안에서 살아가는 생물들의 행동 관찰에 관심이 있는 분야라고 본다. 생물을 이해하려면 생물들이 살아가는 환경을 이해해야 하고, 환경의 변화를 추적하려면 환경안에서 살아가는 존재들의 상호작용을 이해해야 할 것이다.

 

  사실 그것은 보안 전체에 대해서도 마찬가지인 부분 같다. 편의상 여러 분야로 나누어서 보안 지식을 분류하긴 하지만, 크게 보면 모든 분야가 유기적으로 얽혀 있다고 볼 수 있기 때문에, 어디나 분위기는 비슷비슷 하다고 본다. 반대로 여러 분야의 지식이 어느 정도 골고루 쌓여있지 않다면 뭔가 구멍이 뚫려 있듯이 어설픈 상태에 놓였다는 느낌이 들게 된다. 개인적으로 이 두 부분은 스스로 그렇게 느끼고 있는 구멍나 있는 부분들이기도 하다^^ 

 

 

 

 

2. 리버싱 예제 만들어 보기

  어딘가에서 만들어 놓은 샘플을 하나 가져와도 좋겠지만, 좀 더 간단하지만 통제가 되고 이해가 되는 예제를 예로 들기 위해서, 직접 C++ 로 프로그램을 하나 만들어서 비주얼 스튜디오의 정적 환경(뭐 디버깅 중이라서 동적이라고 볼 수도 있겠다)과 디버거 상에서 동적으로 실행되는 2개의 환경에서 코드를 비교해 볼까 한다. 윈도우즈 환경이기 때문에 요즘은 무료로 제공되고 있는 비주얼 스튜디오 커뮤니티(Visual Studio Community) 버전을 설치해 보도록 한다.

 

 

  구글에 "visual studio community" 라고 적으면 아래의 다운로드 링크를 찾을 수 있다.

 

[visual studio community - microsoft 홈페이지]

https://visualstudio.microsoft.com/ko/vs/community/

 

 

  다운로드 하여 실행하면 설치 화면이 나오는데, 설치를 진행하면서 Workload 탭 에서 "desktop development with c++" 기능을 체크해서 설치한다(이미 설치한 상태라서 스크린 샷은 못찍었는데 아래의 블로그를 참조하면 비슷한 듯 싶다). 뭐 다른 언어도 이것저것 공부할때 필요할 듯 하다면 설치해도 좋다.

 

[비주얼 스튜디오 2017 커뮤니티에서 c/c++ 프로그래밍 하기 - 모두의 코드님 블로그]

https://modoocode.com/220

 

 

 

2.1 리버싱 예제 만들기

  이후 프로그램을 실행을 시키고, "새 프로젝트 만들기" 를 선택한다. 위쪽 검색 메뉴에서 "c++" 를 입력하면, 아래와 같이 c++ 로 콘솔 앱을 만드는 항목이 보이게 된다. 해당 항목을 선택하고, "다음" 버튼을 누른다.

 

 

  프로젝트 이름을 "ReversingSample" 로 위치를 매번 파이썬 파일을 만들던 "c:\Python\Code" 로, 솔루션 이름을 "ReversionSample" 로 한 후, "만들기" 버튼을 누른다(나중에 디버거 툴로 선택할 exe 파일을 잘 찾을 수 있다면 기본값으로 설정 하거나 임의로 생성해도 된다)

 

 

  아래와 같이 정말 간단한 샘플 코드가 만들어져 나오는데, 간단히 기본 커맨드 화면에 "hello world" 를 뿌려주는 코드다(파이썬 코드의 print("hello world\n") 문이 하나 있다고 보면 된다)

 

코드 부분만을 분리해 보면 아래와 같다.

1
2
3
4
5
6
#include <iostream>
 
int main()
{
    std::cout << "Hello World!\n";
}
cs

 

 

 

  위의 코드 자체에서 바로 살펴봐도 되지만, 그러기에는 뜯어 볼 게 너무 없는 코드이므로, 해당 내용을 모두 지우고 아래와 같이 for 문을 돌면서 Hello World 문구를 4번 입력하는 코드로 대체해 보자. 마지막에는 출력후 커맨드 창이 바로 종료되지 않도록 getchar() 함수를 넣어 사용자가 글자를 하나 입력해야 종료되도록 만들었다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
    for (int count = 0; count < 4; count++)
       printf("hello, world\n");
 
    getchar();
    return 0;
}
cs

 

 

 

2.2 정적인 코드 보기

  정적인 분석을 하기 위해서 이제 화면에서 c++ 코드에 해당 하는 어셈블리 코드를 봐보자. 원래는 cl.exe 라는 명령어를 사용해서 컴파일을 해서 보는 방법을 많이 쓰는 거 같은데, 구글을 찾다보니 IDE UI 에서 편하게 볼 수 있는 기능이 있어서 해당 방식으로 보려 한다.

 

  아래의 그림과 같이 for 문 코드의 왼쪽 공간의 영역에서 마우스 왼쪽 버튼을 클릭해 빨간 브레이크 포인트를 찍는다. 이후 위쪽 Debug 드롭박스 옆의 환경을 64비트 어셈블리 코드를 보기위해서 "x86 -> x64" 로 바꾼다. 이후 그 옆의 "로컬 Windows 디버거" 버튼을 눌러서 디버깅 모드로 들어간다.

 

 

  그러면 컴파일 및 빌드가 진행 된 후, 아무것도 표시되지 않는(브레이크 포인트 땜에 아직 for 문이 돌아기지 못했으므로... 피들러에서 브레이크 포인트를 건 것과 비슷하다) CMD 창이 하나 뜨면서, 비주얼 스튜디오 창의 브레이크 포인트를 찍은 for 문 코드에서 멈춰 있게 된다.

 

  이 후, 해당 for 문 코드 위에서 마우스 오른쪽 버튼을 누르면, "디스어셈블리로 이동" 이라는 메뉴가 보이게 된다. 해당 메뉴를 클릭하면, 탭이 하나더 열리면서 현재 코드에 해당하는 어셈블리 코드가 보이게 된다(마치 브라우저에서 요소검사를 했다고 생각해도 될듯 싶다).

 

 

  그럼 위와 같은 처음 보게 된다면, 조금은 암호와 같이 느껴질수 있는 코드가 보여지게 된다. 화면을 보면 코드를 비교해 편하도록 기존 c++ 코드를 흰색 글자로 보여주고, 그에 해당하는 어셈블리 코드를 회색 코드로 보여주고 있다. 어셈블리 코드 쪽을 문법을 보면 크게 3가지의 섹션으로 나누어 지는데, 첫번째 섹션이 메모리 주소(00007FF... 같은거), 두번째 섹션이 명령어(mov, jmp 같은거), 세번째 섹션이 인자(eax)라고 보면 된다.

 

  컴파일을 하게 되면 0과 1로만 이루어진 기계어로 된 코드가 만들어 지는데 해당 코드가 CPU 쪽으로 전달되게 되어 정해진 명령어들을 수행하면서 컴퓨터가 동작하게 된다. CPU 를 0과 1로 이루어진 코드를 이용해 움직이게 하는 부분이 상상이 안간다면 파이썬 글에서도 추천했던 아래와 같은 책을 한 권 읽어보길 권한다.

  • CODE 코드 : 하드웨어와 소프트웨어에 숨어 있는 언어

 

  그런 0과 1로 된 코드를 보면서 동작을 상상하는 것은, 사람들에게는 낯설기도 하고 코드도 너무 복잡해 져서 하기 힘든 일이기 때문에, 조금 더 CPU 와 상호 작용하는 부분을 동작에 따라 분류해서 사람이 읽기 쉽도록 정리한 언어가 어셈블리어이다. 지금은 일반적으로는 고대어 처럼 취급되긴 하지만, 처음 컴퓨터가 개발됬을 때에는 기계어가 지금의 c 언어 정도이고, 어셈블리어가 자바나, Python, NET 같은 사람에게 가독성과 프로그래밍을 쉽게 하도록 만들어주는 언어 였을 것 같다. 프로그래머들은 아마도 기계어를 안봐도 되는, 어셈블리 언어가 있는게 무척 고마웠을 것이고 말이다. 그런 어셈블리 언어를 다시 한번 감싼게 c, c++ 같은 언어이고, 그 것을 한번 더 추상화 한게, Java 나 Python 같은 언어일 것이다.

 

  현재는 프로그래밍 분야에서는 컴파일러의 동작을 배우거나, 임베디드 프로그래밍을 하지 않는 이상 어셈블리어를 배울일은 거의 없을 것이다(위키를 보다보니 요즘은 사물인터넷 때문에 작은 하드웨어에 최적화된 프로그래밍을 하기위해서 어셈블리어가 다시 사용이 늘고 있다고는 한다). 파이썬의 numpy 같은 특정 모듈이 성능을 위해 c 언어로 개발됬듯이, 종종 c 프로그램의 성능과 효율성을 높이기 위해서 특정 모듈을 직접 어셈블리어로 개발하는 경우도 있다곤 하지만, 꽤 드문 일일 것같다. 그래서 특정한 분야에 일하지 않고는 해당 언어를 사용하거나 볼 수 있는 기회가 드물다는게 어셈블리어를 배우는데 동기부여가 힘든 부분 중 하나인것 같다. 예를 들면 어떤 사람들에게는 평소에 전혀 안 쓰는 예전의 라틴어 같은 언어를 배우는 거와 비슷한 일이 되버릴 수 있다.

 

 

  또한 어셈블리어와 기계어는 1:1 의 매칭이 될테지만, 상위 수준의 언어와는 1 대 多, 또는 多 대 1의 관계가 될 수도 있다. 예를 들면 고 수준 언어로 갈수록 객체 지향 같은 구조적인 프로그램밍이나 코딩의 편의를 위해서 생긴 여러가지 추상적인 요소가 있을 수 있겠지만, 컴파일이 되어 실제 실행을 하는 기계어 코드 입장에서는 그런 사람을 위한 머리 꼬리들은 모두 띄어내고 실행에 필요한 요소들만 코드화 하게 할 것이다. 그렇게 됨으로서 한번 컴파일 된 언어는 다시 돌아갔을때, 원래 언어의 모양 그대로 복원하기 힘들게 되는 일이 생겨버리게 된다(아주 예쁘게 데코한 케잌을 일하면서 한 손으로 효율적으로 먹기위해 꽁꽁 뭉쳐버렸다고 생각해 보자. 원래 모양에 대한 정보가 없기 때문에 완전한 복원은 힘들다--;).

 

  또 자바나 Python 같은 언어를 사용해 보면 c 같은 언어에 비해서 사용자 코드만 만들면, 나머지 모든 것은 자동으로 연결해 움직이게 해주는 경향이 있다. 그러다 보니 실제 코딩을 한 코드가 빌드 될때, 기계어로 바뀐 후, 여러 미리 만들어진 외부 코드를 링크 하여 사용하기 때문에, 실제 기계어 입장에서 실행되는 코드는 처음 코드보다 몇 배는 부풀려져 있다. 보통 디버거를 돌릴때, 우리가 찾으려는 코드에 닿기 전에, 앞에 OS에서 움직이기 위해서 필요한 여러 코드(또는 디버깅을 방해하기 위한 방어코드)의 숲을 빠져나오는 여러가지 요령을 배우게 된다. 

 

  IDE 의 Hex-Rays 라는 플러그인을 사용하면 어셈블리어가 좀더 가독성이 높은 c++ 로 보여진다고는 하지만, 밑의 블로그에서 볼수 있듯이, 사람이 만든 변수명 등의 추상적인 정보가 다 기계어 쪽에 남아있진 않기 때문에 복원이라기 보단 어셈블리어가 문법만 c++ 로 보이도록 재구성 했다고 보는게 맞을 듯 싶다. 그리고 어째거나 c++ 도 엄청 잘해야 해당 코드를 잘 볼수 있을 테고 말이다.

 

[Reversing C++ programs with IDA pro and Hex-rays - ARIS's Blog]

https://blog.0xbadc0de.be/archives/67

 

  자바같이 여러 환경에서 자바 가상환경만 설치하면 그 위에서 호환되어 돌아가는 언어는, 컴파일을 하면 기계어가 아닌 가상환경에서 해석되는 공통 코드로 만들어지기 때문에 원래의 자바 언어로 디컴파일이 좀더 쉬운 편이다. 하지만 뭐 항상 그렇지만 코드는 점점 복잡해 지고 있고, 해당 언어를 아주 잘하지 않는 이상 남이 만든 코드를 보고 주어진 시간내에 이해하는 건 쉬운일은 아닌 것 같다.

 

[컴파일러 - 나무위키]

https://namu.wiki/w/%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC

 

 

 

  사족이 길었지만 다음 이야기를 진행하기 전에 프로그램의 리버싱에서 왜 어셈블리어를 사용해야 하는지를 이해하고 넘어갔음 해서 그랬다. 그럼 비주얼 스튜디오에서 만든 코드를 보면서 이미 알고 있는 c++ 코드에 기반해서 어셈블리 코드의 동작방식을 해석해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    for (int count = 0; count < 4; count++)
00007FF799ED184A  mov         dword ptr [rbp+4],0  
00007FF799ED1851  jmp         main+3Bh (07FF799ED185Bh)  
00007FF799ED1853  mov         eax,dword ptr [rbp+4]  
00007FF799ED1856  inc         eax  
00007FF799ED1858  mov         dword ptr [rbp+4],eax  
00007FF799ED185B  cmp         dword ptr [rbp+4],4  
00007FF799ED185F  jge         main+4Fh (07FF799ED186Fh)  
        printf("hello, world\n");
00007FF799ED1861  lea         rcx,[string "hello, world\n" (07FF799ED9C28h)]  
00007FF799ED1868  call        printf (07FF799ED11D1h)  
00007FF799ED186D  jmp         main+33h (07FF799ED1853h)  
    getchar();
00007FF799ED186F  call        qword ptr [__imp_getchar (07FF799EE02F0h)]  
    return 0;
00007FF799ED1875  xor         eax,eax  
}
cs

 

  실제 편집창을 보면 저 잘라낸 코드 말고도 전체 코드는 엄청 길지만 앞뒤의 코드는 앞서 말한 "윈도우즈 환경에서, 커맨드 창을 통해서 해당 코드가 실행하기 위한 행사코드(행사 코드는 개발자인 임백준씨의 표현인데 이 경우 적절한 표현같다)" 라고 생각하면 될것 같다.

 

  다음 부터 나오는 모르는 어셈블리 키워드 들은 구글에서 "어셈블리 eax" 등으로 찾아 퍼즐을 맞춰보면 된다. 코드를 보면 rbp, eax, rcx같은 애들이 나타나는데, 이들은 모두 레지스터를 나타낸다. 레지스터는 CPU 입장에서 보면 프로그램의 변수와 같다. 이 변수들에 메모리에 있는 주소 번지, 숫자들과 잘 조합해서 명령을 실행한다.

 

  rbp, rcx 같은 경우는 64비트에서만 해당 표현을 볼수 있는 레지스트로 만약 앞에서 x86 으로 드랍박스를 선택했다면 나오지 않을 레지스트 이름이다(x86에서는 ebp, ecx등으로 e 로 시작되게 지칭한다). 앞으로 코드를 보면 알겠지만 64비트에서는 rcx 도 32비트에서의 동급인 레지스트인  ecx 표현으로도 같이 사용한다(코드 흐름상 추측하면 64비트에서는 rcx 사용 공간을 반만 사용하면 ecx 같다). 각 변수들은 미리 잘 쓰는 용도를 정해 놓은 편인데, 그건 어셈블리 코드를 많이 보면서 패턴상에서 감으로 익혀야 될 문제 같다.

 

   2번째 라인을 보면 "mov  dword ptr [rbp+4],0" 이 있다. 대충 살펴보면 rbp 라는 메모리 스택의 주소를 관리하는 레지스트가 가르키는 스택 위치에 0을 넣는다. 위에서 보면 c++ 코드의 "for (int count = 0; count < 4; count++)" 의 count=0 에 해당하는 주소 같아 보인다. 이후 3번째 줄의 "jmp   main+3Bh (07FF799ED185Bh)" 의 점프(jump=jmp) 명령을 통해 해당 주소인 7번째 줄(07FF799ED185B 주소)의 "cmp   dword ptr [rbp+4],4" 로 간다. 보면 아까 넣어놨던 0을 4와 비교(compare=cmp) 하는 라인이다.

 

  그 다음 8번째 줄을 보면 "jge main+4Fh (07FF799ED186Fh)" 로 앞 줄에서 비교한 결과가 크거나 같으면 루프를 벗어나 다음 코드(07FF799ED186F 주소 - c++ 코드로 보면 getchar())로 점프하는(jump if great or equal = jge) 코드가 있다. 지금은 0과 4를 비교했기 때문에 당연히 만족이 안 될 것이므로 점프 하지 않고 9번 줄로 넘어 갈것이다.

 

  9 번째 줄로 가면, 문자열 "Hello World\n" 가 담긴 주소를 rcx 레지스터로 담는다(Load Effective Address = lea). 이후 10번째 줄에서 "call  printf (07FF799ED11D1h)" 를 통해 윈도우즈의 printf api 를 호출해서 화면에 "Hello World" 를 뿌린다. 이후 11번째 줄에서 다시 루프의 시작에서 실행이 안됬던 4번째 줄로 점프한다(jmp  main+33h (07FF799ED1853h)

 

  4번째 줄을 보면, 아까 스택에 넣었던 0값을 eax 레지스터로 옮긴 후(mov  eax,dword ptr [rbp+4]), 5번째 줄 "(inc  eax)" 에서 1을 증가(increment = inc) 시키고, 6번째 줄에서 다시 1이 더해진 eax 값을 아까는 0이 들어있던 스택 주소로 보내 값을 0->1로 업데이트 한다(mov  dword ptr [rbp+4],eax)

 

  이후 7번째 줄로 가면 다시 위에서 설명했던 비교 명령을 통해서 1과 4를 비교하게 된다. 이렇게 계속 반복되다보면 스택의 0 값은 0, 1, 2, 3, 4, ... 가 되고, 0~3까지는 계속 Hello World 를 4번 표시하다가 4가 담기면서 4보다 크거나 같다는(jge) 조건을 만족하게 되서, 비로서 루프를 빠져나가서 14번 줄의 "call  qword ptr [__imp_getchar (07FF799EE02F0h)]" 을 통해 getchar() API 를 호출하게 된다. 이후 16번 줄의 "xor  eax,eax" 을 통해(자기자신을 xor 하면 0이 되니까 뭐 대충 c++ 의 "retrun 0" 의 느낌이 난다) retrun 0 을 통해 메인 프로그램 함수를 종료하게 된다. 이렇게 저렇게 꼬인 점프 루프 부분만을 빼면 아래와 같은 그림으로 정리함 어떨까 싶다.

 

  이렇게 어셈블리 코드를 보게 되면 기존 고수준 언어랑 많이 다르다는 느낌이 든다. 절대 쓰지 말라고 권고하는 점프도 마음것 사용 하고, 무언가 빠른 실행 속도만을 위해 최적화된 코드의 느낌이다.

 

 

 

  그럼 마지막으로 한가지만 더 생각해 보자. 아래와 같이 다른 코드를 그대로 유지하면서, for 문의 2번째 "<" 만 "<=" 로 바꾸면 어떻게 될까?

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
    for (int count = 0count <= 4; count++)
        printf("hello, world\n");
    getchar();
    return 0;
}
cs

 

 

  유추해 보자면, 어셈블리어에는 jg (jump if greater than)라는 명령어가 있으므로, 기존 코드는 그대로 유지되면서 jge 가 jg 로만 바뀌면 한번 더 루프를 돌게 됨으로서 같아지지 않을까 싶다. 실제 코드를 수정하고 디버그 모드에서 어셈블리 코드를 보면 아래와 같이, jge 가 jg 로만 바뀐 어셈블리 코드가 나온다. 상황에 따라 두꺼운 책을 지루하게 보는 것보다는 이렇게 c 코드와 비교해 가면서 스스로 원본 코드의 난이도를 조정하면서 어셈블리 공부를 하는 것도 나쁘진 않을 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00007FF7046C184A  mov         dword ptr [rbp+4],0  
    for (int count = 0; count <= 4; count++)
00007FF7046C1851  jmp         main+3Bh (07FF7046C185Bh)  
00007FF7046C1853  mov         eax,dword ptr [rbp+4]  
00007FF7046C1856  inc         eax  
00007FF7046C1858  mov         dword ptr [rbp+4],eax  
00007FF7046C185B  cmp         dword ptr [rbp+4],4  
00007FF7046C185F  jg          main+4Fh (07FF7046C186Fh)  
        printf("hello, world\n");
00007FF7046C1861  lea         rcx,[string "hello, world\n" (07FF7046C9C28h)]  
00007FF7046C1868  call        printf (07FF7046C11D1h)  
00007FF7046C186D  jmp         main+33h (07FF7046C1853h)  
    getchar();
00007FF7046C186F  call        qword ptr [__imp_getchar (07FF7046D02F0h)]  
    return 0;
00007FF7046C1875  xor         eax,eax  
cs

 

 

 

2.3 동적으로 보기

  이번에는 여러 리버싱 책에서 하는 것처럼, 디버기를 이용해서 같은 코드(다만 앞에 본것은 디버깅 빌드 버전이고, 이번엔 릴리즈 버전으로 만들어 본다. 디버깅에 필요한 사족이 다 제거된 버전이라고 보면 될 듯 하다)를 봐보자.

 

  우선 디버깅을 할 exe 파일을 만들어 내보자. 위쪽의 네모난 정지 버튼이나 "Shift+F5" 를 눌러 디버깅을 중지한다. 이후 아까 Debug 상태의 드롭박스를 "Release" 로 바꾸고, "빌드>솔루션 빌드" 명령어를 통해 빌드를 한다.

 

1>코드를 생성했습니다.
1>ReversingSample.vcxproj -> C:\Python\Code\ReversingSample\x64\Release\ReversingSample.exe

 

 

 

  이제 디버거 프로그램을 다운받도록 해보자. 구글을 찾아보니 요즘은 64비트 환경에서는

x64dbg 라는 프로그램을 많이 쓰는 것 같다. 아래의 페이지에서 zip 파일을 다운로드 받아서 적당한 폴더에 압축 해제 한다.

 

[x64dbg]

https://x64dbg.com/#start

 

 

  이후 release\x64 폴더내에 있는 x64dbg.exe 파일을 실행 시킨다. 디버거 창이 뜨면 "파일 > 열기" 명령어를 통해서 아까 만들어 놓은 C:\Python\code\ReversingSample\x64\Release 폴더에 있는, "ReversingSample.exe" 을 선택한다. 아래와 같이 디버깅이 시작되는 화면이 열린다.

 

 

  디버거는 프로그램이 시작되면서 cpu 에서 실행되는 명령어 들에 대해서 어셈블리 레벨에서 가로채서 보여주는 프로그램이다. 그러다 보니 특정 명령어를 찾아 건너 뛰거나, 프로그램에서 사용하는 여러 값을 변경하거나 하는 일을 자유롭게(잘 이해만 한다면) 할수 있다. 앞에서 얘기한 웹에서의 피들러로 조작했던 클라이언트 코드와 비슷하지만, 보다시피 어셈블리와 그 어셈블리와 상호작용하는 시스템 자체의 동작을 이해해야 그것이 가능하기 때문에 자유롭게 구사하기 위해서는 브라우저로 실행 공간을 추상화 시킨 웹보다는, 배우서 익숙해야 할 부분이 더 많이 필요하다고 생각한다. 다만 그 더 많이 필요한 부분은 보안 기술 자체의 난이도 라기 보다는 앞 시간들에서 얘기했던 대상을 이해하기 위한 난이도인것 같다. 어셈블리나 해당 어셈블리가 돌아가는 환경들을 이해하는 부분은 보안의 측면이라기 보다는 기본적인 IT 기술의 이해쪽에 좀 더 가깝다고 본다. 다시 한번 강조하지만 보안 쪽은 대상의 기술적 측면을 이해하지 못한다면 할수 있는 것이 아주 적어진다.

 

 

  아까의 비주얼 스튜디오 환경에서는 원하는 코드에 대응하는 어셈블리 코드를 바로 볼수 있었지만(브라우저에서 요소 검사를 통해 해당 코드 위치로 이동한 것과 비슷하다고 볼 수 있다), 실행 파일에서는 우리가 작성한 코드가 실행되기 전에, 윈도우 환경에서 프로그램이 실행되기 위한 여러가지 행사코드들이 먼저 실행 될 것이기 때문에 한줄 한줄을 실행하게 되면 한참을 따라 가야한다. F8(건너서 단계 진행) 같은 키를 눌러 나름 큰 발자국으로 한 단계씩 이동하다가 원하는 코드를 만날 수도 있겠지만, 그러다 보면 원하는 지점을 놓쳐 몇번 시행 착오도 겪을수 있으므로 디버거에는 특정한 문자열이나 명령어, 패턴 등의 어셈블리 코드 요소를 기준으로 원하는 위치를 찾을 수 있게 검색 기능이 제공되어 있다. 해당 코드 화면에서 마우스 오른쪽 버튼을 누르고 "다음을 찾기 > 모든 모듈 > 문자열 참조" 를 선택하면 아래와 같은 프로그램이 가지고 있는 문자열들을 리스팅 하는 화면이 나온다.

 

  오른쪽 문자열 팬에 보면 우리가 뿌려주는 "hello, world\n" 문자열이 보인다. 해당 항목을 더블 클릭 해보자. 자 그럼 아까 비주얼 스튜디오에서 봤던 코드와 비슷하지만 조금은 모양이 다른 코드가 보이게 된다.

 

 

  그럼 한번 코드를 살펴보자. 비주얼 스튜디오와 비슷한 모양이 되도록 16진수 코드가 나오는 중간 부분은 편집해 제거했다.

1
2
3
4
5
6
7
8
9
10
11
12
00007FF62C5F1070 | push rbx                               | 
00007FF62C5F1072 | sub rsp,20                             |
00007FF62C5F1076 | mov ebx,5                              | 
00007FF62C5F107B | nop dword ptr ds:[rax+rax],eax         |
00007FF62C5F1080 | lea rcx,qword ptr ds:[7FF62C5F2220]    | 00007FF62C5F2220:"hello, world\n"
00007FF62C5F1087 | call <reversingsample.printf>          |
00007FF62C5F108C | sub rbx,1                              |
00007FF62C5F1090 | jne reversingsample.7FF62C5F1080       |
00007FF62C5F1092 | call qword ptr ds:[<&_fgetchar>]       | 
00007FF62C5F1098 | xor eax,eax                            | 
00007FF62C5F109A | add rsp,20                             | 
00007FF62C5F109E | pop rbx                                |
cs

 

  뭔가 첨에는 조금 달라 보였지만, 루프 코드를 보면 3번째 라인에서 ebx 레지스터에 5 값을 넣는다(mov ebx,5). 그리고 4, 5번 라인의 앞에서 이미 봐서 익숙한 패턴의 코드를 통해서 "hello world" 를 출력해 준다. 그리고 7번 라인을 보면 rbx(ebx 와 동일한 레지스트의 64비트 이름)에 1씩 빼준다(sub rbx,1). 이후 jne(jump not equal) 가 호출 되어 rbx 가 0인지 체크하여 0일 때까지 다시 5번째 라인인 7FF62C5F1080 주소로 점프하며 프린트를 반복한다.

 

  결국 실제 릴리즈 되어 실행되는 코드는 아까 디버깅에서 봤던 어셈블리와 모양은 다르지만 하는 일은 같은 등가의 코드라는 것을 알 수 있다.

 

 

 

 

3. 리버싱에 대해 생각해 보기

  앞의 예제를 통해서 짧게 격투기 선수의 초보 줄넘기 같은 짧은 리버싱 과정을 살펴 봤다. 그럼 다시 근본적인 문제로 돌아가서 왜 우리가 저렇게 귀찮은 과정을 거치면서 리버싱을 해야할까? 가장 큰 이유는 프로그램 소스가 없기 때문이다. 소스가 있는 프로그램을 굳이 리버싱을 하는 것는 KTX 열차 표가 있는데 뛰어서 서울에서 부산까지 가려하는 행동하고 비슷할 것이다.

 

  보통 리버싱 작업의 유용성을 예로 드는 데 사용하는 악성 코드는 소스를 모르는 채로 시스템을 공격하는 대상이고, 해당 실행 코드의 동작을 예측하고 방지하기 위해서는 내부 동작을 알아야 하기 때문에 궁극적으로는 리버싱 밖에는 방법이 없다(물론 여러가지 정적인이거나 해시 값을 체크하는 등의 히스토리에 기반한 방법도 있겠지만 그 쪽은 운이 좋은 편이라고 봐도 될듯 하다). 물론 악성 코드 분석과 같이 일정한 가상환경에 넣어놓고 동작을 관찰하는 보조적인 방법도 충분히 의미는 있을 것이다.

 

 

  리버싱 책을 보면 보면 보통 간단하게 어셈블리 코드의 동작 원리에 대해 설명한 후, 코드에서 원하는 위치를 찾아 로직을 이해하거나, 값이나 기능을 원하는 대로 패치하는 부분을 설명하고, 이후 PE파일의 구조, DLL 인젝션이나, API 후킹, 리버싱을 방해하는 방어수단에 대처하는 방법들을 설명하곤 한다.  그런데 사실 그러한 측면에서의 리버싱은 합법과 불법을 넘나들기도 한다. 같은 기술을 이용하여 타사의 프로그램의 동작 원리를 이해하여, 카피하고자 하는 용도로도 마찬가지로 사용될 수 있기 때문이다. 또한 긍정적인 측면으로는 많은 백신이나, 보안 툴들이 시스템 사용자의 허가(설치와 동의)를 기반으로 시스템을 보호하는데도 사용되며, 반대로 악성코드들은 그런 기술을 이용하여 사용자가 인지 못하는 사이에 동의를 얻어(택배문자를 클릭해 가짜 택배 프로그램을 설치한다던가) 자신을 숨기거나 사용자가 모르는 사이에 사용자의 자료나 행동을 기반으로 나쁜 행동을 벌이곤 한다.

 

  나아가 파이썬 시간에도 얘기했지만, 프로그램은 혼자서 움직이지 못하는 존재이다. 사용자가 직접 만든 코드 로직은 정말 적은 핵심 부분에 불과하고, 나머지는 타인이 만들어 놓은 모듈을 사용하고 있고, 해당 모듈도 해당 모듈의 언어에서 제공하는 라이브러리나, OS에서 제공하는 API(이것도 마찬가지로 코드라고 볼수 있다)를 사용해 해당 행동을 하게된다. 이 먹이사슬과도 같은 관점에서 보게 되면 여러분이 어셈블리를 잘 이해하고 미지의 프로그램을 잘 이해하려 한다면 해당 프로그램을 구성하는 언어 및 OS 의 API, OS의 동작 원리에 대해 잘 이해해야 한다는 결론이 나오게 된다. "Windows Internal" 같은 OS 동작을 설명하는 책부터, 시스템 프로그래밍 책 들과 같은 부분이 그런 시도의 출발점이 아닐까 생각한다. 좀 더 낮은 레벨에서 시스템을 안전하게 하려고 하면 할 수록, 이러한 평소에는 상위 언어에 감싸여 신경쓰지 못했던 부분들에 익숙해 져야 하는게 어쩔수 없는 면 같다.

 

  추가로 많은 악성 코드의 같은 경우는 외부와의 통신을 통해서 주요 로직 모듈을 다운 받거나 데이터를 받거나 전달하는 일도 많기 때문에, "통신" 이라는 개념에서 웹이나 다른 소켓 프로토콜과도 연관이 되어 버리게 되는 부분 같다. 그러다 보면 데이터 은닉(data hiding) 이라는 정상적인 것으로 위장해 하는 행동을 숨기려고 하는 쪽과도 주제가 만나게 된다.

 

 

 

 

4. 다른 측면의 리버싱

  위에는 코드에 기반한 얘기만 했지만, 블랙박스 테스트를 잘하면 화이트박스 기법과는 다른 측면에서 적절하게 주요 코드의 동작을 예측할 수 있고, 자바스크립트를 잘 분석하면 서버 사이드의 로직을 어느 정도 예측할 수 있듯이, 외부에서 관찰을 하여 분석하는 리버싱 영역도 있을것 같다.

 

  종종 만나는 부분이 게임 커뮤니티 같은데에서 사용자가 여러 실험을 통해서, 데미지 규칙을 유추 한다는지, 하는 부분이다. 사실 이런 부분은 서버 사이드 로직이기 때문에, 클라이언트를 아무리 분석해 봤자, 코드가 없을 테니 알 수가 없다.

["랩이 깡패다" 분석 실험 - 인벤]

http://www.inven.co.kr/board/lineagem/5056/4500

 

  뭔가 잘 짜여진 시나리오로 이런 반복적인 테스트를 하는 부분은 충분히 다른 측면의 리버싱이기도 하고, 클라이언트의 기반의 리버싱의 한계를 넘을 수 있게 하는 부분이기도 하다. 마치 별의 움직임을 관찰해서 행성들의 움직임의 규칙에 대한 가설을 세우는 일과 비슷하다고 보면 너무 거창한가는 싶지만 말이다.

 

 

 

 

5. 포렌식 예제 만들어보기

  뭐 예제라고 그러긴 좀 민망하긴 하지만, 포렌식의 측면을 살짝 엿볼수 있는 2개의 예제를 파이썬 코드로 만들어 살펴 보려고 한다.

 

 

5.1 특정 파일에 대한 생성, 수정, 접근 시간 보기

   첫번째는 파일의 생성일과 수정일 히스토리를 살펴보려고 한다. 우선 메모장을 열어서 "test" 라고 적고 c:\python\code 폴더에 test.txt 라고 저장한다(create). 이후 다시 해당 파일을 열어 "modify" 라고 다음 줄에 적고 다시 저장을 한다(modify). 이후 다시 해당 파일을 열어서 본다(access). 우리는 파일을 수정해서 저장하면 해당 파일만 남는 다고 생각하지만, NTFS 시스템은 해당 파일에 대해 생성, 수정, 접근에 대한 기록을 저장해 놓는다.

 

 

 

  "test.txt" 파일에 대한 해당 속성을 보기 위해 아래와 같은 파이썬 코드를 생성해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os, time, datetime
import stat
 
file = "test.txt"
 
# 파일 생성일 출력
created = os.path.getctime(file)
created_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(created))
print("생성 시간: " + created_time)
 
 
# 파일 수정일 출력
modified = os.path.getmtime(file)
modified_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(modified))
print("수정 시간: " + modified_time)
 
# 파일 접근일 출력
accessed = os.path.getatime(file)
accessed_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(accessed))
print("접근 시간: " + accessed_time)
cs

 

  내용을 보면 os모듈을 이용해서, 각 속성을 얻어와서 화면에 뿌린다. c:\python\code 폴더에 "UTF-8" 포맷으로 "show_timestamp.py" 라고 저장한다(잘 모르겠으면 파이썬 2교시를 참조). 이후 해당 파일을 실행 한다.

 

C:\Python\code>python show_timestamp.py
생성 시간: 2019-09-01 13:23:50
수정 시간: 2019-09-01 13:24:00
접근 시간: 2019-09-01 13:24:00

 

  근데 내용을 보면 이상하게 맨 마지막으로 파일을 열어본 시간이 안 찍히고 있다. 구글을 찾아보면 윈도우즈 7부터 성능을 위해서 파일이나, 디렉토리가 접근되었을때는 해당 정보를 업데이트 안한다고 한다. 이를 수정하기 위해서는 NtfsDisableLastAccessUpdate 레지스트리 키를 0으로 하면 된다고 한다. 뭐 해보고 싶으면 해봐도 좋을듯 싶다.

 

[Enable last access time - Open Tech Guide 사이트]

https://www.opentechguides.com/how-to/article/windows-10/129/enable-last-access-time.html

 

 

 

5.2 엑셀의 최근 항목을 레지스트리에서 찾아보기

   두 번째는 엑셀의 최근 접근 문서를 찾아보자. 엑셀을 열어보게 되면, 아래와 같이 최근 열었던 문서가 표시된다(오피스 16버전 기준).

 

 

  구글을 찾아보면, "HKEY_CURRENT_USER\Software\Microsoft\\Office\16.0\Excel\\File MRU" 경로에 있다고 나온다. 여러 전문적인 포렌식 툴도 있겠지만, 실행 창에서 "regedit" 를 입력하여 레지스트리 편집기를 열어 해당 경로로 이동해 보면, 아래와 같이 엑셀 창에서 봤던 최근 문서들이 키/값 으로 저장되어 있다.

 

  그대로 끝내긴 좀 그래서, 파이썬으로 해당 레지스트리 키를 가져와서, 최근 문서 값 만을 뿌려주는 코드를 하나 작성했다.

1
2
3
4
5
6
7
8
9
10
11
import winreg
# 키를 정의 한다.
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Office\\16.0\\Excel\\File MRU")
 
# 키 안에 담김 value 숫자를 얻어 그 숫자 만큼 루프를 돈다.
for i in range(0, winreg.QueryInfoKey(hKey)[1]):
    name, value, type = winreg.EnumValue(hKey, i)
 
    # Item 이라는 문자열로 시작하는 이름일 때 프린트 한다.
    if name.startswith("Item"):
        print (name + ": " + value)
cs

 

  역시 c:\python\code 폴더에 "UTF-8" 포맷으로 "show_excel_recently.py" 라고 저장 후 실행해 보자. 아래와 같이 원하는 결과가 나온다.

 

C:\Python\code>python show_excel_recently.py
Item 1: [F00000000][T01D415FA193618D0][O00000000]*C:\Python\code\07교시\result.xlsx
Item 2: [F00000000][T01D37D59BFAC7C70][O00000000]*C:\Backup\code\result2.xlsx
Item 3: [F00000000][T01D37D59BBFEB6B0][O00000000]*C:\Backup\code\result.xlsx

 

 

 

 

6. 포렌식에 대해 생각해 보기

  예제를 보았으니, 앞의 리버싱 얘기에 연결해서 전혀 다른 분야 같이 보이는 포렌식(forensic)에 대해 생각을 해보자

 

  포렌식의 가장 기본이 되는 형태는 삭제된 파일을 복구하는 undelete 작업일 것이다. 속도의 문제 때문에 컴퓨터에서 파일을 삭제할때(휴지통에 남기지 않더라도), 시스템에 등록된 파일에 대한 정보 위주로 삭제하고, 실제 파일 내용이 저장된 디스크의 자기 공간 부분은 그대로 남겨두게 된다. 

 

  왜 전체 내용을 다 지우지 않냐고 생각할 수도 있겠지만, 파일이라는 것은 디스크 내에 0과 1로 저장된 정보의 형태고, 그것을 완전하게 지우기 위해서는 랜덤한 형태로 (여러번) 덮어 씌워야 한다. 그렇다는 것은 몇 기가 정도의 파일을 완전히 삭제하려면 적어도 해당 파일을 디스크 사이에서 복제하는 시간만큼 동일하게 소요되게 된다는 것이다.

 

  매번 파일을 지울때마다(이건 사용자의 파일도 있을 수 있고, 시스템이 사용하는 파일일 수도 있다) 그렇게 번거로운 작업이 일어난다면 지금의 컴퓨터는 휠씬 더 많이 느려질 것이다(큰 파일 복사를 할때나 큰 용량의 파일을 여러개 다운로드 받을때 컴퓨터가 느려지는 현상을 생각함 될듯 하다). 그래서 파일을 지운 후 딱히 컴퓨터를 더 사용되지 않아 해당 파일이 저장되 있던 디스크 영역이 덮어써 지지만 않는다면, 원래의 파일로 복구가 가능할 가능성이 높게 된다. 물론 삭제를 해도 기존에 저장되었던 잔여 자기장에 기반하여 복구가 가능하다고는 하지만, 현실적으로는 가용 가능한 정보가 얼마나 복구될까는 싶다. 

 

[소거 프로그램 - 나무위키]

https://namu.wiki/w/%EC%86%8C%EA%B1%B0%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8

 

  앞의 어셈블리(또는 c++같은 낮은 수준의 프로그래밍 언어) 코드를 보면 알수 있지만, 컴퓨터는 의외로 생각보다 단순한 동작의 반복으로 우리가 보는 이 운영체체와 프로그램의 동작들을 구현한다. 단순하게 본다면 레지스트, 메모리, 디스크에 데이터를 썼다 지우거나 하는게 전부라고 봐도 될지 모른다. 그러한 과정에서 파일 삭제의 경우와 같이 무언가 완전히 덮어지지 않고 디스크 내에 남거나, 운영체제나 어플리케이션 들이 모니터링이나, 디버깅, 사후 추적의 목적으로 사용자 몰래 남기거나, 사용자가 부주의 해서 또는 필요해서 남긴 부분들이 존재하게 될 것이다. 또한 특수한 경우를 빼고는 메모리 상에는 대부분의 보호장치가 풀어지고 실행가능한 코드만 남게 되기 때문에 접근이 가능하다면 의미 있는 정보를 추출하는 것이 가능하다. 이런 부분들에 대해서 살펴보고, 복구하고, 시간의 측면에 따라 재구성 하여 사용자의 행동을 이해하고 증명하는 작업이 포렌식이란 영역이 아닐까 싶다.

 

  조금 공식적으로 얘기하자면, 그러한 데이터들을 이쪽에서는 아티팩트(artifact) 라고 부르는 것 같은데 그 아티팩트들이 존재할 수 있는 장소는 메모리 상의 여러 요소(실행되는 프로세스, 문서들), 평문으로 저장된 패스워드, 채팅 등 사용자간에 전달되는 데이터, 네트워크 커넥션 로그, MBR , 레지스트리, 로그 및 컨피그 파일, 프로그램 파일, 임시 파일, 데이터 파일, 삭제 후 유지되는 데이터 공간 등이 있을 것이다.

 

  앞에서 보안은 데이터의 흐름을 따라가는 일이라고 생각한다고 말했었는데, 모든 보안의 분야가 그렇겠지만 포렌식 분야야 말로 데이터에 포커스를 두고 흔적을 따라가는 일이라고 본다. 이상적으로는 데이터가 담긴 메모리, 디스크에 모든 필요한 정보가 담겨있다고 볼수 있지만, 해당 데이터들이 구체적인 사용 주체들과 연결되지 않는다면 0과 1로 만들어진 숫자에서서 쉽게 의미를 찾을 순 없을 것이다. 그렇게 하기 위해 OS 에 따른 디스크 및 메모리의 사용 방식, 프로그램이 동작하는 방식, 사용자가 생각하거나 움직이는 방식(사용자가 생성하는 데이터를 이해하거나, 사용자가 컴퓨터를 사용하는 용도나 스타일에 따라서도 접근이 달라질 수 있다고 본다)까지도 이해할 필요가 있다고 생각한다.

 

  나아가 명시적이진 않은 어플리케이션의 데이터를 추적하고 찾기 위해서는(역시 앞의 리버싱과 마찬가지로 소스를 모르니까) 앞 단에서 다르게 느껴졌던 리버싱 영역의 스킬들이 필요하게 될 가능성이 높아진다. 여기서에 두 개의 영역이 결국 만나게 된다고 생각하는데, 프로그램이나 운영체제의 행동을 쫓던 리버싱은 대상이 만들어내거나 접근하는 데이터에 관심을 가지게 되고, 데이터를 쫓아가던 포렌식은 데이터를 만들어 내는 프로그램이나 운영체제의 행동에 관심을 가지게 되어버린다. 결국 두 개의 분야의 지식이 만나 상호 작용하며 균형을 이루어야만 각 분야가 완전하게 될수 있는 것 같다.

 

 

 

 

5. 마무리 하면서 

  처음부터 많이 돌아오긴 했지만 리버싱과 포렌식은 서로 거리가 있는 분야가 아니며 서로의 영역의 지식이 필요한 연결되어 있는 분야라는 얘기를 하고 싶었다. 더 나아가면 보안의 다른 분야에서도 이러한 낮은 레벨의 지식이 필요한 분야가 전체적인 보안의 균형과 응용을 만들어 주는데 꼭 필요하다는 생각을 한다.

 

  그리고 현실적으로는 포렌식 같은 증명을 하는 작업은 객관적이고 표준적인 방법이 필요하므로(법적으로 증명해야 되는 경우도 점점 더 늘어나고 있을테니), 개인의 특별한 지식 보다는 공인된 기관에서 보안적으로 인증된 툴을 사용하여 증적을 만들어야 하는 경우도 많을 것 같다(회사가 인증이나 감사를 받을 때의 기준을 생각하면 이해가 될 것이다).

 

  다만 해당 툴의 결과를 올바르게 해석하거나 툴의 한계를 극복하는 부분은 앞의 스캐너 부분에서 얘기한 스캐너와 수동 테스트의 관계와 마찬가지라고 본다. 또 다른 측면에서는 취미나, 새로운 방법론을 연구하거나, 자동화 때처럼 공식적인 툴들이 지원하지 않는 마이너한 영역에 대해서 연구하는 측면도 있을 것 같다. 이렇게 보면 보안은 결국 컴퓨터라는 세상을 이해하는 일인 것 같기도 하다.

 

 

2019.9.1 by 자유로운설탕
cs

 

 

 

 

 

 

 

 

 

 

posted by 자유로운설탕
2019. 7. 7. 19:32 보안

  이번 시간에는 자동화에 대해서 얘기하려 한다. 보안은 많은 부분이 자동화를 기반으로 구축되어 있는 분야기도 한것 같긴 하지만, 그러한 부분을 조망하는 얘기를 하려는 것은 아니고, 한 개인의 입장에서 생각 할 수 있는 부분이 어떤가에 대해서 이야기 하려 한다. 



[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 하드닝(Hardening)
10. 보안 설계 문제
11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 


 


1. 들어가면서

  자동화에 대해서는 이전의 파이썬 글에서도 많이 언급이 된 주제이다. 웹 자동화에 대해서는 8교시 정규표현식, 11교시 웹 페이지 파싱, 12교시 웹 자동화편을 걸쳐서 얘기를 했고, 13교시에서 윈도우즈 자동화를, 14교시에서 작업에 대한 자동화를 이야기 했다. 10교시의 Whois API 이용도 API 를 호출해 자동화 한다는 측면에서 마찬가지이다(마음의 여유가 있다면 이번 시간을 보기전에 해당 글들을 가볍게 보고 왔음 한다)

 

  파이썬 글에서는 그렇게 나눠 놓긴 했지만, 지금와서 드는 생각은 실제 자동화는 저 부분들이 서로 섞여서 진행되는 것이라는 것이다. 조금 더 나아가면 우리가 프로그래밍 한 줄을 실행하는 시점 부터 자동화를 수행하고 있다고 봐도 될 듯 하다. 지금 이 글을 쓰고있는  컴퓨터 자체나, OS, 브라우저, 엑셀 포토샵, 일러스트레이트 같은 디자인, 편집 프로그램, 매일 재미있게 하고 있는 게임 등도 결국은 자동화의 결정체라고 보는게 맞지 않을까 싶다. 사람은 항상 효율성을 추구하는 방식으로 생각하기 때문에(어쩌면 사회적 훈련의 결과일지는 모르겠지만), 사람이 하는 많은 일들은 점점 성숙이 되다보면 자동화로 귀착되어 버리는 것 같다.

 

  보안쪽에서 흔히 솔루션이라고 얘기하는 많은 하드웨어(보통 소프트웨어가 기반이긴 하지만)와 소프트웨어도 결국은 보안 업무의 자동화가 구현된 결과라고 볼수 있다. 우리가 당연한게 사용하고 있는 백신이나, 각종 제어 솔루션, 모니터링 시스템, 방화벽, IPS, 여러 회사들의 보안 솔루션들도 결국은 보안적 지식을 자동화 하여(결과적인 판단 부분에서는 아직 사람들이 많이 관여해야 하겠지만) 구현한 결과라고 볼수 있다. 이 부분에 대해서는 파이썬 21교시의 "The five orders of ignorance" 에서 소개한 지식을 담는 매체의 관점으로 이해하면 어떨까 싶다.

 

 

 

 

 

 

 

2. 보안에서의 자동화 

  맨 처음 부분에서 "한 개인의 입장에서"라고 범위를 좁혀놓은 이유는 이젠 보안 쪽에 있는 많은 자동화의 부분이 개인이 감당하기에는 범위 및 정보가 너무 커졌다고 생각하기 때문이다. 백신 같은 분야만 봐도 예전에는 한명의 개인이 만들어 유지보수가 가능한 부분이였다면, 현재는 수많은 사람들이 해당 회사에 속해서 24시간 돌아가면서 새로운 악성코드 들에 대응을 해야되는 분야가 된것 같다. 또한 백신이 최신의 악성코드 공격을 따라기기 위해서 수집 해야하는 다양한 데이터의 양도 이제 한 개인이 수집하기 에는 너무 다양한 환경과 분석해야 될 데이터의 양이 존재한다(데이터를 준다해도 저장한 서버를 살 돈이 없어서라도 못할듯 싶다--;). 백신 회사들이 개인에게는 대부분 무료 백신을 제공하는 부분도 점유율을 높인다거나, 위험한 PC 들이 너무 늘어나게 되는 것을 기본적으로 방지하는 목적도 있겠지만, 다양한 사용자들의 환경과 그곳에서 발견되는 악성코드를 수집하기 위한 목적도 큰 부분을 차지할 것 같다. 그렇게 보면 세상에 공짜는 없어 보인다. 

 

  이러한 부분은 네트워크, 스캐너, 디비 접근 제어 및 모니터링, 매체제어 솔루션, 패치 등의 관리도 그렇고, 포렌식이나 리버싱 툴도 마찬가지인것 같다. 이젠 작은 개인이 이러한 부분에 대해서 무언가 의미있는 성과를 내기에는 관련된 환경과 데이터가 감당할 수준을 넘어버렸고 ROI와 계속적인 유지보수 측면에서 어려운 시기가 된것 같다. 또한 만들어 놓은 부분에 대해서 잘 동작하느냐를 증명하는 QA 부분도 더더욱 해당 부분을 어렵게 만든다. 또한 필요한 기술의 광범위 함도 한 몫 한다.

 

  예로서 만약 디비 접근 모니터링 솔루션을 만들고 싶다면 여러 데이터베이스의 통신 패킷들을 파싱하고 원하는 값들을 뽑을 수 있어야 한다. 물론 현재에 딱 맞는 오픈 라이브러리가 있을지는 모르지만 그 라이브러리가 적용 후, 새로운 데이터베이스 버전이 나왔을 때 더이상 업데이트가 안된다면 어떻게 하겠는가?(다른 많이 사용되는 오픈소스 라이브러리들도 마찬가지라고 생각될지 모르지만, 수많은 사람에게 필요한 데이터베이스에 쿼리를 날리는 범용의 목적을 가진 라이브러리와, 데이터베이스 패킷을 파싱하는 특수한 라이브러리는 유지보수가 계속 될 확률이 많이 차이가 난다고 본다). 물론 커다란 인력 풀을 가진 회사들에게는 직접 개발 및 유지보수에 대한 제약이 적을지는 모르겠지만, 일반적으로는 성숙된 분야의 툴을 스스로 만드는 것은 힘든 부분 같다.

 

 

 

  그렇다면 작은 개인은 무엇을 자동화 해야할까? 이 부분은 틈새 시장이라는 표현으로 생각함 어떨까 싶다. 아직 변동성이 크거나 시장성이 없어서 정식의 외부 솔루션들이 개발되지 않거나, 솔루션이 있더라도 많은 투자를 하지 않아서 어설픈 분야들이 있다. 또는 솔루션을 들여놓기에는 너무 부담이 되서 부족하게 나마 유사한 효과를 가진 프로그램을 만들고 싶을 때도 있을 것이다. 또는 솔루션의 기능이 부족해서, 뭔가 추가적인 보충을 해보고 싶을 때, 솔루션 개발사나 오픈 소스에서는 보통 해당 부분의 개선이 여러 이유로 쉽지가 않다(수많은 요청을 하나하나 들어주다가는 수 백개의 유지해야할 소스 트리가 생길 수도 있다). 아님 최악의 경우 예산이 없어서 몸으로 때울 수는 없고 비슷하게 라도 만들어 땜빵을 해야 할지도 모르겠다.

 

 

  또 다른 측면에서는 일상의 모든일이 해당된다고 봐도 괜찮을 듯 싶다. 엑셀 작업을 돕는 수많은 매크로 및 함수들이 있듯이, 파이썬 같은 간편한 언어를 가지고 일상의 작은 일들을 자동화 하는 것도 괜찮아 보인다.  처음부터 거창한 무언가를 만들려 하는 것보다는 이러한 작은 조각 조각들을 구현 하다보면, 언제가 꽤 큰 퍼즐에 대해서도 그 동안 모은 조각들을 기반으로 처음 생각보다 어렵지 않게 만들어 낼 수도 있다. 개인적으로 생각하기엔 이런 접근이 프로그램을 주 직업이 아닌 보조 기술로 접근하는 사람들에게는 현실적일 듯도 싶다.

 

  예를 들어 데이터베이스의 모든 테이블에 대해서 특정 쿼리를 날려서 원하는 정보를 가져오는 일을 해야 하는 경우, 쿼리 분석기에서 일일히 쿼리를 만들어낼 수도 있겠지만(SQL 이나 텍스트 편집기들을 잘 쓰는 사람들은 여러 팁을 통해서 모든 테이블에 대한 쿼리를 일괄로 만들어 낼수도 있긴 하겠지만), 파이썬으로 데이터베이스에서 테이블 목록을 얻어낸 후, 적당히 조건에 맞는 문법과 조합하여 쿼리를 만든 후, 각각의 쿼리로 조회하여 결과를 엑셀 등에 정리하면 좀더 편할 것이다. 추가로 해당 작업이 매일매일 일어나거나, 쿼리가 자주 특정 패턴으로 변경 되거나, 대량으로 일어난다면 시간을 아끼게 된다고 느껴 매우 만족하게 될 수도 있다.

 

 

  여러 오픈 소스로 제공되는 대시보드 같은 프로그램 들도 결국은 이러한 자동화에 대한 장벽을 해당 분야에 익숙한 전문가들이 낮춰주려 하는 노력인것 같다. 그렇게 보면 Redis 나 Elastic Search 같은 데이터베이스도 키, 값을 쉽고, 효율적으로 저장하게 해주는 많은 노력들이 자동화의 산물로 만들어져 제공 된다고 보면 된다. 파이썬 같은 프로그래밍 언어나 거기에 속해져 전세계 사람들에 의해 무료로 개발되어 제공되고 있는 모듈들도 마찬가지 인것 같다. 우리 중 일부는 가끔 자동화의 1차 생산자가 되기도 하지만, 대부분의 사람들은 2차 생산자나 3차 생산자인 경우가 많은 것 같다.

 

  이렇게 보면 특정 분야의 자동화라는 말은 별 의미는 없는것 같기도 하다. 적용되는 형태는 다를지 몰라도, 적용되는 기술의 배경은 많은 부분들이 서로 겹치게 되는 것 같다. 결국은 자동화는 프로그래밍 또는 프로그래밍으로 감싸진 좀더 사용자 친화적인 2차 기술 들을 이용하여 일을 하는 방식 같다.

 

 

 

 

3. 자동화 예제 구현해 보기 - 데이터베이스에서 개인정보 찾기

  보안 쪽에서 가장 많이 쓰인다고 생각되는 자동화 프로그래밍 요소중 하나는 패턴에 대한 해석인것 같다. 그 중에서도 으뜸은 파이썬 8교시에 얘기한 정규 표현식 이라고 본다. 나중에 얘기할 모니터링과도 연결될 내용이긴 하지만, 결국 데이터를 기반으로 모든걸 자동으로 검사할 수 밖에 없기 때문에, 결국 통계든 Raw 데이터 자체든 패턴을 찾아야 한다. 물론 특정한 데이터베이스 안의 숫자의 변화를 체크하는 경우도 있긴 하겠지만, 그 숫자가 만들어지는 배경을 살펴 보게 되면 특정한 조건이 발생할때 쌓이는 경우라고 볼 수 있고, 그런 특정한 조건이 주어지는 상황 또한 패턴이라고 볼수 있다고 본다.

 

  여하튼 현재 보여줄 예제는 데이터베이스 안의 모든 테이블에 대해서 10건씩 데이터를 조회해서, 모든 컬럼에서 개인정보를 검사한 후, 결과를 반환하는 프로그램이다.

 

1) 데이터베이스내의 테이블을 가져오는 쿼리는 구글에서 "mssql get tables" 로 조회해 얻었다.

 

2) 기본적으로 테이블에 조회 쿼리를 만들고 행을 가져오는 부분은 파이썬 7교시의 내용을 pyodbc 모듈을 사용하는 것으로 변환해 적절히 만들었다(책에는 pyodbc 로 진행되게 됬어서 해당 샘플을 가져왔다). 예전 댓글이 생각나서 가능한 인자가 호출되는 부분은 parameterized query 로 만들었긴 했는데, 사실 어플리케이션 관점에서 보면 작업의 시작인 테이블 리스트를 가져오는 함수에 인자 자체가 없고, 테이블 이름이나, 컬럼 이름은 데이터베이스 내부에서 가져온 값이기 때문에 조작 가능성은 거의 없다고 봐도 될듯하다.

 

3) 개인정보 패턴을 찾는 정규 표현식은 구글에서 찾다가 행정 자치부에서 배포한 "홈페이지 개인정보 노출 가이드라인"의 83페이지를 참조했다(개인적으로 언어별로 최신 패턴으로 유지보수 되는 라이브러리를 제공하면 더 좋을 것 같다).

https://www.privacy.go.kr/nns/ntc/selectBoardArticle.do?nttId=5952

 

4) 마지막으로 어떻게 테이블, 컬럼, 10개 행을 어떤 자료구조에 넣어서 적절히 루프를 돌리면서 개인정보를 찾은 후 결과를 정리할 수 있을까 생각하다보니, Pandas의 Dataframe 을 사용하면 어떨까 싶었다. 개인적으로 dataframe 은 메모리에 떠있는 디비라고 생각하며 사용하고 있는데, 파이썬에서 간단하게 세로 열을 순서대로 가져와 검사할 수 있기 때문이다(예: A 테이블의 10개 행 중에 메모 컬럼에 해당하는 10개 데이터). 왠지 RDB 에서 가져온 결과를 담은 결과 리스트를 인덱스를 통해 조작하는 것보다는, openpyxl 같은 엑셀 형태의 객체나 pandas 같은 데에 넣는 것이 좀 더 편하게 원하는 값을 선택을 할 수 있을 것 같았다.

 

 

  위에 얘기한 것을 그림으로 정리하면 아래와 같다. 

 

 

  코드를 보기전에 개인정보가 담긴 테이블이 있어야 결과가 나올 것이기 때문에, 우선 MSSQL Management Studio 를 실행하여 기존 데이터베이스에 테이블을 2개 추가하고 데이터를 넣자)쿼리를 실행할 줄 모른다면 3교시를 다시 참고한다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
create table Secret_A
(
    MemberNo int,
    MobileTel varchar(20),
)
 
go
 
insert into Secret_A
values (10'010-1111-2222')
 
insert into Secret_A
values (20'010-3333-4444')
 
go
 
create table Secret_B
(
    MemoNo int,
    Memo varchar(100),
)
 
go
 
insert into Secret_B
values (2000'제 전화번호는 011-1234-5678입니다.')
 
insert into Secret_B
values (2001'이건 개인정보가 아니예요 010-2222')
cs

 

 

  테이블을 생성하고 조회하면 아래와 같이 조회 데이터가 나온다. 내용을 보게 되면 Secret_A 테이블에는 2개의 행에 모두 핸드폰 번호가 있고, Secret_B 테이블에는 1개의 핸드폰 번호가 있다. 파이썬과 보안 글을 모두 실습했다면 이 두개의 테이블을 포함해 7개의 잡다한 테이블이 있을 것이다. 

 

  그럼 파이썬으로 만든 코드를 봐보자. 위에 그린 그림과 비슷하게 get_table_names 함수에서 테이블 이름들을 가져오고, get_column_names, make_column_query 함수를 이용해 make_dataframe 함수가 데이터베이스에서 조회한 내용을 dataframe 에 담는다. 이후 check_personal_pattern 를 이용해 각 컬럼별 데이터들에 대해서 루프를 돌리면서 개인정보를 찾아 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import pyodbc
import pandas as pd
import re
 
 
# 연결 문자열을 세팅
server = 'localhost'
database = 'mytest'
username = 'pyuser'
password = 'test1234'
 
# 데이터베이스에 연결
conn = pyodbc.connect('DRIVER={ODBC Driver 13 for SQL Server};SERVER='+server+' \
;PORT=1433;DATABASE='+database+';UID='+username+';PWD='+ password)
 
# 커서를 만든다.
cursor = conn.cursor()
 
 
# 테이블 이름을 얻어옴
def get_table_names():
    table_names = []
    
    sql_get_tables = "SELECT name FROM sysobjects WHERE xtype='U'"
    cursor.execute(sql_get_tables)        
    rows = cursor.fetchall()
    
    for row in rows:
        table_names.append(row[0])
 
    return table_names   
    
 
# 테이블의 컬럼이름을 얻어옴
def get_column_names(table_name):
    column_names = []
    
    sql_get_columns = "SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?"
    cursor.execute(sql_get_columns, table_name)        
    rows = cursor.fetchall()
    
    for row in rows:
        column_names.append(row[0])
 
    return column_names
 
 
# select 컬럼 쿼리 제작(a, b, c 이런 식으로 만들어진다)
def make_column_query(column_names):
    column_query = ''
 
    for name in column_names:
        column_query = column_query + name + ','
    column_query = column_query[:-1]
    return column_query
 
# 테이블의 내용을 읽어서 Panda Dataframe 에 저장한다.
def make_dataframe(table):
    columns = get_column_names(table)
    column_query = make_column_query(columns)
    
    query1 = "SELECT top 10 " + column_query + " FROM " + table + "(nolock)"
    df = pd.read_sql(query1, conn)
 
    return df
 
# 핸드폰 패턴을 찾아서 반환한다
def check_personal_pattern(table, column, check_items):
    # 결과를 반환할 배열
    detected_list = []
    # 핸드폰 정규 표현식
    mobile_tel_pattern = "(01[016789][-~.\s]?[0-9]{3,4}[-~.\s]?[0-9]{4})"
 
    for check_item in check_items:
        check_item = str(check_item)
        match_mobile_tel = re.search(mobile_tel_pattern, check_item)
 
        if  match_mobile_tel:
            matched_word = match_mobile_tel.group(1)
            matched_type = "핸드폰번호"    
            
            # 이미 다른 row가 걸린게 있다면 발견한 정보를 추가해 업데이트 하자 
            if detected_list:
                detected_list[0][3= detected_list[0][3+ " / " + matched_word
            # 처음 걸린 거라면 그냥 넣자
            else:
                detected_list.append([table, column, matched_type, matched_word])
             
    return detected_list
 
 
################
# 메인코드 시작
################
 
# 데이터베이스에서 테이블 이름을 얻어온다
tables = get_table_names() 
 
# 테이블 하나하나에 대해서, 10개의 row 를 불러와 dataframe에 넣은 후,
# 개인정보 검사를 하여 결과를 반환 한다.
for table in tables:
    print("[" + table + "]"
    
    my_dataframe = make_dataframe(table)
    
    # 컬럼 이름을 가져와서
    for column in my_dataframe.columns: 
        # 컬럼 이름에 해당하는 수직 row 데이터를 가져온다.
        lists = my_dataframe[column].tolist()
        
        # 개인정보가 있는지 검사한다
        for item in lists:
            personal_infos = check_personal_pattern(table, column, lists)
        
        # 찾은 개인정보를 출력한다
        for personal_info in personal_infos:
            print(personal_info)
cs

 

 

  c:\python\code 폴더에 인코딩은 UTF-8 로 해서 dbsearch.py 란 이름으로 저장 후 실행해 본다.

c:\Python\code>python dbsearch.py
[supermarket]
[order_record]
[escape_test]
[play]
[sms_cert]
[Secret_A]
['Secret_A', 'MobileTel', '핸드폰번호', '010-1111-2222 / 010-3333-4444']
[Secret_B]
['Secret_B', 'Memo', '핸드폰번호', '011-1234-5678']

 

  결과를 보면 개인정보 패턴이 없는 위의 supermarket 같은 테이블들은 그냥 넘어갔고, 개인정보가 들어있는 Secret_A, B 테이블에서만 개인정보를 찾아서 결과를 보여준다.

 

 

 

 

4. POC 이상의 것

  위의 코드는 어찌보면 POC(Proof of Concept) 레벨이라고 볼수 있다. 실제 환경에서는 몇가지 추가적인 확장점들이 필요하다.

 

  첫 째, 위에는 핸드폰 번호 하나밖에 없지만, 여러 개인정보 패턴을 넣도록 해야한다. 가이드에서 제시된 패턴이 실제 필드의 환경에 맞는 적절한 패턴인지도 검증도 해야된다. 특정 필드에서 쓰는 개인정보 패턴은 가이드에도 없을 수 있으므로, 정규 표현식으로 찾을 수 있는지 고민하고 구현을 해야할지도 모른다. 다른 개인정보 패턴을 넣게 되면 핸드폰 패턴을 검사하는 코드가 아마 더 복잡하게 되어버리게 될것 이다. 추가로 파이썬에서 쓰는 정규표현식 표현으로 일부 조정해야할지도 모른다.

 

  둘 째, 범위의 확장도 해야된다. 지금은 데이터베이스 하나밖에 없다고 가정하지만, 수많은 서버가 있고 해당 서버내에 데이터베이스가 복수개 있다면, 서버 목록을 관리하고, 해당 서버에서 데이터베이스를 찾는 부분이 확장되서 만들어져야 한다.

 

  셋 째, 아마 실제 필드에서 위의 프로그램을 돌리게 되면 수많은 false postive 들이 나올 것이다. 해당 부분이 패턴으로 찾는 방식의 맹점중 하나인데 그러한 부분들을 감소시켜 최종적으로 봐야할 데이터를 줄이는 여러 노력들이 필요하다. 안전한 컬럼명의 제외라든지, 특정 패턴의 예외라든지, 패턴들의 통계적 분포 라든지 등등, 패턴이 찾았지만 실제 개인정보가 아닌 데이터들을 자동으로 예외처리하는 여러가지 로직이 있어야 해당 툴을 사용하는 사람이 현실적으로 스트레스를 안 받고 유용하게 쓸수 있게 된다.

 

  넷 째, 결과를 어떻게 관리하고 보여줄지도 고민해 봐야 된다. 엑셀로 담을지 데이터 베이스로 담아 웹 페이지에서 관리하게 할지 등등, 히스토리나 결과에 대한 관리 및 검증에 대해서도 생각해 보면 좋을 것이다.

 

  다섯 째, 오라클, MySQL 같은 RDB 라면 위의 MSSQL 에 대한 코드를 비슷하게 수정해 쓸 수 있겠지만, Redis, MongoDB 같은 NoSQL 데이터베이스라면 앞의 데이터를 가져와 개인정보 검출 함수에 던지는 부분에 대한 정리를 해당 데이터베이스의 형태에 맞게 고민해 봐야한다. 쿼리 형태도 다를 뿐아니라, 넘어오는 데이터도 Json 베이스의 키, 값 데이터가 여러 깊이로 쌓여있는 경우가 많기 때문에 어떻게 파싱을 해야할지도 고민해야 한다. 또한 위의 테이블, 컬럼, 값에 대한 개념에 NoSQL 데이터베이스의 요소들을 어떻게 매칭 시킬지, 아니면 분리해 다룰지를 고민해야 한다. 인코딩, 디코딩에 관련된 데이터 깨짐 문제도 발생해 해결해야 할 수 있다.

 

  여섯 째, 검사해야 되는 대상자체를 유사도에 따라서 제외해야 할수도 있다. Elestic Search 의 날짜 별로 쌓이는 인덱스 라든지, Redis 의 캐시 성격의 키 같은 경우는 모든 것을 검사안하고 샘플링 하여 검사하는게 현실적일 수도 있다.

 

  일곱 째, 만들어진 프로그램에 대해서 어떻게 동작을 보장할 수 있을지에 대해서 검증 계획도 세워야 한다. 버그가 있을지도 모르는 프로그램이라면 안심하고 있다 뒷통수를 맞게 되거나, 프로그램의 효과를 자신하기 힘들게 된다.

 

  이렇게 열거하고 보면 할일이 엄청 많이 보이지만, 기본이 되는 POC코드가 만들어 졌으므로 하나하나 살을 붙여 보는 것도 꽤 재밌는 과정이 되리라 생각한다. 파이썬 글에서 얘기했던 여러 잡다한 주제들이 이런 과정에서 다들 현실화가 되서 하나의 툴로 동작하게 되는 것을 볼 수 있을지도 모른다.

 

 

 

 

5. 마무리 하면서

  자동화라는 망망한 바다 같은 주제를 짧은 예제하나로 요약해 소개해 좀 그렇긴 하지만, 앞에서 얘기했듯이 프로그래밍 자체가 자동화 이고, 이전 파이썬 글들도 결국은 자동화에 대해서 얘기한 것이라고 봐도 될듯 하다. 무언가 대단한 결과를 꿈꾸기 보다는 자신 주변에 있는 작은 자동화 요소들을 찾아서 조금씩 연습을 하면서 자동화의 씨앗을 키워보는게 어떨까 싶다. 

 

 

 

2019.7.13 by 자유로운설탕
cs

 

 

 

posted by 자유로운설탕
2019. 5. 20. 17:08 보안

  이번 시간에는 보안 쪽에서 백신 프로그램 만큼 자주 볼수 있는 스캐너(Scanner)에 대해서 이야기 하고, 사람이 직접 수행하는 수동 테스트(Penetration Test)에 비해 어떤 측면에서 장점이 있고, 어떤 측면에서 단점이 있는지를 살펴 보려 한다. 그렇게 함으로써 사람으로서 잘 할 수 있는 테스트가 무엇일지 생각해 보는 시간이 될 수도 있을 것 같다.



[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 하드닝(Hardening)
10. 보안 설계 문제
11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 


 


1. 들어가면서

  스캐너는 무언가를 쭉 훝어 가면서 필요한 정보를 체크하는 이미지가 있다. 해당 정보는 문자 인식등에 필요한 글자 영역(광학 스캐너)일 수도 있고, 이상한 징후가 보이는 위치(공항 수하물 스캐너)일수도 있다. 보안 쪽에서 많이 쓰이는 스캐너들 또한 비슷한 역활을 한다. 웹페이지나 서버 등을 스캔하면서 이상하게 열린 포트는 없는지, 알려진 취약점 들이 없는지 체크를 하고 리포팅을 한다.

 

  이런 이상적인 관점으로 봤을 때엔 스캐너를 사용하면 모든 문제가 해결될 것처럼 생각되지만, 현실상으로는 많은 부분에서 제약이 있을 수 있다. 이번 장에서는 github에 공개된 간단한 스캐너 프로그램을 기준으로 스캐너 엔진의 기본적인 동작 원리를 이해하고, 여러 상용, 비상용 스캐너들을 어떤 관점에서 접근하면 좋은지에 대한 이야기를 하려한다. 

 

 

 

 

 

 

2. 공개된 스캐너 돌려보기

  처음엔 Acunetix 같은 상용 웹 스캐너의 트라이얼 버전으로 얘기를 풀어볼까도 했는데, 그렇게 되면 메인 주제를 벗어나 세팅 등 너무 설명할 부분이 많아질 것 같은 생각이 들었다. 또 아래 OWASP 의 정리된 페이즈를 보면 알겠지만, 윈도우즈 환경의 무료 스캐너 툴은 거의 없다고 봐도 될듯 싶다. 윈도우, 리눅스 양쪽을 지원한다고 했던 툴도 몇 개 다운로드 받아봤는데, 점점 리눅스 쪽만을 정식으로 지원하는 분위기로 가는 듯 싶다.

 

[Vulnerability Scanning Tools - OWASP]

https://www.owasp.org/index.php/Category:Vulnerability_Scanning_Tools

 

 

  그래서 "python 3 web scanner" 같은 키워드로 이리저리 찾다가 보니, 아래의 github 에 올려진 웹 스캐너가 괜찮아 보였다. 물론 상용 스캐너에 비해서는 좀 많이 어설프고(이 부분은 소스를 살펴보면서 얘기할까 싶다), 2017년 후에는 전혀 업데이트가 전혀 되지 않아 개선할 점이 많아보이는 소스이긴 하지만, 그 덕분에 소스가 복잡하지 않아 대략 전체적인 구조의 흐름을 따라갈 수 있을 것 같고 해서, 해당 스캐너를 다운받아 현재 진행 중인 파이썬 3.6.4 버전에 맞춰 고쳐서 실행해 보려고 한다.

 

[simple web scanner - lotusirous 님 github] 

https://github.com/lotusirous/simple-web-scanner

 

 

 

2.1 다운로드와 압축 풀기

  우선 해당 github 의 오른쪽에 있는 "Clone or download" 버튼을 클릭 한 후, "Download ZIP" 버튼을 클릭하여 zip 파일을 c:\Python\Code 폴더에 다운 받는다.  

 

 

   이후 바로 압축을 풀면 c:\Python\code 밑에 아래와 같은 폴더 구조가 될 것이다.

1
2
3
4
5
6
7
8
9
10
simple-web-scanner-master/
    xss_scanner.py
    crawler.py
    scanner_core.py
    scan_cli.py
    parser.py
    sqli_scanner.py
    vectors/
        sqli_vectors.txt
        xss_vectors.txt
cs

 

 

 

2.2 소스 조정 하기

  3.6.4 환경에서 돌려보니 에러가 나는 부분이 있어서, 소스를 조금 수정해야 한다.

 

 

  첫번째로 parser.py 이름이 파이썬 내부 모듈과 이름 충돌이 나서 HtmlParser 를 못찾는다고 나오는거 같으니, 파일 이름을 parser_x.py 라고 변경 한다.

 

 

  다음으로 해당 모듈을 불러오는 파일들도 같이 수정해야 한다. 우선 "xss_scanner.py" 와 "crawler.py" 파일을 열어, 처음에 모듈을 읽는 문장인 "from parser import HtmlParser" 문장을 "from parser_x import HtmlParser" 로 수정한다.

1
from parser_x import HtmlParser
cs

 

 

  이후 "scanner_core.py" 파일을 열어 import 문 아래에 "from urllib.error import URLError, HTTPError" 를 추가한다(돌릴 때 HTTPError 가 정의 안됬다는 에러가 났다).

1
2
3
from urllib.parse import urlparse, parse_qs, urlunparse, urlencode, unquote
import urllib.request
from urllib.error import URLError, HTTPError
cs

 

 

 

2.3 샘플 사이트 만들기

  이제 스캔 대상이 되는 사이트가 있어야 하는데, 3교시에 사용한 인젝션 페이지를 쓰면 될거 같은데, 이 스캐너가 몇가지 구조상 제약(개발이나 도메인 특성 반영이 덜 된 부분)이 있다.

 

  일단 처음에 HREF 태그를 기준으로 링크들을 크롤링 해서 가져오는데(상용 스캐너들은 이 스캐너와 달리 자바스크립트로 이루어진 동적인 링크 등도 모두 파싱해 가져온다), 테스트 할 페이지의 인자들을 리스트업 할때, Form 내의 값이나 헤더 값 등이 아닌, GET 으로 날라가는 인자(예: test.asp?name=tom) 를 기준으로 파싱한다. 그래서 "http://localhost/injection_test.asp" 식으로 링크 경로를 주게 되면, 페이지는 인지하지만, 따로 인자가 없다고 판단에 인자에 대한 스캐닝을 하지 않고 그냥 스캐닝이 종료되어 버린다.

 

  그래서 부득이하게 샘플 페이지를 스캐너에 맞추는 작업을 하려 한다. 3교시의 파일을 실습 했다는 가정하에(injection__test.asp 가 있다는 가정하에), c:\inetpub\wwwroot 에 ANSI 인코딩으로 "web_main.asp" 페이지를 하나 만든다. 해당 페이지는 크롤링시 injection_test.asp 페이지를 인지시키기 위해서 인자를 포함한 href 링크 하나만 달랑 가지고 있는 아래와 같은 페이지 이다.

1
<a href="http://localhost/injection_test.asp?txtBuyerID=tom">link</a>
cs

 

 

 

2.4 스캐너 소스 구조 설명

  일단 다른 사람이 만든 소스긴 하지만^^, 돌리기 전에 스캐너 구조에 대해서 이해는 하기위해 간단히 도표를 통해 설명하려 한다.

 

  전체적인 구조는 위의 그림과 같다고 보면 된다. 기능이 작은 쪽부터, 큰 쪽으로 보는게 날거라고 보는데, xss_vectors.txt, sqli_vectors.txt 파일을 보면 정말 간단하지만 XSS 취약점과, SQL Injection 취약점을 체크할 수 있는 몇 개의 인자 패턴들이 정의되어 있다(한번 열어서 내용을 보자). parser_x.py 는 페이지 소스에서 HREF 태그를 파싱하거나, <script> 태그 안의 내용을 가져오는 라이브러리로 크롤링이나 결과 판단시 사용된다.

 

  앞에 얘기한 2개의 txt 파일을 읽는 xss, sqli_scanner.py 파일에서는 txt 파일의 패턴을 가져다가 주어진 URL 뒤에 인자로 결합하는 작업을 하거나, 결과 값에서 공격이 맞다고 하는 패턴을 찾는 역활을 한다.

 

  Crawler.py 파일이 처음 명령어로 주어진 시작 URL 에서 유효한 링크들을 찾는 역활을 하며(확실친 않지만 depth 는 하나만 들어가는 걸로 보인다), 이후 scanner_core.py 파일에서 앞에 설명했던 기능들을 조합해 URL 에 공격 인자를 넣어 던진 후, 결과 페이지에서 해당 공격이 유효한지 소스 기반으로 체크하고 유효할 경우 결과를 화면에 뿌려준다.

 

 

 

2.5 스캐너 돌려보기

  그럼 스캐너를 함 돌려보자. 단순히 스캐너만 돌려서 결과를 보는 것은 큰 의미가 없고, 실제 어떤 요청들이 날라가는지를 함께 보려고 한다. 대량 건이라면 IIS 로그를 남겨서도 확인 할 수 있지만, 몇 건 안날아 갈거기 때문에 피들러를 켜서 보려 한다.

 

 

  우선 필요한 모듈을 설치하자. 설명서에 나온것에 추가해 html5lib 하나를 더 설치해야 한다.

c:\Python\code\simple-web-scanner-master>pip install bs4

c:\Python\code\simple-web-scanner-master>pip install html5lib

 

 

  피들러를 실행 후, 아래와 같이 xss 테스트를 하는 명령어를 넣어보자.

c:\Python\code\simple-web-scanner-master>python scan_cli.py --seedurl http://localhost/web_main.asp --engine xss
SELECTED xss engine
Crawl: 
http://localhost/web_main.asp
Crawl:  http://localhost/injection_test.asp?txtBuyerID=tom
http://localhost/injection_test.asp?txtBuyerID=tom HTTP Error 500: Internal Server Error
http://localhost/injection_test.asp?txtBuyerID=tom HTTP Error 500: Internal Server Error

 !! REPORT !!
http://localhost/injection_test.asp?txtBuyerID=<script>alert("XSS")</script>

 

 

  결과를 보면 web_main.asp 에서 injection_test.asp 페이지를 크롤링 해서 가져오고, 뒤의 인자를 기준으로 공격할 포인트를 찾는다(앞에도 얘기했지만 원래는 페이지 자체를 분석해서 폼이나 AJAX 방식의 공격 인자들을 하나하나 찾는게 정석이다). 이후 공격 패턴(보통 고급스럽게 페이로드-payload 라고 많이 불린다)들을 하나하나 조합해서 보내면서, 결과의 script 태그 안에 XSS 라는 공격이 성공했다는 시그니쳐가 있는지 찾는다(xss_scanner.py).

 

 

  피들러로 보게 되면 아래와 같다.

 

 

 

2.6 스캐너 구조에 대한 문제 생각해 보기 

  자 여기서 스캐너가 잘 돌아가는거 같다고 덜컥 쓰는건 아마추어가 할 행동이다(물론 상용 스캐너인 경우는 프로인 사람들이 만들었기 때문에 스캐너 자체를 의심하기 보다는, 뒤에 얘기할 옵션들과 필터들을 잘 조정하는게 맞다)

 

  여기서 한번 생각해 봐야할 문제는 만약 원래 페이지의 스크립트 태크안에 "XSS" 라는 문구가 있다면 오탐이 나올 수가 있다는 것이다. 보통 그래서 시그니쳐는 우연히 겹치기 힘든 기괴한 문자열 일수록 더 좋긴 하다. "False Positive(진짜라고 하지만 실제 아닌 것)"가 생기는 문제가 된다.

 

 

  한발 더 나아가, 위의 필터 중 나머지 하나인 sql injection 을 테스트하는  필터를 돌려서 다른 문제를 봐보자.

c:\Python\code\simple-web-scanner-master>python scan_cli.py --seedurl http://localhost/web_main.asp --engine sqli
SELECTED sqli engine

Crawl:  http://localhost/web_main.asp
Crawl:  http://localhost/injection_test.asp?txtBuyerID=tom
http://localhost/injection_test.asp?txtBuyerID=tom HTTP Error 500: Internal Server Error
http://localhost/injection_test.asp?txtBuyerID=tom HTTP Error 500: Internal Server Error

 !! REPORT !!

 

 

  문제가 없다고 나온다. 그런데 해당 페이지는 사실 예전에 SQL Injection 을 시연하기 위해 만들었던 페이지므로 SQL Injection 이 있을 수는 없다. 그럼 왜 일까? 실제 해당 페이지를 열어 홑따옴표(') 를 넣어보면 아래와 같은 에러 페이지가 나온다.

 

 

  해당 원인은 sqli_scanner.py 페이지를 보면 알 수 있다. 거기 정의된 SQL Injection 을 판단하는 시그니처는 아래 3가지 이다.

'SQL syntax', 'MySQL server', 'mysql_num_row',

 

  위의 에러 페이지에는 해당 문구가 포함되지 않기 때문에 안 나나 싶다면, 페이지에 나타난 "SQL Server 오류" 같은 시그니처를 하나 더 추가해 볼수도 있겠지만, 피들러로 호출된 내용을 보게 되면 근본적인 문제는 현재 페이지는 SQL Injection 이 나게 되면 500(Internal Server Error) 에러가 나게 되는데, 현재 해당 웹 스캐너의 코드(scanner_core.py 의 analyze 메서드)를 보면 에러가 나면 그냥 화면에 출력을 하고 멈춰 버린다(False Negative-괜찮다 하지만 실제 안괜찮은 것).

 

 

  이 경우는 아래와 같은 코드에서 에러가 난 경우에도 에러 상세 결과를 변수로 받아서 시그니처를 찾아보거나(해당 페이지에서 사용한 urllib 이 에러시에도 결과를 얻을 수 있는지는 고민해 봐야한다. 최악의 경우 사용하는 라이브러리를 다른 걸로 교체해야 될수도 있다), 그게 안된다면 차라리 500 에러가 났을 때 SQL Injection 이라고 판단하는게 좀더 합리적이지 않을까 싶다. 

1
2
3
4
5
6
7
8
9
    def analyze(self, url):
        # This function will get requests each vulnerable url to server to get response
        for mal_url in self.build_malicious_url(url):
 
            try:
                res = urllib.request.urlopen(mal_url)
            except HTTPError as e: # http status is not 200, continue next malcious url
                print(url, e)
                continue
cs

 

 

  마지막으로 해당 SQL Injection 의 시그니처를 만나게 되려면 에러가 발생시 데이터베이스 오류에 대한 자세한 에러 페이지를 보여줘야 한다. 에러시 공통 에러 페이지로 가게 되다면 안전하다고 판단 하는 오류를 가지게 된다(물론 SQL Injection 도 처리 안된 사이트라면 저렇게 라이브한 에러가 날 가능성도 높긴하지만 --;)

 

 

 

 

3. 상용 툴(또는 사람들이 많이 쓰는 오픈소스)과의 비교

  위의 간략한 POC 개념의 스캐너를 기준으로 상용툴과 비교하는 것은 좀 그렇긴 하지만, 어차피 상용툴도 첨에 이렇게 작은 프로그램 으로부터 시작되어 왔을꺼기 때문에 한번 비교를 해보자. 개인적으로 사용해 본 Aucnetix 를 기준으로 말해 보지만, 사용한지 몇년이 지났기도 하고, 툴마다 장단점들도 서로 있긴 한테니 참고만 하길 바란다. 

 

[Acunetix 페이지]

https://www.acunetix.com/ordering/ 

 

 

 

3.1 크롤링 측면 

  우선 크롤링 측면을 보자. 페이지 안의 링크에 걸린 페이지를 무조건 따라가는 것은 조금 위험한 측면이 있다. 사이트 외부로 링크가 나가게 되면, 외부 페이지의 링크를 다시 추적하게 되는 과정에서 과장한다면 www 전체를 돌아다닐 수도 있게 된다. 보통 스캐닝 툴에선 크롤링 시 따라가는 도메인의 제한과, 처음 페이지에서 들어가는 깊이의 제한으로 이슈를 풀어가는 듯 싶다. 

 

  또한 앞에서 얘기한 스크립트 기반으로 동적으로 생성이 되는 링크(폼 버튼을 누를 때의 validation 후  전송하는 스크립트 라든지),  AJAX 기반의 기능 호출의 경우는 단순히 HREF 만을 파싱해서는 찾아낼 수 있다. 좀더 자바스크립트 문법을 이해하는 정교한 크롤링 코드가 필요할 듯 싶다. 

 

 

 

3.2 스캐닝 측면 

  다음은 스캐닝 측면이다.

 

 

  첫 째, 스캐닝 측면에서는 차원의 축소 문제가 있다. 보통 큰 페이지에서는 인자로 취급될 수 있는 form 값이나, http header 값들이 수백개가 되는 경우가 있다. 그런 규모의 페이지가 100개라면 XSS 하나의 필터에 관해서만 500개의 테스트를 한다면 5만개의 리퀘스트가 날라가게 된다. 그런 필터가 수십개라면 몇 백만개의 쿼리가 날라가는 사태도 벌어지게 된다.

 

  그래서 얼마나 효율적으로 인자와 필터를 선택하고, 필터 안에서도 해당 도메인이나 페이지에 유효한 필터만을 날리느냐에 대한 문제가 있다. 그런 부분을 위해서 인자들을 정규식 패턴 등으로 예외처리 하거나, 해당 도메인에 의미있는 필터만을 선택하거나, 도메인 특성에 따라(예 ASP 라면 PHP 용 SQL Injection 을 날릴 필요가 없을 것이다) 자동으로 날릴 Payload 들을 선정 하기도 한다. 다만 해당 부분은 툴 자체에만은 맡길 순 없고, 도메인을 소스 레벨에서 이해한 상태에서 조정을 해야하는 부분 같다.

 

 

 둘 째, 앞과 연결되지만 크롤링된 모든 파일에 모든 필터를 꼭 점검해야 하느냐 하는 이슈가 있다. 점검할 가치가 없는 파일들을 적절한 도메인 지식하에 제외하거나, 특정 필터만을 돌리는 작업도 꼭 필요하다. 예를 들어 SQL 호출을 안하는 페이지에 SQL Injection 필터를 돌리는 것은 시간 낭비이다. 다만 그렇게 정교하게 필터 정책을 주는 것과 어차피 기계가 돌리는 거는 시간이 걸리더라도 덜 제외하는 데 필요한 노력을 들이지 않고 돌리느냐에 대해 유리한 쪽을 잘 판단해야 한다. 어디서나 ROI 문제는 생긴다.

 

 

  셋 째, 아무리 앞에 말한데로 조정을 하더라도 사이트가 크거나 복잡한 페이지가 많을 수록 필연적으로  많은 쿼리가 날라가게 됨을 피할 수는 없다. 그 경우 적절히 병렬로 쿼리를 날리면서도 부하 조절을 하는 등의 경감 정책도 필요할 것 같다(조그만 사이트는 DDOS 공격을 맞은 듯이 뻗을 수도 있고, 개발 팀이나 모니터링 팀에서 장애가 난다고 문의가 날라올 수도 있다)

 

 

  넷 째 필터에 포함된 쿼리가 얼마나 객관적이고 많은 이슈들을 담았느냐도 중요하다. 자질구레한 레벨의 실제 위협을 가져올 가능성이 거의 없어 수정하기 애매한 취약점들을 모두 찾기 위해서 모든 필터를 돌리는게 의미 있는지도 생각해 봐야한다(이건 사람마다 입장에 따라 의견이 다를 듯은 싶다). 상용 프로그램의 경우 상용이라는 부담감 때문에 기존에 나왔던 모든 이슈에 대한 필터가 너무 과도하게 진행되는 감도 있다. 특히 오픈 소스를 쓰는 것이라면 해당 스캐너의 동작을 깊진 않더라도 가능한 소스 레벨에서 이해해서, 앞에서 생기는 여러 문제들을 해결할 수 있도록 조정하고, 적절히 개조해 보려 하는 것도 나쁘진 않을 것 같다.

 

 

  다섯 째, 스캐너의 결과가 통과되었다고 100% 안전하다는 보장은 하진 못할수도 있다. 예를 들어 디폴트 패스워드나 디렉토리 검사를 통과했다고 하더라도, 사용자가 패스워드를 "안녕" 이라고 만들거나 폴더를 01022223333(본인 핸드폰 번호) 이런식으로 만들어 놨다면 스캐너가 통과됬다고 본질적인 디폴드 패스워드 개념에서는 안전하다고는 못할 것이다. 어찌보면 많은 필터들이 사전적 Brute Force 같은 성향을 가지고 있기 때문에 적절히 결과의 유효성을 판단해야 한다. 

 

 

  여섯 째, 비단 스캐너뿐 아니라 유명한 보안 상용 솔루션이나 장비를 쓰는 경우는 인증 및 감사 측면에서 유리한 면도 있는 듯 하다(감사시 ** 스캐너를 사용해서 매번 스캔하고 있어요 같이...). 실제로 많은 보안 장비가 해당 장비 자체가 이미 법이나 인증 획득에 필요한 기능을 구현해 인증 받아놓았기 때문에 여러 감사 측면에 유리하게 대응하기 위해서 도입 하는 경우도 있다. 다만 아무리 그렇더 라도 유명도나 인증, 레퍼런스가 해당 스캐너의 효율성과 등가라고는 할수 없기 때문에, 실무자 입장에서는 해당 필터가 정말로 의미있는 건지, 해당 디폴트 방식의 스캐닝이 의미 있는 건지는 잘 이해하고 따져봐야 한다.

 

 

  마지막으로 사이트에 로그인과 비로그인 시에도 미묘한 문제가 생긴다. 로그인 후 스캐너를 돌리다가 게시판 같은데 이상한 게시물을 잔뜩 달아 열심히 지워본 기억은 한번씩쯤 겪게 될 것이다. 반대로 로그인을 안한 상태에서 돌리게 되면 테스트 하는 페이지가 확연하게 줄어 실효성에 의문이 생길 수도 있다. 여러 상황에 맞춰 선택을 해야한다(테스트 환경에서만 로그인해 돌린다든지...).

 

  또한 스캐너가 날리는 페이로드는 기괴한 문법 문자가 많기 때문에, 당하는 프로그램 입장에서 에러처리가 안되서 프로그램이 죽거나 에러가 빵빵 나 버릴 수도 있다. 해당 부분은 페이로드를 바꾸던지 에러가 나는 쪽을 설득해 에러처리를 제대로 하게 해야한다.

 

 

 

4. 스캐닝 결과 검증 자체의 이슈

  위에서 봤듯이 스캐너의 원리는 적절한 인자를 찾아 페이로드를 보내고, 결과 페이지에서 내가 의도한 패턴을 찾는 것이다. 이것은 비단 웹 뿐만 아니라 네트워크나 다른 물리적인 스캐너도 마찬가지 이다(예를 들어 공항 투시 스캐너는 약한 X선이나 자기를 보내서 반사되는 형태를 기반으로 판단한다고 한다).

 

  앞의 스캐너 소스에서 봤듯이 어떤 가정을 하느냐에 따라서 결과가 의미가 있을 수도 없을 수도 있다. 막상 스캐너를 처음 돌려본 사람들은 쏟아지는 취약점 숫자에 깜짝 놀라다가도, 실제 의미 있는 결과인가를 체크해 보면서 스캐너가 얘기하는 수많은 False Postive 에 또 한번 놀라게 된다. 

 

  하지만 이건 스캐너가 판단 할수 있는 영역이 블랙박스라서 어쩔 수 없다고 봐야 된다. 사람처럼 블랙박스 테스트를 한다는 의미가 아니라, 리퀘스트와 해당 결과로 브라우저에 전달된(리스폰스) HTML 소스를 기반으로 열심히 판단하긴 하지만, 서버 내의 로직을 보거나 이해할 수 없다는 태생적 한계를 가지고 있다는 것이다. 다만 블랙박스 측면에서도 제대로 테스트를 하면 서버의 내부 로직을 유추할 수 있듯이, 스캐너도 필요한 측면에서 잘쓰면 마찬가지의 효과는 있다.

 

  또한 여기에서도 도메인, 프로그래밍적 지식이 필요한데, 왜 스캐너가 해당 페이지에서 해당 패턴의 취약점을 발견했다고 주장하는 지를 스캐너와 페이지 측면 양쪽 모두에서 이해하고, 유죄를 선고할지, 무죄를 선고할지를 판단할 수 있어야 한다. 해당 지식이 앞 시간에 나온 여러 주제들에 대한 지식에 추가해, 스캐너가 테스트를 할수 있다고 주장하는 필터에 대한 동작 원리에 대한 지식이다.

 

  많은 부분 판단이 힘든 경우 스캐너가 어떤 요청을 날렸고 어떤 결과를 받았는지 부터 자세히 살펴보는 것도 도움이 된다고 생각한다. 그게 스캐너가 바라보는 세상의 전부이니까 말이다. 

 

 

 

 

5. 수동 테스트(펜테스트)와의 차이

  그럼 사람들이 펜테스트 하는 방식이 이런 스캐닝 툴과 다른 점은 뭘까?

 

 

3.1 수동 테스트의 장점 

  첫 째, 6교시에서 얘기했던 업로드 같은 부분을 생각해보자. 페이지를 분석하고, 업로드할 악성 파일을 탑재하고, 프록시 단계에서 파일 이름이나 자바스크립트를 바꾸면서 이런저런 조작을 하는 부분을 스캐너 판단하에 자동으로 일어나도록 만들기는 참 어려울 것이다.

 

  일단 업로드 하기 전까지를 구현하는 것도 어렵지만, 업로드된 파일이 실제 어떤 경로에 저장되었음을 판단하는 것도, 업로드 후 업로드된 경로를 명시적으로 알려주는 사이트가 아니라면 자동으로는 많이 힘들 듯 싶다. 물론 100% 불가능 하게는 안보이지만 페이지에서 업로드 기능을 찾아내고, 웹 프록시를 이용하는 과정을 자동으로 구현한다고 생각하면 머리가 많이 아프다. 이런 측면은 수동 테스트가 휠씬 효율적인 부분이라고 생각한다.

 

 

  둘 째, 위와 비슷하게 여러개의 중간 단계를 거치면서 각 중간 단계에서 계속적으로 validation 을 하는 설계를 가진 페이지에는 적용하기 힘들다(요즘은 마이크로 서비스나, 도메인으로 나눠진 서비스가 많아서 페이지 호출이 복잡한 구조가 많다. 많이 가는 큰 사이트를 가서 피들러로 한번 살펴보면 하나의 페이지를 들어갈때 눈이 아플 정도로 많은 호출이 있을 것이다).

 

  위의 어려움 더하기 모든 validatio 포인트를 클라이언트 코드를 통해 회피해주는 작업이 필요하기 때문이다. 일반적으로 스캐너는 요청과 최종적으로 대상쪽에서 온 응답에 기반에 동작하기 때문에, 서버에서 일어나는 여러 단계의 중간 단계에는 관심이 없다. 사실 자동화의 복잡성 대응의 한계 때문에 그렇다고 보는게 맞을 듯도 싶다. 뭐 AI 를 이용한 보안 테스트가 많이 연구된다니 혹시 패턴이 많이 모인다면 나중엔 어쩔진 모르겠지만 말이다.

 

 

  셋 째, 마찬가지로 위와 연결되는 이슈인데, 수동 테스트는 블랙박스와 그레이, 화이트 박스의 조합을 적절히 취할 수 있다. 웹 호출을 보다가 데이터베이스의 변화를 관찰 할 수도 있고, 미심적은 처리 부분의 프로그램 소스를 열어서 볼수도 있다. 물론 자동화된 테스트도 디비를 참고하거나 프로그램 내부를 참조할수 있겠지만 그건 저런 스캐너 같은 도구는 아니고, 테스트를 위해 잘 디자인된 프로그램을 직접 제작하는 경우일 것이다. 또한 이해가 잘 안된다면 개발자와 의논할수도 있다^^.

 

 

  넷 째, 수동 테스트는 단순히 페이지의 분석에서 벗어나, 기존 도메인에 대한 지식과 경험이 녹아있는 테스트를 통해 실시간으로 전략을 바꿀 수도 있다. 이것은 언젠가 AI 가 따라 잡을 분야일진 모르겠지만, 지금 상태로는 죽기전엔 구경 못할 듯은 싶다.

 

 

 

3.2 자동 테스트의 장점 

  첫 째, 자동은 우선 빠른 실행 속도가 장점이다. 서버의 부하나 에러가 허용되는 만큼 병렬로 돌릴 수도 있고, 명확히 문제와 검증 방식이 정의된 영역 부분에 대해서는 장점이 있다.  

 

 

  둘 째, 파이썬 마지막 교시에 얘기했던 five orders of ignorance 에서 얘기한것 처럼 모든 소프트웨어는 지식의 결정체 이다. 솔루션이라는 것은 우리가 가진 보안 지식을 정리하여 소프트웨어 형태로 만들어, 컴퓨터의 파워를 이용해서(어찌보면 밝음을 구현하는 손전등을 위해 건전지를 사용하는 것과 비슷하다) 자동화된 지식을 반복적으로 재사용 하는 것이다.

 

  지식이나 물리적인 힘(물리적 장치와 연결되면 힘의 움직임도 에너지는 다르겠지만 재사용 할수 있다)을 재사용 한다는 측면에서의 자동화는 정말 우수한것 같다. 여러 공장 자동화들의 예에서 이런 것들을 많이 볼수 있고, 몇몇 천재들이 자신의 지식을 잘 담아서 상용화 시킨 편하고 감탄이 나는 소프트웨어들을 많이 볼수 있다.

 

 

  셋 째, 변경이 적은 지식의 영역에서는 일단 세팅이 완료되면 우수한 ROI 가 이루어 진다. 예전 회사에서 같이 일하던 사람이 여러 나라의 언어로 이루어진 설치 프로그램을 자동화 툴을 이용해 검증해 돌렸는데, 한번 만들어 놓으면 약간의 코드 수정만으로 본전을 뽑는 다는 느낌이 들었었다. 게다가 자꾸 만들다 보면 자신만의 라이브러리도 쌓이고, 이미 해결해 놓은 지식들이 많기 대문에 ROI 의 I(Investment) 부분이 점점 줄어들 것이다. 

 

  IT 쪽에서 사람들을 겪어 본 사람들은 프로그래머나 다른 인력이나 사람들마다 많은 속도나 품질, 타인의 대한 좋은 태도의 차이가 난다는 것을 많이 느꼈을 거다. 자신이 평균 이상의 속도나 품질, 타인의 대한 좋은 태도를 가지도록 계속 노력해 보자.

 

 

  넷 째, 객관적인 증명이 된다. 사람은 피곤하면 잘못 보기도 하고, 실수도 하고, 슬쩍 해야될 점검을 모르는 척 빼먹을 수도 있지만(생각보다 종종 일어나는 일이다),  기계의 장점은 항상 객관적이라는 것이다. 물론 설계를 잘못했거나 어설프게 검증하도록 만들었을 경우에도 지나치게 객관적이여서 문제다(gabage in garbage out 이 여기도 적용된다). 앞에서 얘기한 소프트웨어를 구성해야할 올바른 지식들이 필터나 스캐너 프로그램의 철학에 잘 담겨져 있는지를 꼭 확인해 보자. 

 

 

 

3.2 자동 테스트 vs 수동 테스트 

  이건 "엄마가 좋아 아빠가 좋아" 문제이다. 서로의 단점을 보안해줄 좋은 수단이 동시에 있는데 굳이 하나를 외면 해야할 필요가 있는가? 수동 테스트에 익숙한 사람은 스캐너를 잘 다루도록 노력하고 자기만의 스캐닝 정책을 커스터마이즈 하거나, 불편한 부분을 개선하거나 하는 노력을 해야한다. 스캐너는 어찌보면 수동 테스트에서 귀찮았던 부분을 해결하기 위해 만들었기 때문에 업무의 중요한 부분에 집중하는데 많은 도움이 될것이다.

 

  반대로 스캐너에 주로 익숙한 사람들은, 테스트 페이지를 제작해 스캐너가 날리는 쿼리에 대해 웹서버 로그로 저장하거나 피들러로 살펴보면서, 하나하나의 필터가 하는 일을 명확하게 이해해야 한다. 해당 부분을 이해하는 과정에서 결국은 스캐너의 한계를 깨닳고 수동 테스트로 어떻게 보강을 해야될지를 고민하면서 자동으로 공부를 많이 하게 될 것 같다.

 

  양쪽 다 서로 자신을 알면 알수록 상대방을 잘 이해할 수 있고, 상대방을 잘 이해할 수록 자신의 일도 더 잘 이해할 수 있게되는 측면이 있다고 본다.

 

 

 

 

6. 마무리 하면서

  스캐너 쪽이나 펜 테스트 쪽이나 아주 깊게 아는 편은 아니기 때문에, 어설프게 얘기하거나 빠뜨린 부분이 있는지도 모르겠다. 하지만 결국 소프트웨어는 지식을 담아 재사용하는(나아가 제작의도와 다르게 응용해 사용하기도 하는) 부분이기 때문에 스캐너를 해당 관점으로 보면서 스캐너 자체의 빤짝거림 보다는 안에 담긴 지식의 알맹이들에 관심을 가졌음 하는 바램을 가지며 이 글을 마치려 한다.

 

 

 

2019.5.23 by 자유로운설탕
cs

 

 

 

 

 

 

posted by 자유로운설탕
2019. 5. 6. 16:50 보안

  보안 설계(security by design) 문제 부분도 설명하기는 좀 애매한 분야이기는 하지만 1~9교시가 모두 종합된 분야라고 볼수 있고, 앞으로 진행될 모든 사항들에도 해당될 듯한 주제이니까, 여기 쯤에서 한번 언급하고 뒤를 진행하는게 맞을 것 같아서 넣게 되었다.



[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 하드닝(Hardening)
10. 보안 설계 문제
11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 


 


1. 들어가면서

  보안 설계 문제는 어떻게 보면 보안 문제를 예방하기 위한 가장 첫번째 단계라고 볼수 있다. 앞의 모든 시간에 다루었던 주제들을 각각 다른 취약점 타입으로 볼수도 있겠지만, 사실 모든것은 상호 연관되어 있기도 하고, 동시에 일어나기도 하며, 각각의 측면을 종합해서 접근해야 할 필요가 있다. 마치 운동을 배울때 기초체력 부터 하나하나 기술을 배우기는 하지만 결국은 실전에서는 모든게 한꺼번에 조합되어 필요한 것과 마찬가지라고 생각된다.

 

  그래서 하나하나의 주제에 대해서 주의 깊게 바라 보는 것도 중요하지만, 항상 모든건 포괄적으로 움직이는 시스템을 위한 것이라는 것을 잊으면 안된다. 업로드에 대한 보안지식은 업로드 기능이 없는 시스템에서는 생각할 필요도 없는 것처럼(하지만 기술은 보통 다른 기술을 많이 참조하기 때문에 이해한 원리는 다른 기술을 이해할때 도움이 된다), 보안은 대상 자체가 없으면 아무 의미가 없다.

 

 

 

  또한 설계라는 것도(조금더 고급스럽게 얘기하면 아키텍쳐) 사실 어딘가에서 빵 나타나는 고급스러운 존재는 아니라고 본다. 하나하나 작은 기술의 경험이 합쳐져서 전체를 이루는 흐름을 파악할 수 있게 될때, 설계라는 일종의 선택 가능한 패턴으로 만들어지는 것이라고 본다. 고급 스러운 설계 작업이나 일반 적인 설계나 멀리 떨어진 시각에서 보게 되면 도토리 키재기 일수도 있다.

 

 

 

 

2. 보안 설계가 연관된 부분

  해당 문제는 여러 분야에 관련되어 있을 수 있으며 종종 버그라 불리우며 QA쪽 영역과 오버랩 되기도 한다(어떤 회사들은 보안 쪽 관련된 팀이, 어떤 회사에서는 QA 쪽 관련된 팀이 연관될 수도 있을 같다). 

 

  간단한 예로는 모바일 게임등에서 종종 나타나서 게임사를 당황하게 하고 유저의 신뢰를 떨어뜨리는 이벤트 중복 참여 라든지, 아이템 복사 등 부터, 서버와 클라이언트 상의 왔다갔다 하는 호출들의 보호문제, 비밀번호 찾기나 본인인증 창 설계, 주문서 설계 등이 있다.  크게는 하나의 도메인(회원, 결제, 등록) 등의 전체적인 흐름을 잡아야 할 경우도 있을 거고, 작게는 설치되는 여러 데이터베이스나 서버등의 설정 및 호출 방식등이 연관될 수 있다. 또한 외부 회사와의 전문, 키 교환 등의 상호 연동 부분도 있을 수 있다. 추가로 물리적인 보안 부분도 맡고 있는 팀도 있긴 하지만 그건 여기서는 좀 별개로 하려 한다.

 

 

 

2.1 간단한 설계예제

  그럼 어떤 움직이는 예제를 보이면 좋을까 생각하다가, 은행이나 웹 사이트 등에서 특정한 행동을 하기전에 본인인증 수단으로 사용하는 핸드폰 전송 문자를 입력 후, 결제를 하는 예제를 간단하게 만든 후, 보안적으로 잘못된 부분을 패치해 보려고 한다.

 

 

  아래와 취약한 플라스크 코드의 예제를 한번 보자. 먼저 design 이라는 경로를 호출하게 되면 flask_design.html 템플릿 파일이 읽혀진다(이것은 뒤에서 본다).

 

  checkSMS 는 넘어온 인자 중 smsNum 이라는 이름이 "7777" 이라는 값이면(문제를 간단히 하기위해 핸드폰에 "7777" 이라는 문자가 전송되어 왔다고 가정한다), Ture 값을 아니라면 False 값을 보낸다. 적절한 핸드폰 문자를 받지 못한다면 서버 쪽에서 체크하는 로직이기 때문에 이 부분을 통과할 수 없을 것이다. 

 

  doPayment 는 넘어온 price 값을 따로 쓰진 않고^^ True 값을 바로 호출한다. 원래 대로라면 금액은 맞는지, 잔고는 있는지 등등을 체크를 하고, 데이터베이스에 이런 저런 데이터를 넣어 결제를 해야 하지만, 간략히 하기위해서 해당 부분을 처리 했다고 하고 결과만을 성공으로 리턴한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from flask import Flask, render_template, request, jsonify
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/design", methods=['GET'])
def design():
    return render_template('flask_design.html')
 
 
@app.route("/checkSMS", methods=['GET'])
def checkSMS():
    sms_num = request.args.get('smsNum')
    if sms_num == "7777":
        result = "True" 
    else:
        result = "False"
    return jsonify({'var1': result})
 
@app.route("/doPayment", methods=['GET'])
def doPayment():
    price = request.args.get('price')
 
    result = "True" 
    return jsonify({'var1': result})
 
 
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_design_before.py]

 

  c:\python\code 폴더에 UTF-8 포맷으로 flask_desing_before.py 라고 저장한다.

 

 

 

  다음으로 템플릿 파일 쪽을 보자. 처음 로딩 되면 인증번호 입력 하는 텍스트 박스가 있고 인증하기 버튼이 있다. 그 밑에는 금액과 결제하기 버튼이 보인다.

 

  그 중간에  있는 히든필드인 id 가 isCert 인 필드가, SMS 인증이 됬는지를 몰래 숨겨 가지고 있는 중이다. 해당 히든필드가 필요한 이유는 페이지가 일단 로딩 되게 되면 그 후에는 DOM 과 자바스크립트의 세상이기 때문에, 인증이 됬는지 안됬는지를 판단하기 위해서 어딘가에 값을 가지고 있어야 한다(물론 어떻게 만드느냐에 따라서 결제하기 버튼을 눌렀을때 서버 쪽으로 AJAX 호출을 해서 인증 했는지 체크하고, 이후 인증 창을 보여줄수도 있지만, 앞의 클라이언트 코드에서 봤듯이 조작하는 수준은 비슷하다).

 

  자바스크립트 쪽을 보면 "인증 번호 입력" 버튼(id = checkData)를 눌렀을 때, checkSMS API를 호출하면서, 사용자가 입력한 smsNum 값을 넘겨준다. 결과 값이 True 로 오면 isCert 히든 필드 값을 "Y" 로 바꾸고, 인증 완료하고 표시해 주며, 아니라면 다시 입력해 달라는 문구를 표시한다.

 

  "결제 하기" 버튼을 누르면 isCert 값을 체크하여 디폴트 값이 N 그대로 라면 SMS 인증을 받으라는 Alert 을 띄우고, 만약 앞에서 미리 인증 번호를 넣어서 Y 로 바뀌어 있는 상태라면, 바로 doPayment API 를 호출해 결제를 하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<html>
  <head>
    <script src="http://code.jquery.com/jquery-3.3.1.min.js" ></script>
      <title>Call API</title>
      
    <script type="text/javascript">
      $(document).ready(function(){
 
        $("#checkData").click(function() {
          var smsNum = $("#SMSNumber").val(); 
           
          $.ajax({
            url: "/checkSMS",
            data: { smsNum : smsNum },
            contentType: 'application/json;charset=UTF-8',
            success: function(data){
              if(data.var1 == "True"){
                  $("#smsResult").css("background-color","orange");
                  $("#smsResult").html("인증완료");
                  $("#SMSNumber").attr("disabled""disabled");
                  $("#checkData").attr("disabled","disabled");
                  $("#isCert").val("Y");
              }
              else{
                  $("#smsResult").css("background-color","red");
                  $("#smsResult").html("다시 입력해 주세요");
              }
            }
          });
        });
        
        $("#doPayment").click(function() {          
          var isCertified = $("#isCert").val();
          
          if(isCertified != "Y")
          {
            alert("SMS 인증을 먼저 받으세요...");
            return false;
          }
          
          var price = $("#price").val();
          
          $.ajax({
            url: "/doPayment",
            data: { price : price },
            contentType: 'application/json;charset=UTF-8',
            success: function(data){
              if(data.var1 == "True"){
                $("#paymentResult").html("결제 완료");
                $("#paymentResult").css("background-color","cyan");
              }
              else{
                $("#paymentResult").html("결제 실패");
                $("#paymentResult").css("background-color","cyan");
              }
            }
          });
        });
      });
 
    </script>
  </head>
  <body>
    <table>
      <tr>
        <td><input type="text" id="SMSNumber"</td>
        <td><input type="button" id="checkData" value="인증 번호 입력"></td>
      </tr>
      <tr>               
        <td><span id="smsResult"></span></td>
        <td><input type="hidden" id="isCert" value="N"</td>
      <tr>
    </table>
    <table>
      <tr>
        <td><input type="text" id="price" value="1000"></td>
        <td><input type="button" id="doPayment" value="결제 하기"></td>
      </tr>
      <tr>
        <td><span id="paymentResult"></span></td>
      </tr>
    </table>
  </body>
</html>
 
cs

[flask_design.html]

 

  c:\python\code\templates 폴더에 UTF-8 포맷으로 flask_design.html 이름으로 저장한다. 이후 cmd 창에서 실행 시킨다.

c:\Python\code>python flask_design_before

 

  브라우저에서 http://127.0.0.1:5000/design 주소를 입력하고, 결제하기 버튼을 바로 누르면 앞의 스크린 샷과 같은 얼랏 창을 보게 된다.

 

 

  위의 로직을 대충 도표로 그리면 아래 정도가 아닐까 싶다.

 

 

 

 

2.2 해당 설계의 문제

  해당 설계의 문제는 무엇일까? 앞의 클라이언트 코드 쪽을 이해한 경우 쉽게 보안적으로 취약한 부분이 무언지 알수 있다. isCert 히든필드는 사용자에게 Alert 안내를 하기위한 자바스크립트에서는 유용하다. 하지만 해당 값은 "인증 번호 입력" 버튼을 눌러 AJAX 호출을 이용해 수정하는 값이므로, 브라우저 개발자 도구나, 피들러 등으로 결제 하기 버튼을 클릭하기 전에 임의로 "Y"로 수정해 버린다면, 인증번호 체크를 안한 상태로 결제가 가능하다(또한 스크립트 조작 때 처럼, 체크하는 스크립트를 아예 제거해도 된다).

 

  한번 피들러로 조작을 해보도록 하겠다. 피들러를 킨채로 Rules > Automatic Breakpoint > After Responses 를 선택한다(조작 방법이 기억이 안나면 5교시를 다시 보고 오자).

 

  이후 페이지를 로드하고, 피들러가 리스폰스를 잡은 상태에서, 오른쪽 하단의 Response 섹션에서,  Textview 탭을 클릭 후, isCert 히든필드 값을 그림과 같이 "Y" 로 바꾸어준다. 이후 Run to Completion 버튼을 눌러 브라우저로 데이터를 전송해 준다. (위의 브레이크 포인트는 이제 다시 Disabled 로 바꾸어 풀어준다).

 

 

  이제 브라우저 소스를 보면 해당 값이 "Y" 로 바뀌었기 때문에, 인증 번호 입력 없이도 아래와 같이 결제가 되어버린다. 이런 스타일의 조작은 팝업 형태로 띄워지는 통신사 등의 외부 본인 인증 창의 회피 등에서도 마찬가지로 쉽게 적용할 수 있다.

 

 

 

 

2.3 설계 패치하기

  그럼 해당 설계의 문제는 무엇일까? API 로 SMS 문자를 체크한 부분은 서버 사이드 인증이 맞지만, stateless 인 웹의 특성 상, 두번째 API 호출인 결제하기에서는 SMS 문자를 제대로 체크했는지를 기억하지 못한다는 것이다.

 

  사실 보안 설계의 재밌는 점 중 하나는 이러한 문제를 해결하는 방법이 하나는 아니라는 것이다. 여러가지 등가적인 안전한 방법을 만들 수 있고 그 중 가장 현재 코드나, 설계, 주어진 리소스에 적절한 방법을 선택할 수 있다. 하지만 등가적인 부분의 원리는 모두 같다고 보면 된다(물론 가끔은 행운?을 바라며 차선을 선택할 때도 있다).

 

  위의 예에서는 첫번째 API 안에서 SMS 문자를 인증 후, 공격자가 건드릴 수 없는 어딘가에 해당 사실을 저장하고, 결제 API 로직 안에서 해당 정보를 체크해서, 현재 사용자가 올바르게 인증한 사용자인지를 검증하는 방법을 쓸수 있다. 해당 검증 정보는 암호화된 쿠키(재사용을 경계해야 한다)나, 데이터베이스나, 서버 단(AJAX가 아니다) 로직에서 호출해서 체크할 수 있는 다른 API 의 어딘가 내부 공간 등에 자유롭게 저장할 수 있겠지만, 보통 데이터베이스를 이용하는 것이 제일 간단할 듯 싶다.

 

  아래의 수정된 flask_design_after.py 파일을 보자. 템플릿 파일은 어차피 클라이언트 코드므로 서버 사이드 로직이 들어있는 쪽을 수정하면 될듯 싶다. 우선 보면 3교시에서 사용했던 MSSQL 서버를 그대로 이용한다. 하나 가정하는 것은 원래 어떤 사용자 인지는 랜덤 값이 섞여 암호화된 쿠키를 베이스로 가져와야 변조가 안되지만 여기서 로그인 쿠키 코드까지 고려하면 복잡해 지니 암호화된 쿠키를 가져와 현재 사용자인 tom 을 member_id 라는 변수에 넣은걸로 하자^^ 

 

  checkSMS 쪽을 보면 사용자가 입력한 번호가 맞다면, sms_cert 라는 테이블에 ("사용자 아이디", "Y", "인증시간") 을 넣어주는 것을 볼수 있다.

 

  이후 doPayment 쪽에서는 sms_cert 테이블을 뒤져 사용자 아이디가 현재 로그인한 "tom" 이고, 인증 결과가 "Y" 이고, 인증 시간이 15분 이내 인지를 체크한다. 인증 시간 조건을 넣은 이유는 어제 인증을 했는데도 인증 했다고 판단하면 공격자가 다음날 공격해도 성공이 되기 때문에, 사용자가 인증하자마자 15분내에 공격 받는 일은 거의 없다고 보자. 저 시간을 너무 짧게하면(예를 들면 1분) 사용자가 인증 후 결제 버튼을 누를까 말까 고민하다가 1분이 지나면 인증이 안됬다고 나오는 사용성 문제가 생기게 된다. 

 

  만약에 해당 15분 간격도 맘에 걸린 다면 조금 더 나아가 SEQNO 같은 고유값을 sms_cert 에 추가해도 괜찮을 거 같다. 그럼 공격자가 15분내에 시도 한다고 해도, 이전 사용자가 인증 받을때 사용한 SEQNO 를 모른다면, 맞는 SEQNO 를 찾아내기는 거의 불가능할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from flask import Flask, render_template, request, jsonify
# 모듈을 불러옵니다.
import pyodbc
import datetime
 
# 연결 문자열을 세팅합니다.
server = 'localhost'
database = 'mytest'
username = 'pyuser'
password = 'test1234'
 
# 데이터베이스에 연결합니다.
mssql_conn = pyodbc.connect('DRIVER={ODBC Driver 13 for SQL Server};SERVER='+server+'; \
    PORT=1433;DATABASE='+database+';UID='+username+';PWD='+ password)
 
# 커서를 만든다.
cursor = mssql_conn.cursor()
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/design", methods=['GET'])
def test_search():
    return render_template('flask_design.html')
 
 
@app.route("/checkSMS", methods=['GET'])
def checkSMS():
    sms_num = request.args.get('smsNum')
    now_time = datetime.datetime.now()

    member_id = "tom"

    
    if sms_num == "7777":
        result = "True" 
        cert_sql = "insert into sms_cert values (?,?,?)"
        cursor.execute(cert_sql, member_id, "Y", now_time)                
        mssql_conn.commit()
    else:
        result = "False"
    return jsonify({'var1': result})
 
@app.route("/doPayment", methods=['GET'])
def doPayment():
    price = request.args.get('price')
    member_id = "tom"
    before_15minute_time = datetime.datetime.now() - datetime.timedelta(minutes=15)
    
    check_sql = "select top 1 cert_yn from sms_cert(nolock) where member_id = ? and cert_date >= ?"
    cursor.execute(check_sql, member_id, before_15minute_time)   
    cert_yn = cursor.fetchone()
  
    if cert_yn:
        result = "True" 
    else:
        result = "False"
    return jsonify({'var1': result})
 
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_design_after.py]

 

 

  실제 테이블을 하나 만들어야 하니 위와 같이 MSSQL Management Studio 를 이용해서 아래와 같이 sms_cert 테이블을 생성한다(사용법을 모를 경우 파이썬 4교시로...).

1
2
3
4
5
CREATE TABLE [dbo].[sms_cert](
    [member_id] [char](20NOT NULL,
    [cert_yn] [char](1NOT NULL,
    [cert_date] datetime NOT NULL,
)
cs

 

 

 

 

  이후 패치한 파이썬 코드를 실행 한 후, http://localhost:5000/design 페이지를 띄운다.

c:\Python\code>python flask_design_after.py

 

 

  앞의 예제와 같이 피들러로 클라이언트 코드를 회피한 경우에는 여전히 Alert 창은 뜨지 않고 결제까지 넘어가지만, 결제 API 에서 데이터베이스 조회를 통해 인증이 안 됨을 체크해서 막히게 된다. 상황에 따라 저 메시지를 애매모호하게 만들어 왜 막혔는지를 감추어 공격자를 헷깔리게 할 수도 있다.

 

 

  아래의 도표가 위의 보강된 로직을 나타낸 것이다. 이렇게 되면 앞에서 아무리 클라이언트 코드를 공격하더라도 문제가 없다. 뭐 그래도 넓은 측면에서 보면 핸드폰 문자가 탈취되게 되면 어플리케이션으로 막을 방법은 없고, 이후부터는 FDS 같은 결제 모니터링 시스템으로 잡을 수 있는 패턴이기만을 바랄 수 밖에는 없다.  

 

 

 

 

3. 보안 설계 정리하기

  앞에서 간단하지만 종종 만날수 있는 하나의 패턴을 살펴보았다. 그럼 보안 설계를 하는데 중요한 포인트는 무엇일까? 계속 하는 얘기지만 가장 중요한 것은 보호할 대상을 이해하는 것이다.

 

  만약 우리가 미술관의 보안을 책임지고 있는 사람이라 해보자. 그럼 기본적으로 미술관 자체의 운영에 대한 이해, 미술품 자체에 대한 이해, 미술품 도둑들의 심리의 이해를 어느 정도 해야지 무엇을 할 수 있을지 알게 되지 않을까 싶다. 제한된 예산이 주어졌을 때 가장 가치가 있는 미술품들을 어떻게 지켜야 하는지도 결정할 수도 있고 말이다.

 

  마찬가지도 어플리케이션 도메인에 대해서 설계를 잘하려면 우선 현재 방어해야될 대상인 어플리케이션이 동작하고 있는 방식을 이해하는 것이 가장 중요하다. 어찌보면 보안적 패턴에 대한 고민은 그 다음일지도 모른다. 보안 설계는 도메인 전체 설계의 애드온 같은 측면으로 봐야한다. 물론 보안 쪽 일을 하는 입장에서 쉽게 인정하기 싫은 측면이긴 하지만, 넓은 측면에서 보면 보안은 항상 전체 뷰의 한 측면뿐이라는 것을 잊으면 안된다. 사람도 마찬가지로 스스로를 객관적으로 보기는 참 힘들지만, 스스로를 객관적으로 보기 시작했을때 좀더 정확한 판단을 내릴 수 있게 된다.

 

  모의해킹과 비교해 보자면, 모의해킹이 현재 적용된 설계의 문제있는 부분을 증명하는 작업이라면(앞에 얘기했듯이 문제 없음을 증명하는 건 몇백배 더 난해한 일이다), 보안 설계는 다른 측면에서 설계 자체가 문제 있게 되지 않도록 방지하는 측면이 있다. 두 개를 상호 보완적으로 서로를 잘 커버해 주도록 배치해서 사용한다면 좋을 것 같다.

 

  다른 측면에서 여러 가이드나 표준을 보다보면 이러한 패턴화된 보안 설계들을 명문화 하려는 노력이 들어간 느낌을 받게 된다(다만 조각조각 파편화는 되어있다). 권고된 무언가를 수행 해야되는 입장이 주어졌을때, 해당 상황에 대해 무조건적으로 받아들여 적용하는 것보다, 어떤 설계 의도로 주어지게 된 것인지를 생각해보고, 기존 설계 안에 잘 조화되게 넣는 것도 중요한 것 같다.

 

 

 

 

4. 마무리 하면서

  점점 일을 할 수록 모든 IT 분야의 일을 하는데 있어서 가장 중요한 것은 "기본"이라는 생각이 든다. 기본 이라는 것은 "기초적인 지식"의 의미라기 보다는, "해당 기술이 나오게된 원인과 배경을 잘 이해한다"는 의미에 가까운 것 같다. 자신만의 관점을 가지게 되고, 그 관점으로 해당 분야를 해석하기 시작했을 때부터, 정말 그 분야에 발을 들이기 시작하게 되는 것이라고 생각한다. 그런 의미에서 보안은 그렇게 평생 공부해야될 기본 지식들이 무수히 많은 괜찮은 분야인것도 같다. 가끔 길을 잃고 힘들어 머뭇거리더라도 멈추진 말고 계속 앞으로 갔으면 한다. 이건 스스로에게 건내는 말이기도 하다^^

 

 

 

2019.5.12 by 자유로운설탕
cs

 

 

 

 

 

 

posted by 자유로운설탕
2019. 5. 1. 18:52 보안

  하드닝(Hardening) 보안을 "단단하게" 만든다는 의미로 앞에 나온 여러 이슈들을 포괄하면서도 프로그램 이외의 환경이나 설계적인 요소까지도 포괄하는 주제이다. 어찌보면 각종 보안 가이드 문서에 나와있는 ~을 하라는 모든 내용의 시작점 이기도 하다. 너무 포괄적이여서 어떻게 얘기를 풀어가야 할진 잘 모르겠지만, 일단 설명을 시작해 보려한다. 



[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 하드닝(Hardening)
10. 설계 문제
11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터



1. 들어가면서

  하드닝이란 이슈는 꼭 보안 업무에만 관련된 것만은 아니다. 예를 들어 우리가 집에 새로운 와이파이 공유기를 설치 했을때에도 해당 이슈를 마찬가지로 만나게 된다. 공유기 설정을 맘대로 바꾸는게 가능한 admin 계정의 패스워드는 어떻게 할것인지, WIFI 통신의 암호화는 어떤 방식으로 할것 인지, 접속할 수 있는 기기들의 MAC 주소를 지정해 관리할 것 인지 등등 여러가지 설정상의 보안 문제들을 고민하게 된다. 요즘 많이 설치하는 IP 카메라 같은 경우도 마찬가지 이고 말이다. 

 

  물론 해당 부분을 모두 다 고려한다고 해도 100% 안전하다는 보장은 못하겠지만, 100% 안전하지 못할 상황만은 피할 수 있다고 본다. 마찬가지로 운영등에 사용할 OS, 웹서버 설치, 데이터베이스 설치, 어플리케이션 배포 등에 대해서도 마찬가지로 전문가들이 보안적으로 안전하다고 생각하는 사례들이 있다. 물론 해당 항목은 기술적이나, 보안적 측면으로 내린 근거에 기반한다고 생각하는게 맞고, 해당 부분을 일종의 보안적 패턴이라고 봐도 된다.

 

 

 

2. 몇가지 패턴으로 분류해 보기.

  개인적으로 무언가를 단순히 외우는 것은 아무 의미가 없다고 생각하기 때문에, 의미를 부여하기 위해 많이 권고 되는 몇가지 하드닝 사항들을 특정 패턴으로 한번 묶어 보도록 한다.

 

 

2.1 디폴트 배제

  보안에서 최초 기본적으로 나오는 디폴트 설정 문제다. 위에 있는 공유기 어드민 암호도 마찬가지다(바꾸라는데 왜 바꾸어야 하는지 모르거나 귀찮아 하는 사람이 많기 때문에, 근래의 공유기들은 무조건 최초에 기본 admin 패스워드를 변경해야지만 사용할 수 있도록 변경되었다. 하드웨어 적으로 초기화를 시킬수 있는 부분이 있긴 하지만 뭐 패스워드 잃어버려 공유기를 못설정하는 일이 생기면 안되는 부분이니 일종의 현실과의 협상이라고 보면 될듯 하다). 또한 디폴트 패스워드 뿐만 아니라, 프로그램을 처음 설치했을때 정의되어 있는 설정들이 보안적으로 바람직하진 못한 경우도 포함된다.

 

 

 

  이 카테고리에 속하는 것들이 많이들 언급 되는 디렉토리 리스팅, 너무 상세한 에러 페이지, 유추할 수 있는 관리자 페이지 URL 경로, 장비나 오픈소스, 솔루션 어플리케이션의 기본 패스워드, 백업, txt 파일등의 다운로드 문제, 톰캣 관리자 페이지, 패스워드 없이 접근되는 NoSQL 데이터베이스 등이 있을 수 있다. 모의해킹이나 시큐어 코딩, 해킹, 보안 설정 관련된 책들에서 주로 많이 나오는 내용들이다. 웹 검색 등에서 찾아보면 아래의 페이지 들이 그런 주제들을 간단하게 설명하고 있다. OWASP 같은 페이지를 검색해도 될것 이다. 디렉토리 리스팅 같은 문제는 Flask 같은 예제를 보면 전통적인 디렉토리 안에 파일이 있는 방식이 아니기 때문에 기술의 변화에 따라 뭔가 판이 달라지는 측면도 보인다.

 

[directory listing 취약점 막기(Apache2 보안) - intadd 님 블로그]

https://intadd.tistory.com/97

 

[톰캣 에러페이지 설정(정보 및 버전 감추기) - Mr.lee 님 블로그]

https://lee-mandu.tistory.com/327

 

[구글 해킹 유용한 명령어 - 정보보안 기록 저장소 님 블로그]

http://coashanee5.blogspot.com/2017/02/blog-post.html

 

[갈수록 중요해지는 권한 계정 보안 - 보안 뉴스]

http://www.boannews.com/media/view.asp?idx=51321

 

[홈페이지 보안 취약점 - 취약한 파일 존재 - IT 보물창고님 블로그]

https://skynarciss.tistory.com/29

 

[MongoDB 인증 모드 설정 - kkd927 님 블로그]

https://itstory.tk/entry/MongoDB-%EC%9D%B8%EC%A6%9D-%EB%AA%A8%EB%93%9C-password-%EC%84%A4%EC%A0%95

 

 

  이 하드닝 부분의 애매한 측면 중 하나가, 시간이 점점 지나면서 소프트웨어 보안에 대한 인식이 점점 강화 되고 있기 때문에, 앞의 공유기의 예처럼 새로운 버전의 OS 나 솔루션의 경우 많은 기본적으로 방어되도록 배포 되고 있다는 것이다. 예를 들어 앞에서 설치한 윈도우 10에서 설치되는 IIS 7.5 같은 경우에는 기본적으로 디렉토리 리스팅 기능이 OFF 되어있고, txt나 bak 확장자 파일의 경우도 URL 로 접근하면 예전과는 달리 파일이 실제 있더라도 404.4 에러가 난다. ASP 예제를 위해 IIS 를 설정할때도 생각해 보면 에러 또한 디폴트로 자세한 에러를 내보내지 않도록 설정되어 있다. 또한 기본적으로 웹서버나, FTP 서버가 설치되어 있지 않고 명시적으로 사용자가 설치해야 한다.

 

  하지만 보안 쪽의 귀찮음은 세상하는 항상 오래된 시스템이 있고, 해당 시스템은 잘 파악도 되지 않는 경우가 많기 때문에, 최신 환경에서는 일어나기 힘든 여러 과거의 취약점 패턴들에 대해서도 인지하여, 돌다리도 두드려 가는 마음으로 체크를 해야한다는 것이다. 다행이 이쪽은 스캐너 같은 도구나 스크립트 등이 도와줄 수 있는 측면이 많다고 본다.

 

  반대로 해킹을 해야되는 사람들 입장에서도, 모든 장비나 시스템이 최신 OS 로 패치되어있고, 하드닝이나 보안 설계가 잘 되어 있다면, 정말 아무도 모르는 패턴을 먼저 찾아낼 수 있는 소수의 능력자만 먹고 살수 있을지도 모른다. 하지만 세상일은 대부분 사람이 포함되어 하는 일이고 실수나 무지가 존재하기 때문에 항상 적정한 균형이 맞춰 지는 것 같긴하다. 이번 윈도우즈 10 업그레이드 이슈만 봐도 비용과 호환성의 측면이 있기 때문에 XP 처럼 쉬워 보이진 않는다. 아직도 어떨 수 없이 XP 를 쓰는 환경도 가끔 있다. 이런 면에서 보면 모바일 쪽에서는 IOS 쪽이 자유도는 엄청 떨어지긴 하지만 보안이나 유지비용 측면에서는 현명한거 같기도 하다. 

 

 

 

2.2 불필요한 것들 걷어내기

 

  다음 측면은 보안의 다른 철학 중 하나인 필요없는 사항을 걷어내는 것이다. 업로드 폴더의 실행권한을 제거하거나(또는 업로드 폴더를 웹서버 루트 폴더 바깥으로 빼기도 한다), 서버에 백업이나 테스트 파일을 남겨놓지 않는다든지(개념상 2.1과 겹치기도 한다), 웹게시판 이나 PHP 등의 샘플 파일을 제거한다든지, 사용하지 않는 특정 서비스를 Diable 시킨다든지 하는 주제가 포함된다.

 

  이 부분도 앞에 얘기했듯이 OS 나 취약점이 있었던 대상 프로그램이 업그레이드 되면서 일어나지 않게될 가능성이 높아져 스캐너에게 임무를 양보해야 할 과거의 지식이 될 가능성이 높다. 위와 같은 예제를 웹에서 찾으면 아래와 같을 것이다.

 

[파일 업로드 취약점 점검 및 보안 - blackhyuk 님 블로그]

https://bbhyuk.tistory.com/88

 

[FCKeditor 취약점 + 간단실습 -RedScreen 님 블로그]

https://redscreen.tistory.com/69

 

[서비스 관리 - 불필요한 서비스 제거 - IT 보물창고님 블로그]

https://skynarciss.tistory.com/190

 

 

 

2.3 필요한 정도로만 권한을 부여하기

 

 

  다음은 보안에서 가장 전방위로 많이 보이는 권한 제한 부분이다. 웹서버나 어플리케이션 권한을 ROOT 계정이 아닌 적절한 권한을 부여한 계정으로 돌린다든지(비교하자면 전쟁시 쪼랩인 중위에게 핵미사일 장치를 맡기지 않아야 하는 것과 비슷하다), 익명 FTP 를 제공하지 않는다든지(2.2 처럼 필요 없으면 FTP를 아예 설치 안하는게 더 바람직하다),  공용계정을 쓰지 않는다든지(누가 사고 쳤는지 알수 없거나, 어렵게 된다), 필요없이 자세한 로그를 남겨서 아무나 보게 한다든지(개인정보나 사용자의 주요 정보가 익명으로 노출될 수 있다), 어플리케이션에게 필요한 권한만 부여된 DB 계정을 발급한다든지, 루트권한을 안준다든지, everyone 권한의 폴더를 공유한다 든지 이다. 밑에 웹에서 찾은 예제들이 있다.

 

[리눅스에서 톰캣 일반 계정으로 실행하기 - 기록 > 기억님 블로그]

https://kimyhcj.tistory.com/75

 

[서버관리 - Anonymous FTP 비활성화 - IT 보물창고님 블로그]

https://skynarciss.tistory.com/125

 

[리눅스 루트계정을 항상 사용하면 보안에 문제가 되나요? - KLDP]

https://kldp.org/node/152000

 

[DB 접근제어 설계 - DBGuide.net]

http://www.dbguide.net/db.db?cmd=view&boardUid=152805&boardConfigUid=9&boardIdx=146&boardStep=1

 

[서버보안 가이드 - 폴더 권한 설정 - IT 보물창고님 블로그]

https://skynarciss.tistory.com/12

 

 

 

  이 부분은 사실 어플리케션 보안 측면도 있지만, 회사에서 많이 얘기되는 보안 정책 부분이기도 하다. 법적인 부분을 꼭 준수하고, 이외에는 사람들의 사용성 측면과 계속 딜을 해가면서 균형을 맞춰야 하는 부분 같다. 

 

2.4 그 외

  마지막으로 8교시 까지의 여러가지 측면(클라이언트 코드, 암호화, 인젝션, API, 업로드, 다운로드) 측면을 포함하는 포괄적인 영역이다. 해당 부분은 아래와 같이 기관에서 배포한 보안 가이드 문서(모바일, 일반 OS)를 살펴보면 될것 같다. 추가로 10교시에서 얘기할 설계 부분을 고려하여 문제에 접근 하면 어떨까 싶다. 

 

[소프트웨어 개발 보안 가이드]

http://www.kisa.or.kr/uploadfile/201702/201702140920275581.pdf

 

  사실 이 글 시리즈에서 목표하던 것 하나가 저런 보안 가이드를 만들때 왜 이런 항목을 구성해서 보안가이드를 만들었는지를 객관적으로 볼수 있도록 하고 싶었다. 뭐 얼마나 공감했을진 모르지만 일단은 1차 고지에는 도착한 기분이다.   

 

 

3. 대처 방법

  4. 그외 항목 이외에 위의 요소들의 주요 특징중 하나는 대부분은 좀 과거의 히스토리적으로 모아진 취약점의 경향이 강하고, 정형화되 있다는 것이다. 또한 수동 확인의 경우에도 특정 명령어를 날려 확인 한다든가, 화면을 보면 된다(경험상 화면을 봐서 할수 있는 것은 스크립트 등으로 자동화가 가능하다).

 

  결국 반복적으로 자동화된 체크를 수행하기 용이하다는 것인데, 이러한 자동화가 집적화된 결과가 여러 보안 스캐너나, 솔루션 들이다. 단순한 작업을 지치도록 수동으로 하기 보다는 적절한 ROI 를 따져서 무료나 유료, 또는 파이썬 등의 여러 언어로 만들어진 프로그램을 만들거나 코드를 구해 체크하면 효율이 나는 부분 같다. 이렇게 아낀 시간을 설계적인 측면이나, 코드 리뷰 등 자동화된 측면으로는 조금 한계가 있는 부분에 투자하는게 좀더 효율적이지 않을까 싶다. 물론 해당 부분을 자동화 하는 것을 스스로 한다면 많은 시행착오와 시간이 투여되는 부분일 수 있긴 한듯 하다.

 

 

 

4. 마무리 하면서

  다른 블로그들을 링크하여, 구렁이 담넘어가듯 써내려간 느낌이지만, 굳이 책이나 블로그에 잘 설명되어 있는 부분을 반복하는건 읽는 사람이나 쓰는 사람 모두 손해라고 생각해서 라고 생각해 줌 좋겠다. 설정 측면에서의 하드닝은 일반적인 어플리케이션 보안 관점보다는 노력이 덜 들고 자동화 할 수 있는 요소가 많은 부분이긴 하지만, 수동으로 할 경우 시간이 꽤 걸리고, 휴먼 에러도 일어날 수 있는 부분이다. 또한 실제 노출됬을경우 생각보다 큰 이펙을 주게 되는 요소라고 본다, 해당 부분은 단발적인 부분이 아니라 보안 쪽의 종합적인 정책과도 연관되어 결정되야 되는 측면도 있어 보인다. 단순해 보이긴 하지만 여러모로 균형을 잘 맞추면서 접근해야할 부분 같다.

 

 

 

2019.5.5 by 자유로운설탕
cs

 

 

 

posted by 자유로운설탕
2019. 3. 24. 21:38 보안

  API 는 사실 보안적으로 봤을때(사실 프로그램적으로 봤을 때도 비슷한 상황인거 같지만) 일반적인 다른 프로그램 요소와 특별히 다르진 않다. 다만 일반적으로 웹에서는 AJAX 와 연관되어 돌아가기 때문에 피들러 같은 특수한 툴로 살펴보지 않는 이상 호출이 된다는 부분조차 인지하지 못할 수 있어서 개발자의 경험이 없으면 체크를 놓치는 요소라고 보는게 더 맞을 거 같다. 어떤 측면에서는 일반적인 어플리케이션의 인터페이스보다는 표준화(보통 XML 이나 JSON 으로)가 많이 된 요소이기 때문에, 더 투명하게 검증 할 수 있는 측면도 있어 보인다.

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

 

1. 들어가면서

  그럼 API 는 무엇일까? 사실 이 얘기는 파이썬 글 10교시 Whois API 편을 보면 이미 필요한 부분에 대해서 얘기한듯 싶다. 샘플도 API 가  무언지를 보여주는 토큰 까지 있는 좋은 샘플이라고 생각하기 때문에 먼저 해당 글을 보고 이어서 보길 바란다. 여기서는 해당 글을 보고 왔다고 생각하고 얘기를 이어간다.

 

 

  거기에서 나왔던 얘기같이 API는 머리 꼬리를 띈 생선의 몸통 같이 데이터만이 왔다 갔다하는 프로그램이다. 사실 이 API는 소프트웨어의 시작 부터 계속해서 있어 왔다. API 의 약자 자체가 Application Programming Interface 이기 때문에, 어플리케이션에서 무언가를 호출해서(예전엔 주로 Windows API 등 OS 에서 제공하는 함수들-선을 그린다던가, 창을 띄운다던가, 화면에 글자를 뿌린다던가... 이였지만), 원하는 일을 하거나, 결과 값을 받아오는 형식을 말한다.

 

 

  인터페이스는 두 개의 다른 존재가 연결되는 방식을 얘기한다고 볼수 있는데, 우리 현실과 연계를 지으면 쉽게 비슷한 부분이 많다. 예를 들어 우리는 여러가지 인터페이스를 통해 타인 또는 타 물체와 소통 한다. 편의점에 가면 물건을 사기위해서 카드나, 현금을 주고 포스기로 결제한 후 물건을 담아오게 되고, 친구를 만나면 대화를 나누거나, 음식을 먹거나, 술을 먹거나 하면서 우정을 나누고, 은행에 가면 은행에 정해진 규칙에 따라서 상담원과 은행 업무를 본다.

 

  우리가 아이폰이나 안드로이드 폰을 사용하는 것도 폰의 인터페이스(터치, 드래그 및 여러 메뉴에 의한 사용자 인터페이스)를 이용한다고 보면 된다. 앞의 Whois API 예제에서도 해당 사이트에서 설계한 인터페이스 대로 데이터를 보내고 받아야 하는 것을 볼 수 있다. 우리가 생각하는 예의란 부분도 마찬가지여서, 예의에 대한 가치관이 다른(인터페이스가 다른) 두 사람이 만나면 관계가 엉망이 되어버리기도 한다. 

 

  프로그램 언어를 배우는 초입 부분에서 API 와 비슷한 부분을 볼수 있게 되는데, 해당 부분이 바로 함수나 메서드이다. 함수를 보면 정해진 타입의 입력 값을 넣어서, 정해진 타입의 출력 값을 얻게 된다. 파이썬의 모듈도 마찬가지로 해당 모듈에 정해진 방식대로 사용해야 한다. 어떻게 보면 프로그래밍 영역 자체가 수많은 인터페이스들간의 커뮤니케이션으로 이루어져 졌다고 봐도 될것 같다.

 

  

  추가로 API 에는 하나의 더 중요한 부분이 있는데, 호출 권한에 대한 인증 이다. 해당 부분은 3가지  정도의 측면이 있는데, 성능 및 데이터의 중요도, 모니터링 이다.

 

  첫째로 아무리 성능이 좋은 서버가 지원하는 상태라도 많은 사람들이 익명으로 계속 호출하게 된다면 병목 문제가 생길 수 있다. 이 부분은 크롤링 툴 등에 의해 페이지를 자주 호출 하는 경우에도 비슷한 문제가 생길수 있지만 API 는 좀더 특정한 목적을 가지고 내부에서 조회한 데이터를 전달하는 목적을 가지고 있기 때문에 좀더 민감하지 않을까 싶다. 

 

  둘째는 데이터의 중요도 인데, 특정 개인에게만 전달되야 되는 데이터가 다른 사람에서 전달되거나, 특정 권한을 가진 경우에만 전달 가능한 데이터가 아무에게나 보내지는 것은 바람직하지 못할 것이다.

 

  세번째는 모니터링 및 통제다. 현재 데이터를 가져가는 주체가 누구인지 특정할 수 있다면, 데이터가 어디로 나가고 있는지(호출하는 서버나 IP 정보만으로는 불충분하다), 얼만큼 데이터를 제공해야 하는지를 컨트롤 할수 없다.

 

 

 

 

2. 간단한 API 만들어 보기

  앞의 Whois API 를 봄으로서 API 를 사용하는 측면은 살펴봤으니 이번엔 한번 예제를 만들어 보자. 파이썬의 flask 모듈을 이용해 API 를 호출해 데이터를 가져오는 기능을 만들어 보려고 한다. 밑의 글들과 같은 REST API 샘플을 만들수도 있겠지만, 원리를 설명하기에는 간단한 쪽이 좀더 날거 같아서, 앞의 AJAX 예제와 비슷하게 만들었다.

 

[파이썬 Flask 로 간단한 REST API 작성하기 - readbetweentheline 님 블로그]

https://medium.com/@feedbots/python-flask-%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-rest-api-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-60a29a9ebd8c

 

[Designing a RESTful API using flask restful - miguelgrinberg.com]

https://blog.miguelgrinberg.com/post/designing-a-restful-api-using-flask-restful

 

 

2.1 간단한 API 호출 예제

  우선 파이썬 코드 쪽을 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import Flask, render_template, request, jsonify
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/call_api", methods=['GET'])
def test_search():
    return render_template('call_api.html')
 
 
@app.route("/getSecret", methods=['GET'])
def getSecret():
    name = request.args.get('myname')
    secret = name + "'s secret is A" 
    return jsonify({'var1': secret})
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_api.py]

 

  간단히 코드를 보면 call_api 가 기본 페이지이고, getSecret 가 API 역할을 한다. 해당 API 는 인자로 myname 을 받아서 해당 이름의 비밀을 알려준다(원래는 회원이 맞는지, 해당 회원의 비밀이 무언지를 아마 어딘가에 조회해야 할테지만 했다고 친다...). UTF-8 인코딩으로 c:\python\code 폴더에 flask_api.py 라고 저장하자.

 

 

  다음으로 템플릿 파일을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<html>
  <head>
    <script src="http://code.jquery.com/jquery-3.3.1.min.js" ></script>
      <title>Call API</title>
      
    <script type="text/javascript">
      $(document).ready(function(){
 
        $("[id^=checkData]").click(function() {
          var myname = "tom"
           
          $.ajax({
            url: "/getSecret",
            data: { myname : myname },
            contentType: 'application/json;charset=UTF-8',
            success: function(data){
              $("#spanName").html(data.var1);
            }
          });
        });
        
      });
 
    </script>
  </head>
  <body>
    <table>
      <tr>        
        <td><input type="button" id="checkData" value="체크"></td>
      </tr>
      <tr>               
        <td><span id="spanName"></span></td>
      <tr>
    </table>
  </body>
</html>
cs

[call_api.html]

 

  내용을 보면 "checkData" 라는 버튼이 하나 있고, 해당 버튼을 누르게 되면 AJAX 기능을 이용해서 getSecret API 에 myname 을 "tom" 이라는 GET 인자를 포함해서 호출 한다. 역시 UTF-8 인코딩으로 c:\python\code\templates 폴더에 call_api.html 이란 이름으로 저장한다.

 

 

  아래와 같이 플라스크 사이트를 실행 후 주소창에, http://localhost:5000/call_api 라고 입력 후, "체크" 버튼을 클릭한다. 밑과 같이 tom 의 비밀이 API 를 통해 전달되어 나오게 된다(이해가 안되는 경우는 앞에 소개한 피들러로 함 살펴봐도 좋다)

c:\Python\code>python flask_api.py

 

 

  그럼 위의 getSecret API 가 가질 수 있는 문제점은 무엇일까? 앞에서 얘기했듯이 아무나 해당 API 를 호출하면 문제가 생길 수 있다. 웹에서는 쿠키에 저장된 암호화된 인증 값을 이용해서 신원을 확인 하는데, API 의 경우는 HTTP 방식으로 ID/PASS 를 통해 인증할수도 있지만, 보통 호출하는 주체가 자동화된 프로그램일 경우가 많기 때문에 일일히 패스워드를 입력하여 넘기기는 어렵다(물론 어딘가에 저장해 두었다 프로그램이 넘겨도 되긴 한다). 그래서 좀더 자주 쓰이는 방식이 토큰(Token)을 사용하는 것이라고 본다(해당 토큰 개념은 스크립트 문제에서 얘기한 CSRF 의 방어에도 쓰인다).

 

 

 

2.2 토큰 추가 예제

  그럼 간단한 토큰을 사용하는 예제를 만들어 보자. 똑같이 플라스크 쪽 코드 부터 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask, render_template, request, jsonify
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/call_api_token", methods=['GET'])
def test_search():
    return render_template('call_api_token.html')
 
 
@app.route("/getSecret", methods=['GET'])
def getSecret():
    name = request.args.get('myname')
    token = request.args.get('mytoken')
    if token == "@$ABC77":
       secret = name + "'s secret is A"
    else:
       secret = "Need valid token!" 
    return jsonify({'var1': secret})
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_api_token.py]

 

  내용을 보게되면 앞의 코드와 거의 비슷하지만, mytoken 이라는 사용자가 보낸 토큰을 확인하여, "@$ABC77" 값이 아니라면 "Need valid token!"라고 에러메시지를 대신 보낸다. UTF-8 인코딩으로 c:\python\code 폴더에 flask_api_token.py 라고 저장하자.

 

 

  마찬가지로 템플릿 코드 쪽을 보면, API 를 호출하는 인자에 mytoken 값이 추가됬다. 일부러 토큰은 앞에 "X" 가 추가로 들어가 값이 틀리게 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html>
  <head>
    <script src="http://code.jquery.com/jquery-3.3.1.min.js" ></script>
      <title>Call API</title>
      
    <script type="text/javascript">
      $(document).ready(function(){
 
        $("[id^=checkData]").click(function() {
          var myname = "tom"
          var mytoken = "X@$ABC77"
           
          $.ajax({
            url: "/getSecret",
            data: { myname : myname },
            contentType: 'application/json;charset=UTF-8',
            success: function(data){
              $("#spanName").html(data.var1);
            }
          });
        });
        
      });
 
    </script>
  </head>
  <body>
    <table>
      <tr>        
        <td><input type="button" id="checkData" value="체크"></td>
      </tr>
      <tr>               
        <td><span id="spanName"></span></td>
      <tr>
    </table>
  </body>
</html>
 
cs

[call_api_token.html]

 

  UTF-8 인코딩으로 c:\python\code\templates 폴더에 call_api.html 이란 이름으로 저장한다.

 

  아래와 같이 플라스크 서버를 실행 후 주소창에, http://localhost:5000/call_api 라고 입력 후, "체크" 버튼을 클릭한다. 의도했던 대로 토큰 값이 틀리다고 에러가 난다.

c:\Python\code>python flask_api.py

 

 

 

3. 어플리케이션 보안 측면의 API

  그럼 어플리케이션 보안 측면에서 API 를 어떻게 봐야하는지를 생각해 보자. 

 

 

  첫번째로 앞의 예제에서 본 것처럼 요즘의 API 들은 사실 엄청 단순한 모양을 가지고 있다(약간 인자들이 암호같은 Windows API 등에 비해서 말이다--;). 사실 하는 일도 단순하다. 프로그램의 함수가 입력을 받거나 받지 않을 수도 있고, 출력을 주거나 주지 않을 수 있듯이 API 도 마찬가지 이다.

 

  입력을 받아 단순히 로그로 저장할 수도 있고, 구입 목록 같이 입력에 대한 특정한 정보를 줄수도 있고, 빌드나 다른 API 의 호출 등의 특정한 액션을 할수도 있다. 그 부분은 API 가 어떤 기능을 하느냐에 따라서 달라지는 일이라서 블랙박스적으로 보면된다고 생각될수도 있지만, 보안적으로 봤을때는 한가지 중요한 점이 있다. 그것은 API 의 입력으로 들어가는 값들이 API 내부의 로직에 영향을 미칠수 있다는 것이다.

 

  이렇게되면 앞에서 얘기했던 클라이언트 코드와 인젝션에 대한 이야기로 다시 주제가 돌아가게 된다. API 가 사용하는 인자 중 나쁜 영향을 미칠수 있는 인자를 파악하는 방법은 API 내부의 동작을 이해해야하는 문제가 되버린다. 

 

 

  두번째는 앞의 예제에서 봤던 토큰이다. 이 토큰이 변조되거나, 바꿔치기 되는 문제에 대해서 웹에서의 세션관리와 동일한 종류의 문제가 발생되게 된다. 추가로 어떻게 토큰을 최초 사용자에게 발급하고, 토큰으로 인한 자격의 유지 기간에 대해서도 고민해 봐야한다. 프로그램이 주로 호출하는 API 의 특성상 보통 한번 발급한 토큰을 특별한 제약없이 영구히 사용하는 경우도 많은듯 하지만, 관리의 실수로 토큰이 노출되었을 경우 골치아픈 문제가 발생할수 있으므로 여러 측면에서 리스크를 검토해야 한다(마치 암호화페에서 열쇠에 해당하는 개인키가 도난당하는 것 같은 일이 생길 수도 있다).

 

 

  세번째로 전송되는 데이터를 평문으로 보내도 될까 하는 부분이다. HTTPS 로 감싸 보내는 방법도 있겠지만 해당 부분은 보내는 쪽의 의도적인 조작이나 중간자 공격에 100% 안전하지는 않을 것이기 때문에, 아예 공개키/개인키 등을 사용해서 암호화 하여 보내는게 맞을 수도 있다. 해당 부분은 법적인 부분과, 데이터의 중요성, 여러 위험 요소를 고려해서 선택해야하는 부분인것 같다.

 

 

  마지막으로 OWASP 에서도 중요하게 강조하고 있는 모니터링 부분이다. 중요한 API 일수록 해당 토큰을 발급한 쪽에서 필요한 만큼 적절하게 API 를 호출 하는지를 체크해야 하는 경우가 있게된다. 해당 정보가 법적으로 보호되어야 하는 민감한 정보라면 더더욱 그렇다. 해당 부분은 어플리케이션 쪽에서 토큰 및 여러 클라이언트 쪽 정보를 기반으로 기준을 정해 모니터링을 해야되는 문제이다.

 

 

 

4. 마무리 하면서

  조금 싱거운 감이 있지만 개인적으로 API 는 일반 어플리케이션과 별로 다르게 볼 필요는 없다고 생각하기 때문에 여기서 마무리를 하려한다. API 는 전송하는 데이터와 토큰이라는 관점에서 보게 되면, 일반적인 어플리케이션과 특별히 다르지는 않다. 다만 해당 형태의 구조를 명확히 이해하지 못한다면 미지의 영역이 되어 전혀 다른 것처럼 보일 수가 있다. 

 

  요즘처럼 API 로 조각조각 나누어진 어플리케이션을 테스트 하는것은, 인터페이스들이 늘어나는 결과를 가져오기 때문에 테스트를 해야되는 입장으로는 꽤 귀찮은 일이긴 하다. 2교시에서 얘기했던 것처럼 소스 레벨의 관점에서 문제를 바라보는 것도 나쁘진 않을 거라고 생각한다.

 

 

 

2019.3.31 by 자유로운설탕
cs

 

 

posted by 자유로운설탕
prev 1 2 3 4 5 ··· 9 next