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

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

'분류 전체보기'에 해당되는 글 53

  1. 2022.09.04 [책소개] 버그 정글을 헤쳐 가기 위한 테스터 지침서
  2. 2021.09.15 보안 책 출간이 되었습니다.
  3. 2020.05.10 구글로 공부하는 보안 - 15교시(악성코드)2
  4. 2020.05.04 ハッピーエンド(해피 앤드) - back number
  5. 2020.04.18 慢慢喜欢你 - 莫文蔚 (만만시환니 - 천천히 당신을 좋아하기)
  6. 2019.10.06 구글로 공부하는 보안 - 14교시(모니터링 문제)
  7. 2019.08.25 구글로 공부하는 보안 - 13교시(리버싱과 포렌식)
  8. 2019.07.07 구글로 공부하는 보안 - 12교시(자동화 잡)
  9. 2019.05.20 구글로 공부하는 보안 - 11교시 (스캐너 vs 수동 테스트)
  10. 2019.05.06 구글로 공부하는 보안 - 10교시 (보안 설계 문제 security by design)
  11. 2019.05.01 구글로 공부하는 보안 - 9교시 (하드닝 Hardening)
  12. 2019.03.24 구글로 공부하는 보안 - 8교시 (API)
  13. 2019.03.02 구글로 공부하는 보안 - 7교시 (스크립트 문제)2
  14. 2019.02.24 구글로 공부하는 보안 - 6교시 (업로드, 다운로드)
  15. 2019.02.04 구글로 공부하는 파이썬 - 부록 (IIS, Apache 로 Flask 돌리기)
  16. 2018.11.11 구글로 공부하는 보안 - 5교시 (클라이언트 코드)
  17. 2018.07.15 구글로 공부하는 보안 - 4교시 (암호화)
  18. 2018.06.06 구글로 공부하는 보안 - 3교시 (인젝션 Injection)
  19. 2018.03.02 [책 출간 안내] 구글로 공부하는 파이썬25
  20. 2018.01.01 구글로 공부하는 보안 - 2교시 (보안에서의 코드 읽기)2
  21. 2017.12.02 구글로 공부하는 보안 - 1교시 (보안을 바라보는 방법)6
  22. 2017.08.19 구글로 공부하는 파이썬 - 21교시 (정리 - 이런저런 이야기)
  23. 2017.08.06 구글로 공부하는 파이썬 - 20교시 (장고 - Django 살펴보기)
  24. 2017.07.20 구글로 공부하는 파이썬 - 19교시 (Flask 살펴보기 - Feat. D3.js)3
  25. 2017.07.02 구글로 공부하는 파이썬 - 18교시 (Legacy Web)
  26. 2017.06.18 구글로 공부하는 파이썬 - 17교시 (머신러닝에서의 파이썬의 역활)
  27. 2017.05.21 구글로 공부하는 파이썬 - 16교시 (그래픽 라이브러리 살펴보기)
  28. 2017.05.06 구글로 공부하는 파이썬 - 15교시 (수학 라이브러리 살펴보기)
  29. 2017.04.19 구글로 공부하는 파이썬 - 14교시 (작업 자동화)
  30. 2017.04.07 구글로 공부하는 파이썬 - 13교시 (윈도우즈 GUI 자동화 with pywinauto)12
2022. 9. 4. 19:54 프로그래밍

지인이 새로 책을 냈다고 그래서, 관리가 안되 방문이 드문 블로그 이긴 하지만 간단히 책 소개를 하려고 한다.

예전에 테스트를 직업으로 가졌던 시절만 하더라도(벌써 10년이 조금 더  넘은듯 하다), 임베디드와 관련된 테스트를 하게 되는 것은 일반적으로 경험하기 힘든 영역이였던 것 같은데, 요즘은 모바일 기반으로 중계되는 여러 현실 비즈니스 및 퀵보드 같은 사물 인터넷 기반의 사업 때문에, 소프트웨어 자체가 아닌 연결된 사물의 특성 및 주어진 환경과 같이 테스트 설계 적으로 고려해야 할 부분들이 더 많이 늘어난 것 같다. 추가로 IT쪽의 핫한 추세였던(요즘은 기업들도 몇년 전처럼 맹목적인것 같진 않지만) AI 및 자율주행 등과 같이 센서와 데이터를 통한 자동적인 규칙의 해석이라는 측면에서, 처음부터 요구사항에 의해 개발된 제품에 기반하여 이루어졌던 테스트 영역에 대해서 변화가 생긴 측면도 있는 것 같다.

 

그 하나하나의 주제에 대해서는 각각의 산업들의 히스토리 및 자체 기술이 있고, 해당 기술적인 측면은 업무적 경험과 공부를 통해 하나하나 쌓아갈 수 밖에 없는 것 같긴하지만, 여러 산업 분야에서 다양한 측면으로 경험했던 저자의 배경을 기반으로 전체적인 테스팅 쪽 산업의 모습을 정리해 주는 것에 이 책의 강점이 있는 것 같다. 이미 QA 팀이 오래 동안 성숙되온 커다란 회사는 나름의 철학 등을 가지고 있겠지만, 아마도 그 연혁이 짧거나 겸업의 개념으로 그 직무를 유지해온 회사들은 여전히 여러가지 오해 속에서 헤메면서 QA와 테스팅 업무를 수행하고 있을 것 같다. 이 책은 그렇게 헤메는 사람들에게 업계 전체의 표준과 객관적인 시각으로 자신의 위치를 바라보게 하는 참고 자료가 될 수 있을 것 같다. 개인적으로 개발이나 보안이나, 테스팅 모두 같은 기반을 가진 관점만 다른 분야이긴 것 같긴 하지만 말이다.

 

책을 읽으면서 프로세스와 기법적으로 해석 되어지는 테스팅 전반의 사전적 지식들을 기존의 실무적 경험과 당위성을 기반으로 쉽게 엮어 설명하려고 하는 저자의 노력을 엿볼 수 있었으며, 프로젝트 관리 경험과 컨설팅 경험이 풍부한 저자의 관점에서 테스팅 산업 전반에 대한 숲을 관찰할 수 있게 해준다. 테스팅에 대한 의미와 경향, 버그 및 품질에 대한 이해, 테스팅 계획 및 프로세스, 케이스 작성, 수행, 결과보고, 결함의 원인에 대한 고찰, 팀에 대한 관리, 여러 테스팅 업무 포지션에 대한 현실적으로 주어진 환경 및 장 단점을 이 책을 통해서 경험해 볼수 있다.

 

다만 한가지 실제 개발 코드 예제에 기반하여, 세부적인 다양한 테스팅 설계에 대한 예제 및 자동화 예제가 있다면 좀 더 좋았겠다는 생각이 들지만, 해당 부분은 현재 책과는 다른 방향의 영역이며, 현재도 국, 내외에서 그런 종류의 지식을 디테일하게 설명하는 테스팅 관련 책을 찾아보기는 어렵다 생각하므로, 저자의 다음 책의 서브 주제로 확장되어 발간되기를 기대하는 바램을 가지며 소개를 마쳐본다. 

posted by 자유로운설탕
2021. 9. 15. 21:35 보안

블로그 내용을 기반으로 책이 나오게 되었습니다.

http://www.yes24.com/Product/Goods/103385806

 

구글과 파이썬으로 시작하는 보안 - YES24

보안 분야도 여러 사람이 다양한 생각 및 관점을 가지고 접근하고 이해하겠지만, 이 책에서는 데이터를 따라가는 직업이라는 시작점에서 진행하고자 한다. 챕터마다 보안을 이해하는 데 있어

www.yes24.com

 

전체적인 흐름을 유지하면서 다음과 같은 사항들이 업데이트 되었습니다.

  • 전체 예제를 파이썬 코드로만 진행하도록 변경 하였으며, 내용에 맞추어 예제 코드를 개선 및 추가 하였습니다.
  • 컨텐츠 전체가 바뀌거나 추가된 챕터들이 있으며, 글의 전개나 표현을 가독성 있게 다듬었습니다.
  • 설명을 돕는 120장의 예쁜 삽화들을 지인분이 그려주셨습니다~

posted by 자유로운설탕
2020. 5. 10. 10:30 보안

  이번 시간에는 잘 아는 분야는 아니라고 생각하지만 악성코드라는 분야를 어떻게 바라보면 될지에 대해서 나름의 관점에서 얘기해 보려 한다.

 

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

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

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

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

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

 

1. 들어가면서

  앞의 글에서도 한 얘기지만 소프트웨어 보안에 마술 같은 부분은 없다. 모든 건 컴퓨터와 연결된 환경내에서 일어나는 일이며, 결국은 코드에 기반하여 논리적으로 설명이 가능해야 방어나 절충이 가능하게 된다. 그건 악성코드와 같은 분야에서도 마찬가지로 적용된다. 악성코드는 비 기술 적이거나 보안을 잘 모르는 사람들에게는 이해할 수 없는 어려운 현상일 수 있다. 악성코드가 컴퓨터에 설치되는 과정에 대해서는 하나하나 취약점의 기술적 측면을 살펴보며 이해해야 하는 문제(물론 이걸로 밥 먹고 사는 사람들이 있는 어려운 분야이긴 하지만)겠지만, 일단 설치된 후에 하는 행동은 일반 프로그램의 경우와 크게 다른 부분이 없다.

 

  예로서 요즘 많은 화제가 되고 있는 우리에게 익숙한 랜섬웨어의 행동을 살펴보자. 걸리면 특별한 경우가 아님 무조건 포맷을 해야 해결이 가능한 악명 높은 이런 악성코드도 사실 동작은 아주 단순하다고 볼 수 있다. 한번 랜섬웨어의 행동을 사람이 따라해 본다고 해보자.

 

 

  누군가 골탕을 먹이고 싶은 사람이 컴퓨터를 로그인 해 놓은채로 자리를 비웠다 하자. 해당 컴퓨터로 재빨리 다가가서 그 사람이 소중하게 생각할 만한 문서 파일이나, 데이터 파일들을 찾아 선택하여 zip으로 압축을 해본다. 확장자도 zip 보다는 다른 누군가가 악의로 했다는 것을 명확히 알아챌 수 있게 “lupine” 이라고 만들어 보자. 내가 아니면 압축을 풀 수 없도록 압축 파일에 나만 아는 복잡한 암호를 걸어서 압축한다(Brute force 공격에 의해서 쉽게 뚫어지지 않도록). 내가 암호를 알려주지 않는다면 아마 영원히 해당 파일의 원본을 찾을 순 없을 것이다. 이후 압축이 안된 원본 파일을 삭제하는데 가능한 복원이 안 되도록 단순히 지우지(delete) 말고, wiping 을 해서 원복 하기 어렵게 만든다. 주의할 점은 시스템에서 사용하는 중요한 파일들은 압축하거나 삭제하면 안된다. 운영체제 동작 자체가 망가져 켜지지 않는다면 상태가 골탕을 먹었는지도 모르게 되니까 말이다. 메모장을 열어 패스워드가 적힌 종이가 적힌 장소를 알려주는 퀴즈를 적은 후, 압축된 파일과 같은 폴더에 복구방법.txt” 라는 이름으로 저장해 놓는다. 뭐 운이 좋음 암호를 찾을 수도 있겠지

 

  위에 사람이 했던 것과 같은 행동을 랜섬웨어는 기존의 악성코드들과 동일한 방법으로 컴퓨터에 설치된 후, 프로그램 코드를 통해서 자동으로 수행한다. 그리고 최종 목표는 상태의 멸망(모든 것을 새로 설치하고 데이터를 날려버리게 하는)이나, 패스워드를 인질로 하고 비트 코인 등을 송금하도록 요구를 한다.

 

 

 

 

2. 백신 프로그램의 입장에서 상상해 보기

  그럼 반대로 이러한 악성코드를 문제가 일어나기 전에 적절히 찾아야 하는 백신 프로그램의 입장은 어떨까? 총기가 허용된 사회에서 테러를 저지를 수 있는 위험한 사람을 찾아내야 하는 것 같은 모호한 입장에 있다고 생각한다.

  왜 모호할까? 겉에서 보이는 행동이외에 사람의 마음속이나 행동의 의도를 알아내긴 힘들기 때문이다. 우선 총을 가지고 있다고 위험한 사람일까? 물론 가능성은 높을 것이다. 하지만 테러를 일으킬 수 있는 사람일 수도 있지만, 단순히 사복을 입은 경찰관일 수도 있고, 사회 분위기가 어수선해서 자신을 보호하기 위해 총기를 휴대한 사람일 수도 있다. 그럼 총기 허가증이 있거나, 경찰이라면 안심할 수 있을까? 반대로 테러를 위해서 치밀하게 준비된 시나리오일 수도 있다. 그럼 어떻게 해야 할까? 1시간 정도만 그 사람의 행동을 관찰하면 될까? 아니 어쩌면 그 사람의 작전 D-day10일 뒤이기 때문에 하루 종일 살펴봐도 이상한 징후는 없을지도 모른다. 총을 주머니 바깥으로 빼내서 겨눈다고 나쁜 사람일까? 아니 무언가 수상한 범죄자를 보고 총을 겨누고 있는 형사일수도 있다.

 

  마이너리티 리포트 영화 같은 대상의 악의성을 판단하는 이런 판단의 문제는 백신 업체를 무척 머리 아프게 만드는 측면일 것이다. 그래서 백신이 멀쩡히 깔려 있는 컴퓨터에서 새로운 랜섬웨어가 걸리는 것 같은 이해할 수 없는 일들도 일어 날테고 말이다. 위의 테러범을 찾는 문제로 간다면, 검출이 안되는 플라스틱 총이나, 케익 상자로 위장 할 수도 있고, 경찰이 테러범과 비슷한 형태로 행동할 수도 있는 상식적인 선을 넘는 여러 시도가 일어날 수 있기 때문이다. 심지어는 테러범을 수색하는 사람으로 위장한 사람이 나타날 수도 있다. 영화나 부패한 나라에서 일어나는 일이지만 아래와 같이 현실 상에서도 소프트웨어 세계에 비슷한 일이 발생할 수가 있다.

 

[PC PC 검색어 조작 해킹 심은 일당 검거 - 아이러브피씨방]

http://www.ilovepcbang.com/news/articleView.html?idxno=73794

 

[사설 보안 연구소장이 해킹.. 국내 PC방 절반 감염 - YTN]

https://www.ytn.co.kr/_ln/0115_201611142210438443

 

[허위 백신 - 나무위키]

https://namu.wiki/w/%ED%97%88%EC%9C%84%EB%B0%B1%EC%8B%A0

 

 

 

 

3. 백신 프로그램이 할 수 있는 전략을 상상해 보기

  최신의 백신 프로그램이 어떤 무기와 전략을 가지고 돌아가는 지는 해당 회사에 있지 않은 이상은 모를테지만, 책이나 기사에서 본 것을 토대로 상식적으로 접근해 볼 순 있을 것이다. 먼저 정적 분석이라고 하는 분야가 있다. 상식적으로 이해하기 위해 위의 테러범을 찾는 문제의 과점에서 생각하면 이해가 좀더 쉬울 것이다.

 

  먼저 의심이 가는 사람의 외모적 특징을 본다. 그 담에 지문이나 신분증을 확인하며 이상한 이력이 없는지 전산을 조회해 본다. 이후 소지품 검사를 해서 총이나 수상한 물품을 소지했는지 체크하고, 총기 허가증을 체크한다. 상황에 따라 일반적으로 차별이 금지되어 있지만 테러 의심자를 조사하는 특수한 경우에만 허용된 여러 편향적인 부분 또한 체크할 수도 있을 것 같다(국적, 종교, 지인, 직업 등).

 

  그 다음은 동적 분석인데, 해당 부분으로 통과된 사람이라도 뭔가 의도적으로 정상적으로 위장한 사람일 수도 있기 때문에 여러 동적인 행동도 체크해 본다. 위험하다고 분류된 특정한 행동을 한다든지, 불안한 패턴을 보인다든지, 특정한 사람과 연락이나 대화를 한다든지 하는 부분 말이다. 데이터 분석 및 머신러닝 등을 이용해 해당 패턴들을 과거의 테러범들의 데이터에 비교하거나 특이한 부분을 찾아 수상한 패턴을 찾을 수도 있을 것이다.

 

  비슷하게 백신 프로그램도 실행이 가능한 파일들에 대해서 같은 조사를 할 수 있다. 파일내의 특정 이미지나 텍스트를 검사하거나(외모), 파일의 해시 값(지문, 주민번호)을 알려진 악성코드의 해시 값과 비교하거나, 특정 바이트의 특징(신체적, 사회적 특징)을 찾거나, 프로그램 코드를 따라가며 위험한 행동을 하는 코드를 찾거나 할 수 있다. 조금 더 나아가 사용하는 라이브러리나 패커 등의 패턴 등을 기존에 구축된 악성코드 데이터베이스를 기반으로 비교해 수도 있을 것이다.

 

  나아가 샌드박스 등으로 제한된 환경에서 프로그램을 실행 시켜 이상한 행동을 하는지 지켜보거나, 수상한 외부 사이트와 연결해 데이터를 주고 받거나 하는지도 체크할 수 있을 것이다.

 

 

 

 

4. 프로그램 행동 분석의 명암

  이상적으로는 위의 액션들을 통해 위험한 것을 전부 발견할 수 있겠다고 하고 싶겠지만, 여기에는 몇 가지 제약들이 얹어지게 된다. 분석을 방해하는 암호화된 팩커들이 존재하고(이건 뭐 실행 시점엔 원래대로 돌아오니 본질적으론 괜찮다고 싶다하고), 분석시간에 제한도 생긴다(백신 파일이 내가 실행하려는 파일을 한없이 잡고 있는 걸 바라는 사용자는 없다), 감시 당한 다는 것을 알고 감시 안 당할 때 행동을 시작하려는 악성코드도 있을 수 있고, 특정 조건 하에서만 특정한 코드를 실행하는 경우도 많을 것이다. 앞의 피씨방 프로그램처럼 대부분의 기능은 정상적인 프로그램이고 그 안에 숨어 못된 행동을 할 수도 있다.

 

  결국 위의 정적인 분석, (세미) 동적인 분석, 디버거, 디스어셈블리 툴 같은 것을 이용한 파일 분석 등이 필요할 텐데 디버거나 디스어셈블리 툴을 통한 분석은 가장 해당 프로그램의 진실에 가깝게 접근하겠지만 아무리 자동화로 구축을 하더라도 분석 하는 사람의 경험&재능 및 어느 정도의 인력, 시간 등의 제약요소를 가진 노동 집약적인 특징을 피할 수는 없을 것 같다. 머신러닝 등의 데이터를 기반한 분석을 하더라도, 수많은 악성코드 데이터를 모두 모으고 신규 생성되는 코드 또한 실시간으로 수집해 데이터베이스를 재 구축하는 부분도 역시 쉽지 않은 도전이 될 것 같고, 그렇게 하더라도 앞으로 일어날 새로운 패턴의 공격을 막을 수 있을지는 확실히 자신하긴 힘들 것 같고 말이다. 그래서 아무래도 방어하는 쪽 보다는 공격하는 쪽이 맘이 많이 편하긴 할 듯 싶다.

 

 

 

 

5. 동적 분석의 예 및 회피 시도 해보기

  파일내의 문자열이 패턴 분석 같은 정적 분석에 대한 예제 보다는, 동적 분석의 예를 보이면 좋을 것 같아서 처음엔 Cuckoo Sandbox 같은 오픈소스 샌드박스를 구축해 파일을 실행해 볼까 했지만, 찾아보니 원하는 결과를 얻기까지 가야 될 길이 꽤 복잡해 보이고, Virus Total 사이트를 보다 보니 기본적인 동적 분석을 제공 해주는 것 같아서 해당 사이트를 이용해 가볍게 시연을 해보려고 한다.

 

  시연에 사용할 악성 파일 샘플의 경우도 굳이 백신을 잠시 꺼야 할지도 모르는 실제 덜 위험한 악성코드나 자바나 C언어 등을 이용한 실행 파일을 만들기는 번거로울 듯 해서, 12교시 리버싱과 포렌식 편에서 만들었던 엑셀의 최근 열린 파일을 알아내기 위해 레지스트리 키를 읽어왔던 아래의 파이썬 샘플을 파이썬 인터프리터가 포함된 하나의 exe 파일로 만들어 해당 파일을 이용하도록 해본다 (실행 파일이 레지스트리를 읽는 동작도 동적 동작에 포함되기 때문에 샌드박스에서 찾아낼 것이다).

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

[show_excel_recently.py]

 

  현재 진행을 하고 있는 버전이 python 3.6.4 버전이라는 것을 잊지 말고(다른 버전에서는 아래 방식이 지원이 안되면 다른 방법으로 exe 를 만들어야 할 수도 있다), 우선 pyinstaller 를 설치해 보자.

c:\Python\code\>pip install pyinstaller

 

  이후 13교시에 만든 show_excel_recently.py 파일을 아래 명령어를 통해서 하나의 exe 파일로 만들어 보자

c:\Python\code>pyinstaller --onefile -w show_excel_recently.py

 

  해당 명령어가 잘 돌아가게 되면, "c:\python\code\dist" 폴더에 “show_excel_recently.exe” 파일이 생성되어 있을 것이다(해당 파일을 실행한다고 커맨드 창에 결과를 뿌리진 않는다. 아마 그렇게 보여주려면 파이썬 파일 수정이 좀 필요할 듯 보이는데 금번 시연 과정에서 굳이 그럴 필요는 없는 것 같아서 그냥 안 되는채로 두려고 한다)

 

 

  이제 바이러스 토탈 사이트로 이동해 보자. “Choose File” 버튼을 클릭해 방금 전에 만든 exe 파일을 업로드해 보자. 분석이 시작되고 조금 후에 결과 화면이 나온다.

https://www.virustotal.com/gui/home

 

  기본적인 정적 분석 결과가 나오고, 판단 이유는 모르겠지만, 69개 중 4개의 바이러스 엔진이 이 파일이 좀 위험한 거 같다고 얘기했다고 한다. “BEHAVIOR” 탭을 클릭해 본다(처음 올렸다면 분석 후 결과가 표시되는데 조금 시간이 걸릴 수도 있다)

 

  그럼 해당 exe 프로그램이 생성 수정한 파일들 이라든지, 레지스트리 키, 프로세스, 사용한 DLL, 특이한 액션 등 여러 정보 들이 표시되게 된다. 이 파일이 뭔가 특별한 일은 하는게 아니고 레지스트리만 읽어 왔기 때문에, “Registry Actions” 파트 쪽을 보자. 그럼 두 번째 줄에 우리가 코드를 통해 접근한 엑셀 키가(HKCU\…\Excel\File MRU) 보이게 된다.

 

 

 그럼 앞의 테러범 얘기로 돌아가서 한번 더 역으로 악성코드 입장에서 생각해 보자. 내가 동적 검사를 당했을 때, 거기에 검문당하지 않고 순진한 파일로 인식되려면 어떻게 해야 할까? 가장 좋은 방법은 지금 감시당하고 있다는 것을 인지하고 발톱을 숨기는 것일 테고, 아주 단순한 방법은 적당히 단속이 끝날 시간까지 몸조심하면서 아무 일도 하지 않는 것일 것이다.

 

  파일을 올린 후를 생각해 보면 우리가 동적 분석 결과를 얻기까지 그렇게 오랜 시간이 걸리지 않았고, 그렇게 오랫동안 관찰하는 방식은 검사하는 프로그램의 ROI 나 파일이 실행되기를 기다리는 사용자의 불편을 초래하기 때문에 힘들듯 싶으니, 우리가 원하는 레리스트리 키를 읽는 동작을 하기 전에 일정 시간 쉬었다 읽게 됨 동적 검사를 빠져나갈 수 있지 않을까 싶다.

 

  그럼 그 가설을 한번 체크해 보기 위해서 파이썬 코드를 조금 수정하여, 1분 동안 잠시 쉰 후(sleep), 동작을 하게 하도록 만들어 보자. show_excel_recently_sleep_1m.py 이라는 새 이름으로 저장해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import winreg
import time
 
# 10초를 쉰다.
time.sleep(60
 
# 키를 정의 한다.
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

[show_excel_recently_sleep_1m.py]

 

  이후 다시 exe 파일을 만들어 본다. show_excel_recently_sleep_1m.exe 라는 이전 파일과 하는 동작은 같지만 1분 정도 멈췄다 레지스트리 키를 읽는 파일을 만들어 본다.

c:\Python\code>pyinstaller --onefile -w show_excel_recently_sleep_1m.py

 

  같은 방식으로 바이러스 토탈 사이트에 해당 exe 파일을 업로드 후 결과를 보면, 우리가 기대했던 바와 같이 레지스트리의 엑셀 정보를 접근한다는 정보는 보이지 않는다. 이렇듯 검사하는 쪽은 시간에 쫒길테지만 공격하는 쪽은 1분후에 공격한다고 뭐 특별히 문제가 있지 않는 상황이 된다. 그래서 공격하는 쪽이 압도적으로 회피를 하기엔 유리하다고 본다.

 

 

 

6. 마무리 하면서

  생각하면 할수록 악성코드에 대한 전쟁은 방어하는 쪽이 압도적으로 불리한 영역 같다는 생각이 든다. 그래서 자동화 파트에서도 비슷한 얘기를 했었지만 이제는 작은 개인은 백신과 같은 지식 집약적 영역에서 빛을 발하기는 힘들다. 백신 프레임워크를 개발하는 것에 대한 실력의 문제가 아니라 앞서 얘기한 방대한 데이터들과, 악성 행동들에 대한 정의의 모호함, 순진한 프로그램과 유사하게 보이는 사회공학적, 현실적 측면(점유율 높은 PC방 프로그램 회사를 인수해 악성코드를 심으리라고 누가 쉽게 생각하겠는가?)을 추가해 점점 정교해지는 공격 패턴 등에 대한 집단 지성에 가까운 다양성 및 양적 범위를 커버하기 힘들 것 같기 때문이다. 어찌 보면 기술과 돈과 전략을 겸비한 거대한 단체들의 물량과 물량의 싸움으로도 보인다.

 

  악성코드에 대한 안전함은 현실적으로는 OS 개발 사의 취약점 패치와, 잘 알려진 공격 패턴들에 대해 빠르고 정확하게 방어하는 백신, OS 단에서 악성코드와 정상코드의 행동 구분을 원활하게 만드는 보안 설계 등에 의존할 수밖에 없는 것 같다. 그래서 OS 나 프로그램 패치를 열심히 하고, 백신 패턴을 최신으로 유지하며, 불법적인 무료 이익을 주는 프로그램이나 사이트를 이용하지 말고, 랜섬웨어 같은 단순하지만 강력한 무기들을 대비해 백업을 안전하게 해놓는 것이 개인으로선 최선인 것 같다(여러가지 예외 상황 때문에 백신만으로는 100% 못 막을 것이라는 것을 가정하는 게 더 현명한 방어 전략일 것 같다). 방어를 하는 입장에서의 여러 기술들의 최적화와 자동화에 대해서는 우리가 모르는 각 보안 회사들만의 여러 노하우들이 많이 있을 것 같고 말이다.

 

  소극적인 편법의 측면으로는 공격자가 가치를 모르게 데이터를 잘 위장해 놓는 것도 하나의 방법이 될 것 같기도 하다. 공격자 입장에서는 은폐와, ROI를 위해 최소한의 타겟팅 공격을 하는 편이고, 한편으로는 어차피 속고 속이는 싸움 같아 보이기 때문이기 말이다. 방어자 또한 꼭 정직하게 행동해야 할 필요는 없어 보인다. 뭐 하지만 위장 방법의 깊이가 깊지 않다면, 또는 위장이라는 것은 zip 파일의 복잡한 패스워드처럼 (현실적으로) 완벽한 노출에 대한 대응 수단은 아니기 때문에, 수법이 드러나는 순간 당한지도 모르고 당하게 되는 리스크가 있기 때문에 기본적인 정책에 추가해 상대를 어렵게 만드는 보조적인 수단으로만 생각해야 할 듯도 싶다. 원숭이도 나무에서 떨어지는 법 이니까 말이다. 자기확신만큼 무서운 건 없다.

 

 

  또한 악성 코드에 대한 자동이나 수동 판단에 대한 모호성 문제는 사실 보안의 많은 분야에서 마찬가지로 발생한다. 그래서 이상적인 관점만큼 100% 안전한 것은 보안 분야에 걸쳐 사실상 드문거 같다. 그래서 이쪽 분야에 대해 많이 알게 될수록 그 한계 또한 알게 되며, 겸손하게 되는게 일반적인 성장 패턴일것 같다.

 

 

 

2020.5.10 by 자유로운설탕
cs

 

 

 

 

 

 

posted by 자유로운설탕
2020. 5. 4. 19:29 Japan Pop

  중국어를 공부하다 일본어 학원을 다시 잠시 다녔을 때 만난 고등학생 애한테 요즘 듣는 가수들 추천을 해달랬더니, 정리해준 가수들이 모두 락 쪽이였다. 락은 나한테 조금 힘든데 하는 맘은 있었지만, 정리해 준 것에 대한 고마움과 예의 때문에 유투브를 통해 하나하나 가수를 찾아 들어봤다(요즘은 사람들이 좋아하는 취향들에 대해서는 내가 이해를 못하더라도 나름 시간을 들여 이해할수 있다면 그들이 좋아하게 되는 특별한 매력이 있겠지 하는 생각이 든다). 그 중 조금 맘에 드는 노래가 있어서 마침 코인 노래방에도 있고 해서 도전. 막상 불러보니 들을 때 잔잔한 느낌과는 다르게, 후렴에서 뭔가 가볍게 지르는 느낌에 부르는 재미도 있었다. 그래서 곡이 영화의 OST 인것 같아 좀 더 노래의 느낌을 이해하고 싶어서 영화를 찾아 보게 됬다.

 

ぼくは明日、昨日のきみとデートする(나는 내일 어제의 너와 만난다)

https://namu.wiki/w/%EB%82%98%EB%8A%94%20%EB%82%B4%EC%9D%BC%2C%20%EC%96%B4%EC%A0%9C%EC%9D%98%20%EB%84%88%EC%99%80%20%EB%A7%8C%EB%82%9C%EB%8B%A4(%EC%98%81%ED%99%94)

 

  영화는 라이트 노벨을 원작으로 한 거라 해서 그닥 거슬리진 않는 약간의 판타지가 섞여 있다. 다만 조금 일본 드라마, 영화 특유의 미약한 사람의 손이 닿지 않는 커다란 운명의 열려있는 결말로 끝나서, 보고나면 마음 한쪽이 시큰거리는 허무함이 있다. 아마 왠지 이 노래는 영화를 보고 만든건 아니였을 것도 같은데, 가사랑 영화의 내용이 은근 잘 매치가 된다. 다만 영화쪽이 좀 더 깊고 섬세한 감정이라 노래가 좋다면 영화도 봐보는 것을 추천 한다. 아는 지인은 반대로 영화를 보고 엔딩 부분에 나오는 이 노래가 좋아 찾아봤다는 경우도 있다. 코인 노래방을 좋아하는 사람이라면 코로나 끝나면 꼭 가서 도전해 보기를(요즘 지역 경제엔 미안하지만 주변의 안전을 위해 코인 노래방을 몇달째 못 가서 금단 현상이 가득하다)...

 

  중국 노래는 성조 때문에 노래로 언어를 배우는 것에 찬반이 있는 것 같다. 하지만 개인적으로는 아무거라도 해당 언어의 교과서 공부 틀을 벗어나는 경험을 하는 것은 나쁘진 않은거 같다. 노래 안에서도 성조가 들린다는 것은 아직 이해가 잘 안가지만 말이다. 경험상 노래엔 필요없어 보여서 성조를 같이 안외고 발음만 외어버리면 나중에 엉터리로 각인되버린 성조들을 다시 교정하느라 돌 엄청 맞게되니 모르는 한자가 많이 나오고 노래속에선 성조를 표현을 못하더라도 성조를 같이 외자. 다른 한편으로는 회화는 형편없는 편이라 해당 가설이 맞나 싶기도 하다.

 

  하지만 일본어의 경우는 노래를 보다보면 동사의 변형, 원형 등도 반복해 유추하게 되고 교과서엔 안 나오는 단어나 어휘도 많이 보게되서 장점이 더 많은 듯 싶다. 또 빠른 노래를 연습 하다보면 한자 읽기에 대한 순발력이 길러지지 않나도 싶다. 노래할 때 도움을 주는 히라가나로 된 한자 발음이 오히려 귀찮게 눈에 자꾸 거슬리게 된다면 나름 성공이다. 중국 노래와 마찬가지로 나름 해석 해보고 애매하거나 자신 없는 부분은 지인한테 물어서 보충했다. 다른 일도 마찬가지 겠지만 언어를 공부할 때 편하게 물어볼 수 있고, 귀찮아 하지 않고 알려주고 싶어하는 사람이 있는 건 정말 좋은일 같다.

 

 

 

さよならが喉の つっかえてしまって

안녕이라는 말이 목 안에 걸려 버리고,

咳をするみたいに ありがとうって言ったの

기침을 하는 것처럼 고맙다고 말했어

次の言葉はどこかと ポケットを探しても

다음엔 어떤 말을 할까 하며 주머니를 뒤져봐도

見つかるのはあなたを好きな私だけ

찾을 수 있는 건 너를 좋아하는 나 자신 밖에

 

よ大丈夫だよ

아무렇지 않아, 괜찮아

優しくなれたと思って 願いにわって最後はになって

상냥하게 되었다고 생각하고, 소원으로 변해, 마지막엔 거짓말이 되어

(뭔가 이별을 예고되 맘이 아프지만 상대방이 알아채지 못하게 친절하고, 다정하게 대하려고 노력했지만 결국엔 모든 게 현실의 무게에 치여 거짓말이 되어버렸다 정도로 이해하면 어떨까 싶다)

いまま枯れてゆく あなたを好きなままで消えてゆく
푸르른 채로 시들어 가, 당신을 좋아하는 채로 사라져 가

(이게 정말 푸르다는 건지 아직 다 익지 못한 설익은 단계라는 건지는 알송달송)

私みたいと手に取って

나 처럼 손에 쥐고

にあった想いと一に握り潰したの

안에 있던 마음과 함께 쥐어 부셔 버렸어
(위의 두 줄은 어떤 이미지를 표현하는 가사인지 잘 모르겠음)

大丈夫 大丈夫

괜찮아 괜찮아

今すぐに抱きしめて

지금 바로 껴안고 싶은데

私がいれば何もいらないと それだけ言ってキスをして

내가 있으면 아무 것도 필요 없다고 그렇게 말하며 키스를 해줘

なんてね だよ ごめんね

농담이야 거짓말이거든 미안해

 

こんな時思い出す事じゃ ないとは思うんだけど

이런 때에 생각이 날 일은 아니라고 생각하지만

一人にしないよって あれは嬉しかったよ

혼자 있지 않아도 되서 그거 사실은 기뻤어

(しないよって 해석이 애매함)

あなたが勇を出して 初めて電話をくれた

당신이 용기를 내서 처음으로 전화해 줬던

あの夜の私と何が違うんだろう

그 밤의 나와는 무엇이 다른 것  일까?

どれだけ離れていても  どんなにえなくても

얼마나 멀리 떨어져 있더라도, 아무리 못 만나게 되더라도

持ちがわらないから ここにいるのに

마음이 변하지 않기 때문에 여기에 있는 거니까

いまま枯れてゆく あなたを好きなままで消えてゆく

푸르른 채로 시들어 가, 당신을 좋아하는 채로 사라져 가

私をずっとえていて

나를 계속 기억해 줘

なんてね だよでいてね ああ

농담이야 거짓말이거든 건강하게 잘 지내줘

ララララ ララララ

ララララ ララララ

 

泣かない私に少し ほっとした顔のあなた

울지 않는 나에게 조금은 안심한 얼굴의 너

わらず暢 そこも大好きよ

여전히 느긋한 거 같아 그 것도 너무 좋아

が付けばにいて 別に君のままでいいのになんて

맘에 걸린 다면 옆에 있어줘, 그냥 당신 그대로 괜찮으니까

勝手に 拭いたくせに

제멋대로 눈물을 훔쳤던 주제에

見える全部こえる全て 色付けたくせに

보이는 것 전부, 들리는 것 전부 색칠해 버린 주제에

(아마 실제와는 다르게 상대방의 맘이 편하게 괴롭지 않은 척 연극했다는 것 같음)

いまま枯れてゆく あなたを好きなままで消えてゆく
푸르른 채로 시들어 가, 당신을 좋아하는 채로 사라져 가

私みたいと手に取って

나 처럼 손에 쥐고

にあった想いと一に握り潰したの

안에 있던 마음과 함께 쥐어 부셔 버렸어

大丈夫 大丈夫

괜찮아 괜찮아

今すぐに抱きしめて

지금 바로 안고 싶은데

私がいれば何もいらないと そう言ってもう離さないで

내가 있으면 아무 것도 필요 없다고 그렇게 말하며 더 이상 떠나지 말아줘

なんてね だよ さよなら

농담이야 거짓말이거든 그럼 안녕

 

 

 

 

[서비스] 한자 발음 및 원형

(のど), (おく), (せき), 言う(いう)

(つぎ), 言葉(ことば), 探す(さがす), 見つかる(みつかる), 好き(すき), (わたし)

 

(へいき), 大丈夫(だいじょうぶ)

優しい(やさしい), 思う(おもう), 願い(ねがい), わる(かわる), 最後(さいご), (うそ)

(あおい), 枯れる(かれる), 消える(きえる), (), 取る(とる)

想い(おもい), (いっしょ), 握り潰す(にぎりつぶす)

 

(いま), 抱きしめる(だきしめる), (なに), (うそ)

 

(とき), 思い出す(おもいだす), (こと), 一人(ひとり), (じつ), 嬉しい(うれしい)

(ゆうき), 初め(はじめ), 電話(でんわ), (よる), 違う(ちがう)

 

離れる(はなれる). (あう), (きもち), える(おぼえる), (げんき)

 

泣く(なく), 少し(すこし), (かお), わらず(あいかわらず), (のんき), 大好き(だいすき)

(よこ), 別に(別に), (きみ), 勝手(かって), (なみだ) 拭く(ふく)

全部(ぜんぶ), (きく), 全て(すべて), 色付ける(いろづける), 離す(はなす)

 

posted by 자유로운설탕
2020. 4. 18. 23:34 China Pop

  역사적인 관계가 얽혀있는 다른 나라의 언어를 배운다는 것은 종종 묘한 부분이 있다. 특히 두 나라 사이의 감정이 격해 질때면, 관련된 언어를 공부하고, 해당 언어로 만들어진 여러 문화적인 부분을 경험하고 노출하는 것 조차 괜히 눈치가 보일때가 있다(개인적으로는 코인 노래방에서 중국-일본 노래 부르기, 지하철에 앉아 해당 언어 펼치고 공부하기 등). 뭐 살아가면서 역사적 관계나 이익의 충돌이 전혀 없는 나라의 언어를 배우게 될 일이 있을지는 모르겠지만, 하나의 언어를 배우면서 그 나라 사람들을 만나게 되면 추상적으로 생각해 오던 해당 나라가 구체적인 모습으로 다가 오게 되는 측면도 있는 것 같다.

 

  이 노래는 강사분이 추천해 줬던 노래로 이미 여러 블로그에서 소개되어 있지만, 처음으로 스스로 번역해서(이상한 곳 체크를 받고) 올렸다는 데에 의미가 있을 듯 싶다^^. 워낙 노랫말처럼 가랑비에 옷 젖듯 천천히 공부해온 탓에 그렇긴 하지만, 노래 가사는 시와 비슷하게 생략된 문장 형태인 경우가 많고, 중국어 특유의 함축적이고 중의적인 한자의 느낌 때문에, 원래 쓴 사람의 의도를 잘 파악하기 어려운 때가 많은 듯 싶다. 뭐 그것은 이 글의 해석의 어딘가가 매끄럽지 않은데에 대한 변명이기도 하다--;

 

  개인적으로 중국은 고전적인 가치관이 여전히 사회의 많은 부분을 감싸고 있는 것으로 보인다. 현재의 시대를 살아감에 있어 고전적이라는 것은 구닥다리 느낌의 부정적인 측면도 있지만, 나아가는 것이 꼭 좋아지는 것만은 아니라는 측면에서는 긍정적인 부분도 동시에 존재하는 것같다. 물론 한 개인이 사회에서 둘 사이에 적당한 균형을 이루고 살긴 많이 어려운 것 같지만 말이다. 이 노래는 중국 사회의 고전적인 감정에 담긴 잔잔한 우아함을 한자에 담아 표현하고 있다고 생각한다. 참고로 한국 노래방 기계에는 없는 듯해서 많이 아쉽다^^~

 

 

 

 

书里总爱写到喜出望外的傍晚
Shūlǐ zǒng àixiědào xǐchūwàngwài de bàngwǎn

책속에는 항상 뜻밖의 기쁨을 주는 저녁 무렵의 풍경이 담겨 있죠.

 

骑的单车还有他和她的对谈
de dānchē háiyǒu tā hé tā de duìtán
자전거를 타거나 그와 그녀가 이야기를 나누는 모습

(책 속에 담긴 여러 사랑 이야기 들의 장면을 떠올리는 듯...)

 

女孩的白色衣裳男孩爱看她穿
hái de báishang nánhái àikàn tā chuān

하얀 옷을 입은 여자아이를 바라 보는 것을 좋아하는 남자아


好多桥段    好多都浪漫
hǎoduō qiáoduàn   hǎoduō dōu làngmàn
얼마나 많은 일들이,  얼마나 많은 낭만이

(桥段 은 보통 영화 연극 등의 scene 같은 것을 말하는 듯. 특정한 갈등이 있는 상황이나 계기 등? 적당한 표현할 단어가 생각 안나 함축적으로? "일"이라고 해석. 연인들 끼리의 친해지는 과정이나, 다툼에 대한 에피소드라고 보면 어떨까 싶다)

 

好多人心酸    好聚好散
hǎoduō rén xīnsuān    hǎo jù hǎo sàn
얼마나 많은 사람이 가슴이 아프고, 모였다 흩어졌을까요

 

好多天都看不完
hǎoduō tiān dū kànwán

여러 날이 지나도 다 못볼 만큼 많죠

(위와 같은 많은 감정의 편린들이 있어서 여러 날을 봐도 못 볼만큼 많다는 의미)

 

 

 

 

刚才吻了你一下你也喜欢对吗
gāngcái wěn le nǐ yīxià nǐ yě xǐhuān duì ma
방금 내가 입맞췄을 때 당신도 좋았던 맞죠? 

 

不然怎么一直牵我的手不放
rán zěnme yīzhí qiān wǒ de shǒu bù fàng

그렇지 않다면 내손을 왜 줄곧 잡고 있겠어요.

 

你说你好想带我回去你的家乡
nǐ shuō nǐ hǎoxiǎng dài wǒ huíqù nǐ de jiāxiāng
당신은 나를 고향으로 데리고 가고 싶다 말했죠

 

绿瓦红砖    柳树和青苔

wǎ hóng zhuān    liǔshù hé qīng tái
녹색기와와 붉은 벽돌, 버드나무와 푸른 이끼

 

过去和现在    都一个样
guòqù hé xiànzài    dōu yī gè yàng
과거와 현재, 모두 한결같죠 

 

你说你也会这样
nǐ shuō nǐ yě huì zhè yàng

당신은 당신도 그렇게 변하지 않을거라고 말했죠

 

 

 

 

慢慢喜欢你    慢慢的亲密
mànman xǐhuān nǐ    mànman de qīnmì
천천히 당신을 좋아하고, 천천히 가까워지고

 

慢慢聊自己    慢慢和你走在一起
mànman liáo zìjǐ    mànman hé nǐ zǒu zài yīqǐ
천천히 자신에 대해 얘기하고,  천천히 당신과 걸어가기

 

慢慢我想配合你    慢慢把我给你
mànman wǒ xiǎng pèihé nǐ   mànman bǎ wǒ gěi nǐ

천천히 당신과 어울리고, 천천히 당신에게 나를 주고 싶죠

 

慢慢喜欢你    慢慢的回忆
mànman xǐhuān nǐ    mànman de huíyì
천천히 당신을 좋아하고, 천천히 추억하고

 

慢慢的陪你慢慢的老去
mànman de péi nǐ mànman de lǎoqù

천천히 당신과 함께 있고, 천천히 나이를 먹어가기

 

因为慢慢是个最好的原因

yīnwèi mànman shì gè zuì hǎo de yuányīn

왜냐하면 느리다는 건 아주 좋은 이유 중 하나기 때문이죠

(原因 은 "원인"이라는 뜻이긴 하지만, "느리게" 무언가를 쌓아가는게 무언가 하나의 두 사람 사이의 완전한 사랑을 만들게 된다는 의미로 생각하면 어떨까 싶다. 같은 완전함 이래도 불 같은 사랑과 반대라고 할까? 그래서 사랑이 이루어지게 되는 "이유" 중 하나라고 옮김)  

 

 

 

晚餐后的甜点就点你喜欢的吧
wǎncān hòu de tiándiǎn jiù diǎn nǐ xǐhuān de ba
저녁 식사 후의 디저트는 당신이 좋아하는 걸로 주문해요

 

今晚就换你去床的右边睡吧
jīnwǎn jiù huàn nǐ qùchuáng de yòubiān shuì ba

오늘 밤엔 당신이 오른 쪽에서 잠을 자요

(아마 침대 오른쪽이 벽이고, 평소에는 바깥 쪽에서 남편이 자다가 뒤에 나오 듯 내일은 여행을 가는 일에 설레는 부인이 먼저 일어나서 준비하며 기다리기 위해서 바깥 쪽인 왼쪽에서 자겠다는 의미로 추측) 


这次旅行我还想去上次的沙滩
zhècì lǚxíng wǒ hái xiǎngqù shàngcì de shātān
이번 여행 때 지난번 갔던 그 해변에 다시 가보고 싶어요.

 

球鞋手表    袜子和衬衫都已经烫好    放行李箱
qiúxié shǒubiǎo    wàzi hé chènshān dōu yǐjīng tànghǎo    fàng xínglǐxiāng
운동화와 시계, 이미 다려놓은 양말과 셔츠는, 트렁크에 넣어놨어요.

早上等着你起床
zǎo shang děng zhe nǐ qǐ chuáng

아침에 당신이 일어나기만 기다리고 있어요.

 

 

 

[이 후는 반복]

慢慢喜欢你    慢慢的亲密
mànman xǐhuān nǐ    mànman de qīnmì
천천히 당신을 좋아하고, 천천히 가까워지고

 

慢慢聊自己    慢慢和你走在一起
mànman liáo zìjǐ    mànman hé nǐ zǒu zài yīqǐ
천천히 자신에 대해 얘기하고,  천천히 당신과 걸어가기

 

慢慢我想配合你    慢慢把我给你
mànman wǒ xiǎng pèihé nǐ   mànman bǎ wǒ gěi nǐ

천천히 당신과 어울리고, 천천히 당신에게 나를 주고 싶죠

 

慢慢喜欢你    慢慢的回忆
mànman xǐhuān nǐ    mànman de huíyì
천천히 당신을 좋아하고, 천천히 추억하고

 

慢慢的陪你慢慢的老去
mànman de péi nǐ mànman de lǎoqù

천천히 당신과 함께 있고, 천천히 나이를 먹어가기

 

因为慢慢是个最好的原因
yīnwèi mànman shì gè zuì hǎo de yuányīn

왜냐하면 느리다는 건 아주 좋은 이유 중 하나기 때문이죠

 

 

书里总爱写到喜出望外的傍晚
shū lǐ zǒng ài xiě dào xǐ chū wàng wài de bàng wǎn

책속에는 항상 뜻밖의 기쁨을 주는 저녁 무렵의 풍경이 담겨 있죠.

 

 

posted by 자유로운설탕
2019. 10. 6. 18:05 보안

  이번 시간에는 모니터링 이라는 주제에 대해서 얘기해 보려 한다. 사실 모니터링은 보안 뿐만 아니라 모든 IT 영역에서 관심을 가지는 부분이기도 하지만, 간단한 예제와 함께 보안에서의 모니터링이란 무엇일까에 대해서 가볍게 생각해 보려 한다.



[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

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

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

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

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터


 


 


1. 들어가면서

  흔히 물리적 보안을 소프트웨어 적인 보안 분야보다 명확하고 쉬운 분야로 취급하는 경향이 많은 것 같긴하지만, 두 개의 분야는 무척 유사하고 구분 하긴 힘든 연결 고리를 가지고 있다고 생각한다. 예로서 특정한 가게를 도난에 안전하게 지키려고 노력한다고 생각해 보자. 가장 기본적으로는 CCTV, 동작 감지기 등의 가게 주변의 환경의 변화를 모니터링 하고 알려주는 센서들을 설치 할텐데 왜 그러한 센서들을 설치하려 하는 걸까? 결국은 특정한 상황을 알려주는 데이터를 얻기 위해서 라고 볼수 있을 것이다.

 

  만약 CCTV 가 적절하지 않은 장소에 설치된다면 어떤 일이 벌어지게 될까? 잘못되거나 의미 없는 데이터를 가지고 판단(또는 모니터링) 하는 결과를 가지게 될 것 이다. 또한 현실적으로 사람이 잠시도 빼먹지 않고(또는 지치지 않고) 모니터링 하는 모든 데이터들을 100% 보고 있을 순 없기 때문에 여러가지 데이터에 여러 소프트웨어 적인 요소들을 적용하여 이상 현상을 찾으려고 한다. CCTV 를 예로 들면 화면의 변화, 화면안의 객체의 움직임, 해당 움직임의 의미 등을 프로그래밍(나아가 데이터의 통계나 구조적인 의미를 알려주는 머신러닝)을 통해 해석해서, 침해가 일어난 다고 의심되는 특정 이벤트 만을 알람으로 발생하게 하여 효율적인 모니터링을 하려고 한다. 이러한 관점을 생각하게 되면 물리적 모니터링은 소프트웨어 적인 모니터링과 문제의 성격으로 보아 별 차이는 없어 보이게 된다.

 

  또한 이러한 데이터 부분은 현실적인 행동이 있어야만 의미가 있을 수도 있다. 경험상 가게 자체적인 CCTV 의 구축이 효율이 적은 이유 중 하나는 누군가 계속 제대로 모니터링을 해야하며, 사건이 발생했을 때 조치할 방법이나 즉각적으로 행동할 사람이 없다면 효용성이 많이 떨어지기 때문이다(몇일 치의 CCTV 를 뒤지면서 원하는 장면을 찾아본 경험이 있다면 사후 조치란 의미로 원인을 찾기 것이 얼마나 피곤하게 만드는 일인지 알수 있을 것이다). 이러한 부분이 보호해야할 자산을 가진 사람들이 중앙 집중적인 관제 및 조치하는 사람이 있는 서비스를 제공하고 있는 **원 같은 보안 서비스 들을 사용하는 이유라고 생각된다. 해당 서비스에서 이상적으로 영상 및 이벤트 데이터 들은 실시간으로 원격에 저장되어 안전하게 보존, 백업 되며, 각 이벤트 들은 관제사 및 프로그램들에 의해서 체크 되며, 문제가 있을 시 물리적 조치를 행사할 수 있는 인원들이 움직이기 때문일 것 같다. 물론 실제로는 상징적인 효과일 수도 있고 비용이나 프라이버시 문제가 있을 수도 있을 것이다.

 

 

 

 

2. 결국 중요한 것은 데이터

  이 글의 맨 처음에서 보안의 주요한 부분중 하나는 데이터의 흐름을 따라다니는 일 이라고 얘기 했었는데, 모니터링도 크게 그 범위를 벗어나지는 않다고 본다. 앞의 리버싱과 포렌식 글에서 컴퓨터는 0과 1로 이루어진 세계로 얘기 했는데, 점점 시간이 지날 수록 현실의 많은 데이터는 이 0과 1로 이루어진 형태로 등가적으로 변형되어(디지털 화) 컴퓨터 안에 들어가고 있다.

 

 

  CCTV 의 영상도, 다른 여러 센서의 데이터도, 사람들의 행동들도 모두 디지털화 됨으로서, 어떻게 보면 현실의 많은 부분들이 이젠 컴퓨터내의 문제로 등가적으로 치환되었다고 볼수 있을 듯하다. 그렇게 컴퓨터 내의 데이터 문제가 되어, 그 동안 사람들이 고안해낸 여러 컴퓨터 내의 데이터 문제를 해결하는 기법들을 사용할 수 있게 됬지만, 그렇게 됨으로서 몇 가지의 새로운 차원의 문제도 발생하게 되었다고 본다.

 

 

  첫째로 정확하게 현실의 특징을 충분히 반영하여 디지털로 변환 되었냐는 문제가 있다. 예를 들어 예전의 해상도가 낮았던 시절의 CCTV는 나중에 해당 데이터를 가지고 100% 정확한 판단을 하기 힘든 문제가 있었다. CCTV 위치가 잘못 되어 태양 빛이 너무 밝게 들어와 영상을 제대로 인지 못했을 수도 있고, 센서가 고장날 수도 있으며, 센서의 설계가 처음부터 적합치 않았을 수도 있다.

 

  이건 소프트웨어 보안 쪽에서도 마찬가지 라고 보는데, 우리는 모든 데이터가 로그나 데이터베이스의 형태로 쌓였다고 생각하지만, 실제 그 데이터를 일으키게 한 대상은 컴퓨터 바깥 세상의 존재일 가능성이 높다. 그럼 그 데이터를 만들어낸 대상이 가진 특징을 올 해당 데이터를 기준으로 올바르게 판단 가능할 정도로 정확히 가지고 있는가를 우선적으로 따져봐야 한다. 영화에서 나오는 CCTV 나 센서를 보고 있는 감시 요원들이 주인공을 놓치게 되는 이유는 이 판단을 위한 원천 데이터 자체가 왜곡되는 경우라고 볼수 있을 것 같다. 

 

  혹은 아예 처음 부터 소프트웨어에서 자체에서 생겨난 데이터일 수도 있겠지만 그 경우도 우리가 최종 모니터링에 사용할 기반 데이터를 제대로 만들어낸 것인 지에 대해서 항상 여러 측면에서 고민을 해봐야 하는것 같다. 그래서 데이터를 만들어낸 도메인을 제대로 이해한 상태에서, 데이터의 수집부터 판단에 필요한 데이터를 만들어 내고, 위의 소프트웨어 적인 해결 도구들을 적절히 사용했는지를 잘 검토해야 하는 것 같다.

 

 

  둘째로 첫번째와 비슷하지만 우리가 현재 데이터라고 믿고있는 기준이 실제 우리가 원하는 현상을 모니터링 하기에 충분하냐는 문제가 있다. 세상이란게 실제로 많은 부분 근사와 추정으로 이루어져 있긴 한듯 하지만, 가능한 현재 모니터링 하고 싶은 부분에 대해 적절한 이벤트를 만들어 낼 수 있는 데이터를 수집하고 있는지는 체크해 봐야 한다.

 

  보안이나 QA가 좀 그런 면이 있긴 하지만, 시스템을 움직이는 데이터와 시스템을 모니터링 하는 데이터는 겹칠때도 많지만 별개일 수도 있다. 시스템의 여러 동작 간에 모니터링을 위한 데이터를 일부러 쌓아야 하는 경우도 있고, 뒤에서 천천히 집계하여 효율성 있게 모니터링 하기 위한 기반 데이터를 만들어야 하는 경우도 많아 보인다. 이러한 부분은 저장 비용, 퍼포먼스와도 밀접히 연관되어 있는 경우도 많으므로, 뻔한 얘기긴 하지만 새로운 시스템이 만들어지고 적절한 보안적인 모니터링이 필요하다면 설계 단계 부터 여러 측면에서 설계나 예산 측면에서 고려를 해야 하는 부분 같다. 일단 시스템이 본 궤도에서 돌아가게 되면 모니터링을 끼워넣기는 엄청 힘들어지는 것 같다.

 

 

  셋째로 과거의 데이터를 기반으로 만들어낸 규칙이 새로운 데이터에 얼마나 적합한지에 대한 부분이다. 머신러닝에서도 학습된 모델이 현재 얼마나 유효한 지에 대해 항상 고민을 하긴 하지만, 굳이 그렇게 복잡한 상황이 아니더라도 기본적인 모니터링 판단에서도 마찬가지다.

 

  만약 CCTV 를 열심히 설치해 놨는데 새로운 문이 생기면 어떻게 될까? 새로운 권한을 가진 사람들이 같이 근무하게 될수도 있을 것이다. 회사의 근무시간이 고정된 시간에서 자율 출퇴근 제로 바뀌어도 마찬가지 일것이다. 또한 재택근무가 된다면 등등... 현재의 고정된 판단을 가졌던 데이터의 규칙이 언제라도 특정 시점에 바뀔 수 있을 것이다. 기존 모니터링 시스템에서 받아들이 데이터 들의 그런 변화를 적절히 2차 모니터링 하여 데이터의 대상 및 룰에 대한 변화를 알려 줄수 있는 부분도 필요할 수 있다. 물론 그렇게 변할 가능성이 있는 데이터를 판단의 기준으로 삼지 않는 접근 방법도 있지만, 당장 효과가 있는 판단 기준들을 쉽게 빼는 것은 쉽지 않은 일이다. 애시당초 부터 적절한 판단을 할 수 없을 지도 모르고 말이다.

 

   여러 현실의 트랜드나 구조의 변화 때문에 생성되는 데이터의 성격이 충분히 변할 수 있기 때문에 이러한 변화를 모니터링 하는 부분도 역설적으로 모니터링을 구축하는데 또 다른 숙제가 되는 듯하다. 

 

 

  넷째로는 데이터가 조작에 얼마나 취약하느냐를 따져봐야 한다. 보안 쪽 모니터링을 하면서 데이터가 항상 주어진 그대로의 특성을 유지할 것이라고 믿는 것은 피해야할 시각이다. 앞에서 얘기한 인젝션 같은 문제로 기대하지 않은 데이터가 들어와 순진한 프로그램을 악용하는 것처럼, 순진한 모니터링 프로그램은 조작된 데이터를 그대로 놓쳐버릴 수 있다. 특히 외부의 움직임에 의해 생성되어 들어오는 데이터를 기반으로 모니터링을 할때는 항상 이런 부분을 더 신경써야 하는 것 같다. 

 

 

  다섯째로 만들어진 모니터링 프로그램은 대부분 명확한 답이 없이 "임계점"을 기준으로 Alert 을 띄우는 경우가 많으므로, 해당 임계점을 어떻게 조정하느냐도 민감한 주제가 될것 이다. 좀더 확실히 놓치지 않기위해 임계점을 낮추게 되면 수많은 Alert 의 늪에 빠지게 될것이고, 운영의 효율을 위해서 높이게 되면 삶의 질은 나아지겠지만 문제를 놓치게 될 가능성이 높아지기 때문이다. 

 

  마지막으로 결국은 모니터링은 사람의 문제로 귀결된다는 거다. 그건 모니터링 하는 입장이나 해당 처리를 하는 입장에 모두 해당 된다. 아무리 잘 만들어진 시스템이 많이 이벤트와 좋은 대시보드 화면을 보여준다고 해도 결국 최종적으로 오탐인지, 위험이 있는지 판단하는 부분은 사람일 수 밖에 없다. 또한 많은 부분에서 주어진 데이터에 대해 알고있는 도메인 지식을 기반으로 해석해야만 정상적인 판단을 할 수 있는 경우도 많다. 

 

  사람이란게 필연적으로 먹고 자는 존재이며, 기분에 따라 컨디션이 많이 달라질 수 있는 존재이기 때문에, 그러한 부분에 따른 판단 오차 및 대응 지연을 최소화 시킬 수 있는 시스템 및 모니터링 리소스의 배치도 마찬가지로 필요한 듯 싶다. 시스템 적인 입장만 생각하자면 최종 결과를 메일, SMS, 메신저 등의 연락처로 보내고 관련된 데이터와 그래프를 보여주면 된다고 생각할 수 있지만 그 메시지와 데이터를 보고 판단하는 사람들이 처한 환경을 간과 해서는 안된다. 물론 그러한 판단을 모니터링 하는 사람들이 힘을 덜 들이면서도 정확하고, 오차없게 하게 하는 여러 노력들은 필요하겠지만 말이다.

 

  때로는 100% 자동으로 이루어지는 선처리도 있긴 할테지만(예를 들어 과열시의 차단), 어쨋거나 해당 원인을 밝혀 개선하거나, 적절한 차단인지 되돌아 검토하거나, 룰을 수정하는 등의 일은 사람의 판단이 결국 필요하게 된다. 그래서 모니터링은 어찌보면 데이터의 생성 방식부터 최종 판단 까지 전체를 물리적, 디지털 적으로 잘 케어해야 하는 종합적인 분야같다.  

 

 

 

 

3. 간단한 모니터링 예제 보기

  어떤 예제를 보여줄까 고민하다가, 결국은 데이터의 한 측면을 보고 판단을 하는 예제를 간단히 보여주는 것으로 결정했다.

 

  우선 모니터링 대상은 파이썬으로 웹페이지를 하나 만들며 해당 페이지는 호출 할때마다 1부터 하나씩 숫자가 증가하면서 해당 숫자를 <td> 태그 안에 넣어 보여준다. 다음은 모니터링을 하는 프로그램인데, 해당 페이지를 1초 마다 한번 호출 하면서 5가 발견되면 SQLite 디비에 해당 결과를 저장 후, 화면에 읽어 표시한다.

 

 

3.1 모니터링 대상 페이지 만들기

  우선 숫자를 표시해 주는 flask 웹 페이지를 보자. 내용을 보면 global 변수를 이용하여 계속 0부터 1을 증가 시키면서 monitoring.html 을 랜더링 하면서 해당 값을 웹페이지에 표시해준다(코드가 이해 안가는 경우는 파이썬 플라스크 편을 보고 오자).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, make_response, render_template
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
num_count = 1
 
@app.route("/monitoring", methods=['GET'])
def xss():
    global num_count 
    count_string = str(num_count)
    num_count = num_count + 1
    return render_template('monitoring.html', count = count_string)
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

 

   해당 파일을 c:\python\code 폴더에 "flask_monitoring.py" 이름으로 "UTF-8" 포맷으로 저장한다.

 

 

  다음은 해당되는 템플릿 파일을 보자. 정말 간단하게 <td> 태그 하나만 하나 있다. class 이름으로 check_point 를 지정한 이유는 나중에 beautifulsoup 라이브러리로 가져올때 td 태그가 여러개 있을 때 기준을 가지고 쉽게 가져오기 위해서이다.

1
<td class="check_point">{{ count|safe }}</td>
cs

 

   c:\python\code\templates 폴더에 "monitoring.html" 이름으로 "UTF-8" 포맷으로 저장한다.

 

 

  이제 커맨드 창에서 실행 시켜 페이지가 잘 동작하나 본다.

c:\Python\code>python flask_monitoring.py
....
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  브라우저로 웹페이지를 띄워 리프레시를 해보면 호출할 때마다 1씩 숫자가 증가됨을 볼 수 있다.

http://localhost:5000/monitoring

 

 

 

3.2 모니터링 대상 페이지 만들기

  이제 해당 페이지를 모니터링 하는 프로그램을 만들어 보자. 해당 프로그램은 http://localhost:5000/monitoring 페이지를 30번까지 호출 하며, class 가 check_point 인 <td> 태그의 내용을 가져와 5를 발견하게 되면 로컬 SQLite 디비에 이벤트와 시간을 저장하고 for 루프를 먼춘 후, 해당 디비에 저장한 내용을 읽어와 화면에 표시한다.  

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
import sqlite3
import requests
from bs4 import BeautifulSoup
import datetime
import time
 
 
# sqlite 파일을 열음
conn = sqlite3.connect("monitoring.db", isolation_level=None)
cursor = conn.cursor()
 
# BookInfo 테이블을 생성한다.
cursor.execute("""CREATE TABLE IF NOT EXISTS SiteInfo(checkDate timestamp, checkNum text, result text)""")
 
# url 요청할 세션 만들기
= requests.session()
 
# URL 만들기
searchurl = 'http://127.0.0.1:5000/monitoring'
 
# 30번 루프를 돈다. 
for i in range(30):
    # URL 호출
    con = s.get(searchurl)
 
    # html 파서 사용
    soup = BeautifulSoup(con.text, 'html.parser')
 
    # 숫자가 들어있는 태그 가져오와 표시해 보기
    check_num = soup.find("td", class_="check_point")
    print(check_num.string)
 
    # 5가 발견 됬다면
    if check_num.string == "5":
 
        # 발견했다 출력하고 테이블에 저장하기
        print("find it: " + check_num.string)
 
        sql = "INSERT into SiteInfo(checkDate, checkNum, result) values (?, ?, ?)"
        cursor.execute(sql, (datetime.datetime.now(), check_num.string, "find it"))
        break
    else:
        # 못 찼았다 출력 하기
        print("no event")
    
    # 1 초 쉰다
    time.sleep(1)
 
# 테이블에 저장한 값 불러오기
sql = "select checkDate, checkNum, result from SiteInfo"
 
cursor.execute(sql)
row = cursor.fetchone()
 
if row: 
   while row:
      print(row)
      row = cursor.fetchone()
cs

 

  c:\python\code 폴더에 "flask_monitoring.py" 이름으로 "UTF-8" 포맷으로 저장 후 커맨드 창에서 실행해 보자(아래와 같이 1부터 다시 나오게 하려면 앞의 웹 서버 실행을 취소했다 다시 시작하면 된다)

 

c:\Python\code>python monitoring_job.py
1
no event
2
no event
3
no event
4
no event
5
find it: 5
('2019-10-13 20:48:09.088506', '5', 'find it')

 

 

 

 

4. 마무리 하면서

  사실 예제처럼 웹 호출을 기반으로 데이터를 가져와 모니터링 하는 것은 마이너한 경우일 테지만, 꼭 가져오는 방식이 중요한 것은 아니다. 웹에서든, 디비든, 장비든, 로그 파일이든 결국은 파이썬 1교시의 프로그래밍의 입력과 같이 형태만 다를뿐 결국 파싱해 가져오고 싶어하는 건 최종적으로 데이터라는 데엔 변함이 없다. 모니터링은 과감히 간략화 하자면 컴퓨터 내에 적재한 여러 데이터를 기반으로 적절한 체크 포인트를 찾아서, 특정한 데이터의 변화가 일어났을때 알려주며, 종종 그래프 같은 시각적인 형태로 모니터링하는 사람에게 데이터의 추이를 설명해주는 단순한 작업이다.

 

 

  생각보다 모니터링 업무를 하는 사람에게는 지루하거나 스트레스를 받는 일이며, 발생하는 이벤트를 수동적으로 계속 따라가게 됨으로서, 그다지 생산적이지도 않게 느껴진다. 일적으로도 대상을 지키는 업무라는 인식이 강하기 때문에 하는 일의 중요도에 비해 많은 인정을 받지도 못하는 경향도 있다. 때로는 자신의 메인 업무에 부록처럼 따라오는 시간을 조금씩 갏아가는 귀찮은 일일 수도 있다. 또한 운영 업무의 대부분은 이런 모니터링과 일부 또는 전부 연관되어 있다.

 

  반면에 모니터링을 정확히 잘하려면 대상 데이터를 정확히 이해해야 하며, 데이터를 정확하게 이해하기 위해서 데이터를 만들어내는 전반적인 도메인과 관련된 시스템, 사물, 사람의 행동을 이해해야 하기 때문에, 깊이 들어가고자 하면 생각보다 복잡한 일이기도 하다(예를 들어 SQL 을 모니터링 하려면 기본적인 SQL 문법과 사용자 들이 사용하는 패턴의 이해, 해당 SQL의 대상이 되는 서버, 디비, 테이블, 컬럼의 성격을 이해해야만 한다). 뭐 하지만 이런 부분은 IT를 포함한 어떤 분야의 일이나 마찬가지인듯 하다. 다들 익숙하게됨 지루하게 보자면 무척 지루한 일일 수도 있지만, 의미를 가지고 데이터의 원천에 관심을 가진다면 다른 차원의 일로도 볼수도 있다. 스스로 모니터링 시스템을 설계하거나 개선하는 업무의 롤이라면 더 더욱 그럴 것이고 말이다.

 

  모니터링 업무를 도메인의 데이터를 이해하고 적절하게 데이터를 해석할수 있는 툴을 적용하는 일이라고 정의해 보면 어떨까 싶다. 그러면 보안의 다른 분야와 마찬가지로 무척 공부할 것이 많아지는 듯 싶다.

 

 

2019.10.14 by 자유로운설탕
cs

 

 

 

 

 

 

 

 

 

 

 

 

posted by 자유로운설탕
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 자유로운설탕
2019. 3. 2. 22:32 보안

  사실 스크립트 문제는 명시적으로 클라이언트나 서버에서 돌아가는 모든 스크립트가 문제이긴 하지만, 여기서는 클라이언트 쪽(정확히는 브라우저)의 자바스크립트 만을 대상으로 얘기하려 한다.

 

  일반적으로 자바스크립트 문제로 얘기되어 지는 XSS(Cross Site Script)는 인젝션 문제와 기본적으로 유사한 형태라고 생각된다. 다만 브라우저와 밀접하게 움직이는 클라이언트 코드인 자바스크립트의 특성 때문에, 최악의 상황에는 브라우저를 뒤에서 조정한다고도 할 수 있는 비동기 루프까지 일으킬 수 있기 때문에 더 특별히 다뤄지는게 아닐까도 싶다. 여튼 상황에 따라 단발성이 아닌 살아 있는 것 같이 사용자를 계속 괴롭힐 수 있는(물론 브라우저를 끄기 전까지에 한정되 있지만) 특이한 요소이다.

 

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

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

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

1. 들어가면서

  자바스크립트 문제에 영향을 줄 수 있는 요소들은 브라우저, 프레임워크 특성, 요구사항 정도 일거 같다.

 

  먼저 브라우저는 워낙 다양한 브라우저가 존재하고, 해당 브라우저 마다 XSS 시도에 대해서 서로 다른 디폴트 대응을 하고 있다(자바스크립트를 이용해 웹 프로그램을 만들어 보면 브라우저 호환성이 얼마나 속을 썩이는 지를 알수 있다). 어떤 브라우저는 모든 XSS 공격 시도에 대해서 허용하고 있고, 어떤 브라우저는 특정의 XSS 공격 시도에 대해 명시적으로 방어해 주기도 한다. 다만 상황에 따라 특정 자바스크립트 코드의 실행이 XSS 시도인지 브라우저 쪽에서 판단하는 것은 참 애매한 일이다. 잘못하면 정상적인 자바 스크립트 코드의 실행을 막을 수도 있기 때문이다. 해당 부분은 관련된 코드를 직접 보면서 한번 생각해 보도록 하자.

 

 

  두 번째로 프레임워크 특성 부분이다. 보통 요즘의 프로그래밍 언어들은 웹프로그래밍을 위해 제공하는 웹프레임워크 들이 있다. 해당 프레임워크마다 권고하는 설계가 있는데, 해당 설계를 따라 프로그램을 만들었을때, 자동으로 어디까지 XSS 로 의심되는 시도가 막히게 되느냐에 대한 문제가 있다. 어떤 언어는 해당 부분을 특정한 라이브러를 제공해서 개발자에게 명시적으로 해당 라이브러리를 사용해 방어코드를 넣도록 하는 경우도 있을테고, 어떤 경우는 프레임워크 자체에서 모든 전달된 변수에 대해 해당 방어 매커니즘을 자동으로 반영하는 경우도 있을 것이다. 그런데 여기서도 마찬가지로 지나친 안전을 보장하게 되면 모든 코드를 XSS 위험요소가 있는 것같이 다루게 되어 기능의 유연성이 떨어지는 문제가 생기게 될수도 있다. 이 부분도 뒤에서 플라스크 코드를 통해서 살펴보도록 하자. 

 

 

  마지막으로 요구사항 측면도 많은 영향을 미칠 수 있다. 예를 들어 게시판을 하나 만든다고 생각해 보자. 해당 게시판의 컨텐츠의 구성요소가 텍스트 기반이냐, HTML 기반이냐, 자바스크립트도 포함하느냐에 따라서 XSS 방어의 구현 난이도는 많이 차이가 나게 된다.

 

  텍스트 기반인 경우는 XSS 로 악용이 될 수 있는 모든 이상한 문자들을 HTML 인코딩 또는 제거를 통해서 나름 안전하게 데이터를 필터링 할 수 있다. 하지만 블로그와 같은 HTML 코드의 허용이 필요해버리면, 어떤 HTML 태그나 요소는 허용하고, 어떤 것은 제한 시킬지에 대해서 정교한 정책의 설계와 보안 테스트가 되어야 한다. 게다가 해당 부분은 위에 얘기한 브라우저 별로 검토되어야 하며, 흔히 여러 환경에서의 호환 테스트가 그렇듯이 그렇게 될경우 테스트 케이스의 수는 컨트롤 하기 힘들 정도로 계속 증식되게 된다.

 

  아래 링크에서 자바스크립트 체크 회피를 시도하는 XSS 공격에 악용 될수 있는 HTML 태그나 이벤트, 쿠키, 스타일 태그 등의 예제를 보여주고 있다. 보기만 해도 머리가 아파지지 않나 싶다. 만약 요구사항에 관리자들에게 필요하다며 게시판 작성시 자바스크립트 기능도 넣어달라고 해버리면 머리는 더 아파진다. 이런 경우 자바스크립트 방어를 포기하고 사용자 권한 관리 쪽에 포커스를 두거나, 커스텀한 편집 마법사를 만들어서, 사용가능한 자바스크립트 코드 기능을 제한하면서, 사용자가 직접 자바스크립트 편집을 하지 않게 하고 실제 코드의 생성 및 수정 처리는 서버단에서 하게 하는 방법도 있을 듯 싶다(공수는 엄청 들겠지만 말이다).

 

[XSS Filter Evasion Check Sheet - OWASP]

https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet

 

 

  특정 사이트 들에서는 BBCODE 라는 게시판용 커스텀 스크립트 언어를 쓴다고도 하지만, 아무래도 일반성 부분에서의 HTML 호환성을 포기하는 대가가 있을 것은 같다. 그리고 아마 저런 새로운 스크립트가 유명해지고, 자바스크립트 만큼 기능성이 확장된다면 역시 복잡도에 기반한 악용을 하는 시도는 늘어날 것이기 때문에 현실에 완벽한 해답은 없어 보인다.

 

[BBCode - WIKI]

https://ko.wikipedia.org/wiki/BBCode

 

 

 

 

2. 자바스크립트 공격의 종류

  보통 웹을 찾아보면 자바스크립트 공격은 3가지 종류로 나눌 수 있게 되는데, Reflected(반사), Stored(저장), DOM Based(돔 기반) 정도로 나누어지고, 부록으로 CSRF(Cross Site Request Forgery) 같이 연관된 항목이 있다.

 

  개인적으로 Reflected 와 DOM Based XSS 는 비슷한 종류라고 보고 싶다. 왜냐하면 두 항목의 차이는 브라우저 외부에서 입력된 스크립트 조각이 프로그래밍 언어의 출력 명령(PHP의 echo 같은)을 통해 화면에 표시되느냐, HTML 코드내에 포함된(또는 외부 링크일수도 있지만) 기존 자바스크립트에 의해서 표시되느냐의 차이일 뿐이기 때문이다. 여튼 두 가지는 클라이언트 코드, 인젝션 시간에 얘기한 브라우저 파서가 외부에서 들어온 자바스크립트를 어떻게 처리하느냐의 측면이라고 보면 될 듯 싶다.

 

 Stored 의 경우는 보통 Reflected 보다 심각한 문제라고 보는데 해당 부분에는 2가지 측면이 있다. 첫 째는 보통 해당 데이터가 게시판의 게시글 같이 DB에 저장된 상태로 보관되어 게시글을 보는 불특정 다수에게 노출이 된다는 부분이다. 둘째는 사실 이 글에서 얘기하고 싶은 요점이기도 한데 XSS 를 적극적으로 막아주는 브라우저도 이 타입의 스크립트에 대해서는 쉽게 선뜻 악용의 여부를 판단하기 힘들어 소극적이라는 부분이다. 왜냐하면 이 타입은 사실 Stored 란 이름 때문에 저장된 타입이라는 관점에서 보는 일이 많지만, 프로그램의 관점에서 보면 프로그램 내부에서 생성한 자바스크립트를 브라우저가 막을수 있겠냐는 딜레마의 측면이 있기 때문이다.

 

  마지막으로 CSRF 는 이름만 보면 난해해 보이지만, Forgery 가 '위조'라는 의미므로 요청(Request)을 위조한다는 의미라서 보통 인증에 사용하는 쿠키를 보유한 사용자가 모르는 사이에 XSS 공격을 이용해서, 대상 쿠키를 만들어낸 사이트의 주요 URL을 호출해 원하는 기능을 실행(예를 들어 패스워드 변경, 주소지 변경)하게 하는 요소로서 사이트의 주요한 기능에 대해서 쿠키 이외의 제대로된 추가 인증을 하고 있느냐가 문제가 되는 부분이라서, 사실 XSS 와는 관계가 있으면서도 관계가 없는 부분이기도 하다.

 

  그럼 주요 요소인 Reflected 와 Stored 공격에 대해서 브라우저들이 어떻게 반응을 하고 있는지 코드와 함께 살펴 보도록 하자.

 

 

 

 

3. Reflected vs Stored 공격 살펴보기

  아무래도 옛날 웹 프로그램 언어들이 이런 보안 부분에 아무 관심이 없는 편이므로, 3교시에 봤던 ASP 코드로 관련 코드를 만들어 본다.

 

 

 

3.1 POC 코드 만들고 엣지브라우저 또는 IE 에서 실행해보기

   해당 코드는 아래와 같다. 간단한 코드로 하나의 페이지에서 폼 전송과, 받기가 동시에 이루어진다. 위 쪽으로 보면 strFromGet 이란 변수는 폼에서 전송된 fromGet 이름을 가진 사용자가 입력한 input 박스 내용을 받고, strFromDatabase 변수는 원래는 데이터베이스를 갔다와야 하지만, 어차피 쿼리 결과를 변수에 담는거니 저런 스크립트 코드 데이터가 데이터베이스 안에 저장되어 있었다고 가정하자(이 부분이 앞서 얘기한 프로그램 내부에서 생성한 데이터 측면이 된다). 하단을 보면 Response.Write(PHP 의 echo 와 동일하다고 보면 된다)를 이용해서 해당 두 변수를 웹 화면에 뿌려준다.

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
<%
    ' 폼 값 받기
    strFromGet = request("fromGet")
    strFromDatabase = "<script>alert('stored xss run')</script>"
%>
 
<html>
   <head>
      <title>XSS 시연</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="show_xss.asp">
        <table width = 500>
            <tr>        
                <td> ID </td>
                <td> <INPUT maxlength="100" name="fromGet" size="50" type="text"> </td>
                <td> <input name=button type=submit value="ID 보내기"> </td>
            </tr>      
        </table>   
    </form>      
    
    <hr>
    <br><b>결과</b>
    <br><br>
        
<%
    ' 두 개의 변수 표시    
    Response.Write "Reflected XSS : " & strFromGet & "<br><br>"
    Response.Write "Stored XSS : " & strFromDatabase & "<br><br>"
%>
 
</body>
</html>
cs

[show_xss.asp]

 

  그럼 위의 코드를 c:\inetpub\wwwroot 에 ANSI 포맷으로 show_xss.asp 라고 저장한다. 이 후 우선 엣지브라우저를 열어 http://localhost/show_xss.asp 라고 입력한다(윈도우 10 기준이고, IE11 이나 엣지 브라우저 모두 같은 결과를 가져오니 아무거나 해도 된다).

 

 

  처음에 뜨는 Stored XSS Run 이라는 alert 은 일단 무시하고, ID 입력 창에 "<script>alert('reflected xss run')</script>" 라고 적은 후,  "ID 보내기" 버튼을 누른다.

 

 

 그렇게 되면 처음에 입력한 값을 웹 화면에 표시했기 때문에 페이지에서 자바스크립트가 실행되어 "reflected xss run" 이라는 alert 이 뜨고, 확인을 누르면 다시 "stored xss run" 이라는 alert 이 뜬다.

 

 

 

  F12 를 눌러 개발자 도구에서 페이지 소스를 보면 아래와 같이 2개의 alert 을 띄우는 스크립트 코드가 내부안에 들어가 있다.

 

 

 

3.2 크롬에서 실행해 보기 

  이번엔 IE 가 아닌 크롬에서 같은 페이지를 열어 시연해 보자. 현재 크롬 최신 버전은 72 버전이다. 크롬에서 실행해 보면 처음 "stored xss run" 얼랏이 뜨는건 마찬가지지만, 입력 박스에 스크립트를 넣어 전송하게 되면 아래와 같이 XSS_AUDITOR 에 의해 에러가 났다는 페이지가 뜨면서 실행이 막히게 된다.

 

 

  마우스 오른쪽 버튼을 클릭해 소스 보기를 하면 크롬이 페이지를 막은 부분의 스크립트가 표시된다. 크롬이 자체적으로 외부에서 들어온 악성 스크립트가 실행된다고 판단해서 실행을 막은 것이다.

 

 

 

3.3 브라우저 입장에서 생각해 보기.

  자 그럼 브라우저 입장에서 한번 해당 코드를 생각해 보자. 우선 위의 코드 실행 결과를 보고 어떤 브라우저가 더 안전하구나 생각하는 건 사실 좀 애매한 관점이다. 그것보다는 XSS 보안에 대해서 어떤 접근법을 취하느냐에 대해 살펴보는게 더 정확할 듯 하다.

 

 

  Reflected XSS 경우 브라우저는 해당 현상이 일어나고 있다는 것을 누구 보다 잘 알고 있다. 왜냐하면 해당 스크립트 코드 조각을 보낸 것이 바로 자기 자신이고(사용자가 입력한 값을 브라우저가 전달했으니), 응답 받은 코드에 보낸 코드 조각이 똑같이 있으므로(일단 브라우저는 요청된 결과를 소스로 받아서 이리저리 해석하니까 처음 받는건 스크립트가 포함된 HTML 소스이다), 자기가 보낸 코드가 반사(reflected) 되었다는 사실을 알수 있다(사실 다른 시간에 다시 얘기하겠지만 웹 취약점 스캐너의 주요 원리도 대충 이런 식이다).

 

  그래서 refelected XSS 의 경우는 엣지나 크롬 모두 잘 인지하고 있지만, 엣지는 해당 부분은 통과시켜주고, 크롬은 막고 있다.

 

  ※ 엣지 브라우저에 XSS 필터가 제거된 것은 2018년 7월 인거 같다. 자세한 것은 아래 글 같은 부분을 참고하면 될 듯 하다. Content Security Policy 로 보호가 대체 될수 있다고 얘기하고, 여기서도 XSS 필터가 reflected XSS 만 막을 수 있다는 부분을 얘기하고 있다.

[edge to remove xss auditor - scotthelme 사이트]

https://scotthelme.co.uk/edge-to-remove-xss-auditor/

 

 

  문제는 XSS 취약점 중 영향이 크다고 얘기되고, 실제로 영향이 큰 경우는 stroed XSS 라는 것이다. 위의 예제에서 봤듯이 두 브라우저 모두 해당 코드를 실행 시켜준 이유는, 이게 해킹이 된 내부 데이터로 부터 날라온건지, 원래 프로그램이 실행시키기를 원해 만든 정상적인 스크립트 인지를 판단하는 것이 종합적인 배경지식이 없다면 현실적으로 불가능 하다는 점이다(저 위의 Content Security Policy 라는 나름 이런 부분을 정의하여 접근을 구분하겠다는 것으로 보이긴 하지만, 모든 브라우저가 지원해야 되고, 페이지 마다 권한을 정의해야 되니 설계가 많이 복잡해지긴 할 듯 싶다.

 

  사실 그런 종합적인 배경지식은 페이지를 설계한 기획이나 개발자가 제일 잘 알고 있다. 그래서 어느 정도 브라우저에도 의존할 수도 있고, 해당 스크립트 코드가 들어간 링크를 누군가 명시적으로 클릭하기 전에는(물론 인지 못하고 클릭할 가능성이 높지만) 일어날 수 없는 reflected XSS 와는 달리 stored XSS 는 100% 의도적인 보안 설계로 풀어야 한다. 

 

 

 

 

4. flask 방어 코드 예제 보기

   XSS 필터를 이용한 예제도 괜찮겠지만, flask 코드에서 자동 방어해주는 부분을 한번 보도록 하자. 정확히 얘기하면 플라스크에서 사용하는 jinja2 템플릿에서 지원해준다고 보면 될것 같다. 역시 간단한 코드를 보도록 하겠다(flask 동작에 대해 잘 모르겠으면 파이썬 19교시를 보고 오자).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, make_response, render_template
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/xss", methods=['GET'])
def xss():
 
    xss_string = "<script>alert('flask reflected xss run')</script>"
    return render_template('xss.html', name = xss_string)
 
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_xss.py]

 

  위의 코드를 c:\python\code 에 UTF-8 포맷으로 flask_xss.py 이름으로 저장한다. 

 

 

  이후 템플릿 파일을 만들어 보자. 한줄짜리 간단한 html 이다.

1
<td>{{ name }}</td>
cs

 

  위의 코드를 c:\python\code\templates 폴더에 UTF-8 포맷으로 xss.html 로 저장한다. 이후 플라스크 웹서버를 실행 시킨다.

 

c:\Python\code>python flask_xss.py
 * Serving Flask app "flask_xss" (lazy loading)
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

  이후 엣지 브라우에서 http://localhost:5000/xss 주소를 열어본다. 아래와 같이 스크립트가 실행되지 않고, 일반 문자로 표시된다. html escape 처리가 된 것이다.

 

 

  실제 이스케이프 처리가 된 것을 보기위에 소스보기를 하면 아래와 같이 이스케이프 처리가 자동으로 된 것을 볼 수 있다. 

 

 

  그럼 여기서 이런 생각이 들 수 있다. "그런데 만약 자바스크립트를 flask 로직쪽에서 생성해서 동적으로 HTML 페이지 쪽에 랜더링하는 경우라면 어떻게 할까?" 이다. 이 경우 해당 필터를 제거하는 방법은 간단하다. 아래와 같이 xss.html 파일에서 받은 변수의 뒤에 "|safe" 문장을 추가하여 저장하면 된다. 

1
<td>{{ name|safe }}</td>
cs

 

 

  이후 다시 같은 url 을 실행하면, 자바스크립트가 이스케이프 처리가 안되고 그대로 실행되는 것을 볼 수 있다. 다만 이 경우는 무조건 좋아할 건 아니고, 스크립트를 무효화 하는 기능이 해제된거니, 본인이 직접 해당 넘어온 코드의 안전성을 보장하는 부분을 고민해야 하는 문제가 생긴다. 세상에 완전한 공짜 점심은 없다. 

 

 

 

 

5. 응용편 - 비동기 호출과의 만남.

  추가로 공격의 XSS 의 최악의 상황을 묘사해 보기 위해서, 극단적이긴 하지만 비동기 코드에서 일어나는 상황을 한번 만들어 보자.

 

 

  우선 flask 코드는 아래와 같다. 보게 되면 2개의 라우팅 경로가 있는데, /xss_ajax 는 기본 인젝션 페이지로 stored XSS 가 전달되고 있고(reflected 보다 POC 코드 만들기가 편해서 이렇게 했다^^;), xss_ajax.html 을 랜더링 패이지로 쓴다.

 

  밑의 /getData 경로는 웹페이지에서 AJAX 호출을 하는 경로인데, 내용을 보면 단순하게 id 값 인자를 받아서 "hello, 받은 id" 이라는 값을 조합해서 반환한다. 랜더링 때 인젝션 되는 스크립트 코드를 보면 setInterval 함수를 이용해서 매 3초(3000)마다 GetData() 함수를 반복 호출하는 코드이다.

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
from flask import Flask, make_response, render_template, request
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/xss_ajax", methods=['GET'])
def xss_ajax():
    
    xss_string = """<script>
                       setInterval(function() {
                          GetData();
                       }, 3000);
                    </script>"""
    return render_template('xss_ajax.html', name = xss_string)
    
    
 
@app.route("/getData", methods=['GET'])
def getData():
    your_id = request.args.get('id')
    return "hello, " + str(your_id)
 
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

 [flask_xss_ajax.py]

 

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

 

 

 다음은 템플릿 소스이다(기본적인 AJAX 코드 동작을 모르는 경우는 클라이언트 코드 편을 보고 온다). 스크립트를 보면 단순하게 페이지가 로딩이 완료(document.ready)되는 순간에 GetData 함수를 호출 한다. GetData 함수는 my_id 인자에 tom 이라는 값을 넣어, flask 의 getData 라우팅 주소를 호출한다. 그리고 반환된 결과를 아래의 span 태그 안에 표시한다. 그 밑의 {{ name|safe }} 는 일부러 XSS 취약점을 일으키게 위해 HTML 인코딩을 제거했다.

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
<html>
    <head>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <title>AJAX XSS Sample</title>
    </head>
    <body>
        <script type="text/javascript">
 
            $(document).ready(function(){
                GetData();
            });
 
            function GetData(){
                var my_id = "tom"
                $.ajax({
                    url : '/getData',
                    type : 'GET',
                    data : {id: my_id},
        
                    success: function(res){
                        $("#spanName").html(res);
                    }
                });
            }
        </script>
                        
        <span id="spanName"></span>
        <td>{{ name|safe }}</td>
    </body>
</html>
cs

[xss_ajax.html]

 

  해당 소스를 c:\python\code\templates 폴더에 UTF-8 포맷으로 xss_ajax.html 이름으로 저장한다. 이후 페이지를 한번 실행해보자. XSS 공격만 없다면 최초 페이지를 로딩했을때, getData 경로를 한번 호출하는 ajax 코드를 이용해 결과를 받아, 페이지에 hello, tom 이라고 표시해주는 작업을 하게 되는 페이지이다. 이후 플라스크 웹서버를 실행 시킨다.

 

c:\Python\code>python flask_xss_ajax.py
 * Serving Flask app "flask_xss_ajax" (lazy loading)
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  엣지나 크롬 브라우저에 http://localhost:5000/xss_ajax 경로를 실행 하면, 아래와 같이 평범한 페이지가 실행이 된다. 사용자 입장에서는 특별한 일이 없는 것으로 보인다.

 

 

  하지만 피들러를 띄운 상태에서 해당 페이지를 띄워보면 전혀 다른 상황이 되어 버린다. 아래와 같이 한번 호출 하고 말아야 했던 /getData 경로가 3초에 한번씩 무한으로 실행되고 있다(브라우저를 끄면 종료된다). 이론상 스크립트를 어떻게 작성하고 어떤 AJAX 기능을 호출하냐에 따라서 마음대로 할수 있다(이렇게 되면 SQL Injection 이 일어났을때 공격자가 마음대로 쿼리를 만들어 내는 거나, 업로드 취약점으로 웹쉘이 올라갔을때와 비슷한 느낌이 된다). 인증된 토큰이나 쿠키는 이용하지 않지만, 위의 예제를 CSRF 공격의 AJAX 버전이라고 봐도 될듯 하고 말이다.

 

 

  보안 교육 할때 브라우저를 마음대로 조정한다고 많이 시연되는 Beef 라는 툴도 결국은 이와 비슷한 원리로 잘 짜여진 ajax 스크립트가 기반일 것이다.

 

[beef project]

https://beefproject.com/

 

 

 

 

6. XSS 방어 설계

  그럼 우리가 생각했던 여러 포인트 들을 정리해서 XSS 이슈에 어떻게 접근해야 될지 한번 고민해 보자. 우선 XSS 의 패턴은 너무 많다. 그러므로 가장 바람직한 경우는 영향을 최소화 하기 위해서 스크립트나 HTML 을 표시하는 기능을 필요한 만큼만 최소화해서 구현 하는 것이다. 사용할 수 있는 태그 제한이 가능하다면 가능한 태그와 문법만 허용하는 화이트 박스 방식으로 체크하면 좋을 서이다.(이런 이상적인 경우는 드물긴 하겠지만...)

 

 

  reflected XSS 는 브라우저가 인지해서 어느정도 방어할 수는 있지만, 세상 사람들이 가진 브라우저는 워낙 종류와 버전이 다양하므로 브라우저만의 방어를 믿을 수는 없다. 앞에서 보듯이 IE 계열은 그대로 실행도 시켜주고 있다. 그래서 적절한 방어 코드를 넣어야 하는데, 원칙상으로는 페이지의 자바스크립트나 프로그래밍 언어에서 참조하는 외부 모든 인자에 대해서(HTTP 헤더의 모든 값 포함) 사용 전에 안전하게 필터링 해줘야 한다. 

 

  그런 부분을 충분히 설계하기 힘들다고 판단된다면, 아래와 같은 신뢰성 있는 기관에서 만든 필터를 이용하는 것이 현실적이다. 하지만 기본적인 XSS 에 대한 이해와, 자바스크립트의 이해, 방어의 한계 등을 잘 인지하고 있어야지 문제가 있을때 대처 할 수 있다는 것은 잊으면 안될것 같다(때로는 과하게 막아 프로그램과의 호환성을 위해서 해당 필터 라이브러리의 일부를 커스터마이즈 해야 될 수도 있을 듯 싶다). 또 위의 flask 예에도 보았듯이 괜찮은 웹 프레임워크나, 여러 자바스크립트 라이브러리를 쓰는 것도 해결책일 거 같다. 해당 라이브러리에 문제가 생긴다면 그쪽에서 빠르게 패치를 제공할 것이기 때문에 말이다(사실 보안은 어느정도 이렇게 신뢰성 있는 외부 프로젝트 들에 많이 의지하는 것 같다. 회사에서 구입해 사용하는 상용 백신 솔루션 같은 경우도 어찌보면 백신 회사의 지적인 프로젝트를 믿고 의지하는 것이라고 볼수 있다).

 

[Cross Site Scripting Prevention Cheat Sheet - OWASP]

https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md

 

[XSS 나무위키] - 4.1.1.2 에 네이버에서 제공되는 필터가 언급되어 있다.

https://namu.wiki/w/XSS

 

 

  reflected XSS 의 경우는 웹서버로 전달되는 모든 인자를 체크해야 하는 부분이지만, 사실 명확히 얘기하면, 프로그램이나 자바스크립트에서 실제 사용하는 인자가 실제 대상이다(뭐 미래에 사용 안하던 다른 인자가 사용될 수도 있지만 그때는 적절한 개발 단계의 보안 점검을 통해서 변화를 인지하고 팔로우업해야 될듯 하다).

 

  또 브라우저가 reflected XSS 를 인지할 수 있다는 것은 브라우저에 들어가는 입력과(여러 GET, POST 인자와 HTTP 헤더) 출력(서버로부터 전달되어 화면에 뿌려지는 HTTP 소스)을 알 수 있다면, reflected XSS 가 일어나는 후보 값들을 쉽게 발견할 수 있다는 것이다. 이 부분이 웹 취약점 스캐너가 XSS 를 찾는 원리라고 볼 수 있고, reflected XSS 또는 DOM based XSS 영역의 취약점을 점검하는데 많은 도움이 된다. 스캐너는사람이 하면 거의 불가능한 모든 인자를 리스트업 해서, 스크립트를 입력하고 결과 HTML 에서 해당 스크립트가 어떻게 되었는지 확인하여 판단을 해준다. 다만 이경우 앞에서 얘기한 실제 사용안하는 외부 값까지도 모두 검사하는 낭비 측면이 있지만 뭐 그건 자동화의 단점이자 장점인 듯 싶다.

 

  반대로 얘기하면 좀더 민감한 stored 같은 경우는 운이 좋지 않은 이상 스캐너로 발견하기 무척 힘들다. 해당 데이터에 우연히 접근되거나, 해당 데이터를 전송한 후에 바로 확인이 가능해야 한다. 전략적으로 스캐너를 이용해 stored XSS 테스트에 접근한다면, 기존 기능을 기반으로 입력 후 바로 디비에 저장된 결과를 보여주는 테스트 페이지를 만들어 스캐너를 돌린다든지 하는 작업이 필요할 듯 하다. 추가로 스캐너는 문제를 발견하는 것이지 문제가 없음을 100% 증명하는 툴은 아니라는 것을 인지하자. 스캐너가 사용한 수많은 XSS 패턴들이 모든 현존하는 XSS 패턴을 커버한다는 보장은 할수 없기 때문에, 스캐너 검사에 통과 되었더라도 여러 다양한 요소에 해당하는 취약점 패턴이 숨어있을 수도 있다.

 

 

 

 

7. 마무리 하면서

  추가로 위의 예에서는 유입 변수에 <script> 태그가 들어간 예제만 보았지만, 실제로는 해당 태그가 없어도 스크립트 동작이 가능한 경우가 많다. 적절한 코딩 방법은 아니지만 <script> 태그내의 스크립트 내용 중 일부를 외부 값을 사용하는 경우다(실제 이렇게 코딩하는 개발자들이 가끔 있다). 위의 플라스크 템플릿을 예를 들면 아래의 코드 형태일 것이다.

1
2
3
4
5
<script>
 
var a = {{ name|safe }};
....
</script>
cs

 

 

  name 변수에 "1;setInterval(function() {GetData();}, 3000);" 을 넣는다면 아래와 같은 반복 실행을 하는 문법이 똑같이 만들어질 것이다.  

1
2
3
4
5
<script>
 
var a = 1setInterval(function() {GetData();}, 3000);
....
</script>
cs

 

 

  뭐 여튼 결론적으로 스크립트 문제 및 인젝션 문제에 대해서는 요구사항이나 설계의 자유도가 높아질 수록 점점 방어하기는 복잡하게 되어 버린다. 모든 보안 분야가 마찬가지로 자유도를 높게 줄수록 방어하기는 더 어려워지며 포기해야할 트레이드 오프도 생길 수 있다. 결국 해당 부분의 영향을 최소화 하기 위해서는 대상이 되는 시스템과 요구사항, 기술, 프로세스, 관련된 사람들을 잘 이해하는 방법 밖에는 없을듯 싶다. 생각보다 실제에서는 보안은 명확하지 않은 애매한(놓여진 코드나, 요구사항, 여러 외부 환경에 따라 달라지는) 영역들이 상호 작용하는 분야이기도 한거 같다. 그나마 어플리케이션 보안 부분이 제일 명확한 면이 있어 보이는 듯 싶다. 점점 진행될 수록 앞의 다른 챕터의 얘기들이 조금씩 반복되곤 하는데 맨 처음 얘기한 듯이 보안의 원리가 결국 비슷비슷하기 때문이다. 어플리케이션 보안은 프로그램과 데이터 간의 술래잡기 같은 측면이 있다.

 

 

2019.3.3 by 자유로운설탕
cs

 

 

 

 

posted by 자유로운설탕
2019. 2. 24. 23:38 보안

  이번 시간에는 웹 어플리케이션에서 자주 이슈가 되는 업로드, 다운로드 문제를 살펴보려고 한다. 이 부분은 내부에 돌아가는 원리와 패턴을 이해하기만 하면 해법이 아주 명확한 편인 부분이기도 하다. 각각 실제 돌아가는 코드를 한번 살펴본 후, 해당 문제를 보안적으로 바라보는 관점을 설명하려고 한다.

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

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

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

1. 들어가면서

  사실 컴퓨터안의 데이터라는 것은 사용자나 프로그래머를 위해서 많이 추상화 되긴 했지만, 변하지 않는 사실은 0과 1로 이루어진 일련의 숫자를 로직과 표준이라는 임의의 규칙에 맞춰 구분한 것이라고 볼 수 있다. 물론 이 0과 1로 이루어진 숫자를 보고 실제 환경과 동작을 알수 있는 사람은 현대에는 거의 없을 것 같긴하지만, 추상적인 상위 OS 에서 하위로 파고 내려가는 리버싱이나 포렌식, 악성코드 분석 같은 부분을 보면 그런 하위로 내려가는 노력도 필요한 분야는 여전히 건재한 듯 하다.

 

  보안 쪽에서는 그런 부분 이외에도 파일의 특정 공간에 숨겨진 데이터 라든지, 프로토콜 안에 숨겨진 데이터 등등 보통 밖에서 보기엔 완전하게 하나의 개체로 추상화된 데이터의 조작된 내부를 봐야 하는 경우가 많은 것 같다. 이러한 해체적 관점(좀 과장은 됬다)에서 한번 업로드, 다운로드를 살펴보려고 한다.

 

 

 

2. 다운로드 문제

  일단 파이썬으로 이루어진 엑셀 다운로드 기능을 하나 보자.

 

 

2.1 모듈 설치

  우선 필요한 모듈을 설치한다. 어떤 글을 봤냐에 따라 이미 설치된 모듈도 있을 것이다.

C:\Python\code>pip install pandas

C:\Python\code>pip install openpyxl

C:\Python\code>pip install numpy

C:\Python\code>pip install flask

 

 

2.2 엑셀 다운로드 구현

  예전에 파이썬 때 만들었던 pandas 코드와 구글 검색으로 찾은 flask 코드를 대충 합치면 아래와 같다. (플라스크 구현 부분이 이해가 안되면 파이썬 글 19교시 flask 편을 보고 오면 된다)

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
from flask import Flask, make_response
import pandas as pd
import numpy as np
import io
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
@app.route("/excel_down", methods=['GET'])
def excel_down():
   # pandas 객체를 하나 만든다.
   df = pd.DataFrame({'A''fruit drink cookie fruit'.split(),
                      'B''orange Coke chocopie mango'.split(),
                      'C': np.arange(4)})
 
   # 메모리에 pandas 객체를 이용해 엑셀을 만들고 저장한다.
   output = io.BytesIO()
   writer = pd.ExcelWriter(output)
   df.to_excel(writer, 'Tab1')
   writer.save()
 
   # 엑셀 형태로 HTTP 응답을 주어 브라우저가 파일로 저장하게 유도한다.
   resp = make_response(output.getvalue())
   resp.headers['Content-Disposition'= 'attachment; filename=output.xlsx'
   resp.headers["Content-type"= "text/csv"
   return resp
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_excel_down.py]

 

   c:\python\code 폴더에 "flask_excel_down.py" 이름으로 UTF-8 인코딩으로 저장 후 실행하면 플라스크 웹 서버가 실행 된다.

C:\Python\code>python flask_excel_down.py
....
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  이후 브라우저 주소창에 http://127.0.0.1:5000/excel_down 이라고 적으면 아래와 같이 pandas 데이터가 저장된 엑셀 파일이 다운된다.

 

 

2.3 코드 살펴보기

  이제 동작은 확인했으니 위의 코드를 한번 살펴보자. 첨에 테이블 형태의 pandas 객체가 만들어지고, io.BytesIO() 라는 바이트 형태의 파일 스트림이 하나 만들어 진 후(output = io.BytesIO()), 해당 스트림안에 pandas 를 사용하여 엑셀 데이터를 만들어 채워 넣게 된다(writer = pd.ExcelWriter(output)).

 

  이후 해당 파일 스트림을 저장 후에, HTTP 형태로 응답을 생성하면서, 엑셀 파일의 전달에 맞는 헤더를 설정한다(resp.headers['Content-Disposition'] = 'attachment; filename=output.xlsx', resp.headers["Content-type"] = "text/csv"). 브라우저는 해당 응답을 받아 엑셀 파일 형태임을 인지하고, 사용자에게 엑셀 파일로 저장하라는 선택 창을 보여주게 된다.

 

 

2.4 피들러로 살펴보기

  이제 네크워크 단에서 전송 되는 부분을 살펴보기 위해서 피들러를 통해 봐보자. 피들러의 resonse 섹션에서 raw 탭을 클릭해 보면(이제 기존 글을 봤다면 피들러 사용 법은 어느 정도 익숙해 졌다고 생각해서 자세한 스탭은 생략한다. 잘 모르는 경우는 파이썬 글 WhoIS API 편에 설명되어 있으니 그 쪽을 보고 오면 된다), 아래와 같이 raw 탭을 클릭해 보면 헤더 값 뒤에 암호화 된 것만 같은 엑셀 파일 내용을 인코딩한 데이터가 브라우저로 전달되고 있다.  

 

 

  헤더 부분 탭만 클릭해 따로 보게 되면 아래와 같이 text/csv 라는 content-type 으로 지정되어, 브라우저가 이게 엑셀 파일이구나를 명시적으로 알게되어 파일로 저장하도록 유도한다.

 

 

2.5 다운로드 문제 생각해 보기

  위의 예제를 보면 우리가 일반적으로 받아들이는 것과는 조금 다르게 내부적으로는 실제 무언가가 다운된다는 행위가 실제로는 엑셀 파일 형태의 일련의 데이터 스트림이 서버에서 만들어져 브라우저 쪽으로 복사되어 전달되는 것이라는 것을 볼 수 있다. 정확하게 얘기하면 복사(또는 반사)에 가깝다. 이렇게 보면 사실 다운로드의 보안 문제는 복사된 데이터가 넘어올때, (누군가가) 원하지 않은 복사된 데이터가 넘어오는 경우라고 볼 수 있다.

 

  그럼 어떤 경우 원하지 않는 데이터가 복사되어 올까? 사실 취약점이란 공격자에게 이점을 주는 관점에서 명명되는 것이기 때문에, 기대하지 않았던 이상한 데이터가 사용자가 넘어오는 부분은 보통 버그지 보안 쪽에서 초점을 두는 부분이 아닐 것이다. 그래서 보통 다운로드 취약점하면 서버 쪽에서 보내길 원하지 않는 데이터(파일, 데이터)가 전달 될때 문제가 된다. 

 

 

  웹에서의 다운로드는 굳이 분류하자면 4가지 정도 타입으로 나눠 볼수 있는데, 첫 번째는 우리가 브라우저에서 이미지 URL 을 입력했을때 이미지가 다운되서 보이게 되는 것 같은거, 두 번째는 서버쪽 프로그램에서 임의의 URL 을 조합해 생성해서 우리한데 해당 경로의 파일이나 이미지를 전달해 주는 경우, 세 번째는 위의 엑셀 다운 프로그램과 같이 서버쪽에서 조합된 데이터(보통 관리자 화면 같은데서 데이터베이스에서 조회한 데이터를 엑셀 형태로 만들어 다운로드가 되는 경우가 많다)를 다운로드 하는 경우, 마지막으로 가장 특수한 경우긴 하지만 사용자가 자신이 올린 파일이나 권한 있는 파일만 다운로드 해야 할 경우가 있는 경우이다.

 

  첫 번째 URL 을 입력하는 경우는 사실 뭐 거의 막을 수 없는 경우인데, 중요한 파일이 직접 URL 을 입력해서 다운로드가 되는 부분이 문제이므로, 중요한 파일은 웹 루트 바깥의 경로에 두어 URL 로 다운로드가 되지 않게 하거나, 웹 서버 설정에서 접근 가능 확장자 필터링을 하여 다운로드가 불가능하게 하는 방법 밖에는 없다. 또한 디렉토리 리스팅 등의 웹폴더 내부의 파일 이름을 볼수 있는 등의 힌트가 되는 경우를 막고(사실 요즘 웹서버들은 기본적으로 disable 되어있긴 하지만) 근본적으로는 중요한 파일이라면, 뒤에 얘기할 네 번째 경우와 같이  설계를 해야된다.

 

  두 번째 서버 쪽 프로그램에서 사용자가 전송한 인자나 header 의 특정 값 요소에 따라 임의의 URL 을 조합해서 해당 파일을 전달해 주는 경우인데, 이 경우는 5교시에서 얘기한 클라이언트에서 날라간 인자를 믿는 클라이언트 코드 문제라고 볼 수 있다. 이 경우 부터는 사실 좀 애매모호하게 흘러가는데, 보통 Directory Traveral(디렉토리 트레버셜) 문제인 ../../ 같은 상위 디렉토리를 거슬러 가거나 하는 문자를 파일 이름이나, 경로로 사용될 인자에 넣어 악용해 생기는 문제이므로, 밑의 업로드 예제에서 보이게될 werkzeug.utils 모듈의 secure_filename 같은 함수를 사용하여 인자에서 해당 문자들을 제거하거나 하는 방법이 있긴하다. 하지만 보통 이 부분이 레거시 코드를 수정하는 경우는 디렉토리를 만드는 규칙이나, 파일 이름의 생성 등과 대치되는 상황이 생길수도 있다(뭐 새로 코드를 다시 만든다고 해도 기존에 이미 있는 파일은 어떻게 해냐하는 마이그레이션 문제가 생길 수 있다)

 

[디렉토리 접근 공격 - H@ck.pe.kr]

https://hack.pe.kr/91

 

  대부분 이런 경우는 요구사항 구조상 파일 이름이나 번호 등을 인자로 받아 지정한 파일을 전달할 수 밖에 없는 경우므로, 개인적으로 추천하는 방법은 디렉토리 경로는 고정(상수나 config 값으로)시키고, 파일이름만 넘겨 받아 위에 언급한 secure_filename 와 비슷하게 각 언어에서 지원해주는 이름에서 위험한 문자들을 제거해 주는 메서드를 적용하는 것이다. 물론 이렇게 하려면 업로드 할때도 같은 함수를 파일 이름에 적용하거나, 아예 알파벳과 숫자로만 제한된 파일 이름으로 저장해야 호환성 문제가 발생 안한다.

 

  세 번째는 사실 위와 같이 허용되지 않는 파일을 다운로드 하는 측면의 문제라기 보다는, 허용되지 않는 데이터를 조회하는 측면에 가깝다(그 조회된 내용이 엑셀 형태로 브라우저로 전달될 뿐이다). 이 경우 실제 파일이 다운로드 되는게 아니고 데이터베이스를 조회한 데이터가 엑셀로 만들어 지는 일이 일어날 뿐이므로, 데이터베이스에 조회하는 인자들의 SQL Injection 이라든가, 또는 비슷하게 데이터를 만들어 낼때 악용될 수 있는 클라이언트 인자의 조작 부분을 방어하면 될 듯하다.

 

  마지막으로 네번째는 가장 엄격한 형태라고 볼수 있는데, 사용자의 이력서가 올라가는 사이트라든지, 해당 회원만 다운받을 수 있는 파일이라든지(예를 들어 웹툰 미리보기?) 같은 문제이다. 이 경우는 보통 권한을 관리해 주는 추가적인 데이터베이스 설계가 필요한데, 파일의 리스트를  관리하면서 해당 파일들을 다운로드 할 수 있는 사용자 권한을 지정하는 형태이다. 사용자가 해당 파일의 다운로드를 요청했을때, 테이블의 다운로드 권한을 조회해 해당 파일이나, 데이터의 로드(사용자에게 보내기 위한)을 결정하게 된다.

 

  그런 파일들은 보통 웹경로로는 다운 받을 수 없는 위치에 있고, 어플리케이션은 해당 사용자의 토큰을 통해 권한을 획득한 후, 가져와 메모리 상태에서 사용자 브라우저에 전달하게 되며, 개인정보 파일이나 아주 중요한 파일의 경우는 저장 할때도 암호화하여 저장하여 일반적인 권한과 루트로는 접근을 하지 못하도록 해야 된다. 다만 이 부분은 모든 다운로드 가능 파일의 사용권한을 체크하는 번거로움이 있고, 첫번째나 두번째 경우같이 일반적인 웹 서버의 GET 요청에 의해서 파일을 가져가지 못하므로 서버하 하는 일이 많아 조금 더 힘들어할 거 같은 부분이 있긴 하다.

 

  뭐 여튼 위와 같이 여러 상황과 니즈가 생길 수 있으므로 그때그때 마다 적절히 설계를 하면 될것 같긴하지만, 중요한거는 처음 얘기한 것과 같이 다운로드 문제는 원하지 않는 데이터가 복제되어 나가는 상황이라는 것을 잊지않는 것이다. 해당 이해를 기반으로 다운로드가 필요한 어플리케이션 형태를 고려해 찬찬히 고민해 본다면 적절한 설계를 할 수 있게 될 것 같다.

 

 

 

 

3. 업로드 설계

  이번에는 업로드 동작을 살펴 보도록 하겠다. 마찬가지로 파이썬 flask 로 구현하고, 딱히 추가로 설치할 모듈은 없다.

 

 

3.1 파일 업로드 구현

  구글을 찾아보니 아래 샘플을 이용하면 될 것 같다. 오타를 수정하고, 내용을 적당히 바꾼 소스를 보면 아래와 같다.

 

[Flask – File Uploading - Tutorialspoint 사이트]

https://www.tutorialspoint.com/flask/flask_file_uploading.htm

 

 

  우선 템플릿을 하나 만든다.

1
2
3
4
5
6
7
8
9
<html>
   <body>
      <form action = "http://localhost:5000/upload_process" method = "POST" 
         enctype = "multipart/form-data">
         <input type = "file" name = "upfile" />
         <input type = "submit" value = "파일 업로드" />
      </form>
   </body>
</html>
cs

[upload.html]

 

실습을 하는 c:\python\code 폴더에 templates 폴더를 만들고, 해당 폴더안에 upload.html 이라고 UTF-8 포맷으로 저장한다. c:\python\code\templates\upload.html 파일이 된다.

 

 

  이후 실제 로직을 처리할 flask 코드를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, render_template, request
from werkzeug import secure_filename
 
# flask 웹서버를 실행 합니다.
app = Flask(__name__)
 
# 업로드 창을 보여줍니다. 
@app.route('/upload')
def upload():
   return render_template('upload.html')
   
# 실제 업로드 처리를 합니다.
@app.route('/upload_process', methods = ['GET''POST'])
def upload_process():
   if request.method == 'POST':
      f = request.files['upfile']
      f.save(secure_filename(f.filename))
      return '파일 업로드 완료'
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시합니다 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

[flask_upload.py]

 

 

  해당 파일을 c:\python\code 폴더에 "flask_upload.py" 이름으로 UTF-8 인코딩으로 저장 후 실행하면 플라스크 웹 서버가 실행 된다. 

C:\Python\code>python flask_upload.py
....
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  이후 브라우저에서 http://127.0.0.1:5000/upload 입력하면 아래와 같이 업로드할 파일을 선택하는 창이 뜬다.

 

 

  다른 폴더에 있는 그림 파일을 하나 선택한 후, "파일 업로드" 버튼을 누르면 아래와 같이 업로드가 완료되었다는 화면이 나온다. 플라스크를 실행한 c:\python\code 안을 보면 업로드한 이미지가 올라가 있다. 

 

 

3.2 코드 살펴보기

  마찬가지로 위의 업로드 코드를 살펴 보자. /upload 호출에서 업로드 upload.html 파일을 랜더링해 화면에 보여준다. 해당 HTML 소스를 보면 파일 형태를  보내기 위해 전송방식이 POST 로 되어있고, 파일 업로드시 내용이 깨지지 않게 인코딩을 해달라는 (enctype = "multipart/form-data") 인코딩 방식이 정의되어 있다(앞의 암호화 파트에서 본 Base64 인코딩을 생각하며 된다)

 

  실제 올라온 파일을 처리하는 /upload_process 코드를 보면 POST 로 넘어온 값 중에 이름이 upfile 인 폼 요소 값을 가져와서 파일을 저장하는 객체(f)에 담고, 해당 넘어온 파일의 원래 이름을 f.filename 기준으로 플라스크 실행 폴더 루트에 저장을 한다. secure_filename 이라는 함수는 라이브러리에서 넘어오는 보통 업로드나 다운로드 등에서 많이 얘기되는 ../ 같은 악용문자들을 제거해주는 함수 이다. 위에 다운로드 설명할때 언급했었다.

 

 

3.3 피들러로 살펴보기

  다운로드와 동일하게 업로드 파일이 올라가는 순간의 모습을 피들러로 봐보자. 버튼을 눌렀을때 호출되는 http://localhost:5000/upload_process 호출의 Request 부분에서 Raw 탭을 마찬가지로 보면, 다운로드와 비슷하게, 헤더 쪽에는 데이터의 종류를 알려주는 content-type 이나, 이미지 데이터가 언제 끝나는지 알려주는 boundary 값(하단으로 스크롤 하면 맨 마지막에 저 값이 있다), 폼 요소 이름(name="upfile")이나, 파일 이름(IMG_0071.JPG), 파일의 종류(Content-Type: image/jpeg) 도 있다. 그 밑에 글짜가 깨진거 같이 보이는 이미지 파일이 인코딩된 데이터가 있게 된다.

 

  형태를 보게되면 앞의 다운로드 코드를 본 것과 많이 비슷하며, 전달되는 여러 정보들이 이번에는 브라우저가 아닌, 서버쪽 처리 프로그램을 위한 것이라는 부분만 상이하다고 봐도 될것 같다.

 

 

3.4 업로드 문제 생각해 보기

  결국 업로드도 다운로드와 비슷하게 전달된 데이터의 올바른 처리 문제라고 볼수 있다. 다만 조금 다른 측면은 이번엔 보내는 쪽이 사용자 쪽이므로, 데이터 형태가 비교적 정형화됬던 다운로드와 별개로 적극적으로 공격자의 조작이 가능하다는 것이다.

 

  정상적인 파일의 헤더 같은 부분에 악성 코드를 살짝 숨겨 놓을 수도 있고(관련 해석기의 취약점이 있다면 영향을 줄수도 있다), 리눅스 쉘을 사용하듯이 특정한 명령어를 원격에서 웹 인터페이스를 통해 실행할 수 있게 해주는 웹쉘(WebShell)이라고 불리우는 악성 코드를 업로드 하려고 시도하거나, CMD Injection 취약점 처럼 특정 파서와 만나서 실행되는 코드를 파일내에 삽입하거나, 확장자나, 저장되는 경로를 속이려는 여러 시도를 할 수 있다.

 

 

  업로드는 보통 2가지 정도의 타입으로 분류할 수 있는데, 첫번째는 엑셀이나 텍스트 파일을 업로드해 일괄 처리를 하는 형태가 있고, 두 번째는 아까 얘기한 이미지나, 이력서 파일 등을 서버로 올려서 해당 형태 그대로 저장해 게시물이나, 증빙 목적으로 열람하는 형태가 있다.

 

  첫번째의 경우는 보통 업로드된 파일을 굳이 서버의 특정 경로에 저장해서 일어나는 경우라고 볼 수 있는데 종종 장애 등의 디버깅 상황을 위해서 그런 경우도 있고, 구글 등 검색 엔진에서 노출되는 레거시 코드를 참조해 만들다가 그런 경우도 있는듯 싶다(경험상 예전 프로그램 코드들이 좀 이런 형태가 많은 듯 하다). 요즘은 아예 자바스크립트에서 파싱해서 데이터만 JSON 형태로 보내거나, 서버에서 바로 메모리상에서 처리하고 저장하지 않는 경우도 많아 잘 발생하지 않는 듯도 싶다. 해당 언어에서도 그렇게 가이드 하고 말이다. 

 

  파일을 저장하는 레거시 코드인 경우 같은 경우는 코드 전체를 뜯어고치기 힘들다면, 저장 경로를 웹 서버 바깥으로 저장하고, 처리 후 삭제를 하게하는 것이 보안적이나, 웹서버에 쓸데없는 파일이 남게 되는 것을 방지하는 차원에서도 좋다. 물론 이 경우에도 뜻하지 않던 경로를 가리키도록 조작하여 접근하는 이슈는 남을 수 있어서, 뒤에 얘기할 부분들도 함께 적용을 고려해야만 한다.

 

  두번째의 경우가 보통 웹의 업로드 문제에서 자주 일어나는 부분인데, 보통 웹 형태로 된 게시판 등에서 이미지를 업로드 하거나, PDF 등의 문서 등을 업로드 하는 경우이다. 이 경우 확장자를 체크한다던지, MIME 타입을 체크한다든지, 이름과 경로가 될 인자에서 특수문자를 제거한다든지, 실제 업로드된 이미지 같은 파일이 실제로 그런 종류의 파일인지 체크하는 등 여러 방법을 혼용해서 가이드 하긴 하지만, 개인적으로 가장 안전한 방법은 저장 경로나 이름을 결정할 때 클라이언트 코드를 참고 안 하는 거라고 생각한다.

 

  예를 들어 서버 쪽의 저장 폴더 경로를 고정된 상수나 config 값으로 정해 놓은 후, 파일 이름을 날짜와 해시값, 난수 등을 조합한 서버를 기반한 고유 값으로 생성하고, 검증이 끝난 특정 확장자를 명시적으로(예를 들어 exe = ".jpg" 후) 확장자로 연결해 저장한다면, 외부에서 공격을 하고 싶어도 공격할 수 있는 부분이 안 생기게 된다(이 부분은 클라이언트 코드 편에서도 얘기를 했다). 만약 실제의 포맷이 다른 이상한 파일이 올라가는게 탐탁치 않은 경우는(예로서 .jpg 인데 안의 내용은 스크립트) 서버쪽에서 저장하기 전에 실제 해당 포맷의 파일이 맞는지를 확인하는 메서드를 쓰면 될듯 싶다. 물론 업로드 폴더의 스크립트 실행권한을 제거한다든지, 상황에 따라 업로드 폴더를 웹 경로 바깥으로 뺀다든지 하는 부분도 기본적으로 필요하다.

 

  다만 이러한 설계라해도 100% 안전한다고는 볼수는 없다. 예를 들어 웹서버 파싱 모듈에 취약점이 있어서 스크립트로  내용이 구성된 .jpg 확장자의 파일이, 호출시 스크립트를 실행한다는 경우가 생길 수도 있겠지만, 현실적으로 일어나긴 힘들고 해당 경우는 일어나더라도 해당 해석 모듈의 패치로 해결하는게 정석일 거 같다. 뭐 여튼 버그건 취약점이건 간에 앞에 얘기했듯이 컴퓨터의 모든건 0과 1의 세상이기 때문에 발생할 가능성이 zero 라고는 절대 말하진 못한다. 

 

 

 

 

4. 마무리 하면서

  이번에는 방어코드 보다는 구현코드 위주로 구성한게 조금 걸리긴 하지만, 어차피 업로드, 다운로드 취약점에 대한 가이드 들은 구글을 찾아보면 많이 나오고, 그 세세한 내용 보다는 그 글들이 왜 이런 방어를 하자고 얘기하고, 그 경우 생길 수 있는 blind spot 을 생각해 보자는 의미에서, 동작 구조를 이해할 수 있는 코드를 위주로 설명했다. 사실 공식화 되어 있는 듯한 다른 보안 이슈들도 이런 모호한 측면들이 언제든 현실에서 발생할 수 있다. 그래서 계속적으로 관련된 프로그램의 구조나 환경을 이해하려 해야 되는 것이고 말이다.

 

  또 하나 더 강조하고 싶은건 결국 모든 컴퓨터 내를 구성하는 요소들은 0과 1들을 임의의 로직대로 추상화해 놓은 것이라는 것을 마음속 한 구석에 담아두길 바라는 맘도 있었다. 예전에 파이썬 글에서도 소개했었지만 마음의 여유가 있으신 분들은 아래 책을 꼭 한번 읽어보기를 추천한다. 결국 스스로에게 갇히지 않기 위해서는 다양한 관점에서 같은 대상을 바라보는 훈련도 계속 필요한 듯은 싶다. 

 

CODE 코드 : 하드웨어와 소프트웨어에 숨어 있는 언어(이건 프로그래밍 공부가 의미 없게 느껴지는 분들을 위해서 덤으로~ 불빛 신호에서 컴퓨터가 만들어지기 까지의 과정을 설명한다. 중간에서 길을 잃어 필름이 끊기더라도 읽어볼 가치가 있다고 본다. 비 전공자한테는 생각보다 어려울지도 모르지만, 해당 책의 저자가 비전공자도 읽을 수 있도록 노력해 쓴 책이고 파이썬 공부를 하고자 하는 의지 정도만 있음 가능할 듯 싶다.)

 

  그럼 즐거운 공부되길 바라며, 아직 어떻게 표현하면 좋을지 감이 안잡히는 주제들도 있지만 금년엔 가능한 자주 올려 이 시리즈를 마쳐볼 수 있도록 노력해 보려 한다 --;

 

2019.2.25 by 자유로운설탕
cs

 

posted by 자유로운설탕
2019. 2. 4. 22:31 프로그래밍

  보안글이 어느정도 정리되면(아직 갈길은 좀 멀어보이긴 하지만--;;) 그 동안에 개인적으로 업데이트된 부분들을 기반으로 파이썬 부록 편을 몇 개 작성하고 싶었는데, 마침 그 부분 중 하나에 대해서 콕 찝어 문의해 주신 "독자(닉네임)"님이 있으셔서 간단하게 정리를 해보려고 한다. 물론 구글을 잘 찾으면 있긴 한 내용이지만, 아무래도 처음 하게되면 개념도 잘 안잡히고, 버전이 달라서 헤멨었던 경험이 있어서 한번 더 모아 정리하는 것도 웹 공해는 안될 것 같았기 때문이다. 기본적으로 책 기준인 파이썬 3.6.4 에 IIS 7.5(윈도우즈 10 기준), Apache 2.4 를 기준으로 세팅해 보려고 한다.

 

 

 

 

1. 들어가면서

  플라스크(flask)던 장고(django)든 굳이 IIS, Apache 같은 웹서버와 연결해야 하는 이유는 예제에서 보였던 CMD 창에서 띄우는 방식은 CMD 창이 닫혀 버리면 웹사이트 자체로 사라져 버린 다는 것 때문이다. 특히 사용자가 로그 아웃을 하게 되면 모든 CMD 창은 사라지기 때문에, 다른 사람이 해당 페이지를 원격에서 접속해서 지속적으로 사용할 수 없게 된다. 물론 서비스 형태로 등록해 본다던가 하는 꽁수는 있을 것 같긴한데, 아무래도 정상적인 절차는 예전 플라스크 시간에 얘기했던 WSGI(Web Server Gateway Interface) 방식을 사용해, 사용자 요청은 안정적인 웹 서버에서 전담하고, 로직 처리만 WSGI 를 통해서 파이썬으로 전달해 처리하여 다시 웹 서버를 통해서 사용자 브라우저에 전달해 주는 방식일 것이다. 윈도우즈 쪽에서는 Fast-CGI 라고 보통 얘기하는 듯하고, 아파치 쪽에서는 WSGI 라고 얘기하는 거 같은데, 뭐 두가지 사이에 설계적 차이는 있다고는 하는데, 딱히 선택의 여지도 없으니까 무시해도 될 갭인 것은 같다.

 

[불곰님 블로그 - WSGI, WAS, GgI 이해]

https://brownbears.tistory.com/350

 

 

 

 

2. Windows 10 + IIS 설정 하기

 기본적으로 구글에 "windows 10 IIS flask" 라고 검색하면 몇 개의 링크가 나오는데, 몇 몇 시행착오를 겪어본 결과 가장 현재 시점에 맞춰 간략히 잘 정리된 페이지는 아래의 링크들 같다. 이 블로그의 글은 해당 글들을 여기서 연재했던 샘플에 맞춰 좀더 쉽게 풀어 정리한 버전이라고 봐도 될듯 하다.

 

[github - Python Flask on IIS with wfastcgi]

https://gist.github.com/bparaj/ac8dd5c35a15a7633a268e668f4d2c94 

 

[Medium.com - Deploying Python web app (Flask) in Windows Server (IIS) using FastCGI]

https://medium.com/@bilalbayasut/deploying-python-web-app-flask-in-windows-server-iis-using-fastcgi-6c1873ae0ad8

 

  간단하게 흐름을 설명하자면, 예전 설명글들을 보다보면, 여러가지 IIS  환경 변수를 직접 설정해야했는데(사실 좀 이런 오래된 리눅스 스타일의 하나하나 설정하는 작업은 요즘엔 좀 번거롭게 느껴지는 일 같기는 하다), wfastcgi 는 모듈이 점점 업데이트 되면서 wfastcgi-enable.exe 파일을 같이 제공해서, 해당 작업들을 한꺼번에 자동으로 해준다. 이 후 우리는 적당히 웹서버를 하나 만들고(디폴트 웹서버를 써도 되고), 웹서버에 대한 설정 파일인 web.config 에 연결된 fastcgi 환경과 연결된 파이썬 게이트웨이 파일을 정의하는 작업을 하면 된다. 여기서는 19교시에서 진행한 DB에서 가져온 테이블을 보여주는 "myweb.py" 파일을 기준으로 설명한다.

 

 

2.1 IIS 설치하기

  18교시 레거시웹 편을 참조하여 IIS 를 설치하고 기본적인 동작을 확인 한다. 추가로 하나 더 해야 할 일이 있는데, "Windows 기능 켜기/끄기" 에서 "인터넷 정보 서비스" > "World Wide Web 서비스" > "응용 프로그램 개발 기능" > "CGI" 를 추가로 선택해 설치해 줘야 한다(그래야 fastcgi 기능을 쓸수 있다).

 

 

2.2 관리자 권한으로 cmd 창 실행

  뒤의 설정 파일 때문에 관리자 권한으로 cmd 창을 실행해야 된다. 아니면 권한 에러가 난다.

 

 

2.3 wfastcgi 설치

C:\Python\code>pip install wfastcgi
Collecting wfastcgi
...
Successfully installed wfastcgi-3.0.0

 

 

2.4 설정 자동화 파일 실행 하기

C:\Python\code>wfastcgi-enable
구성 커밋 경로 "MACHINE/WEBROOT/APPHOST"에서 "MACHINE/WEBROOT/APPHOST"의 "system.webServer/fastCgi" 섹션에 구성 변경을 적용했습니다.
"c:\python\python.exe|c:\python\lib\site-packages\wfastcgi.py" can now be used as a FastCGI script processor

 

  이렇게 설정하면 다른 설명 글들에서 얘기하는 여러가지 IIS fastcgi 설정이 자동으로 된다. 

 

 

2.5 IIS 루트 폴더내에 파일 복사하기

  19교시에서 예전에 예제로 만들었던 SQL 서버에서 데이터를 가져와 HTML 테이블로 표시하던 myweb.py 파일과 templetes 폴더안에 있는 myweb.html 파일을 IIS 루트 디렉토리인 c:\inetpub\wwwroot 에 복사한다(관리자 권한으로 해야된다). 아래와 같은 구조가 될것 이다.

1
2
3
4
5
wwwroot/
    myweb.py
    templetes/
        myweb.html
        web.config
cs

 

 

2.6 web.config 수정하기

  이제 IIS 의 사이트별 config 파일인 web.config 파일을 아래와 같이 수정하자. 관리자 권한이 있어야 수정되기 때문에 메모장을 관리자 권한으로 실행해서 해당 파일을 읽어 들여 수정해야 한다.

 

 대충 내용을 보면, 사용자 요청을 처리할 핸들러를 정의하고(<handler> 섹션), 핸들러 메인 파일이나, 루트 패스, 로그 위치 등(<appSettings> 섹션)을 설정한다. WSGI_LOG 폴더를 실습용 폴더인 c:\python\code\ 안으로 잡은거는 기본 로그 폴더인 c:\inetpub\logs 폴더가 기본적으로 IIS 사용자 권한으로 쓰기가 안되서 권한 있는 폴더로 잡은 것이다(좀 이상한건 IIS 로그 폴던데 원래 이런건진 모르겠다--;). 만약 logs 폴더를 사용하고 싶다면 해당 폴더의 속성의 보안 권한에서 IIS_IUSRS 사용자에게 쓰기 권한을 주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <handlers>
            <remove name="Python27_via_FastCGI" />
            <remove name="Python34_via_FastCGI" />
            <add name="Python FastCGI" path="*" verb="*" modules="FastCgiModule" scriptProcessor="C:\Python\python.exe|C:\Python\Lib\site-packages\wfastcgi.py" resourceType="Unspecified" requireAccess="Script" />
        </handlers>
        <httpErrors errorMode="Detailed" />
    </system.webServer>
 
    <appSettings>
        <!-- Required settings -->
        <add key="WSGI_HANDLER" value="myweb.app" />
        <add key="PYTHONPATH" value="C:\inetpub\wwwroot" />
        <add key="WSGI_LOG" value="C:\python\code\my_app.log" />
    </appSettings>
</configuration>
cs

 

 

2.7 기능 위임 부분 수정 하기

  아마도 이 오류는 windows 10 타입(Home, Pro 나 정책 설정?)에 따라서 다른거 같긴한데, 현재 실습 환경인 Windows 10 Home 에서는 위와 같이 모든 설정을 다하고 "http://localhost/sqltable" 을 실행하게 되면, 아래와 같은 에러가 발생한다.

 

HTTP 오류 500.19 - Internal Server Error
요청된 페이지와 관련된 구성 데이터가 잘못되어 해당 페이지에 액세스할 수 없습니다.

이 구성 섹션은 이 경로에서 사용할 수 없습니다. 섹션이 부모 수준에서 잠겨 있는 경우 이 오류가 발생합니다.

 

  구글을 찾아보면 c:\windows\system32\inetsrv\config\applicationHost.config 에서 핸들러의 겹쳐쓰기(overide) 모드를 allow 로 바꾸라던데, 막상 저 폴더에 가면 해당 파일이 없어 좀 당황을 했다. 조금 더 다른 문서를 찾다보니 이제는 저 파일을 바로 수정하려 할 수는 없고 관리화면에 기능 위임 부분을 수정하라길래, IIS 관리 모듈로 들어가 보니 아래와 같이 왼쪽 트리에서 가장 위의 항목을 선택하면 오른쪽의 관리 섹션에 기능 위임 아이콘이 있다.

 

  해당 아이콘을 더블 클릭해 들어간 후, 아래와 같이 처리기 매핑 항목을 "읽기" 에서 "읽기/쓰기" 로 바꾸어 준다.

 

 

2.8 샘플 실행해 보기

  이제 설정이 완료된 듯 하니 "http://localhost/sqltable" 이라고 치면 아래와 같이 IIS 환경을 통해서 flask 페이지가 나오게 된다(첨엔 많이 신기했음;).

 

 

 

 

3. Windows 10 + Apache 설정하기

  다음으로는 아파치에 설정하는 부분을 똑같이 해보려 한다. 아파치 설정하는 부분은 다른 시리즈 글인 "구글로 공부하는 보안 3교시" 뒤쪽을 보면 PHP 설정하는 곳에 놓여있다. 거길 그대로 따라해서 "It Works!" 샘플페이지 까지 봤다고 가정하고 연결해서 쓴다(PHP 설정도 그대로 해도 된다. 단 현재 설정을 config 파일에 하게되면 php 해석기는 안 돌아가긴 한다)

 

  구글을 찾아보니 아래 2개의 사이트를 주로 참조하면 될듯 하다.

 

[ZEIZ님 블로그 - Windows 10 64bit에서 Apache 64bit에 Flask with Python 3.6 배포]

https://zeiz.com/post/158725706804/windows-10-64bit%EC%97%90%EC%84%9C-apache-64bit%EC%97%90-flask-with

 

[STOREHUBS 사이트]

https://www.storehubs.com/Blog/deploy-python-flask-application-apache-windows-server/

 

 

3.1 mode_wsgi 모듈 설치하기

C:\Python\code>pip install mod_wsgi
Installing collected packages: mod-wsgi

 

  다른 환경에서는 VS 컴파일러 에러가 나서 wheel 파일을 설치하기도 한거 같은데, 아마 실습을 쭉 해왔다면 이미 깔려있어서 괜찮은 것 같다. wheel 파일을 설치해도 되고, VS 컴파일러를 설치해도 되는거 같다. 뭐 일단 잘 되니 패스.

 

 

3.2 mod_wsgi.cp36-win_amd64.pyd 파일을 복사

  mod_wsgi 모듈을 설치하게 되면 파이썬의 모듈 폴더안에 c:\Python\Lib\site-packages\mod_wsgi\server\mod_wsgi.cp36-win_amd64.pyd 파일이 설치되는데, 이게 IIS 에 있던 wfastcgi.py 와 비슷한 역할을 한다고 보면 될듯 하다. 뭐 참조 사이트에 있는 것처럼 직접 해당 경로를 지정하도 되지만 경로가 조금더 단순해 설정이 쉬워 보이길 바라는 맘에서 mod_wsgi.cp36-win_amd64.pyd 파일을 c:\Apache24\modules 에 복사한다. 그 안을 보면 다 .so 확장자 파일만 있는데, 예전엔 mod_wsgi 도 .so 확장자로 지원했다가 요즘은 .pyd 확장자로 지원하는 거 같다(요 부분도 좀 그래서 구글을 참조하다보면 헷깔린다)

 

 

3.3 파이썬 용 폴더 만들고 파일 복사하기

  IIS 와 비슷하게 기존에 세팅한 httpd 폴더에 설정해도 될듯 하나, php 도 나중에 다시 써야하고, flask 웹서비스용 폴더를 한번 분리해 보고 싶어서 c:\test 폴더를 만들고, IIS 때와 마찬가지로 myweb.py 파일과 templetes 폴더안에 있는 myweb.html 파일을 복사한다. 아래와 같은 구조가 될것 이다.

1
2
3
4
test/
    myweb.py
    templetes/
        myweb.html
cs

 

 

 

3.4 WSGI 게이트웨이 파일 호출 하기

  mod_wsgi 설계 구조상 IIS 처럼 해당 파일을 직접 매칭 시키지는 못하는 것 같고, 중계하는 게이트웨이 파일을 하나 만들어야 한다. 설명에는 .wsgi 확장자로 만들긴 하지만 .py 로 만들어도 괜찮길래 "gateway.py" 란 파일로 c:\test 폴더에 만들어 본다. 내용을 보면 application 이란 호칭으로 myweb.py 파일 안에 있는 app 라는 이름의 플라스크 객체를 가져온다. 

1
2
3
4
import sys
 
sys.path.append('c:/test/')
from myweb import app as application
cs

[gateway,py]

 

  폴더 구조를 보면 아래와 같이 하나의 파일이 추가되었다.

1
2
3
4
5
test/
    gateway.py
    myweb.py
    templetes/
        myweb.html
cs

 

 

3.5 config 파일 수정하기

  이제 IIS 에서 햇던 것처럼 이것저것을 수정해야 되는데 결론적으로 아파치 설정 파일인 httpd.config 파일만 수정하면 된다. 해당 파일을 열어 맨 아래에 아래 값들을 추가한다. 여러 글들에 보면 글마다 옵션이 좀 다른데, 하나씩 빼보면서 샘플에서 에러가 안나는 최소 값으로 추려봤다.

 

<httpd.config 에 추가>

# 사용하려는 test 폴더에 모두 허용하는 것으로 설정한다(운영에서는 뭔가 고민할 부분이 있을 수도)

<Directory "c:/test/">
    Require all granted
</Directory>

 

# 복사했던 모듈 파일을 읽어오고

LoadModule wsgi_module modules/mod_wsgi.cp36-win_amd64.pyd
# flask 게이트웨이 파일 위치를 알려준다

WSGIScriptAlias / "c:/test/gateway.py"

 

 

3.6 아파치 재시작

  아파치를 재 시작해야 config 파일이 반영되므로 예전 글에서 설명한 트레이 아이콘 이용해 재시작한다. 뭔가 수정이 잘못됬음 재시작시 에러가 날텐데 해당 경우는 c:\Apache24\logs\error.log 로그를 보고 원인을 해결해야 한다.

 

 

3.7 동작 확인해보기

  "http://localhost:9999/sqltable" 를 호출해보면 IIS 때와 같은 테이블 결과를 볼 수 있다.

 

 

 

 

4. 에러 추적 및 웹서버 구성 시 차이점

  마지막으로 WSGI 방식으로 서버를 구성하면서 CMD 창에서는 잘 돌아가던 코드가 안돌아갔거나 구성 설계를 바꾸거나 해야될 필요가 있던 부분에 대해서 얘기하려 한다.

 

 

4.1 에러 추적

  저렇게 연결했을때 제일 속터지는 상황이, CMD 창에서는 잘 실행이 되는데 fastcgi나 mod_wsgi 를 연결하게 되면 에러가 날때 이다. 에러 원인을 찾아 해결하는 것도 힘든 일이지만 처음 만나게 되면 일단 화면에서 아래와 같이 에러의 상세 내용 없는 500 대 화면만 보게 되기 때문이다. 

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

 

  아파치 같은 경우는 앞에서 얘기한 c:\Apache24\logs\error.log 를 보게 되면 우리가 CMD 창에서 봤던 에러를 똑같이 볼 수 있는 것 같고, IIS 의 경우는 따로 남기는 에러는 없는 것 같아(아시면 댓글 좀_ _;), 아래의 스택오버플로우 글에서 제시한 코드를  app = Flask(__name__) 뒤에 추가해서 500에러 메시지를 보이게 하는게 현재까지로는 제일 난 것 같다.

 

[스택 오버플로 - python flask login form throws 500 error on iis]

https://stackoverflow.com/questions/31804800/python-flask-login-form-throws-500-error-on-iis

1
2
3
4
5
6
7
8
9
10
11
12
app = Flask(__name__)
 
@app.errorhandler(500)
def internal_error(exception):
    app.logger.exception(exception)
    file_handler = RotatingFileHandler('C:\inetpub\wwwroot\logs.log''a'1 * 1024 * 102410)
    file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
    app.logger.setLevel(logging.INFO)
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)
    app.logger.info('microblog startup')
    return render_template('500.html'), 500
cs

 

 

  해당 코드를 넣으면 아래와 같이 나름 상세한 에러가 브라우저 화면에 보이게 된다.

Error occurred: Traceback (most recent call last): File "c:\python\lib\site-packages\flask\app.py", line 2292, ..... , in make_response 'The view function did not return a valid response.

 

 

4.2 CMD 개발과 웹서버 연결시 차이점.

  불행하게도 잘 돌아가던 flask 코드를 IIS 나 Apache 와 연결하게 되면 에러가 발생하는 경우가 종종 생기는 것 같다. 개인적으로 기억 나는 일은 SQLite 를 사용하는 경우와 flask-excel 이라는 엑셀 다운로드 모듈을 사용하는 경우였다. SQLite는 IIS 에서 돌리니 기본적으로 웹서버 계정에 로컬 파일을 쓰는 권한이 없어서 에러가 났고, flask-excel 은 excel 초기화 함수가 if __name__ == "__main__": 안에 정의해 놓으니 WSGI 모듈이 해당 코드를 무시해 버려서 초기화 작업이 안되서 에러가 나게 됬다.

 

  Flask 로 샘플 페이지를 만들다 보면, 웹프로그램이라는걸 깜빡 잊고 일반 파이썬 어플리케이션 처럼 파일도 읽고 쓰면서 개발하게 될 경우가 많은데, 막상 웹 서버로 배포하려 하다보면, 기본적으로 로컬에 있는 파일을 왠만하면 접근 못하도록 막아놓은 웹 서버 설계를 무시하고(보통 보안적으로 이렇게 되는 부분이 안전하기도 하다) 로컬 파일을 읽고 쓰는 방식으로 만드는 게 맞나 하는 생각이 들게 된다. 그래서 SQLite 쓰는 부분을 DB 서버 자체가 권한을 관리하는 일반 SQL 로 변경하거나 하는 일도 필요할 수 있다. 이게 좀 자유롭게 취향대로 쓸 수 있는 플라스크의 조심스러운 측면인것도 같긴하다.

 

  뭐 이런 연결방식에 따른 코드나 설계의 차이는 시행착오를 겪으면서 하나하나 다른 사람들의 경험에도 도움을 받으면서 해결 할 수 밖에는 없을 듯 싶긴하다.

 

 

 

 

5. 마치면서

  뭔가 간략히 정리하려 했는데, 이왕 정리하는거 하다보니 꽤 긴 글이 되긴 했다. IIS 와 아파치 설정 부분이 다른 글에 이미 정리되어 있어서 그나마 다행이라고 생각되는 상황이였다. --; 아마 Flask, WSGI, IIS, Apache 버전이 서로 계속 변하면서 또 상황은 바뀔 수도 있겠지만, 여기서 설명한 개념만 잘 잡고 있다면 그리 많이 변하는 건 없을 거 같긴하다. 그럼 이렇게 파이썬 게릴라 포스팅을 마치려고 한다~

 

 

2019.2.4 by 자유로운설탕
cs

 

 

 

 

 

posted by 자유로운설탕
2018. 11. 11. 20:36 보안

 이번 시간에는 인젝션과 비슷한 무게로 어플리케이션 보안을 이해하는 데 필요한 클라이언트 코드에 대해서 살펴보려고 한다. 개인적으로 인젝션이 "행위"라면, 클라이언트 코드는 그 행위가 일어나는 "환경"이라고 보면 어떨까 싶다. 이 시간에는 웹(이건 좀 자세하게)과 윈도우, 모바일 환경(이건 간단한 예를 들어서)에서 클라이언트 코드가 어떤 범위를 가지고, 어떤 의미를 가지고 있는지 설명해 보려고 한다. 

 

P.S.: 너무 점잖은 말투로 블로그를 적다보니 왠지 블로그 글 같지도 않고, 쓰는데도 부담이 많이 되서 다시 평상어로 적으려고 합니다~ 틀리면 말고 모드로 막 적겠습니다. 앞에 미리 쓴 글들도 여유가 생김 같이 바꾸려 해요. 이해 부탁 드려요. ㅎㅎ

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

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

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

1. 들어가면서

  우선 클라이언트 코드(Client Code)의 의미는 무엇일까? 개인적으로 해당 의미를 "조작 가능한 코드"라고 말하고 싶다. 추가로 해당 범위는 코드 자체만을 가리킨다기 보다는 클라이언트 코드에 영향을 줄 수 있는 모든 행위와 매체(하드웨어 포함)를 포함하는게 맞지 않을까 생각한다. 

 

  예를 들어 아주 예전 코인 오락기에 전기 충격을 주어 코인을 늘리려고 했던 장치나, 게임을 조작하는 매크로 및 기계 장치들 부터 시작해, 로컬에 설치된 SQLite3 파일을 조작한다 거나, 쿠키 수정, 메모리 수정, 패킷 수정, 전통적인 웹 사이트 공격 기법 등이 모두 포함된다. 서버를 침투하려는 여러 시도의 경우도 서버 사이드 측면의 공격이라고 생각할 수도 있지만, 반대로 생각하면 필요없는 서비스를 실수로 오픈한 경우를 빼고는 모두 클라이언트(서버 끼리의 서비스라고 해도 넓은 범위에서는 마찬가지로 외부의 사람이나 프로그램등 클라이언트를 위한 거니까)를 위한 인터페이스를 제공하기 있기 때문에 클라이언트 측면의 공격이라고 우길 수도 있을 것  같다. 사실상 내부로 들어갈 수 있는 인터페이스가 없다면 공격은 불가능 한게 맞을테니까 말이다.

 

  또 비교적 안전하다고 생각하는 모바일 환경의 어플리케이션 등도 탈옥 등의 적절한 상황을 만나게 되면 웹, PC내의 프로그램과 마찬가지로 자유롭게 조작할 수 있다고 보는게 맞을 듯하다(물론 그러한 조작을 할 수 있는 능력이나 툴이 있느냐는 별개의 문제겠지만 말이다).

 

[오락실 딱딱이 - 나무위키]

https://namu.wiki/w/%EC%98%A4%EB%9D%BD%EC%8B%A4%20%EB%94%B1%EB%94%B1%EC%9D%B4

 

[자동사냥 - 나무위키]

https://namu.wiki/w/%EC%9E%90%EB%8F%99%EC%82%AC%EB%83%A5

 

  개인적으로는 이 클라이언트 코드를 이해하느냐의 문제가 어플리케이션 보안 문제의 70% 정도는 차지하지 않을까 하는 중요한 주제라고 생각된다. 이 클라이언트 코드를 제대로 이해하기 위해서, 프로그래밍 언어를 공부하고, 네트워크를 공부하고, 이런저런 프로토콜과, 웹, 컴퓨터 내부의 동작등을 이해하기 위해서 계속 노력해야 되지 않을까 싶다. 

 

   반대 관점에서 서버 사이드 코드는 서버안에 존재하고, 조작이 불가능하게 설계가 보장되어 있는 로직을 의미한다. 이 서버 사이드 코드를 안전하게 설계하기 위해서는 여러가지 노력이 필요하지만, 모순되게도 안전한 설계를 위해서는 클라이언트 코드가 어떤 범위와 의미를 가지느냐를 정확히 이해하고 있어야 하는 것 같다. 물론 외부의 변수를 받지 않는 폐쇄적인 로직을 구현한다면 가능하지도 모르지만, 현대의 프로그램 환경은 외부의 데이터와 연결 없이 프로그램 자체만 돌아가는 경우는 거의 없을 듯하기 때문에 불가능한 희망이라고 볼 수 있다.

 

  그럼 이 글에서 얼마나 해당 부분을 표현할 수 있을지 모르겠지만, 개인적으로 가지고 있는 관점을 이해시키기 위해 노력해 보려 한다. 물론 앞에도 얘기했지만 저는 딱 넘 못하지도 잘하지도 않는 중간 정도 레벨이라고 생각하기 때문에, 더 상위 레벨의 관점을 가진 사람들은 다르게 생각할 수도 있을 것 같지만... 제 블로그니까 뭐 제 맘대로...

 

 

 

 

2. 웹에서의 클라이언트 코드

  보안 공부하는 사람들이 많이들 하는 얘기중 하나가 웹 보안이 보안에 입문하기에 가장 쉽다고 하는 얘기다. 보안 공부를 가이드하는 글들을 찾다보면, 웹 보안을 먼저하고 점점 윈도우나 리눅스나 모바일 같은 보안으로 가는 식으로 얘기가 되서, 웹이 엄청 난이도가 낮은 것처럼 얘기되지만 해당 부분에는 좀 오해가 있다고 생각한다. 사실 웹 쪽 보안이 좀더 쉽게 느껴지는 이유는 기존 사람들의 노력과 표준적인 HTTP 프로토콜, 브라우저라는 제한된 환경 때문이라고 생각한다.

 

  만약 우리가 쿠키도 조작할수 없고, 버프, 피들러, 파로스 같은 좋은 프록시툴도 없고, 브라우저의 개발자 도구 같은 것도 없다면, 웹에 대한 보안의 난이도는 많이 높아질 것이다. 아마 제한된 소수의 사람만이 직접 http 패킷과 브라우저의 메모리 공간을 어렵게 조작하면서 웹 보안에 대한 지식을 독점했을 것 같다. 옛날에 SQL 인젝션 같은 기초적인 취약점들이 웹사이트 들에 엄청 퍼져 있던 것처럼 말이다.

 

  그리고 사실 웹은 서버사이드 코드로 들어가게 되면, SQL 이나 여러 외부 시스템과 맞물리게 된다. 물론 비교적 단순히 돌아가는 시스템도 많긴 한지만, 내부에 무엇이든 만들어 연결할 수 있다는 면에서 일반 어플리케이션과 딱히 틀린 점이 있나 싶은 측면도 있다. 요즘 같이 모든 기술이 점점 연결되어 가고, 데이터를 공유하는 상황에서는 웹에 대한 보안을 이해하기 위해서 서버 깊숙히 숨은 어플리케이션 부터 부터 전달되고 나오는 데이터를 처리하는 부분들에 대해서도 모두 이해해야 될 필요가 있기 때문에, 사실 꽤 오래전부터 해당 경계는 깨어져 버렸다고 생각한다. 그래서 웹에 대한 보안 공부를 하면서 웹에 제한된 관점으로만 공부하는 것은 잘못된 것이라고 생각한다. 어차피 모든 프로그램의 보안 문제는 원리적으로 보면 비슷비슷한 것도 같다.

 

  그래서 여기에서 얘기하는 웹이라는 것은 웹 환경의 전부가 아니라, 브라우저에 제한된 관점에서의 웹만을 얘기한다는 것을 우선 전제하고 싶다.

 

 

 

2.1 웹의 1세대 - 창고

  웹을 세대를 나누는게 맞을까 싶기도 하지만, 웹이 어떻게 진행되었느냐를 설명하기 위해서 개인적인 관점에서 나누자면, 1세대는 문서의 보관소 같은 창고 같은 역활을 하는 정적인 페이지로 이루어진 웹이라고 볼 수 있다.  

 

HTML 은 Hypertext Markup Language 의 약자로 HyperText 는 지금은 너무 평범하게 보이는 웹페이지에서 다른 페이지로 이동할 수 있는 링크를 얘기한다. 예전에 HTML 이 널리 퍼지기전 테스트 파일들만을 가진 환경에서 컴퓨터를 써보지 않았다면 해당 링크 기능이 얼마나 유용한 건지 알기 힘들 수도 있다. 하나의 문서를 보다가 연결된 다른 문서를 보기위해서는 편집기에 여러개의 텍스트 파일을 동시에 띄워놓고, 전체 검색을 통해서 열려진 문서를 왔다갔다 할수 밖에 없었다.  현재 비슷한 상황을 생각해 본다면, IDE 에서 메서드가 정의된 위치를 자동으로 연결해 이동해 찾는 것과, 파일 매니저를 통해 여러개의 소스 파일들을 뒤져서 해당 호출 위치를 찾는 것과 비슷한 차이가 있다.

 

  Markup Language 는 페이지의 구조를 정의 한다는 의미이다. 우리가 HTML 문서 안에서 보는 여러 태그(<html>,<body>,<td>,...)들이 그런 역활을 한다. 하얀 종이에 글을 적어서 의미있는 문장을 만들듯이, 미리 약속된 이런 태그와 속성들을 이용해서, 브라우저는 그림파일과 함께 배치해서 우리가 보는 네이버, 다음 같은 웹 페이지들처럼 예쁘게 화면을 표시한다. 사실 웹은 우리가 미적인 관점으로 화면을 보면서 이해하기 때문에 공감각적 의미가 있는 것이지, 내부적으로는 이런 마크업 언어들이 돌아다니는 세상이라고 보면 될듯 하다. 보안이나 개발에서는 이러한 세상을 주로 보면서 살고있다고 보면 될테고 말이다.

 

  웹의 1세대에에서는 우리가 URL(브라우저의 주소창에 우리가 적는 http://~~ 하는 사이트 주소들)경로를 호출하면 해당 웹사이트를 찾아가 원하는 위치에서 원하는 파일을 가져오는 역활에 제한 되었다. 해당 HTML 로 구성되어있는 문서를 가져다가(물론 꼭 HTML 문서만 있는건 아니고, 텍스트든 그림 파일이든 웹서버 폴더에 있는 파일들을 모두 지정해서 가져올 수 있다), 브라우저 엔진에서 구조적으로 표시해주는 역할을 할 뿐이였다. 이러한 브라우저 엔진이 IE냐 크롬이냐 사파리냐에 따라서 화면에 표시해 주는게 미묘하게 달라서 호환성 문제가 발생했으며, 이것은 다음에 설명할 자바스크립트에서도 마찬가지로 일어나서 XSS 취약점같은 경우 브라우저 종류 및 버전별로 해당 현상이 일어나기도 하고 안 일어나기도 하는 애매한 상황을 가져오기도 한다.

 

 

2.2 웹의 2세대 - 동적 페이지

  웹의 2세대(다시 말하지만 개인적인 분류다^^)에서는 페이지가 움직이고 체계를 잡기 시작했다고 보면 될것 같다.

 

  프로그래밍 코드와 디자인 코드를 분리하기 위해서 디자인 부분만을 담당하는 CSS 란 언어가 나왔고, 자바스크립트가 나와서 사용자 행동에 반응을 하며, 브라우저의 DOM(Document Object Model) 구조를 이용하여, 사용자에 행동에 따라 움직이는 것같이 생각되게 하는 동적인 웹 페이지를 지원하게 되었다. 또한 단순히 페이지 주소만을 호출하던 1세대와는 다르게 클라이언트와 서버사이에 Form 이라는 HTML 요소를 가지고 데이터를 전달(GET-HTTP 헤더, POST-HTTP 바디 내부)하였으며, 서버 쪽도 단순히 원하는 페이지를 찾아 전달해 주는 것을 벗어나 사용자의 입력을 통해 폼으로 전달해진 데이터를 기반으로 CGI(Common Gateway Interface), ASP, JSP 같은 로직을 이용해 처리함으로서 로직을 가지기 시작했다.

 

  그리고 가공된 데이터를 데이터베이스 안에 저장함으로서 1회적인 행동에서 벗어나, 사용자의 여러 행위나 상태등을 저장하여, 지금 우리가 사용하고 있는 여러가지 포털 사이트나, 은행 등의 사이트들이 돌아갈 수 있는 기반 구조가 만들어 졌다고 보면 될 듯 하다.

 

 

  HTTP 메시지는 아래와 같이 크게 헤더와 바디로 나누어져 있는데, 여기에서의 헤더, 바디는 HTML 에서의 문법에서의 헤더, 바디와는 조금은 다른 의미이다.

 

  나중에 피들러로 살펴보겠지만, HTTP Body 부분에 우리가 알고 있는 HTML 소스와(<header>, <body> 를 포함한 모두), 폼 전송시의 Post 데이터가 들어가게 된다(이미지 업로드시 MIME 인코딩된 데이터도 마찬가지이다). HTTP Headers 부분에 들어가 있는 데이터들이 웹 프로그래밍을 공부 하다보면 자주 보게 되는 사용자 에이전트 정보(user agent), 페이지를 어떤 언어로 보여줄것인가 하는 인코딩(encoding), 바로 이전 호출됬던 페이지를 나타내 주는 레퍼러(referer), http only 같은 쿠키 옵션들이 들어가게 된다.

 

 

  그럼 샘플 페이지를 피들러를 이용해 살펴 보면 HTTP 헤더, 바디 부분과 쿠키가 만들어지는 과정을 봐보도록 하자. ASP 코드의 실행 방법은 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<%
    ' 폼과 쿠키 값 받기
    strCustomID = request("txtCustomID")
    strLoginUser = Request.Cookies("LoginUserName")
%>
 
 
<html>
   <head>
      <title>쿠키 만들기</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="MakeCookie.asp">
        <table width = 600>
            <tr>        
                <td width = "60"> 아이디 </td>
                <td width = "100"> <INPUT maxlength="10" name="txtCustomID" size="10" type="text" value=<%=strCustomID%></td>
                <td>&nbsp;<input name=button type=submit value="쿠키 만들기"></td> 
            </tr>      
        </table>   
    </form>      
        <hr>
        <br><b>결과</b><br><br>
        
<%
    ' 1) 전달된 쿠키 표시    
    Response.Write "서버로 들어온 쿠키는 : " & strLoginUser & "<br><br>"
        
 
    ' 2) 등록된 회원일 경우 DB 에 저장하고, 쿠키를 생성함
    Select Case strCustomID
        Case "tom"
            Response.Write("tom 의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserName"= "tom"
        Case Else
            Response.Write("잘못된 ID 임"& "<br><br>"
            Response.end
    End Select
%>
 
</body>
</html>
cs

[makecookie.asp]

 

  편의상 하나의 페이지로 만들어 좀 헷깔리겠지만, 코드를 보면 맨 위에서, form 값인 txtCustomID 와 쿠키 값인 LoginUserName 을 받는다. 처음 페이지를 열게 되면 물론 쿠키 값이나 폼 값은 넘어오지 않으니 표시된 내용이 없을 것이다. 이후 코드를 보면 폼안에 txtCustomID 이름을 가진 입력 박스가 있고, 밑의 ASP 코드(VBScript 문법이다)를 보면 전달 되어온 쿠키 값을 표시하고, 아이디가 tom인 경우 LoginUserName 가 tom 인 쿠키를 만들어 낸다.

 

 

  해당 파일을 makecookie.asp 이름으로, c:\inetpub\wwwroot에 복사후, 피들러를 띄우고, 브라우저 주소창에 http://localhost/makecookie.asp 를 입력하여 이동하면, 아래와 같이 아이디를 입력하는 화면이 나오게 된다. 아직 쿠키나, ID가 전달 안 되었기 때문에, 밑에는 빈 칸으로 표시되거나 잘못된 아이디라고 나온다. 

 

 

  이때 피들러 쪽을 보면 아래와 같이 표시된다. 위쪽의 Request 된 항목을 보면 GET 으로 makecookie.asp 를 요청했고, 밑의 Response 된 헤더를 보면 SET-Cookie 라는 명령어와 함께 ASP 에서 디폴트로 만들어 주는 세션 쿠키가 생성된다.

 

 

  이 후 아이디 입력 란에 "tom" 을 넣고, "쿠키 만들기" 버튼을 누른다. 브라우저 주소창을 보면 ? 뒤에 인자로 txtCustomID=tom 이 날라가는게 보이고, 출력 화면을 보면 "tom 의 쿠키를 만듬" 이라는 메시지가 보인다. 서버 응답 쪽에서는 쿠키를 만들어 달라고 클라이언트 브라우저 쪽에 요청을 해서 브라우저는 쿠키를 만든 상태지만, 우리가 "tom" 을 넣고 전송한 과거 시점의 데이터는 아직 해당 쿠키가 없는 상태이기 때문에, 서버에서 인지한 쿠키 내용은 여전히 비어 있다. 

 

 

  피들러 쪽에서 보면, 위 쪽 request 에서는 아까와 조금 다르게 GET 부분에 txtCustomID=tom 이라는 인자가 날라가는 것이 보인다(WebForms 탭에서는 GET, POST 값을 모두 보여 주기 때문에 거기서 봐도 된다). 쿠키 쪽도 보면, 맨 처음 빈 페이지를 호출할 때와 다르게 아까 처음 페이지를 열때 서버 쪽에서 요청되온 ASP 세션 쿠키를 생성해서 날리고 있다. 밑의 response 부분을 보면, 현재 페이지를 만들어서 보여 주고 싶어한 부분이 보이게 되는데, "Set-Cookie" 라는 명령어 뒤에 우리가 ASP 페이지에서 쿠키를 만드는 코드에서 지정했던 "LoginUserName=tom" 이라는 쿠키를 만들어 달라는 명령어가 response 헤더를 통해 전달되게 된다.

 

 

  이 후에 다시 한번 jerry 같은 의미 없는 값을 넣고, 쿠키 만들기 버튼을 누르면, 헤더에는 아까 만들어진 LoginUserName=tom 이라는 쿠키가 전달되게 된다. 여기서는 브라우저 쪽을 보면, 아래와 같이 쿠키가 서버로 날라갔기 때문에, tom 이라는 쿠키를 표시하게 된다(jerry 로 쿠키를 만드는 코드는 없으니 잘못된 ID 라고 나오게 된다)

  

 

  해당 부분을 피들러로 보게 되면 이제 request 되는 쿠키 값에 "LoginUserName=tom" 이라는 항목이 날라가게 된다. 해당 쿠키는 다른 사이트를 호출할 때는 날라가지 않지만(예를 들어 네이버를 호출해 본다든지), 브라우저의 메모리 안에 계속 존재하며 언제든지 http://localhost 사이트를 호출 할때는 헤더에 첨부되어 날라가게 된다. 반대로 얘기하면 브라우저를 끄게 되면 사라지게 된다. 그래서 대부분의 사이트에서 ID/PASSWORD 를 통해 로그인 한 후, 브라우저를 모두 닫아 버리게 되면 해당 사이트에서 브라우저에게 만들게 했던 사용자 신원을 보장하는 쿠키가 날라가 버리게 되어서 로그아웃 되게 되어 버린다(만약 로그아웃이 안되는 사이트가 있다면, 로컬 파일 쿠키를 사용한다든지 하는 바람직하지 않은 경우일 것이다. 다만 모바일 기기의 경우는 같은 로컬이라도 좀더 소유기반으로 안전하게 보기 때문에, 로컬의 안전한 장소에 특정기간 인증코드를 남겨놓는 것은 약간은 별개의 문제이긴 한것 같다)  

 

  이 단순해 보이는 쿠키 생성을 통해, 사이트는 사용자의 신원을 보증하고 로그인 상태에서 사이트를 사용할 수 있게 한다. 정상적인 사이트의 경우는 저렇게 아이디만 던지는 경우는 없을 것이고 보통 아이디, 패스워드 쌍을 던져서, 사이트의 내부 디비에 저장한 아이디와 해시되어 보관된 패스워드 값과 비교한 후 인증이 된 경우만 쿠키를 생성하며, 일반적으로 쿠키 재사용을 방지하고 특정 시점의 만료처리를 할수 있는, 시간 및 기타 값들과 믹스하여 안전한 알고리즘으로으로 암호화 하여 관리한다. 

 

  이후 브라우저는 해당 값을 모든 브라우저가 닫히기 전에는 메모리에 안전하게 보관하면서, 해당 사이트의 페이지를 요청할때 위와 같이 헤더에 포함해 보내게 된다. 서버는 해당 쿠키 값을 받은 후 암호화를 해제해서 안의 사용자의 신원을 나타내는 고유값을 기반으로 사용자가 정상적으로 로그인 되었다고 믿고 작업을 하게 된다. 이 부분이 stateless 라는 http 통신에서 세션을 유지하게 되는 쿠키의 역할이며, 웹 어플리케이션 보안 쪽에서 많이 얘기하는 권한 상승, 세션 탈취 등의 어려운 용어가 저기에서 시작된다고 보면 된다. 해당 부분은 다른 측면도 있긴 하지만 주로 저렇게 인증을 위한 쿠키가 만들어 지는 부분을 파고들어 여러가지로 악용해 보려는 시도라고 봐도 될 듯 하다.

 

 

  앞의 얘기한 부분을 요약 하면 아래의 그림이라고 보면 될듯 하다.

 

 

 

 

2.3 웹의 3세대 - 비동기 호출 페이지(AJAX)

  개인적으로 3세대라고 얘기하고 싶고, 사실 현재 웹이 실질적으로 머무르고 있는 단계라고 생각하는 부분이 비동기 호출 페이지의 등장이다. 비동기 호출 이라는 것은 우리가 앞에서 봤던 것처럼 명시적으로 폼을 전송하는 방식이 아니라, 사용자 입장에서는 멈춰 있는 페이지의 뒷단에서, 자바스크립트 라이브러리인 AJAX 를 통해서 데이터를 주고 받는 것을 이야기 한다.

 

  아마도 2세대까지의 정식으로 HTML 페이지가 요청되고, 받아지는 부분과 상이한 움직이라고 해서, 비동기라고 지칭되긴 했지만, 사실상 엄밀히 따지면 요청하고 받는 행위가 페이지의 뒷단에서 벌어지는 것 뿐이므로 사전적 의미에서의 비동기 라는 말이 적합한가도 싶긴하다. 여하튼 페이지가 멈춰진 상태에서, 보통 사용자의 액션이나 주기적인 스케줄링에 기반해 트리거 되어 API 등을 호출해 데이터만을 교환하며, 가져온 데이터를 화면에 업데이트 하거나, 사용자가 인지 못하는 뒷단에서 광고, 사용성 추적 등을 목적으로 서버로 데이터를 전송하는데 사용하기도 한다.

 

 

  위의 그림에서 보듯 클라이언트와 서버 사이에 데이터의 교환만이 있기 때문에, 2세대와 같이 폼이 왔다갔다 하지 않는다. 아마도 초기에는 스트링 형태로 전송되다가 복잡한 데이터를 구조적으로 전송하기 위하여 XML 을 시용하다가, 점점 실용적으로 프로그램 간에서 교환할 수 있는 JSON 으로 변환되어 왔다고 보며, 현재는 거의 XML 과 JSON 으로 통일되어 데이터가 교환되는 것 같다.

 

  JSON 을 살펴보게 되면 항상 Serialize, Deserialize 라는 낯선 용어가 언급되게 되는데, Serailize 는 "일반 문자열로 만들기", Deserialize 는 "프로그램에서 사용하는 객체 형태의 데이터로 만들기" 정도로 해석하면 어떨까 싶다. 더 궁금하면 파이썬 19, 20교시에서 예제를 보면 된다. 

 

  클라이언트 프로그램 내(자바스크립트)에서 사용하는 딕셔너리나, 리스트 구조를 웹으로 전송하기 위해 {}, 등의 문자로 이루어진 문자열 형태로 압축해서 만들고, 네트워크 상에서는 문자열 형태로 전송 된다. 다시 서버 쪽의 언어에서 해당 값을 받은 후 다시 프로그램내에서 사용하는 자료구조인 리스트나 딕셔너리 구조로 바꾸어 다차원적인 데이터 형태를 무너뜨리지 않고 자연스럽게 데이터를 교환하는 형태이다. 뭐 만화의 한 장면 같은 예를 들자면 냉장고를 접어서 서류봉투안에 넣어 배달하고 다시 꺼내어 펼치면 냉장고가 되는 느낌이라고 할까. 그래서 단순한 교환을 위한 데이터 형태인 XML 보다, 프로그램에서 자주 사용하는 데이터를 유지할 수 있는 JSON 이 인기 인것 같다(물론 XML 도 같은 역활을 해주는 라이브러리가 있지만 두 개의 언어를 써보면 JSON 이 압도적으로 실용적인것 같다. 반대로 얘기하면 데이터 정합성에 엄격한 분야에서는 아직도 XML 을 선호할 것도 같다). 물론 여기서 보안적으로 중요한 사실은 전송되는 형태가 평범한 "문자열" 이라는 것이다.

 

 

  Ajax 에 대한 코드 샘플은 파이썬 글 18교시를 보거나, 뒤의 피들러로 시연하는 샘플을 통해 보면 될것 같고, 여기서는 일상속에서 많이 보이는 예제 하나를 보여주고 마무리 하려 한다. 아래는 구글의 검색어 추천 로직으로, 사용자가 c, a, t 즉 cat 이라고 적을 때마다 하단 박스에 해당되는 추천 검색어가 나오게 된다. 그럼 이 부분을 피들러를 켠 채로 실행시키고 피들러로 잡힌 호출을 보도록 하겠다.   

 

 

  피들러를 보게되면(요즘은 웹 뒤에서 몰래 돌아가는 호출이 많아서, 잡다한 호출 등은 가독성을 위해 정리했다), 아래와 같이 /complete/search 라는 url 이 3번 호출됬다. 상단 request 섹션의 Webforms 탭을 클릭해보면, 사용자가 입력하는 글자는 q 라는 이름을 가진 인자와 매핑되어 있다. 위의 요청 부터 하나하나 클릭해 보면 q 값이 c, ca, cat 로 변해가는 것을 볼 수있다. 하단 response 탭의 JSON 탭을 보면, 피들러가 왔다갔다 하는 데이터를 JSON 형태로 해석한 부분이 보인다.

 

  거기에 보면 트리 형태로(원래는 아까 얘기했듯이 {}, 등으로 구성된 문자열을 피들러가 해석해 트리 모양으로 보여주는 것이다) "cat", "cat<b>egory</b>" 등의 상단 그림에서 봤던 추천 검색어 들이 구성되어 있다, 아마도 구글의  메인 페이지를 찬찬히 뜯어보면 저 /complete/search url 을 AJAX 방식으로 호출하고, 결과 값인 JSON 데이터를 받아, DOM 구조를 이용해 숨켜놨던 네모 박스를 보여주면서, 그 안에 가져온 데이터를 뿌려주는 로직이 있을 것이다.

 

  그러면 평범한 사용자들은 해당 액션을 보고 내부에 이런 왔다갔다 함이 있음을 인지하지 못하고, 내가 입력한 값에 대해서 사이트가 신기하게도 추천 검색어를 뿌려주는 거나 생각을 할 것이다. 다만 보안적인 입장에서는 화면보다는 이 뒷단의 데이터로 주로 봐야하는 것이 다를 뿐이다(그러고 보면 웹은 거대한 눈 속임수 같은 느낌도 있지만 그건 뭐 디지탈화된 영상이나 사진 등도 실상은 마찬가지니...)

 

 

 

 

2.4 클라이언트 vs 서버 코드

  위의 장황한 이야기의 결론은 아래의 그림을 표시하기 위해서였다. 우리는 여러가지 예제를 피들러를 통해 살펴 봄으로써 HTML 코드나, CSS, 자바스크립트 등을 평문으로 볼수 있고(피들러의 Response 섹션의 TextView 탭을 보면 된다), 날라가는 폼 값도 피들러로 볼수 있고(이건 WebForms 탭), HTTP 헤더내에 있는 여러 쿠키나, 사용자 에이전트, referer 등도 볼수 있었다(이건 Headers 탭). 또 한 한발 더 나아가 브라우저의 뒷면에서 남 모르게 날라가는 AJAX 데이터의 입력과 출력도 하나하나 볼수 있었다. 결국 이러한 데이터들을 피들러로 볼수 있다는 얘기는 피들러로 "조작"이 가능하다는 얘기가 된다. 다음 섹션에서 이러한 코드를 하나하나 피들러로 조작하는 예제를 보여주면서 해당 부분을 살펴보려 한다.  

 

  반대로 우리가 피들러로 볼수 없어 조작할 수 없는 코드는 서버 사이드에서 돌아가는 로직 들이다. C++(레거시 CGI 에서 사용), C#, JAVA, PHP 같은 언어로 구성된 코드들은 우리는 내용을 볼 수도 조작할 수도 없다. 하지만 불행하게도 서버 뒷단의 로직은 사용자의 액션에 따라 특정한 로직을 지원하기 위해 생성된 경우가 대부분이기 때문에, 자바스크립트나 HTML 을 잘 분석하거나 폼이나 AJAX 의 입출력 인자를 살펴보거나, 히든 필드를 보거나, 비즈니스 로직을 살펴서 많은 유추가 가능하다.

 

  그리고 서버 뒷단의 로직은 결국 클라이언트에서 날라간 폼 값이나 AJAX 데이터 값을 기반으로 동작하기 때문에 해당 로직을 조작하여, 코드에 영향을 미치려는 시도를 할 수 있다(이게 웹 해킹의 많은 부분을 차지한다고 본다). 마지막으로 서버가 탈취 당했을 때도 공격자의 역량에 따라 서버 사이트 코드에 영향을 미칠 수도 있다(요즘은 빌드된 언어가 많아서, 스크립트 코드보다는 빌드된 형태의 코드를 원하는 데로 조작해야 되서 난이도가 더 높을 듯은 싶다) 

 

 

 

 

 

2.5 피들러로 클라이언트 코드 조작해보기

  그럼 앞에 설명한 클라이언트 예제들을 하나하나 조작하는 것을 시연해 보면서, 각 행위가 웹 어플리케이션의 보안적 측면에서 어떤 의미가 살펴보도록 하자. 앞에 만든 쿠키 페이지에 코드를 추가해 샘플 페이지로 만든 것이 아래와 같다.

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
<script>
function validateForm() {
    var x = document.forms["Custom"]["outMoney"].value;
    var y = document.forms["Custom"]["customID"].value;
    
    if (x > 10000) {
        alert("10000원 초과 출금 안됨");
        return false;
    }
    else if (x == null || x == "") {
        alert("찾으실 금액을 넣어주세요");
        return false;
    }
    
    if (y == null || y == "") {
        alert("아이디를 넣어주세요");
        return false;
    }
    
}
</script>
 
<%
    ' 폼과 쿠키 값 받기
    strCustomID = request("customID")
    strOutPut = request("outMoney")
    strLoginUserID = Request.Cookies("LoginUserID")
%>
 
<html>
   <head>
      <title>인출 하기</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="GiveMe.asp" onsubmit="return validateForm()">
        <table width = 600>
            <tr>        
                <td width = "60"> 아이디 </td>
                <td width = "100"> <INPUT maxlength="4" name="customID" size="10" type="text" value=<%=strCustomID%></td>
                <td> | </td>
                <td width = "130"> output money </td>
                <td width = "100"> <INPUT maxlength="10" name="outMoney" size="10" type="text" value=<%=strOutPut%></td>
                <td>&nbsp;<input name=button type=submit value="현금 인출하기"></td> 
            </tr>      
        </table>   
    </form>      
    
    <hr>
    <br><b>결과</b>
    <br><br>
        
<%
    ' 1) 전달된 쿠키 표시    
    Response.Write "1. 서버로 들어온 쿠키는 : " & strLoginUserID & "<br><br>"
        
 
    ' 2) 등록된 회원일 경우 쿠키를 생성함
    Select Case strCustomID
        Case "tom"
            Response.Write("2. tom의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "tom"
        Case "lucy"
            Response.Write("2. lucy의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "jerry"
        Case "secret_code"
            Response.Write("2. secret_code의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "secret_code"
        Case Else
            Response.Write("2. 인식 못하는 에러 발생"& "<br><br>"
            Response.end
    End Select
    
    '3) 금액을 인출 해줌.
    Response.Write "3. 인출 금액 : " & strOutPut & " 원" & "<br><br>"
%>
 
</body>
</html>
cs

[GiveMe.asp]

 

  코드를 보면 맨 위에 자바 스크립트로 입력 값이 빈칸인지를 간단히 체크하고, 인출 하려는 금액이 10000원을 초과하면 alert 을 띄우는 체크 코드가 있다. 그 밑에 보면 넘어온 폼 내의 아이디와 금액 값과, LoginUserID 라는 쿠키 값을 받아서 프로그램 내부 변수에 담는다.

 

  그 아래의 HTML 코드는 사용자에게 값을 넣게 하거나, 처리 결과를 보여주게 하는 페이지이다(복잡성을 낮추기 위해서 하나의 페이지에서 입력과 출력을 동시에 하게 했다).

 

  그 다음 VBSCRIPT 코드(ASP 는 해당 VBSCRIPT 문법임)에서 1) 서버에서 인지한 쿠키를 표시하고, 2) 맞는 아이디가(tom, lucy, secret_code)가 들어왔을때 해당 쿠키를 만들어 준다. secret_code는 사실 이 페이지에서는 입력할 수 없는 디버그 아이디로 HTML 코드에 보면 custID 라는 name 속성을 가진 인풋 박스의 maxlength 값이 4로 되어 있어 페이지에서는 4글자 이상이 입력되지 않고 있다. 3) 마지막으로 아이디가 잘 맞는 경우 해당 입력한 금액을 인출해 준다(사실 이 부분도 해당 아이디에 지불할 금액이 있는지 등등의 체크를 하기 위해서 디비를 다녀오고 그래야 하지만 예제의 간결성을 위해서 과감히 생략했다).

 

 

  c:\inetpub\wwwroot 폴더에 GiveMe.asp 라고 저장하고, http://localhost/GiveMe.asp 를 호출해 실제 페이지가 뜬 화면을 보면 아래와 같다.

 

 

 

2.5.1 전송하는 폼 값 바꾸기

  첫 번째로 전송하는 폼 값을 바꿔 보는 예제를 보기로 하자. 현재는 자바스크립트로 막혀있어 10000원을 초과하는 금액을 인출할 수 없는데 1,000,000원을 인출해 보기로 한다. 우선 피들러를 키고 빈 페이지를 호출 한다(http://localhost/GiveMe.asp). 피들러의 숨은 기능 중 하나가 IDE 같이 호출 중간에 디버깅 기능을 걸수 있는데, Burf 나 Paros 같은 프록시 툴에서 하나하나 페이지를 보면서 넘기는 것과 비슷하다(피들러가 동작하는 원리는 파이썬 글 10교시에서 설명했으니 궁금하면 보고 오면 된다)

 

 

  피들러 상단 메뉴에서 "Rules > Automatic Breakpoints > Before Requests" 를 체크 한다.

(이 상태에서는 피들러가 브라우저의 모든 호출을 잡아버리기 때문에 작업을 끝내면 꼭 피들러를 끄거나 해당 값을 "Disabled" 로 바꾸어야 웹 페이지 들이 정상으로 호출된다)

 

 

  이후 GiveMe.asp 페이지에서 lucy 를 입력하고, 1000000 을 입력한 후, "현금 인출하기" 버튼을 클릭 한다. "1000000원 출금 안됨" 이라는 alert이 뜰 것이다. 자바스크립트에서 막은 것이기 때문에 브라우저 프로세스 내에서 일어난 일이며 실제 서버로 요청은 안 간것이기 때문에 피들러에는 아무것도 잡히지 않는다.

 

 

  이제 금액을 1000 으로 조정 한후, "현금 인출하기" 버튼을 클릭한다. 브라우저 화면을 보면 탭 이름 부분의 앞이 빙글 빙글 돌아가는 표시가 보이며 localhost 서버의 응답을 기다리는 중이라는 메시지가 표시된다. 현재 상태는 브라우저가 사용자가 입력한 폼 값을 서버로 전송했지만(여기서는 로컬 서버이긴 하지만), 피들러가 중간에서 잡고 서버한테 전달을 안하면서 땡깡을 부리는 중이라고 볼수 있다.

 

 

  땡깡을 부리고 있는 피들러 쪽으로 가보면 기존에 못보던 "Run to Completion" 이라는 버튼이 오른쪽 상단의 Request 섹션에 생겨 있다. "WebForms" 탭을 선택하면 우리가 입력한 lucy 와 1000 이라는 숫자가 있다. 아래 그림과 같이 outMoney 에 있는 값을 1000000 으로 살포시 바꿔보자. 이후 "Run to Completion" 버튼을 누른다.

 

 

  이렇게 되면 브라우저가 체크해 주던 자바스크립트의 방어 구역을 벗어나 네트워크 상에서 피들러가 해당 값을 수정한 것이기 때문에 서버에는 해당 값이 그대로 전달되어 버리게 된다. 서버의 페이지를 보면 아래와 같이 인출 되지 말아야될 lucy 의 돈 백만원이 인출 되고 만다.

 

  해당 케이스가 아주 간단해 보이지만, 거의 모든 가격 및 사용조작에 영향을 미치는 큰 행위의 원리가 되는 행위이다. 해당 부분은 브라우저의 사용자 인터페이스로 제한된 한계를 넘을 수 있느냐 없느냐를 인지하는 부분이기 때문이다.

 

  예를 들어 주문서 페이지의 HTML 과 자바스크립트 구조를 분석하여 서버로 날라가는 여러 값들을 잘 정리할 수 있다면(이 변수는 가격, 이 변수는 적립금, 이 변수는 쿠폰, 이 변수는 할인 금액, 이 변수는 본인인증 여부 등등) 해당 값들이 왔다갔다하는 적절한 타이밍에 값을 바꿔치기 해주면 원하는 값을 조작할 수도 있다. 신문에 가끔 나는 해커가 몇백만원 짜리 물건을 몇백원에 결제해서 악용하다 감옥에 갔다는 얘기는 사실 이 단순해 보이는 행위에서 시작된다. 물론 페이지를 분석해 관련 로직을 이해하고 원하는 요소들을 찾아내는 능력은 별개로 필요하긴 하지만 말이다. 

 

 

  그럼 이렇게 큰일에 대해서 어떻게 대비를 하냐 얘기한다면, 해결 방법은 클라이언트 코드를 신뢰하지 않음 된다고 얘기하고 싶다. 위의 ASP 코드 중 인출 부분을 방어하는 코드로 바꿔본다면, 아래와 같이 폼으로 넘어온 값을 한번 더 체크하는 수정을 하면 된다.

1
2
3
4
5
6
    '3) 금액을 인출 해줌.
    If strOutPut > 10000 Then
       Response.Write("3. 너무 많은 금액임<br><br>")
    Else     
       Response.Write "3. 인출 금액 : " & strOutPut & " 원" & "<br><br>"
    End If
cs

 

 

  해당 코드를 수정 한후 저장 후, 같은 행위를 하게 되면 아래와 같이 서버에서 너무 많은 금액이라고 막히는 화면이 나온다(확인 후에는 시연을 위해서 다시 예전 코드로 돌리거나 주석을 하자^^. 브레이크 포인트도 disabled 시키는거 잊지 말고..)

 

  이 아주 작은 차이 하나가 사이트를 취약하게 하느냐 취약하지 않게 하느냐의 차이를 만든다는 사실은 조금 우습기도 하지만, 사실 이 차이는 개발자가 클라이언트 코드와 서버 사이드 코드의 차이점을 정확하게 이해하고, 클라이언트 코드를 믿지않고 서버 사이드에 최종 방어 코드를 넣어두었냐는 큰 차이기도 하다. 기능 구현과 성능과 버그에 집중하고 있는 개발자가 보안적인 관점에서 코드와 코드가 돌아가는 시스템 환경을 보는 것은 어느날 쉽게 얻을 수 있는 부분은 아닌 것도 같다.

 

 

 

2.5.2 HTML DOM 구조 바꾸기

  두 번째는 HTML 소스 구조를 바꾸는 부분이다. 이 부분은 개발자 도구를 이용해서, 브라우저 메모리 상에서 바꿔치기 할수도 있지만, 여기서는 피들러를 통해서 네트워크 상에서 바꾸는 예제를 보려한다. 시나리오는 아까 아이디 입력 부분이 maxlength 로 제한되어 4글자 이상 안들어 가고 있는 상황에서, 숨겨진 아이디인 secret_code 를 넣어서 돈을 인출 하게 하는 것이다. 물론 앞에 있던 전송되는 폼 값에서 아이디를 바꿔치는 방법도 있겠지만, 여기서는 다른 측면을 보려고 한다.

 

  이번엔 피들러의 브레이크 포인트를 아까와는 반대로 "Rules > Automatic Breakpoints > After Reponses" 를 선택하여 건다(아까의 아래 메뉴니 굳이 화면은 첨부 안해도 될듯 하다)

 

  피들러가 reponse 를 잡는 부분을 확실히 보기 위해서 브라우저를 모두 종료한 후 다시 띄운 후 빈 창에서 http://localhost/GiveMe.asp 페이지를 호출 한다. 그럼 다시 아래와 같이 페이지가 안 뜨고 완전히 빈 화면만 나오게 된다(브라우저에 따라서 아까 처럼 빙글빙글 도는 표시가 있을 수도 없을 수도 있다). 현재는 브라우저가 서버 쪽에 요청을 한 후, 서버 쪽은 브라우저에게 표시할 페이지에 대한 HTML 을 전달해 줬는데 피들러가 잡고 브라우저에게 아직 안 주고 있는 상황이다.

 

 

  피들러 화면으로 가면 이번에 하단 reponse 쪽에 "Run to Completion" 버튼이 보이고 있다. "TextView" 탭을 클릭하면 브라우저로 전달될 HTML 코드가 보이고 있다(참고로 인코딩이 뭔가 호환이 안되는지 한글은 깨져보인다). 해당 소스에서 name=custumID 인 인풋 박스를 찾아서 maxlength 를 4에서 30으로 바꾸어 본다. 이후 "Run to Completion" 버튼을 누른다(이제 브레이크 포인트는 풀어보자)

 

  이제 브라우저로 돌아가면 피들러가 이제야 전달해준 소스가 보이게 된다(한글이 깨지지만 신경쓰진 말자). 이제 인풋 박스 제한이 30글자가 되어 secret_id 가 입력이 되며, 실제 브라우저 소스 보기를 하면 30으로 조정된 값이 보인다. secret_id를 넣어 전송하는 것은 뻔하니까 굳이 시연하진 않는다.

 

 

  이 부분도 그래서? 라고 생각할 수도 있지만, 서버에서 클라이언트가 사용하기를 원해 전달했던 HTML 소스를 마음대로 변경할 수 있다는 부분에서 큰 의미를 가지게 된다. 그런 일은 없겠지만 마치 XSS 코드가 외부에서 들어온것 처럼 커스텀 코드가 들어갈 수 있으며(물론 자바스크립트 문법은 준수해야 페이지가 깨지지 않고 동작한다). 여러 설정해 놓은 히든 값이나, 숨겨놓은 코드, 더 나아가면 다른 페이지의 코드를 가져다가 붙이는 행위도 할 수 있다(원래는 특정 아이디나 조건에서만 쓸수 있는 결제 화면을 넣는다든지). 이 부분은 공격하는 사람의 상상력에 따라 얼마든 창의적일 수 있으며, 브라우저는 서버가 전달해준 코드라고 생각하기 때문에 XSS 공격같이 방어해주는 일도 없다(설사 방어해줘도 의미는 없겠지만 말이다).

 

 

 

2.5.3 자바 스크립트 조작하기

  이번에는 아까 백만원을 입력할 수 있었던 예제를 자바 스크립트를 조작해서 시연해 보도록 한다. 아까는 request 의 폼인자를 조작했다면 이번엔 response 를 조작해 본다. 3.5.2 와 마찬가지로 피들러의 브레이크 포인트를 "Rules > Automatic Breakpoints > After Reponses" 를 선택하여 건다.

 

  이후 똑같이 브라우저를 모두 끄고, GiveMe.asp 페이지를 호출 한다. 역시 마찬가지로 Response 섹션의 "TextView" 탭으로 가면 아까 ASP 소스내에서 코딩해 놨던 자바 스크립트 코드가 보인다. 해당 코드를 선택해서  함수 이름과 {} 스크립트 뼈대만 남기고 모두 지운다(뼈대를 남기는 이유는 아까도 얘기했지만 문법을 깨뜨리지 않기 위해서다. 문법을 깨뜨리면 브라우저가 동적으로 동작을 안하게 된다. 물론 저 함수를 지우고 HTML 쪽 이벤트를 수정해도 되지만 이게 더 편해 보인다). 이후 "Run to Completion" 버튼을 누르고 1000000 이상의 금액을 넣고 전송하면 자바스크립트 체크로직이 없어져서 전송이 된다.

1
2
3
4
<script>
function validateForm() {
}
</script>
cs

 

 

  여기에서 사실 자바스크립트는 페이지의 일부 이기 때문에 3.5.2 와 같이 당연히 조작 가능하긴 하지만,  보통 프로그램에서 자바스크립트 파일은 따로 떨어져 있는 경우가 많은데 해당 부분도 역시 마찬가지이다. 해당 현상을 이용해서 여러가지 브라우저 단에서 체크하는 검증 로직들을 이론적으로는 모두 우회할 수 있다고 보면 될것 같다(물론 서버쪽에 최종으로 검증하는 설계가 없다는 가정하에지만 말이다). 

 

 

 

2.5.4 쿠키 조작

  이번엔 지금까지 서버가 돈을 지불해야 하는 사람을 판단하는데 사용했던 쿠키 값를 수정해 보려고 한다. 브라우저에서 GiveMe.asp 페이지를 연 후 lucy, 100 을 입력 후, "현금 인출하기" 버튼을 클릭해 쿠키를 생성한다. 이후 그대로 한번 더 "현금 인출하기" 버튼을 클릭해 만들어진 쿠키를 서버로 전송해 쿠키가 만들어진 아래 화면을 확인 한다.

 

 

  그리고 피들러에 가서 3.5.1 에서 했던 것처럼 "Rules > Automatic Breakpoints > Before Requests" 를 체크 한다. 이후 다시 "현금 인출하기" 버튼을 누르면 피들러가 전송되는 요청을 잡게 된다.

 

  피들러 화면으로 가서 상단 request 섹션에서 Headers 탭을 선택하게 되면 HTTP 헤더로 전송되는 여러 데이터를 볼 수 있다. 그중 쿠키 부분의 값을 선택하고, 마우스 오른쪽 버튼을 클릭해서 컨텍스트 메뉴를 띄워 "Edit Header..." 메뉴를 클릭한다.

 

 

  편집 화면이 나오면 Values 부분에서 LoginUserID 를 lucy 에서 tom 으로 바꾼다. 이후 Save 버튼을 눌러 저장한다. 이후 앞에서와 마찬가지로 "Run to Completion" 버튼을 클릭하여 전송한다.

 

 

  이제 브라우저 쪽을 체크해 보면 넘어온 쿠키 값이 tom 으로 바뀌어서, 서버쪽에서 tom 쿠키를 출력해 주게 된다.  

 

  물론 실제 현실에서는 이렇게 쿠키를 평문 상태로 사용하는 경우는 별로 없지만, 암호화 하더라도 해당 값을 바꿔치기 할 수 있다는 사실은 변하지 않는다. 4교시에도 얘기했지만 암호화 하더라도 내부의 값의 고유성은 변경되지 않거나 혹은 1회성 토큰이 아닌 경우는 특정 시간 동안은 유효하기 때문에 때문에 relpay 등에 재사용 되는 경우는 있기 때문이다.

 

 

 

2.5.5 비동기 호출 조작(AJAX, API)

  마지막으로 AJAX 등의 비동기 호출을 조작하는 방법에 대해서 알아보자. 이 부분은 비동기 적인 호출 뿐만 아니라 API 등의 호출도 마찬가지이다(API 는 파이썬 글 10교시, AJAX 는 파이썬 글 18교시에 비교적 자세히 설명되어 있다)

 

  AJAX 예제를 보여주기 위해서 위의 페이지를 조금 수정 하고 getMoneyInfo.asp 라는 AJAX 호출시 호출할 페이지를 추가로 만든다

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
<script>
function validateForm() {
    var x = document.forms["Custom"]["outMoney"].value;
    var y = document.forms["Custom"]["customID"].value;
    
    if (x > 10000) {
        alert("10000원 초과 출금 안됨");
        return false;
    }
    else if (x == null || x == "") {
        alert("찾으실 금액을 넣어주세요");
        return false;
    }
    
    if (y == null || y == "") {
        alert("아이디를 넣어주세요");
        return false;
    }
    
}
</script>
 
<script>
function showMoneyInfo() {
  var xhttp;
  var loginUserCookie = getCookie('LoginUserID');
 
  xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (xhttp.readyState == 4 && xhttp.status == 200) {
      document.getElementById("txtMoneyInfo").innerHTML = xhttp.responseText+"원";
    }
  }
  xhttp.open("GET""getMoneyInfo.asp?myID="+loginUserCookie, true);
  xhttp.send();
}
 
 
function getCookie(name) {
  var value = "; " + document.cookie;
  var parts = value.split("; " + name + "=");
  if (parts.length == 2return parts.pop().split(";").shift();
}
 
 
</script>
 
<%
    ' 폼과 쿠키 값 받기
    strCustomID = request("customID")
    strOutPut = request("outMoney")
    strLoginUserID = Request.Cookies("LoginUserID")
%>
 
<html>
   <head>
      <title>인출 하기</title>
   </head>
 
<body>
     <form name = "Custom" method="get" action="GiveMe_Ajax.asp" onsubmit="return validateForm()">
        <table width = 600>
            <tr>        
                <td width = "60"> 아이디 </td>
                <td width = "100"> <INPUT maxlength="4" name="customID" size="10" type="text" value=<%=strCustomID%></td>
                <td> | </td>
                <td width = "130"> 출금 금액 </td>
                <td width = "100"> <INPUT maxlength="10" name="outMoney" size="10" type="text" value=<%=strOutPut%></td>
                <td>&nbsp;<input name=button type=submit value="현금 인출하기"></td>
            </tr>
            <tr>
            <td colspan=6>
                내 잔고: <input type="button" value="잔고 보기" onclick="showMoneyInfo()">
                : <span id="txtMoneyInfo"></span>
            </td>
        </tr>  
        </table>   
    </form>      
    
    <hr>
    <br><b>결과</b>
    <br><br>
        
<%
    ' 1) 전달된 쿠키 표시    
    Response.Write "1. 서버로 들어온 쿠키는 : " & strLoginUserID & "<br><br>"
        
 
    ' 2) 등록된 회원일 경우 쿠키를 생성함
    Select Case strCustomID
        Case "tom"
            Response.Write("2. tom의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "tom"
        Case "lucy"
            Response.Write("2. lucy의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "lucy"
        Case "secret_code"
            Response.Write("2. secret_code의 쿠키를 만듬"& "<br><br>"
            Response.Cookies("LoginUserID"= "secret_code"
        Case Else
            Response.Write("2. 인식 못하는 에러 발생"& "<br><br>"
            Response.end
    End Select
    
    '3) 금액을 인출 해줌.
    'If strOutPut > 10000 Then
       'Response.Write("3. 너무 많은 금액임<br><br>")
    'Else     
       Response.Write "3. 인출 금액 : " & strOutPut & " 원" & "<br><br>"
    'End If
%>
 
</body>
</html>
cs

[GiveMe_AJAX.asp]

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ Language=VBScript %>
 
 
<%
    ' 폼 값 받기
    customID = request("myID")
    
 
    '등록된 회원을 검색해 잔고를 얻어옴
    Select Case customID
        Case "tom"
            Response.Write("100,000")
        Case "lucy"
            Response.Write("200,000")
        Case "secret"
            Response.Write("100,000,000")
        Case Else
            Response.Write("error")
    End Select
%>
cs

[getMoneyInfo.asp]

 

  기존 자바 스크립트 코드 아래를 보면 showMoneyInfo 라는 새로운 메서드가 만들어져 있으며, 그 안에 보면 특정 쿠키 값을 받고(var loginUserCookie = getCookie('LoginUserID');), 해당 쿠키 값을 AJAX 호출을 통해 myID 라는 인자로 getMoneyInfo.asp 페이지에 전달해  잔고를 받아오며, 해당 응답 값을 받아오는게 성공 한다면 가져온 값을 HTML 소스에 추가한 txtMoneyInfo 라는 span 태그의 값으로 치환한다.

 

 

  그래서 아래와 같이 우리가 잔고보기 버튼을 눌렀을때, showMoneyInfo 메서드를 호출해서 해당 아이디의 잔고를 받아와 화면에 표시하게 된다. 조금 복잡해 보이는 AJAX 코드는 스트레스 받지 말고 그 라이브러리를 설계한 사람이 그렇게 만든거니 그렇게 쓰면 되려니 생각하면 된다.

 

 

  피들러를 켜고 위의 그림과 똑같이 lucy 와 100을 입력하고 현금 인출하기 버튼을 2번(쿠키 값을 페이지에 표시하기 위해 한번 더 눌렀다) 클릭한다. 이후 "잔고 보기" 버튼을 눌러보자. 그럼 페이지가 리프레쉬 되지 않고도 200,000원 이라는 잔고를 가져와 화면에 뿌려주게 된다(참고로 AJAX 호출을 할때, 같은 값이 인자로 넘어가게 되면 브라우저는 실제 호출을 안하고 캐시된 값을 보여주게 된다. 그래서 같은 쿠키 ID 값으로 다시 AJAX 호출이 되게 하고 싶으면 인자에 랜덤 값을 추가로 첨부하거나 브라우저를 껐다 켜면 된다)

 

 

  이 상황에서 tom 의 잔고를 알려면 어떻게 해야할까? 앞의 예제들을 생각해 보면 전송되는 쿠키 값을 바꿔치기 한 후 페이지에 들어와 "잔고 보기" 버튼을 눌러도 될것 같다. 그런데 조금 더 쉽고 직접 적인 방법이 있다. 피들러 화면으로 가면 "잔고 보기" 버튼을 눌렀을때, 우리가 만들어 놓은 로직에 의해 getMoneyInfo.asp 페이지(사실 여기서는 API 같은 역활이다)를 호출한 내역이 있다. request 의 WebFroms 부분을 보면 쿠키 값인 lucy 가 myID 에 담겨 날라가고 있고, response 의 TextView 를 보면 200,000 이라는 숫자가 넘어오는 것이 보인다(잘 시연할 수 있을듯 해서 스크린샷은 생략한다).

 

  그럼 피들러의 다른 기능인 Replay 기능을 이용해 보도록 하겠다. 해당 기능은 기존에 호출한 URL을 다시 똑같이 호출해 주게 되는데, 비단 URL 뿐만 아니라 우리가 앞에서 살펴본 쿠키등이 담겨있는 HTTP 헤더를 포함해 통채로 보내주게 된다. 그래서 만약 인증에 사용되는 쿠키나 토큰이 만료되지만 않았다면, 이전에 실행시켰던 환경과 똑같이 로그인 한 듯이 재생을 하게 해주는 것이다.

 

  추가로 아래 화면 처럼 마우스 오른쪽 버튼을 눌러서 Replay > Reissue and Edit 메뉴를 선택하면 재생 하면서 날라가는 값을 임의로 편집 할 수 있게 해준다 

 

 

  팝업 창이 뜨면 위쪽 request 쪽의 WebForms 탭에서 lucy 를 tom 으로 바꾼다. 이 후 "Run to Completion" 버튼을 누르면 우리가 조작한 값이 getMoneyInfo.asp 페이지로 넘어간다.

 

 

  이후 response 의 TextView 을 보면 tom 의 잔고인 100,000 원이 보이게 된다.

 

  이 사용자에게 숨겨진 AJAX 호출의 문제 중 하나는 개발자 들이 이 호출이 브라우저 상에서 보여지지 되지 않기 때문에, 서버 단에서 호출되는 것으로 착각하는 경우가 있다는 것이다. 그래서 이 호출을 조작 불가능한 호출이라고 생각하는 경우가 많다. 물론 서버 사이에서 서버끼리 API 를 호출하는 경우도 실제  있지만, 해당 경우도 전달되는 인자가 클라이언트에서 전달되는 값의 영향을 받는 다면, 마찬가지로 인젝션에서 자유로워 질수는 없다(결국 클라이언트 코드의 조작도 넓게 보면 인젝션의 일종이라고 볼수 있다)

 

  요즘은 많은 페이지들이 MVC 타입의 패턴을 채용하고 있다(이 부분이 궁금하면 파이썬 19, 20 교시를 보면 된다). 페이지는 화면에 뿌려주는 기능을 주로 맡고, 대부분의 데이터를 API 형태의 다른 서버에 질의해 가져오게 된다. 문제는 그러한 경우가 대부분 AJAX 코드로 이루어져 있고, 피들러로 보게되면 이렇게 잘 보이고 조작이 가능하다는 것이다. 뒷단에서 호출되는 AJAX 페이지나 API 들이 적절한 사용자 권한 체크를 하지 않는다면(예를 들면 이 종류의 데이터를 해당 사용자에게 전달해 줘도 되는가?), 합법적인(?) 경로를 통해서 시스템의 중요 데이터를 웹을 통해 외부에서 가져갈 수 있게 된다. 이런 합법적인 호출은 하루에도 다른 사용자 들에 의해서 수없이 호출되기 때문에, 운이 좋지 않은 이상 이러한 부분을 모니터링 하기도 무척 힘들다. 실제로 이런 사고들이 종종 나고 있고, 앞으로도 종종 날 것으로 예상된다.

 

 보통 일반적으로 개발자나 테스터는 시스템이 올바르게 돌아가는 측면에 대부분의 노력을 쏟기 때문에, 이렇게 페이지 뒤에서 돌아가는 상황에 대한 자세한 고찰을 하지 않는 면이 많다. 하지만 반대로 어플리케이션 보안을 생각하는 사람들은 이렇게 프로그램의 어두컴컴한 뒷골목의 쓰레기통을 뒤져야 하는 경향이 좀 있다. 

 

  수많은 개발자들이 만들어놓은 코드와 페이지를 소수의 사람들이 이렇게 삿삿이 뒤지는 것은 사실 시간이나 리소스상 어려운 일이고 그래서 열심히 여러 부가적인 활동을 통해서(설계 단계에서의 참여라든지, 소스 리뷰라든지, 가이드 라든지, 개발자 교육이라든지) 개발자의 도움을 받으려고 하는 노력들이 있다. 물론 스캐너 같은 자동화 툴도 일부 도움은 되지만 해당 부분에 대한 유용한 점과 한계에 대해서는 뒤의 다른 시간에 찬찬히 살펴보려고 한다. 

 

 

 

2.5.6 모든것에 대한 믹스 및 정리

  위의 시연한 예제를 기반으로 생각해 보면, 우리가 HTTP 통신을 사용할때 사용하는 폼, 히든 필드, HTML 소스, 자바스크립트, 쿠키 및 referer 등의 헤더 값들은 모두 피들러로 볼수 있었고, 결국 피들러로 수정 하여 조작할 수 있다고 볼 수 있다. 반대로 우리가 처음 1000000원 인출을 막았을 때 ASP 내에 넣었던 서버사이드 코드는 클라이언트 코드에서 영향을 미칠 수는 없다(다만 코드 설계에 취약점이 있다면 교묘하게 조작을 통해 우회만 할수 있을 뿐이다).  공격자는 사이트의 로직에 대해서 외부의 비즈니스와 내부의 볼수 있는 코드(스크립트, HTML)를 분석하여 공격 시나리오를 만들 수 있다. 웹 어플리케이션의 불행한 점 중 하나는 대부분의 방어를 위한 내부 로직들이 사용자 에러를 막기위해 자바스크립트 단에도 똑같이 복사되어 있어 내부 로직을 파악하기에 일반 어플리케이션 보다 좀 편하다는 것이다.  

 

  해당 클라이언트 쪽의 조작을 막을 수 있는 방법 중 하나로는 인자 값의 암호화 같은 수단이 있을텐데, 클라이언트 쪽에서 자바스크립트 등을 이용해 암호화 하는 것은 어차피 공격자도 훤히 볼 수 있는 상황이기 때문에 별 소용이 없고(자바스크립트 난독화를 한다고 해도 능숙한 공격자에게는 사실 시간끌기 퀴즈 풀이 정도 밖에 안된다고 본다. 현실에선 항상 히어로가 빌런보다 강한건 아니니까). 서버 쪽에서 암호화를 해서 클라이언트에 주더라도 난수, 시간 등의 더미 값을 넣어 항상 다른 값으로 만들어 재사용이 힘들게 하고, 상황에 따라 적절히 만료 관리를 해줘야 하는 부분이 있다. 실제로 전달되는 값이 암호화 되었다고 안심하다가 돌 맞는 경우도 있으니, 해당 부분은 설계 부분에서 잘 고려해야 한다. 결국은 클라이언트 코드를 조작하는 부분에 대해 최선을 다해 막는것이 맞겠지만, 한편으로는 어느 정도 마음을 비우고 서버사이드 설계와 사용자 액션에 따른 데이터에 기반한 모니터링에 초점을 맞춰야 하는것 같다. 이 보안적으로 안전한 설계 부분도 나중에 이 글의 마지막 시간 쯤에 함 다뤘음 좋겠다.

 

 

 

2.6 클라이언트 코드의 OWASP TOP 10 에서의 의미

  그럼 웹 클라이언트 코드의 마지막으로 지금 것 다뤄온 클라이언트 코드들이 실제 의미가 있는 요소인지를 보기 위해서 OWASP TOP 10 항목을 한번 보도록 하겠다. OWASP 는 자원자들이 모인 커다란 보안 커뮤니티로 2년에 한번 정도 빈도수와 중요도에 따라 세계에서 가장 많이 일어나는 10개의 웹 취약점을 정리해서 발표한다.

 

[OWASP 2017 한글판]

https://www.owasp.org/images/b/bd/OWASP_Top_10-2017-ko.pdf

 

  

  A1인 Injection 은 3교시에서 설명한 것 같이 외부에서 악의적인 코드를 실행하는 인자가 들어와서 이리저리 돌아다니다가 해당 코드를 해석하는 파서와 만나게 되면 실행되어 문제를 일으키는 SQL 인젝션, CMD 인젝션 등등을 얘기하고, A2 Broken Authentication 은 위에서 본 평문 ID 로 계정 관리를 한다든지, 암호화 하지만 만료 정책이 적절하지 않다든지, 패스워드 찾기에 취약점이 있다든지 하는 인증에 대한 설계가 외부에서 봤을때 악용할 수 있는 부분이 있을 경우를 얘기한다.

 

  A3 Sensitive Data Exposure 는 평문으로 중요 데이터를 전송해 공공 네트워크에서 사용했을때 피들러 비슷한 네트워크 툴들로 패킷이 해석되어 내용이 노출된다든지, 와이파이 등에서 암호화 방식이 부실해 내용을 크랙할 수 있다든지 하는 부분이고, A4 XML External Entities 는 XML 코드안에 이상한게 들어가서 문제가 나는 XSS 의 XML 버전이라고 봐도 될듯 하다.

 

  A5 Broken Access Control 은 허용되지 않은 방식으로 API 나 AJAX 를 호출한다거나, 인자를 바꾼다거나, 권한 없는 사용자로 권한 있는 데이터를 얻으려 한다던가 하는 부분이다. A6 Security Misconfiguration 은 9, 10교시에서 다룰게 될 내용으로, 이런저런 웹서버나 서버, 어플리케이션의 세팅 중, 나름 보안에 적절한 수학의 정석 같은 설정 항목들을 잘못 세팅한 경우이다.

 

  A7 Cross-Site Scripting 은 자바스크립트가 들어와 브라우저에게 영향을 주어 원하는 이득을 얻으려 하는 시도로 동기적, 비동기적으로 일어 날수 있으며(저장되어 영구적이냐 인자로 임시적이냐는 측면도 있다), A8 Insecure Deserialization 은 사실 A5의 하위 부분에 포함되어야 할 항목이라고 생각되는데, JSON 을 인자로 받아들이는 프로그램에서 조작에 대한 방비가 안되 있어 당하는 것이라서, AJAX 가 대중화된 요즘 워낙 자주 일어나는 일이라서 좀더 주의를 기울게 하기 위해서 따로 번호를 분리한게 아닐까 싶다.   

 

  A9 Using Components with Known Vulnerabilities 는 취약점이 있는 컴포넌트를 사용하는 것으로 네이버, 제로보드 같은 웹 게시판의 취약 버전을 패치 안하고 사용한다든지 취약할 가능성이 있는 오픈 소스를 사용하는 것인데, 사실 최신 버전을 사용해도 취약점이 없다는 보장을 100% 할순 없고(모든 최신은 또 조금만 지나면 과거기도 하고, 기능 변경등에 의해서 새로운 취약점이 나올 수도 있고 하니), 요즘 오픈 소스들은 사실 워낙 많은 라이브러리들을 공유해 사용하는 거 같아서, 하나의 핵심적인 모듈만 걸리면 우르르 걸리는 문제가 되는 부분이라서 참 애매한 것 같다. 여튼 상용으로 오랫동안 많은 회사에서 사용되거나, 소스가 공개되어 사람들이 많이 사용하고, 유지보수 하고, 안정화 된 오픈 소스를 사용하는 것이 최대한 현실적인 관점일 것 같다. 뭐 능력이 된다면 전체적으로 소스를 검토하고 쓰면 더 좋을 것 같고 말이다.

 

  A10 Insuffient Loging&Monitoring 은 2017년에 새롭게 나온 항목(내용이 새롭다긴 보다 항목이)으로 보안 팀들이 점점 데이터의 분석에 관심을 많이 기울이고 있다는 것을 증명하는 것 같다. 사용성과 보안은 반비례 한다고 하던 과거의 관점에서, 점점 두 가지를 양립시켜야 살아남을 수 있다는 관점의 변화와, 점점 살펴봐야 할 시스템 들의 다양함에 따른 복잡성과 상이한 데이터가 늘어나고 있는 상황에서는 어쩔 수 없는 부분인 것 같다. 하지만 여전히 개발자 입장에서 보안성에 필요한 데이터라는 주제는 피부에 닿기 힘든 부분이며(보통 개발자가 모니터링에 필요한 데이터를 개발 때 명시적으로 저장해 줘야 쉽게 가져갈 수 있지만, 기능에 필요없는 데이터가 저장되긴 참 힘든 것 같다), 정말 어떤 데이터가 모니터링에 필수적인 데이터인지를 찾아내는 작업도 쉽진 않기 때문에 꽤 어려운 주제 같긴 하다.

 

 

  음 이렇게 보면, 세계에서 제일 중요하다고 하는 10개의 취약점 중 앞에 별이 표시된 7개의 주제가 앞에서 설명한 클라이언트 코드에 대한 이슈라고 볼 수 있다. 그래서 앞에 설명한 간단한 예제들이 꽤 보안적으로 중요한 개념들이라고 말하고 싶다. 물론 자꾸 반복해 말하지만 해당 클라이언트 코드에 대해 제대로 이해하려면 시스템, 프로그램, 네트워크를 데이터의 흐름 관점에서 잘 이해해야 하고, 그 부분을 기반으로 새로 개발에 사용되는 주요한 시스템들을 계속 배워나가며 한다고 생각한다. 게다가 이 부분은 PC 나 모바일 코드로 넘어가게 되면 또 다른 측면의 이해가 필요하게 된다. 하지만 웹이든 OS 환경이든 클라이언트 코드라는 측면에서는 두 개의 타입과 원리는 비슷하다고도 말하고 싶다(물론 관련 기술을 이해할수 있다면 말이다). 그래서 보안 분야를 제대로 이해하고 싶다면 시스템과 데이터를 이해하려고 끝없이 노력해야 하는것 같다(물론 기술 이외에도 가끔 더 현실로 느껴지는 프로세스나 법률적, 관리적 측면에 대한 이해도 균형이 맞춰져야 하겠지만 말이다)

 

 

 

 

3. 모바일, PC 에서의 클라이언트 코드

  이 부분은 간단히 하나의 현실 예제를 보여주면서, 해당 영역에서의 클라이언트 코드의 의미와 가능한 방어 전략에 대해서 생각해 보려고 한다.

 

  앞에서 얘기한 것과 같이 이러한 일반 어플리케이션 레벨에서의 조작은 웹에 비해서 진입 장벽은 높은 것 같다. 일단 HTTP 같이 표준되는 규약으로 투명하게 데이터 흐름이 이루어진 것도 아니고, 피들러 같이 쉽게 특정 주제를 간단히 조작할 수 있는 툴은 드물다. PC 는 거의 디버거나 헥사 에디터의 도움을 통해서 분석 및 조작해야 하는듯 하고, 모바일은 디컴파일러나(보통 공통 가상 환경에서 돌아가는 코드들이 이 부분이 대부분 가능한 것 같다) 마찬가지로 디버거를 사용해서 코드를 해석하거나 조작 해야하는 것 같다. 그래서 기본적으로 어셈블리나 해당 저수준 API 레벨에서의 프로그래밍 지식에 익숙해져 있지 않으면, 관련 툴이 있어도손가락만 빨고 있을 수 밖에 있다. 또 프로그램들은 그러한 분석을 어렵게 하기위해 여러 디버깅 방지 로직이나, 난독화등을 적용하고 있다. 또한 플랫폼에 따라 OS 나 언어도 다양하기도 하지만, 무언가를 자세히 분석하는데에 그렇게 호의적인 환경을 제공하는 것 같지도 않다.

 

  또한 해당 프로그램 및 시스템의 지식은 낮은 레벨의 동작 분석에 집중되어 있기 때문에 파이썬 같은 다방면으로 쓸수 있는 언어보다 일반적인 관점에서 투자한 시간에 비해 유용성이 떨어지기 때문에, 해당 분야의 잡을 가지기 원하는 사람이 아닌 이상 시간을 많이 투여하기가 망설여지는 측면도 있다. 하지만 잘 경험해 놓는다면 남들이 가지지 못한 독특한 다른 시각을 하나 보유하게 되는 것은 부정하기 힘들 듯 싶다.

 

 

3.1 Flex2 살펴보기

  그럼 본론으로 들어가자. 일반적으로 모바일 어플 조작 방지를 위한 가이드를 보면 탈옥, 루팅시 설치되는 파일의 위치를 체크, 시스템 함수를 호출해 사용자의 권한을 체크, 바이너리 해시 크기를 체크, 프로세스를 체크하는 등 공식화 된 방어 방법을 권고 한다. 실제로 은행 등의 중요한 앱들은 이러한 부분들을 의무적으로 적용하고 있다. 여기에서는 해당 방식의 방어 로직들이 정교한 레벨의 클라이언트 코드 방어이긴 하지만, 공격자 입장에서는 완벽한 방어는 아니라는 부분을 주장해 보려 한다.

 

  아래의 어플은 flex2 라는 IOS 탈옥을 했을 경우 시디아라는 탈옥용 웹스토어 같은데서 구입할 수 있는 제한된 어플이다. 현재는 3버전이 나온 것으로 안다. 해당 어플은 탈옥 환경에서만 돌아가며 실행 중인 프로그램이 사용하고 있는 메서드 들을 열람할 수 있으며, 원하는 메서드를 선택 하여 지정한 값을 항상 반환하게 할 수 있다. 

 

  밑의 어플은 중국에서 서비스하는 스트리밍 음악을 들을 수 있는 무료 음원 어플이다. 아직 중국은 음원에 대한 무료 어플들이 일부 있지만 점점 저작권이 강해지는 추세라서(아마 글로벌 기업들이 중국 시장의 미래 수익를 고려하여 중국 정부와 협의해 특정 기간의 유예 특례를 준듯하다), 중국 내부의 IP 에서는 들을 수 있지만 그 외의 IP 에서는 재생을 막는 제약이 걸려있다. 아래 항목은 누군가 해당 부분의 코드 실행을 회피하게 만들어 flex2 에서 바로 다운 받을 수 있는 클라우드 환경에 올려놓은 회피 코드를 다운 받은 것이다. 

 

 

  해당 부분을 클릭하여 들어가게 되면 프로그램 내부에서 사용하는 중국내 IP 체크에 사용하는 여러 메서드 들이 보이게 된다. 물론 해당 패치 파일을 올린 사람은 실제로 디버거나 디컴파일러로 해당 프로그램을 분석해서, IP 체크를 수행한다고 판단되는 메서드 들을 한땀 한땀(이 표현이 맞지 않을까--;) 정리했을 가능성이 높다.  

 

 

  그 중 하나의 항목인 isChinaUser 항목을 클릭해 들어가 보면 "Return Value" 값에 1을 강제로 리턴하도록 설정 하고 있다. 해당 패치를 활성화 한 상태에서 조작하려는 특정 음악 어플을 실행하면 실행 시점에서 중국내 IP 를 체크하는 몇개의 메서드들의 반환 값이 강제되어 버려, 중국 외부에서도 중국 내부 처럼 음악을 들을 수 있게 된다(보안 교육 목적이지만 블로그에 올리기엔 불법적인 사항이라 판단하여 가능한 블라인드 처리를 많이 했다)

 

  실제로 이 어플을 이용해서 은행의 탈옥 감지등을 우회하는 예제도 종종 공유되고 있다. 우회 원리는 위와 마찬가지로 아까 얘기한 탈옥 경로 체크, 프로세스 체크 등의 여러 방어 기법들이 결국은 사용자 스마트폰에 설치되어 있는 모바일 바이너리 안에서 특정 API 를 호출하는 메서드로 구현되기 때문에, 해당 메서드를 정확히 분석하여 탈옥이 안됬다는 거짓된 값만을 리턴한다면 가능한 듯 싶다(제가 이 부분을 잘못 이해하고 있다면 누군가 잘 아시는 분이 교정 댓글 좀 부탁드려요^^) 

 

 

  물론 앞에서 얘기했듯이 해당 코드를 발견하는 부분은 어플리케이션의 난독화, 새로운 보안적 개발언어, 디버그 탐지 코드 등으로 더 어려워 질수는 있지만, 해당 트릭을 풀수 있는 능력을 가진 사람이 관심을 가진다면(금전적 이익을 얻을 수 있는 앱이라면 더 더욱 가능성이 높다고 본다), 파이썬 21교시에서 얘기한것 처럼 그러한 개인의 지식은 소프트웨어나 데이터 형태로 구현되어 일반적인 사람이 쉽게 사용할 수 있는 형태로 제공 될 수도 있다.

 

  결국 방어하는 입장에서는 강력해 보이는 바이너리 기반의 방어 방법이, 환경적 한계로 클라이언트 상의 방어일수 밖에 없기 때문에(OS 단에서 제공해 주는 보안 기능도 외부 공격자에게는 강력하지만, 내부 시스템 어드민 권한을 가진 악의적인 사용자의 조작에는 생각보다 취약할 수 있을 것 같다), 해당 방어 부분에 100% 기대지 말고, 프로그램이 탈옥, 루팅이 되어 많은 부분을 회피가능하다고 가정하고, 해당 상황에서도 최대한 방어할 수 있는 서버 연동 기반의 설계를 하는게 맞을 듯 싶다. 

 

  반면에 사용자 인증 측면에서는 한번 소유기반의 기반한 안정성에 이렇게 의심을 가지게 되면, 많은 민감한 부분들이 과도하게 위험하게 느껴지는 모순이 생기는 듯 싶기도 하다. 하지만 선의의 사용자와 악의의 사용자가 같은 무기를 지닐 수 있을 때는 악의의 사용자에게 촛점을 맞추는게 방어자 입장에서는 맞을 것은 같다. 이런 모호한 상황때문에 OWASP 같은 데서도 점점 데이터에 기반한 모니터링을 강조하는지도 모르겠다. 

 

  또한 웹 쪽과 비교하자면 서버 역할과 비슷한 부분은 OS 환경인것 같다. 백신이 상위권한을 가지고 엑셀을 열기전에 가로채 파일을 검사하는 것과 마찬가지로, 언제든지 OS의 취약점이 있어 어플리케이션의 입출력이 악용되거나 중간 통신을 가로챌 수 있기 때문에 클라이언트 코드 측면의 방어를 너무 믿으면 안 된다고 생각한다. 또한 현재의 많은 어플리케이션의 경우 껍데기는 해당 OS 의 프레임워크를 사용하지만, 백단에서는 실제로는 외부 API 서버와 HTTP 기반의 통신을 하는 경우가 많기 때문에, 피들러 같은 툴을 프록시 서버로 설정해 전송되는 데이터를 살펴보거나 조작할 수 있다. 이런 타입의 어플리케이션의 경우는 웹 프로그램의 관점에서 좀더 깊게 체크 하는 것이 더 맞을 듯 싶다.

 

  앞 부분에서 flex2 어플을 샘플로 보여주며 설명한 행위를 실제 코드 관점에서 설명하고 있는 글이 아래 블로그 글 일듯 싶다.

[IOS Hooking#2(Frida) - Bach's Bolg]

http://bachs.tistory.com/entry/iOS-Hooking2Frida?category=892887

 

 

 

 

4. 마무리 하면서

  여기 까지 오면서 웹 코드로 예시를 들면서, 클라이언트 코드에 대해 개인적으로 가지고 있는 생각을 전달하려 했는데, 얼마나 공감을 얻었을지 모르겠다. 개인적으로는 웹이나 어플리케이션이나 기술의 결이 다를 뿐이지 프로그래밍 보안이란 관점에서는 그리 큰 차이는 없는 듯 싶다. 모든 걸 깊게 잘 알고 있는 건 아니기 때문에 이런 종류의 의견엔 크게  자신은 없기만, 공부를 시작하는 사람들 한테는 이런 부분에 대한 소개도 의미가 있진 않을까 싶어서 글로 남겨 본다.

 

  개인적으로 점점 시간이 지날 수록 보안 뿐만 아니라 IT 전체에 있는 많은 분야에서 하는 일들이  결국은 다른 관점에서 시스템과 데이터를 이해하는 본질적으로는 같은 업무가 아닌가 하는 생각이 들곤 한다. 그 덕분에 왠지 알고 있던 사실들이 갑자기 무의미 해지기도 하고, 쉽게 생각했던 분야들이 어렵게 느껴지거나 그 반대의 일들도 종종 일어나고 있다.

 

 

2018.12.16 by 자유로운설탕
cs

 

  

 

posted by 자유로운설탕
2018. 7. 15. 20:18 보안

  이 시간에는 보안에서는 거의 필수라고 할수 있는 몇 가지 타입의 암호화와 복호화에 대한 얘기를 해보려고 한다. 다만 RSA 나 AES 와 같은 암호화 방식에 대한 수학적 전개에 대한 얘기를 하려는 것은 아니고(해당 부분은 많은 책이나 인터넷에서 자세히 설명하고 있다. 쉽게 이해가 잘 안되서 문제긴 하지만...^^), 보안 및 IT 쪽에서 암복호화가 어떤 의미로 쓰이고 있는지와 이해가 필요한 미묘한 점에 대해서 아는 범위 내에서 얘기해 보려 한다.

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계 문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

1. 들어가면서

  사실 암호화 라는 것은 현실과 비교하자면 우리가 주민등록증을 가지고 다닐 때, 남들이 다 볼 수 있도록 손바닥위에 놓고 다니지 않고, 지갑에 넣은후 주머니에 넣거나 가방에 넣어 가지고 다니는 것과 비슷하다고 볼 수 있다. 보통의 사람들 한테는(뭐 초능력자나 어벤저스가 있다면 모르겠지만) 그것을 투시할 수 있는 능력이 없기 때문에  남에게 보이지 않고 안전하게 다닐 수 있게 된다. 

 

  과거의 암호들은 셜록홈즈 소설에서 나오는 그림 암호 같은 형태로 숨겨진 특정 규칙에 의해서 문자를 뒤섞거나 매칭되어 있기 때무네, 그 숨겨진 규칙이 암호화 방식 차체가 되는 방식이였지만, 현대에서는 수학적 도메인을 이용하여 관련 계산식은 모두 공개하고, 몇 몇 변수 값을 감추어 어렵게 풀어야 하는 수학적 문제로 바뀌게 되었다. 그렇게 된 이유 중의 하나는 아마도 우리가 컴퓨터에 저장한 데이터 들이 결국은 2진수의 형태로 저장되어 있기 때문일 것 같다. 해당 저장된 수를 적당히 잘라서(블록화) 뒤섞거나 암호키인 숫자와 연산하여 의미가 없어 보이는 다른 숫자의 형태로 만들 수 있게 되었기 때문이라고 생각한다(인간이 인지할 수 있는 텍스트나 실행이 되는 exe 파일들도 디스크 상의 저장된 형태를 기반으로는 무척 큰 2진수나 블록으로 잘라진 작은 2진수 묶음 들에 불과하다). 결국 그러한 숫자를 뒤섞는 수학적 로직들이 AES, DES, RSA, ECC, SHA(해시) 등의 이름으로 암호화 알고리즘 이라고 불리우게 된다(머신러닝에서 통계적 기법들이 각각 하나의 머신러닝 알고리즘을 차지하는 거랑 비슷한 듯도 싶다). 

 

 

[셜록홈즈 춤추는 인형 - Sherlock님 블로그]

https://m.blog.naver.com/thdgnstjs/50077677206

 

 

  뭐 개인적으로 나눠본다면 암호를 1세대, 2세대, 3세대, 특수 형태, 제3자 인증 형태의 5가지로 나눠보고 싶다.

 

   1세대는 위의 셜록홈즈의 그림 암호같이 다른 매칭되는 매체들(예를 들어 이미지)과 연관되서나, 문자나 비트 등을 특정한 규칙에 의해 교환하거나 뒤 섞어 만드는 방식을 얘기한다. 예를 들면 프로그래밍에서 많이 쓰이는 Base64 인코딩이나, HTML 인코딩, URL 인코딩 등이 그러한 타입이라고 볼수 있다.

 

  2세대는 엑셀이나 zip 파일에 암호를 걸때 "1234" 를 입력해 암호를 걸었다면, 해당 문서나 zip 파일을 열때 같은 암호인 "1234" 를 넣어야 되는 것과 마찬가지인 타입이다. DES 나 AES 같은 대칭키 암호화 알고리즘이 이런한 타입이라고 볼수 있다.

 

  3세대는 우리가 계약서에 날인 할때 인감 도장을 두 개의 나눠가질 계약서 사이에 찍는 간인(間印)과 비슷하다고 봐도 될듯 하다. 한 쪽 계약서에 찍힌 반쪽의 도장은 다른 한쪽의 계약서에 찍힌 도장과 매칭이 되어, 두 개의 계약서가 서로 유효하다는 것을 증명하게 된다. 보통 양쪽 당사자의 도장을 다 찍기 때문에 상호 증명이 된다. 이러한 쌍이되는 키를 수학적으로 구현한 것이 RSA 나 ECC 라고 불리우는 공개키, 개인키를 가지는 비대칭키 암호화 알고리즘이다. 아래와 같은 증표의 나눠 가지는 것도 비슷하다고 봐도 될 것 같다.

 

[메데이아와 테세우스, 배신하는 딸들의 운명 - 윤단우. 그녀의 시선 블로그]

http://blog.yes24.com/blog/blogMain.aspx?blogid=iconblue&artSeqNo=6998225

 

  특수 형태는 개인적으로 프로그래밍을 공부하다 처음 접했을때 이해하기 힘들었던 해시 함수 이다. 해시 함수는 비유 하자면 사람이 지닌 지문이나 홍채와 비슷하다. 지문 또는 홍채가 해당 개인 자체는 아니지만, 개인만의 고유한 특징을 소유하여 한 개인의 신원을 증명을 할 수 있는 것 처럼 컴퓨터에 저장된 특정한 문장, 파일, 숫자에 대해서(앞에서도 얘기했지만 내부적으로는 결국 숫자로 이루어져 있기 때문에) 수학적 연산을 통해서 일정한 길이를 가진 고유의 숫자로 함축해서 표현하게 된다. 최종적으로 만들어진 것은 우리가 발급받은 주민번호와 같아서 정말 아주 특별한 경우가 아니라면 현실적으로는 겹치지 않는다(해당 부분은 홍채나 지문도 마찬가지이다). 실제 우리가 입력한 홍채, 지문, 패스워드 등의 정보는 숫자 형태로 변환한 후(디지털화 라고 한다), 해시함수를 통해서 고유 값으로 변환하여 데이터베이스나 OS에서 관리하는 비밀 장소에 저장하게 된다.

 

  마지막으로 제3자 인증 형태는 일반적인 관점에서는 암호화의 한 종류라고는 할수 없지만, 암호화의 쓰임과 등가거나 밀접히 연결되어 있기 때문에 여기서 연관하여 같이 설명하려고 한다. 제3자 인증 형태는 신원의 증명을 위해서 외부의 기관이나, 장치, 서버 등을 신뢰하는 것을 말한다. 현실로 따지면 계약시 동사무소에서 발급 받은 인감 증명서에 의해 계약이 효력이 생기는 것 같이, 신뢰할 수 있는(물론 해당 부분은 국가와 비슷하게 내부의 구성원들이 약속이나 이익이나, 법률등에 기반해 신뢰하는 것과 마찬가지로 절대적인 것은 아니다. 다만 믿지 않으면 현실적으로 개인이 살아갈 수 없거나, 많은 사람이 동의 하는 것이기 때문에 믿는 것이라고 볼수 있다. 우리가 현실의 돈이나 게임 내의 아이템, 비트코인 등을 교환 가능한 가치로 인정하듯이 말이다).

 

  이러한 부분들은 응용적인 측면이 강하기 때문에 앞의 4가지 타입의 암호화 와도 같이 맞물려 사용된다. 공개키-개인키를 이용한 전자서명의 기반이 되는 인증서의 존재를 특정한 상위 기관이 증명한다거나 하는 일도 벌어지고, 휴대폰, ARS 인증같은 소유기반의 인증이나, OTP 와 같은 소유와 서버 인증이 동시에 이루어지는 경우도 있고, 패스워드나 토큰 기반의 인증도 마찬가지로 서버 기반의 인증이라고 볼수 있다. 꼭 서버가 아니라더라도 모바일 폰에서 특정 횟수의 패스워드의 틀림이 있을때, 폰을 잠가버리는 행위도 제3자 인증 형태가 일부 차용된 것이라고 볼수 있을 것 같다.

 

  왜 암호화에 대한 얘기에 이런 제3자 인증을 끼워넣느냐고 생각할 수도 있지만, 암호화 라는 것은 키 측면에서 보면 해당 키를 가진 사람만이 암호화된 정보에 접근할 수 있는 인증의 개념도 포함되고 있기 때문이다. 마치 바위에 박혀있는 엑스칼리버를 아서왕 만이 뽑을 수 있는 것처럼 말이다. 여러 인증에서 사용하는 서버 쪽 만이 알고 있는 특별한 검증 정보 및 인증 로직은 암호화 알고리즘과 비슷한 측면이 있다. 뭐 여튼 암호와의 응용 측면까지 포함하려 생각하다보니 해당 개념을 빼고는 너무 사전적인 설명이 될것 같아서 포함하게 되었다.

 

  다만 앞에도 얘기했듯이 이 분류는 개인적으로 글을 구성하다 보니 개념적으로 나눠보게 된 것이고, 일반적인 보안 지식이나, 자격증 지식의 측면에는 타당한 분류는 아니라는 것을 잊진 말았음 한다. 그럼 각 요소들에 대해서 조금 더 자세히 들여다 보도록 하겠다.

 

 

 

2. 1세대

  1세대 암호화는 아주 옛날에는 풀기 불가능했던 암호화일지는 모르지만, 여러가지 수학적, 과학적 툴들이나 컴퓨터 프로그래밍이 발달한 현재에는 약점을 쉽게 드러낼 가능성이 높은 방식이다. 물론 특정 물리적 개체의 특성과 연관짓는다든지 하는 특수한 내부집단에서만 공유되는 비전서 적인 방법도 있을지는 모르겠지만, 보통 키 자체 또는 암호화 로직이 엄청 복잡해 지는 경우일 것이기 때문에 엄청 비효율 적일 것이다(예를 들어 특정 책의 본문 내용과 암호화 할 대상을 1:1로 섞는다든지 하는 방식이 있을 듯 싶다. 우리가 강력한 암호를 선택할 때 책의 좋아하는 문장을 적절한 특수문자와 섞어 암호로 사용한다든지 하는 것도 비슷할 것 같고 말이다). 이 쪽에서 보안 측면에서 생각할 문제는 이러한 방식에 대해서 유효한 암호화 라고 생각하는 보안적 측면에 경험이 적은 개발자 들을 종종 볼 수 있다는 것이다. 코드 안에서 AES 암호화를 수행 해야될 위치에 베이스64 함수를 이용해 인코딩을 한다든지 하는 식으로 말이다(쿠키에 베이스64로 인코딩된 인증 쿠키를 숨겨 놓는 등의 설계를 할 수 있다).

 

 

2.1 Base64

  세상에는 수많은 인코딩이 있지만(유니코드나, 각 언어별 인코딩, 더 나아가 스스로 만든 인코딩도 있을 수 있다) 몇 가지 보안적 측면에서 많이 거론되는 인코딩을 본다면 우선 "베이스64(BASE64)"는 어떠한 문자라도 웹을 통해 전달할 수 있는 아스키 형태로 변환 하는것을 말한다. 예를 들면 그림 파일을 업로드시 바이너리의 내용(이것도 앞에서 얘기했지만 어치피 특정한 숫자의 연속이다)이 베이스64식으로 인코딩 되서 HTTP Body 에 첨부되어 날라가게 된다(아마 뒤에서 업로드 코드를 시연할 때 실제로 보게 될것 같다).

 

[베이스64 - 위키백과]

https://ko.wikipedia.org/wiki/%EB%B2%A0%EC%9D%B4%EC%8A%A464

 

  실제로 베이스64 인코딩이 되는 부분을 보기 위해서, 여러 인터넷에 있는 툴이나 페이지를 이용해도 되지만, 여기에서는 다음 클라이언트 코드 시간에 사용하게 될 피들러를 설치한 후, 내장된 유틸리티인 TextWizard를 이용해 보도록 하겠다. TextWizard는 여러 종류의 인코딩 디코딩을 하게 해주는 유틸리티 이다. 해당 부분도 파이썬 10교시 WhoIS API 글에서 설명을 했기 때문에 그쪽에 있는 피들러 설치와 설정 방법을 참조해서 세팅하기 바란다. 현재 버전은 바뀌었지만 설치과정은 거의 비슷하다. 피들러를 실행하고 상단에서 Tools > TextWizard 나 TextWizard 아이콘을 클릭하면 아래와 같은 유틸리티 창이 나오게 된다.

 

  기본적으로 Transform 부분에 "To Base64"가 선택되어 있을 것이다. 위의 입력란에 넣은 텍스트를 베이스64로 인코딩 해준다는 뜻이다. 그럼 위쪽에 "안녕하세요." 이라고 넣게 되면, 하단에 베이스64로 인코딩 된, "7JWI64WV7ZWY7IS47JqULg==" 값이 나오게 된다.

 

 

 그럼 반대로 "7JWI64WV7ZWY7IS47JqULg==" 를 복사해서 위쪽 칸에 넣고, Transform 드롭박스에서 "From Baset64" 를 선택해 본다. 베이스64로 인코딩된 문자열로부터 평문을 만든다는 뜻으로, 다시 하단에 "안녕하세요."라는 문장이 나오게 된다.

 

 

2.2 URL Encoding

  2번째로 URL 인코딩은 브라우저의 주소창에서 사용되는 부분인데, 영어가 아닌 한글 같은 문자나, 공백 문자 등을 %OO 의 형태로 바꾸어 URL에서 사용할 수 있도록 하는 것을 말한다. 서버는 저 %OO으로 변환된 URL을 받아서 원래의 문자로 바꾸어 처리하게 된다.

 

[URL - Encoding URL - Regular Motion님의 블로그]

http://regularmotion.kr/url-encoding-url/

 

  이 부분도 TextWizrd 를 통해서 실제로 보도록 하겠다. 위쪽의 베이스 64위키 링크였던  "https://ko.wikipedia.org/wiki/%EB%B2%A0%EC%9D%B4%EC%8A%A464" 를 복사해서, TextWizard 에 복사하고, Transform 드롭박스에서 "URLDecode"를 선택 한다. 그럼 URL 뒤쪽에 암호와 같이 있었던 "%EB%B2%A0%EC%9D%B4%EC%8A%A464" 부분이 "베이스64" 라는 것을 알수 있게된다. 자세히 보면 뒤의 숫자 64는 아스키 코드이기 때문에 따로 인코딩이 되지 않고 한글 부분만 인코딩이 되어있다.

 

 

2.3 HTML Encoding

  마지막으로 HTML 인코딩은 설명하려 한다. <table>, <b>, <script> 와 같은 태그들은 원래 브라우저에서 문법 문자로 해석이 되어 테이블 형태, 볼드체 표기 등의 UI적 측면을 표시하거나 자바스크립트를 실행 하게 된다. 그런데 만약 해당 태그를 HTML 인코딩을 하게 되면, 태그 그 자체의 글자로 브라우저 화면에 표시되게 한다. 이러한 원리를 이용해서, XSS 공격을 방어하기도 한다.

 

[difference between url encode and html encode - 스택오버플로]

https://stackoverflow.com/questions/1812473/difference-between-url-encode-and-html-encode

 

  이것도 실제 보기 위해서 아래의 테이블을 보여주는 HTML 파일을 보도록 하겠다.

 

  가장 간단한 구조의 테이블 예제는 아래와 같다. <table> 태그가 맨 밖에 있고, 선(border) 굵기가 1 사이즈를 가진다. <th> 태그안에 제목인 '과자'와 '초콜릿'이  들어있고, 줄을 나타내는<tr> 태그가 두개 있고, 하나에는 '파이, 카카오45%' 가, 나머지 하나에는 '머랭, 카카오100%'  가 각각 <td> 태그안에 나눠 담겨 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<table border=1>
  <tr> 
     <th>과자</th>    
     <th>초콜릿</th>  
  </tr>  
  <tr>    
     <td>파이</td>
     <td>카카오45%</td>
  </tr>  
  <tr>   
     <td>머랭</td>
     <td>카카오100%</td>  
  </tr>
</table>
cs

 

 

  아무 폴더에나  파일형식을 "모든 파일"로 선택하고 "table_sample1.html" 이라고 저장한다. 이후 탐색기에서 해당 파일을 더블클릭해 실행하면 브라우저가 뜨면서 아래와 같이 테이블이 표시된다.

 

 

  이제 HTML 인코딩이 되는 경우를 보기 위해서, 해당 소스를 복사해서 TextWizard에 붙여 넣습니다. 이후 Transform 드롭박스에서 "HTML Encode" 를 선택한다.

 

   위와 같이 변환된 결과가 나오면, 아래의 결과를 드래그해 복사 후, 아무 폴더에나  파일형식을 "모든 파일"로 선택하고 "table_sample2.html" 이라고 저장한다. 이후 탐색기에서 해당 파일을 더블클릭해 실행하면 브라우저가 뜨면서 아래와 같이 랜더링된 테이블이 아닌 테이블 소스 자체의 문자로 표시 된다. 저 내용 안에 <script> 태그가 있다해도 일반적인 글자로 취급되서 실행이 안되니(마치 이전 SQL Injection 에서 Prepared Statement 호출시 인자가 문법 문자로 취급되지 않는 것과 원리가 비슷합니다), XSS 공격의 방어 수단으로 사용되기도 한다(이것도 좀 인젝션의 스토어드 프로시저나, Prepared Statement 처럼 원래 만들때의 의도는 XSS 방어를 생각하고 만들어진 것은 아니였을 것 같다)

 

 

  이렇게 위의 예제 3개를 보면 인코딩 이란것은 변환 로직에 기반한 암호화 이기 때문에 중요한 문자열을 저런 타입의 인코딩으로 변화해 보호하는 것은 의미 없는 행위라는 것을 이해할 수 있다. 이 세가지 인코딩을 확실히 이해하지 못하면 보안 공부할때 헷깔리는 경우가 종종 있으니 꼭 이해하고 넘어가길 바란다.

 

 

 

3. 2세대

  2세대가 되면서 암호화에 중요한 진전이 나타나는 데, 그것은 수학을 본격적으로 이용한다는 것이다. 그래서 2세대 이후의 암호화 방식은 수학 문제를 푸는 것과 같은데, 로직을 숨겨 놓은 1세대 암호화와는 다르게 2세대 이후는 전체 로직을 모두에게 공개 한다. 다만 그 수학문제를 풀 때 힌트인 암호화키를 모를 경우는 풀이 난이도가 엄청 상승되게 되어, 하나하나 숫자 대입 방식으로 풀려면 컴퓨터로 몇백년 만큼의 시간이 걸린다는 점이다.

 

  2세대 암호화는 대칭키의 성질을 이용한 암호화이다. 위에서 얘기했듯이 엑셀이나 zip파일에 암호화를 할 때 사용하는 방식으로, 암호화를 사용할때 사용한 키가 복호화 할때도 동일하게 사용된다.

 

 

  어찌보면 현실의 자물쇠-열쇠 조합과 비슷해 아주 직관적이라 암호화 계의 모범생이라고 보면 될것 같다. 그래서 현실에서 복사한 열쇠를 가족들과 나눠갖는 것과 마찬가지로 복사한 열쇠를 얼마나 조심히 잘 전달하고, 관리하는 지가 중요하다. 한 사람이라도 어수룩한 사람이 있게되면 자물쇠는 어처구니 없이 열리게 된다(물론 보안과 마찬가지로 중요한 자물쇠가 있는 장소 앞을 여러가지 물리보안이나, 추가적인 논리적 보안 수단을 통해서 보호해야 해야한다. 집문 앞에 CCTV 가 있다든지, 추가로 홍채 인식을 해야한다든지 말이다).  

 

 대칭키 암호화는 엑셀, zip 같은거 이외에도 우리가 잘 알고 있는 로그인이나, Gmail, 은행 등에서 사용하고 있는 HTTPS(SSL) 연결에서 키 교환 후 실제 데이터를 주고 받을 때도 사용하고, VPN 에서 VPN 장비와 컴퓨터 사이에서의 모든 데이터(여기서 암호화되어 전달되는 데이터 흐름을 터널링이라고 표현합니다)도 마찬가지 이다. 모바일 폰 내의 데이터 암호화도 사용자 패스워드에 기반한 키를 생성하여 대칭키 방식으로 저장하는 것으로 알고 있다. 그래서 패스워드를 모르면 데이터를 복구할 수 없다는 얘기가 나오게 된다. 또는 여러 회사나 단체에서 고객의 정보를 저장할 때, 중요한 개인정보나 카드 정보 같은 경우는 실제 프로그램 내부에서 불러와 사용해야 하기 때문에, 데이터베이스에 저장할때는 대칭키로 암호화해 저장하고, 필요할때 적절한 권한을 획득하여 복호화 된 평문 정보를 얻게된다.

 

  대칭키는 암호화 키에 풀이가 의존적인 수학 문제기 때문에 암호화키를 사용하지 않고 숫자들을 하나하나 대입할 경우 언제 쯤 답을 찾을 수 있는지가 중요하게 된다. 해당 시간이 짧거나, 많은 컴퓨터를 병렬로 연결하여 계산하거나, 몇 십년후의 컴퓨터에서는 금방 찾을 수 있는 수준이라면 나중에 현재 데이터를 암호화 해서 보호하고 있는 기술들이 모두 무용지물 되는 상황이 생길 수 있게된다. 그래서 여러가지 안전한 암호화 방식이 권장되고 있지만 가장 잘 알려지고 널리 쓰이고 있는 것은 AES 방식이다(보통 128, 256 정도의 키 길이를 가지며 키길이가 길어질 수록 2의 N승으로 풀이 난이도가 올라가게 된다).

 

 

  대칭키를 쓰다보면 몇 가지 정도의 주의해야 하는 이슈가 있는데, 그 중 하나가 키의 안전한 전달(교환)과 보관, 관리 이다. 만약 두 사람이 인터넷에서 특정한 대칭키를 기반으로 암호화해서 데이터를 보내기로 하면 최초 한쪽에서 만든 키를 전달해 줘야한다. 전화로 불러주거나, 직접 만나서 얘기해주거나, 종이로 적어 전해주거나, 암호화 해서 보낸후 구두로 암호화 키를 알려주거나 하는 많은 방법의 경우 "낮말은 새가 듣고, 밤말은 쥐가 듣는다"는 속담을 피해갈 수는 없게 된다. 또 암호화 키라는게 보통 의미 없는 숫자로 길게 만들어지기 때문에 가독성이나 암기하기에도 좋지 않다. 그래서 3세대에 설명할 비대칭 키와 인증이라는 개념을 이용해서 보통 현실적인 키교환을 하게 된다. 해당 부분은 비대칭 키 부분에서 설명하도록 하겠다(하지만 어쩌면 현실적으로는 사람이 와서 주위에 아무도 없는 것(도청장치 포함)을 확인한 후에 외운 내용을 불러주고 가는게 더 안전할 수도 있을 것도 같다).

 

  추가로 보관하는데 있어서도 암호키와 키를 사용하는 프로그램이 분리되어야 하는 이슈가 있게 된다(해당 부분은 법적인 이슈이기도 하다). 왜 그래야 하는지 생각해 본다면, 집문 앞의 화분뒤에 열쇠를 숨겨놓는 것과 같은 행위이기 때문이다. 물론 해당 집 문앞까지 가는 행위가 IT 로 이루어진 세상에서는 해당 서버내의 프로그램 실행 경로까지 침투해야되는 어려운 일이긴 할테지만, 일단 행위가 일어나게 되면 키의 탈취가 아주 쉬워지기 때문이다. 그래서 많은 프로그램 예제에 있듯이(이 글의 예제도 마찬가지 입니다) 소스 내부나, 컨피그 파일내에 암호화키가 덩그라니 들어있는 것은 옳지 않은 일이다.

 

  JAVA 등의 컴파일 되는 언어라도 바이너리를 분석석하여 끄집어내거나, 디버그 툴 등으로 해당 키 기능을 호출 할 수 있기 때문에, 집 앞에 구덩이를 파서 몰래 열쇠를 숨겨놓는 정도 밖에 안전하지 않다고 본다. 그래서 사실 여러가지 키관리 솔루션이나 설계를 이용하여, 권한 제어를 통해 키를 외부에 숨겨서 사용하긴 하지만 그것도 100% 안전한 방법이라고는 하긴 힘들것 같고(보안적으로 100% 안전하다는 것은 사실 좀 거짓말인듯도 싶다), 결국은 분리된 키관리와 함께 문앞에 낯선 사람이 맘대로 오지 않게하고, 혹시 오더라도 수상한 행동을 하지 않나 모니터링이 되도록 defense in depth 측면의 설계를 택하는게 현실적인 것 같다.

 

  마지막으로 한번 정해진 암호화 방식이나, 키를 바꾸는 것은 상당히 어렵기 때문에(프로그램 변경 이외에도 이미 기존 방식으로 암호화된 데이터가 있기 때문에, 해당 데이터를 모두 복호화 해서 평문으로 복구 후, 다시 새 방식으로 암호화를 하는 데이터마이그레이션을 해줘야 한다. 게다가 패스워드 등을 저장한 해시 같은 일방향 암호화 같은 경우는 원래 평문 값을 알 수 없기 때문에, 다시 사용자에게 값을 받기 전에는 기존 것을 없애고 새로 만드는 마이그레이션 자체가 불가능 하기도 하다), 처음 설계부터 이런 쪽은 제대로 표준과 법률에 맞추어 설계하는 것이 맞을듯 싶다.

 

 

  AES256 에 대한 예제는 파이썬에서 있던 예제를 가져와 보려한다. 우선 파이썬 3.7이 c:\Python 폴더에 설치되 있다는 가정에서 시작한다. 설치 및 실행 방법을 모른다면 파이썬 글 2교시를 참조하면 된다.

 

  파이썬 3.7에서는 pycrypto 의 상위 호환버전인 pycryptodome 의 한글 인코딩 문제가 해결되어 그대로 쓰면 될것 같다. 커맨드 창을 열고 PIP 명령어를 이용해 설치한다.

c:\Python\code>pip install pycryptodome

Collecting pycryptodome
...
Successfully installed pycryptodome-3.6.4

 

  예제는 아래와 같다.

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
# 모듈을 불러옵니다.
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
 
# 암호화할 문자열을 일정 크기로 나누기 위해서, 모자란 경우 크기를 채워줍니다.
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS).encode()
unpad = lambda s : s[0:-s[-1]]
 
 
# 암호화를 담당할 클래스 입니다.
class AESCipher:
 
    # 클래스 초기화 - 전달 받은 키를 해시 값으로 변환해 키로 사용합니다.
    def __init__( self, key ):
        self.key = key
        self.key = hashlib.sha256(key.encode()).digest()
 
    # 암호화 - 전달받은 평문을 패딩 후, AES 256 으로 암호화 합니다.
    def encrypt( self, raw ):
        raw = raw.encode()
        raw = pad(raw)
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key, AES.MODE_CBC, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) ).decode()
    
    # 복호화 - 전달 받은 값을 복호화 한후, 언패딩해 원문을 전달합니다.
    def decrypt( self, enc ):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv )
        return unpad(cipher.decrypt( enc[16:] )).decode()
 
 
# 암호화 클래스를 이용해 cipherinstance 객체를 만들면서, 암호화키를 넣습니다.
cipherinstance = AESCipher('mysecretpassword')
 
# 암호화를 합니다.
encrypted = cipherinstance.encrypt('감추고 싶은 말')
 
# 암호화 한 값을 다시 복호화 합니다.
decrypted = cipherinstance.decrypt(encrypted)
 
# 암호화 한 값과 복호화 한 값을 출력 합니다.
print('암호화된 값 : ' + encrypted)
print('복호화된 값 : ' + decrypted)
cs

[파이썬 소스 - aes_example_in_python.py]

 

   그럼 위의 파일을 aes_example_in_python.py 로 UTF-8 포맷으로 저장하고 실행 한다.

c:\Python\code>python pycrypto_aes256_sample.py

암호화된 값 : C0u2hrCZrAca69P7D3VFnSitOkTlPZbCxat9vMvZXN2cPSJUMu+Et3Ca+nZoYEO9
복호화된 값 : 감추고 싶은 말

 

  실행된 내용을 보면 같은 키로 암호화와 복호화를 하는 것을 볼 수 있다.

 

 

 

4. 3세대

  3세대는 비대칭 키라고 하며 공개키와 개인키라는 특이한 개념의 수학적 도구를 사용하게 된다. 2세대와 틀린 부분은 키가 하나가 아니라 2개로 이루어진 한 쌍이라는 것이다. 개인키로 암호화된 값은 공개키로 복호화 될수 있고, 공개키로 암호화된 값은 개인키로 복호화 될수 있다. 개인키를 가진 쪽이 좀더 빨리 복호화가 된다고 하는 것 같으나, 가장 주요한 차이는 공개키 쪽이 열쇠의 역활도 하지만 자물쇠의 역활도 한다는 것입니다(물론 개인키도 전자서명의 측면에서는 자물쇠의 역활도 한다고 볼수 있지만, 주로 하는 역활은 열쇠라고 보는게 맞을 듯 싶다)

 

  보통 알고리즘으로는 RSA 와 좀더 최근에 나온 ECC(elliptic curve cryptography-타원곡선암호) 를 사용하게 되는데 일반적으로 ECC가 같은 수준에서 키가 더 짧아 간결하다고 하고, 자세한 차이점을 정말 이해하려면 수학적 차이일 테니 양쪽의 수학적 원리를 이해해야 할듯하다.

 

[Which one is better: elliptic curve cryptography or RSA algorithm and why? - Quora 사이트]

https://www.quora.com/Which-one-is-better-elliptic-curve-cryptography-or-RSA-algorithm-and-why

 

 

  가장 흔히 쓰이는 분야가 앞서 얘기한 HTTPS(SSL)의 경우인데, 최초 브라우저와 서버가 서로 사용할 대칭키를 비밀스럽게 교환하기 위해서 사용한다. 다만 여기서 현실적인 딜레마가 생기게 되는데, 서버가 보내준 공개키를 어떻게 믿고 클라이언트 쪽의 브라우저가 암호화에 사용할 대칭키를 해당 서버의 공개키로 암호화해 보내느냐 이다.

 

  이러한 딜레마를 해결하기 위해 인증서의 개념이 생기게 됬는데, 사실상 해결책이 신뢰할 수 있는 국제 기관이나 국내 기관의 공개키를 클라이언트쪽 OS(예:윈도우즈, 리눅스)가 인지하여 관리하고, 해당 기관의 개인키를 이용해서, 서버 사이트의 공개키를 보증 하는 방식이다. 뭔가 절대적으로 안전해 보이진 않지만 마치 우리가 은행이나 국가를 믿듯, 일단은 OS나 저러한 (루트) 인증 기관을 믿는것이 전제로 되어있다(앞에도 얘기했지만 보안은 100%인 것은 없어 보인다. 사실 우리가 사는 현실자체가 100% 는 없기 때문에 당연할 것을 수도 있다). 

 

 여기서 또 전자서명이라는 개념이 추가로 들어오게 되는데, 하나의 개인키로 특정한 값을 암호화 해서 만들어 내면, 공개키를 가진 사람이 해당 값을 복호화 하게 되서 기대했던 일련의 값들을 얻을 수 있다면, 해당 복호화된 내용은 해당 개인키를 가진 사람이 작성했다는 것을 증명하게 된다는 것이다(왜냐하면 개인키는 그 사람만 가지고 있어야만 하는 것이기 때문이다-물론 해킹을 당하는 경우는 아니겠지만, 이런 아닌 경우는 비트코인 지갑이 털리거나 할 때 일어나는 일이 된다).

 

 

  예를 들자면 특정 A 회사가 서버가 추가되서 SSL 통신을 해야되는 일이 생기면, 베리사인 같은 루트 인증 기관에 1년에 특정한 금액을 내고 인증서를 발급 받게 된다. 인증서 안에는 A 회사 도메인의 주소, 인증서 유효기간 등등의 정보와, A 사의 공개키가 포함되어 있고, 그 모든 정보를 적절히 믹스하여 루트인증기관의 개인키로 암호화한 전자서명이 들어있다. 사용자의 브라우저에서는 해당 인증서를 받아서, 알고 있는 루트인증기관의 공개키로 전자서명을 풀어, 복호화된 내용이 인증서에 기재한 내용들과 일치하는지 확인한다. 내용이 일치하게 되면 A 사의 공개키는 믿을 수 있으므로, A사의 공개키로 HTTPS 통신에 사용할 대칭키를 암호화해 보낸다. A 사의 서버는 해당 암호화된 내용을 받아 개인키로 풀어 대칭키를 얻어낸 후, 이후 통신 부터는 해당 대칭키로 암호화해 내용을 주고 받게 된다(공개키-개인키 방식이 연산 비용이 많이 들어 대칭키로 실제 암복호화를 한다고 한다).

 

  개인이 받는 공인인증서도 비슷하다. 공개키를 증명하는 인증서를 신용평가 회사에서 받게 된다. 조금 다른 부분은 개인키 또한 같이 발급받아 보관하면서, 개인키는 유출이 되면 큰일이 나니까 따로 대칭키 방식을 이용해서 개인키를 암호화 하여 보관한다(공인인증서를 발급 받을때 입력하는 개인의 비밀번호가 해당 대칭 암호키 이다). 그럼 아마도 확실한 내부 동작은 모르겠지만, 은행 사이트에 접속하면 은행 쪽에서 개인의 인증서를 가져가 신용평가 회사의 공개키로 풀어서 내용을 확인해, 신용평가사에서 해당 개인 계좌주인에게 발급한 공개키라는 것을 확인하할 것이다. 이후에는 그 공개키를 이용해서 거래에 관련된 정보를 암호화해 보내거나, 같이 저장된 개인키를 이용해서 거래를 증명하는 전자서명을 보내거나, HTTPS 에서 사용할 대칭키를 보내거나 할것 같다.  

 

[공인인증서 비밀번호의 안정성 - CPUU의 DayDreamin']

https://cpuu.postype.com/post/177466

 

[네이버 애플리케이션의 전사 서명 원리 - 네이버 D2]

https://d2.naver.com/helloworld/744920

 

  실제로 보안 분야 뿐만 아니라(물론 안전하게 인증을 하면서 편하게 사용하기 위한 보안적 측면을 위해 도입된 것이겠지만), 비트코인, GIT, SSH, PGP, 빅데이터 시스템 쪽에서도 이러한 개념을 가져다 사용한다. 비트코인은 코인주소를 나타내는데 공개키를, 지갑 열쇠를 나타내는데 개인키를 사용하고, GIT 또한 SSH(Secure Shell) 연결을 위해 공개키-개인키 쌍을 생성하여 깃서버와 개인 컴퓨터가 나눠가져 해당 키 인증으로 사용을 한다. 리눅스도 마찬가지로 id/password 대신 공개키 인증을 통해 로그인을 하여 사용할 수 있다(그게 id/password 보다 더 안전하냐는 별개의 문제겠지만).

 

  PGP 도 각각의 공개키를 공개키 서버에서 얻어 공개키로 메일 내용을 암호화 안전하게 보내는 것이라고 보면되고, 하둡 등의 여러 클러스터링 설정 시에도 아마 주키퍼 등에서 공개키, 개인키를 이용해 서버간의 작업 수행에 대한 권한 인증을 하기도 했던 것으로 기억한다(앞의 공인인증서의 개인키와 마찬가지로 추가로 패스워드를 지정하기도 한다). 공개키, 개인키 쌍으로 로그인을 하는 것과 같은 경우는, 공개키가 본인만의 자물쇠를 나타내며, 등록한 자체가 해당 공개키를 믿는 것을 의미하는 것이기 때문에(이 자물쇠를 연다면 들여보내줘...), 마냥 공개적인 것만은 아닌 측면도 살짝 보이는 것 같다. 그래서 공개키-개인키의 이름에 넘 칩착하면 뭔가 꼬여 버릴 수도 있다. 공개키는 쓰임에 따라서는 마냥 공개된 키만이 아니라는 것을 염두에 두어야 한다. 

 

 

  그럼 뭔가 예제가 없으면 섭섭할 것 같아서, 조촐하지만 파이썬을 이용해서 RSA 공개키와 개인키 쌍을 하나 만들어 보도록 하겠다. 구글에서 "python generate rsa key" 라고 찾으면 아래의 깃허브 페이지가 나온다.

 

[Python PyCrypto: Generate RSA Keys Example.py - lkdocs 깃허브]

https://gist.github.com/lkdocs/6519378

 

  밑의 댓글하고 섞어서 RSA2048 키를 만들도록 적당히 주석을 달았다. PEM 포맷은 공개키, 개인키를 교환할때 사용하는 표준 포맷 중 하나이다(편하게 위의 인코딩+형식을 정의한 규칙이라고 봐도 될 듯 하다)

1
2
3
4
5
6
7
8
9
10
11
12
13
from Crypto.PublicKey import RSA 
 
# 새로운 RSA 생성 
new_key = RSA.generate(2048)
 
# 공개키를 얻어 옵니다.
public_key = new_key.publickey().exportKey("PEM"
 
# 개인키를 얻어 옵니다.
private_key = new_key.exportKey("PEM"
 
print (private_key)
print (public_key)
cs

 

   그럼 위의 파일을 aes_example_in_python.py 로 UTF-8 포맷으로 저장하고 실행한다.

c:\Python\code>python rsa_gen.py

b'-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAkhB0qnHrNpFO5rlyiMzBkU55pvIjj5Zgk3TfPEFXzMvyORW/\nuuiL8mIfmIaouPUj+eEPvfG7Zdk2rV2rf6bUL1Of9 .....

 

 

 

5. 특수형태 - 해시

  해시는 보안에 들어오기전 처음 프로그래밍 공부할때 봤을때 참 신기한 기능을 하는 함수라고 생각이 들었었다. 사실 해시의 유용함은 보안적 관점을 추가해 봤을때 가장 잘 이해되는 것도 같다. 

 

  해시 또한 수학적 개념으로 아무리 큰 숫자라도 해시라는 함수를 적용하게 되면 고정 길이를 가진(SHA256 같은 경우 256bit 길이의 숫자가 나온다는 의미이다) 숫자로 변환되게 된다. 해시가 유용한 것은 서로 다른 숫자라면 같은 해시 값이 나올 가능성이 거의 없고, 만약 있더라도 같은 해시값을 가지는 서로 다른 숫자를 의도적으로 만들기에는 너무 많은 시간이 걸린다는 성격을 가지고 있다. 또한 해시값으로 부터 원래 값을 유추하기가 아주 많이 힘들다는 특징도 가지고 있다(이것은 2, 3 세대 암호화와 비슷한 부분이다)  

 

 

  해시는 보통 검증을 위한 측면과, 정체를 들키지 않기 위해 숨기려는 2가지 측면을 동시에 가지고 있다. 검증하기 위한 측면은 백신 프로그램에서 특정 악성코드 파일의 해시 값을 비교 하거나, 게임이나 어플의 패치시 패치파일의 유효성이나, 설치된 어플이 조작되지 않았다는 것을 체크하는데도 해시 값을 사용한다. 또는 여러 파일 공유 사이트에서 불법, 저작권 파일 등을 체크해 내기 위해서도 파일의 해시값을 비교하곤 한다. 파일의 내용이 1비트만 바뀌더라도 파일을 구성하는 숫자가 달라지는 것이기 때문에, 새로운 해시값이 만들어지는 특징을 이용한 검사 이다. 비트 코인 같은데서도 채굴 및 블록의 검증을 하는 경우에도 사용하고 있다.

 

  또한 가입한 회원들의 패스워드를 저장할때도, 법적으로 해시화 해서 저장하도록 되어 있다. 사람들은 보통 여러 사이트에 동일한 패스워드를 사용하기 때문에, 특정 회사에서 저장한 패스워드의 평문을 복구할 수 있다면 위험 부담이 생길 수 있을 것이다. 그래서 혹시나 인터넷의 수상한 사이트(?)를 회원가입을 통해서만 사용할 수 있다면, 첫째는 사용하지 않는 것이 제일 좋을 테고, 어쩔수 없이 사용해야할 때에는 다른 주요 사이트에서 사용하는 id/password 로는 등록하지 않는 것이 좋아보인다.  그 사이트에서 패스워드를 해시를 해서 저장할지, 평문으로 저장할지도 모르고, 그것과 상관없이 중간에서 빼내어 다른데에 나쁘게 쓸지는 아무도 모르니까 말이다. 또 내부 직원은 풀수 있는 양방향 암호화로 저장하거나 하는 실수를 할 수도 있고 말이다.

 

  해시로 저장한 경우 원본 값을 알 수 없기 때문에,  사이트에서는 사용자가 입력한 패스워드를 받아서 해시함수로 넘긴 후, 결과에 나온 해시 값을 회원 가입때 입력했던 패스워드 해시값과 비교해서 검증하게 된다(그래서 사이트에서 비밀번호 찾기 할때 무조건 새로운 비밀번호로 바꾸지, 원래 비밀번호를 알려주진 못하게 된다. 만약 잊어버린 비밀번호를 알려주는 사이트라면 평문으로 저장됬거나 양방향으로 잘못 저장된 경우라고 논리적으로 확신할 수 있다).

 

 

  반대로 해시값을 숨겨야 하는 측면도 있다. 여기서 숨긴다는 의미는 레인보우 테이블 같은 시도로부터 정체를 숨긴다는 의미이다. 레인보우 테이블은 평문을 해시함수로 만들면 항상 일정한 값이 나온다는 것에 착안해서, 해시와 평문의 매칭 테이블을 만든다고 한다. 뭐 단순하게 1:1 매칭으로 테이블을 만드는건 아니고 빠른 검색을 위해 이런저런 구조의 최적화를 한다고는 한다. 이런 문제 때문에 현실에서의 외모의 변장과 같은 일들을 해시값을 만들어 낼때 추가로 해야한다.

 

  첫번째는 솔트(salt)로 요리에 소금을 치듯이 사용자가 입력한 패스워드에 특정한 랜덤 문자열을 더한 후 해시 함수를 적용해서, 만들어진 해시 값을 다르게 한다. 패스워드마다 서로 다른 솔트 값을 만들어 내야 하는 이유는, 안 그러면 하나의 패스워드만 알아내게 되면, 이후 고정된 솔트값을 알수 있게 되기 때문에 해당 솔트가 가미된 새로운 레인보우 테이블도 만들수 있게 될 것이기 때문이다. 두 번째는 해시를 몇번씩 다시 해싱을 해서 전혀 다른 값을 만든다는 것인데, 이건 마치 악성 코드들이 코드를 안 들키기 위해서 여러번 포장을 하는 것과 비슷해 보인다(가끔 방어 기법과 공격기법이 마찬가지인 경우가 있다. 랜섬웨어 처럼 말이다). 이 경우 값도 달라지고 brute force 방식으로 진행시 한번 계산하는 시간을 많이 걸리게 해서 찾기 힘들 게 만든다고 한다. 뭐 여튼 이것도 100% 완벽하다고는 못하겠지만 안 그런 데이터를 공격할 때 보다는 공격자의 ROI가 확실히 안 나오게는 만들 순 있을 것 같다. 

 

[패스워드 털려서 써보는 패스워드 암호화 - 장모님 브런치]

https://brunch.co.kr/@jangcnet/11

 

[안전한 패스워드 저장 - 네이버 D2]

https://d2.naver.com/helloworld/318732

 

 

  그럼 python.exe 파일을 열어 SHA256 해시값을 얻는 예제를 하나 보겠다. 구글에서 "python file sha256" 라고 검색하여 제일 처음의 github 샘플을 봅니다.

 

[simple SHA256 hashing example, written in Python using hashlib - rji님 깃허브]

https://gist.github.com/rji/b38c7238128edf53a181

 

  역시 적당히 한글 주석과 함께 python.exe 파일을 가져오도록 편집을 해보면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hashlib
import sys
 
# 파일 읽어들일 블록 사이즈
block_size = 65536
 
sha256 = hashlib.sha256()
with open("c:\python\python.exe"'rb') as f:
    for block in iter(lambda: f.read(block_size), b''):
        # 읽어 들인 값을 계속 해시객체에 추가한다.
        sha256.update(block)
    # 최종 해시 계산
    print (sha256.hexdigest())
 
cs

 

  그럼 위의 파일을 "aes_example_in_python.py" 로 "UTF-8" 인코딩으로 저장하고 실행 한다. 아래와 같이 해시 값이 나온다. 여러번 실행해도 계속 같은 값이 나오는 것을 볼 수 있다.

c:\Python\code>python sha256_gen.py
e964f2e498ab3141d86da5f0d6e135be82986c66aaafef356b6e7751b779a796

 

 

 

 

6. 제3자 인증 형태 - 설계적 인증

  이제 마지막 부분이다. 이 부분은 앞에서 얘기한 베리사인같은 CA 기관이나 신용 정보회사 같은데서 하는 서비스하는 공개키의 대한 신원의 보증같은 측면도 있지만, 추가로 언급하고 싶은 부분은 설계적 인증 형태라고 보는 것이 어떨까 싶다. 이 부분을 암호화 글에 넣어 설명하는 경우가 맞을까는 싶지만(뒤에서도 아마 계속 언급되어질 주제같다), 암호화와 연관지어 설명해야 가장 맘에 닿을 것 같기 때문에 일단 이 곳에서 우선 언급하려 한다.

 

  1번째는 서버 측면의 로직이다. 패스워드 추측 공격을 피하기 위해서 시도 횟수를 제한하거나,  특정 횟수 후 캡챠를 걸거나, 입력 할때마다 인증 받는 시간을 증가시키거나 하는 경우도 안전한 암호화를 돕는 방법이 될 것 같다.

 

  2번째로 핸드폰, 태그 방식의 도어키, OTP 등 소유에 대한 안정성을 기반으로 한 인증도 역시 마찬가지로 암호화와 동등한 레벨의 방어를 가지게 된다(defense in depth 측면이기도 하지만, 법적으로 암호화 행위와, 전용선의 사용이 등가적인 보호수단으로 취급되는 것도 이런 측면이라고 생각한다).

  

  세 번째로 I-PIN 같은 기술이 주민번호를 대체하기 위해 나와서, 이전 같으면 주민번호가 양방향 암호화 되서 저장될 수 있는 부분을 개인정보와 관련있는 임의의 고유키의 저장으로 대체하게 되었다(물론 I-PIN 회사 내부에서는 주민번호와 해당 CI, DI 값의 연결고리를 알고 있겠지만, CI나 DI 값만을 사용하는 일반 회사들은 해당 주민번호를 알 순 없다). OTP 도 개인만 아는 패스워드로 인증 받는 체계를, 소유 기반 인증에 더해서, 서버만이 알고 있는 임의의 패스워드(OTP 생성번호)로 대체했다고 보면 될것 같다.

 

  네 번째로 쿠키 등의 특정 장소에 보관된 암호화된 인증 값에 대한 재사용 방지 이슈도 있다. 특정 값을 암호화 했더라도 해시나, AES 같은 경우는 해당 값의 고유성이 사라지진 않는다. 그래서 해당 데이터를 그대로 복제해 사용할 경우 인증을 할 수 있는 재사용 이슈가 있다. 해당 부분에 대해서는 랜덤 값인 시간, 거래 고유 번호 등 항상 달라지는 서버 측면 요소들을 더해 암호함으로서 항상 값이 달라지게 만들고, 해당 달라지는 값들을 서버 측면에서 검증(시간이 5분이상 지났다든지, 거래 고유 번호 등의 값이 맞는다 든지)해 사용해야 되는 이슈가 있다. 뭐 이렇게 특정 값을 unvalidate 시키는 것은, 토큰인, 비트 코인등 다른 쪽에서도 마찬가지 인것 같다.

 

 

 

7. 마무리 하면서

  써 놓은 글을 뒤돌아 보니, 요약해 보면 간단한 암호화에 대한 얘기를 너무 거창하게 펼쳐 놓지 않았나도 싶다. 하지만 앞에서 봤듯이 암호화 부분이 정말 많은 기술 분야 안에 기초 구조로 숨어 있기 때문에, 해당 측면들을 인지하지 못한다면, 어플리케이션의 취약점에 대해 불완전하게 이해할 가능성이 높다. 아마 다음 시간에 얘기할 클라이언트 코드 쪽 보안에 대한 얘기도 불완전하게 될 가능성이 높고 말이다.

 

  그리고 전공이 맞아 천천히 공부해서 수학적으로 깊게 이해한 사람들도 있겠지만, 그렇지 못한 사람에게는 논리적으로라도 해당 개념을 정확하게 잡고 있는 것이 중요한 듯 싶다. 안 그럼 자꾸 상상의 나래를 펼치게 되어 잘못된 의견을 가지게 될 수도 있으니 말이다. 그럼 이렇게 이번 시간을 마무리를 하도록 하겠다. 

 

 

2018.7.22 by 자유로운설탕
cs

 

 

posted by 자유로운설탕
2018. 6. 6. 19:36 보안

  이번 시간은 인젝션(Injection) 이라는 주제로 진행하려고 한다. 굳이 보안의 수많은 주제 중, 인젝션을 맨 처음 이야기 하는 이유는 어찌 보면 보안에서 악성행위라고 할 수 있는 가장 기초적이면서도 본질적인 행위라고 생각하기 때문이다.

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 설계 문제

8. API

9. 설정, 패치, 하드닝

10. 그밖의 사전식 주제들

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

1. 들어가면서

  사실 OWASP TOP 10 이나 여러 보안가이드, 책에서는 취약점을 여러가지의 타입으로 나누어 설명하고는 있지만, 사실 현실에서는 그렇게 명확히 나눠지는 것은 아닌 것 같다. 여러가지가 복합적으로 연관되어 일어나는 경우도 있고, 때로는 특별한 취약점 타입이라고 하긴 힘든 비즈니스나 설계적 관점 등에서 일어나는 경우도 있기 때문이다. 반대로 다른 관점에서 보면 모든 취약점은 그 대상이 되는 기술을 이해한다는 가정하에서 몇가지 요소가 섞인 단순한 원리로 구성된 것 같아 보이기도 한다. 그래서 앞으로 보안을 이해하는데 중요하다고 생각되는 하나하나의 주제를 가지고 이야기를 할테지만, 때로는 다른 시간의 이야기와 겹칠 때도 있을 것 같다.

 

  인젝션(Injection) 이란 OWASP TOP 10 – 2017 의 첫번째를 차지하는 항목으로, 보통 SQL 인젝션 이나, 커맨드 인젝션 같은 대표적인 주제로 인지가 되어진다. 인젝션을 묘사하는 이미지를 생각해 본다면, 아래의 주사를 놓는 그림과 같이 무언가 외부의 물질이 내부로 침투되는 행위라 할수 있다. 그렇게 침투된 물질이 바이러스가 몸 안의 생태계 및 면역 체계와 반응하여 나쁘거나 이상한 효과를 나타내는 것 같이, 프로그램 안의 로직에 영향을 미쳐 이상한 행동을 일으키게 하는 것이 보안에서의 인젝션이라고 보면 어떨까 싶다. 

 

  보통 인젝션이 되어 들어오는 형태는 문자열, 숫자, 바이너리 등 여러 형태이며(종종 페이로드라도 칭해지는 코드 덩어리도 들어온다), 해당 들어온 요소들에 대해서, 파서(Parser - 구문을 분석해 처리함. 파서에 대해서는 파이썬 글 웹페이지 파싱에서 설명을 했다), 또는 비슷한 의미지만 인터프리터(Interpreter - 특정 구문을 가져와 명시된 로직을 실행하는 행위)가 정상적인 데이터라고 안심하고(뭐 이부분은 프로그래머를 믿기 때문에...) 실행하는 경우라고 볼 수 있다.

 

  SQL 문이 들어와 나쁜 문제를 일으키면 SQL 인젝션이 되는거고, 커맨드 명령어가 들어와 나쁜 문제를 일으키면 커맨드 인젝션이 되고, XML 구문이 들어와 나쁜 문제를 일으키면 XML 인젝션이 되는 어찌 보면 단순한 형태라 할수 있다.

 

 

  인젝션을 명시적으로 무언가를 넣는 행위가 일어나는 경우라고 제한할 수도 있지만, 좀 더 넓은 의미로 바라보면, 우리가 컴퓨터나 모바일 게임을 할 때 세이브 파일을 조작해 능력치를 조작한거나 어려운 스테이지를 넘긴다든지, "show me the money" 같은 치트키를 넣거나, 게임이 실행되는 메모리 영역의 숫자를 추적해 체력 수치를 고정시킨다거나 하는 작업, 파이썬 시간에 설치해 봤던 피들러(fiddler) 같은 툴로 HTTP 통신을 가로채 보거나 조작한다거나, XSS, API 인자 조작, 악성코드 들이 컴퓨터에 들어와서 하는 행위 등 모든 일이 사실 모두 인젝션의 행위를 일으킨다고 할 수도 있다. 

 

  왜냐하면 해킹이나 악성코드 감염 등은 결국 무언가 정상적으로 돌아가는 시스템 안에 원하는 타입의 (나쁜) 이상을 일으키는 행위이기 때문에, 시스템에 영향을 주기 위해서 인젝션 행위는 필수적이기 때문이다. 다만 그렇게 넓게 보게 되면 이 시간에 앞에 얘기한 모든 주제들이 비슷비슷해죠 헷깔리기 때문에, 여기서는 SQL 인젝션 같은 전통적인 인젝션 영역에 한정해서 얘기를 하려고 한다. 다만 맨 앞에서 얘기했듯이, 취약점 타입이라는 것은 어찌보면 보안을 체계적이고 계단식으로 잘 설명하기 위해서 나눈 기준이라는 측면도 있다는 것을 염두에 두고, 좁은 범위로만 생각하지 않았음 하는 바램이 있다. 그래서 조금 공식적인 개념에 비해 혼란이 있더라도 광의적인 관점에서도 짚고 넘어가려 한다.

 

 

2. SQL Injection

  우선 인젝션 중에서 가장 유명하고, 블로그나 책에서도 많이 다루는 SQL 인젝션에 대해서 얘기를 하려 한다. 사실 SQL 인젝션은 그만큼 유명한 인젝션 타입이기도 하지만 현대의 프로그래밍 언어 환경에서는 거의 멸종 위기에 도달한 타입같기도 하다. 왜냐하면 새로 나오는 언어들이나 기존 언어들의 업데이트 버전에서는 SQL 인젝션은 언어가 대처해야 되는 당연한 보안 위협이라고 인지하여, 디폴트로 보안 방어(Prepared Statement 나 파이썬 글 장고 파트에서 볼 수 있는 ORM 형식의 쿼리방식)를 제공하기 때문이다. 아마도 많은 개발자들은 해당 SQL 호출 함수를 쓰는 것이 SQL 인젝션을 방어하게 된다는 것을 인지도 못하고 사용할 가능성이 높다(보안이 개발프로세스에 들어간 가장 바람직한 예인것 같다). 

 

  하지만 개인적으로 생각했을 때, SQL 인젝션이란 것은 인젝션의 원리를 설명할 때 참 직관적인 샘플을 보여줄 수 있고, 또 예전 언어를 쓰는 레거시 시스템은 여전히 우리나라를 포함한 세계 곳곳에 남아 있기 때문에(마치 아직도 윈도우 XP 가 많이 남아있는 것처럼) 여전히 세계에서 가장 많이 일어나고, 보안에서 많이 언급되는 항목인 듯 싶다. 

 

  여기서는 SQL 인젝션의 예제와 원리를 보여주고, 인젝션의 방어로 많이 언급되는 스토어드 프로시저(Stored Procedure), Prepared Statement 가 나쁜 데이터가 어떻게 정화(Sanitized-원래는 사전을 찾아보면 살균의 의미에 가깝지만...)되는지를 실습하며 눈으로 확인해 보도록 하겠다. 다양한 예제를 보여주기 위해 스토어드 프로시저는 ASP, MSSQL을 이용하고 Prepared Statement 는 PHP, Mysql 로 시연하려고 한다.

 

  먼저 ASP 와 MSSQL 설치는 파이썬 글에서 이미 설명했기 때문에 우선 MSSQL 설치와 실습용 테이블의 생성 부분은 파이썬 글 4교시 데이터 베이스 조회 부분을 참조해 설정하길 바란다. 현재 조금 바뀐 부분은 MSSQL 2017 Express 를 다운 받을 수 없고, MSSQL 2018 Express 버전을 다운받게 되는데, 기존 버전과 설치과정이 바뀐건 없으니 그대로 따라 진행 하면 된다. 설치 후 supermarket 테이블 생성까지 하면 되고, 최근에 답변을 하다 알았는데, MSSQL 2016~2018 은 윈도우 8 이상에만 설치가 되니, 구 버전을 구하지 않는 이상 윈도우 10 에만 설치할 수 있다. 현재 글도 윈도우 10 환경에서 진행 중이니 양해 부탁 드린다.

 

  두 번째로 ASP 설정과 샘플 페이지 생성하여 확인하는 부분은 파이썬 글 18교시 Lagacy Web 부분을 참고해서 IIS를 설치하고 ASP 샘플 페이지를 실행하는 부분까지 설정해 주길 바란다. 웹을 잘 모른다면 18교시를 한번 전체적으로 읽고 오면 이해가 좀더 쉬울 듯 싶다.

 

 

2.1 IIS 추가 세팅

  앞에서 세팅한 IIS(Internet Information Server) 세팅으로는 에러가 났을때 상세 내용이 보이지 않기 때문에(500 대 서버 에러만 보이게 된다. 이것도 뒤에 얘기하겠지만 보안적 디폴트 설정 때문에 그렇다). 인젝션 시도시 어떤 에러가 나는지 자세히 살펴보기 위해서 상세 에러가 나오도록 세팅을 하려한다.

 

  먼저 앞에 했던 IIS 기본 설정에는 에러 모듈이 설치되 있지 않아서 IIS 세팅시에 사용했던 "Windows 기능 켜기/끄기" 창에서 "인터넷 정보 서비스" > "World Wide Web 서비스" > "일반적인 HTTP 기능" > "HTTP 오류" 를 체크하고, 확인 버튼을 누른다.

 

 

  이후 커맨드 창에서, "inetmgr" 을 입력하여, "IIS(인터넷 정보 서비스) 관리자" 화면을 띄운 후, 왼쪽 트리에서 "Default Web Site" 를 선택하고, IIS 섹션에서 "ASP" 항목을 더블 클릭 한다.

 

 

  이후 가운데에서 디버깅 속성 항목을 펼쳐서 "브라우저에 오류 전송", "클라이언트 쪽 디버깅 사용" 2가지 항목을 True 로 바꾼 후, 오른쪽의 작업 창에서 적용 버튼을 클릭한다. 

 

 

  다음엔 같은 식으로 "오류 페이지" 아이콘을 더블 클릭한다.

 

 

  이후 오른쪽의 "기능 설정 편집..." 을 클릭 하여, "오류 페이지 설정 편집" 창이 나오면, "자세한 오류" 라디오 단추를 선택하고, 아래의 "확인" 버튼을 누른다.

 

 

  이 상태에서 엣지 브라우저 같은 경우는 정상으로 에러를 볼수 있는데, IE 같은 경우는 하나를 더 해 주어야 한다. "도구(브라우저 우측 상단의 톱니 바퀴모양 아이콘)" > "인터넷 옵션" 으로 들어가 고급 탭을 클릭하고, "HTTP 오류 메시지 표시" 를 언체크 하고, 하단의 "확인" 버튼을 누른다(브라우저를 종료했다 다시 실행해야 반영된다).

 

 

 

2.2 테이블 추가 생성 및 ASP 쿼리 준비

  기존에 파이썬 쪽에서 세팅해 놓은 상품 정보가 들어간 supermarket 테이블이 있긴 하지만, 조금이나마 더 현실에 가까운 샘플을 보이기 위해서 거래 관련 테이블을 하나 만들어 보보려 한다. SQL Server Management Studio 에서 supermarket 이 있는 mytest 디비에 아래와 같은 거래 테이블을 만든 후(파이썬 4교시 설명 같이 선택 후, F5 키를 누른다),

1
2
3
4
5
6
7
8
create table order_record
(
    OrderNo int,
    MemberId char(20),
    ItemNo int,
    BuyCount int,
    InsDate datetime
)
cs

 

 

  데이터를 입력한다.

1
2
3
4
5
6
7
8
9
10
11
insert into order_record
values (1000'tom' , 13'2018-01-01')
 
insert into order_record
values (1001'jerry' , 21'2018-01-02')
 
insert into order_record
values (1002'tom' , 31'2018-01-02')
 
insert into order_record
values (1003'tom' , 32'2018-01-03')
cs

 

 

  이후 ASP 코드에서 호출 할 쿼리를 하나 만들어 실행해 확인해 본다

1
2
3
4
5
select o.memberid, s.Foodname, o.buycount, o.InsDate, s.price, (s.price * o.buycount) total_price from order_record o(nolock)
join supermarket s(nolock)
on s.itemno = o.itemno
where o.memberid = 'tom'
order by o.buycount desc
cs

 

 

 

2.3 인젝션이 일어나는 ASP 페이지 만들기

  ASP 는 .NET 이 생기면서 유지보수가 거의 멈춰진 언어이기 때문에, 인터넷에 있는 많은 샘플이 SQL Injection 에 취약한 형태로 구현이 되어 있다(뒤에 보겠지만 계속 유지보수된 PHP 도 하위 호환성 때문에 예전 스타일 대로 코딩 하면 비슷하긴 하다). 자세한 부분은 ASP 문법이라 중요하진 않으니 대략적으로 보면, 위의 SQL 문에 대해서 구입한 사람(memberid)를 입력하면 해당 되는 회원이 구입한 물품들을 보여주는 페이지 소스는 아래와 같다.

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
<%@ Language=VBScript %>
 
<%
' 컨넥션 스트링
strOrderList = "Provider=SQLOLEDB; Data Source=localhost; Initial Catalog=mytest; User Id=pyuser; Password=test1234"
Set objCnn = Server.CreateObject("ADODB.Connection")
objCnn.Open strOrderList 
 
%> 
 
 
<%
' 폼 값 받기
strBuyerID = request("txtBuyerID")
 
 
' 실행할 SQL + 인자
strSQL1 = "select o.memberid, s.Foodname, o.buycount, o.insdate, s.price, (s.price * o.buycount) total_price from order_record o(nolock) " & _
"join supermarket s(nolock) " & _
"on s.itemno = o.itemno " & _
"where o.memberid = '" & strBuyerID & "'" & _
" order by o.buycount desc"
 
Response.Write "<h3>날라가는 쿼리 표시 </h3>" 
Response.Write strSQL1
Response.Write "<hr>"
%>
 
 
<html>
   <head>
      <title>손님 관리</title>
   </head>
 
 <body>
     <form name = "BuyForm" method="get" action="injection_test.asp">
        <table width = 600>
            <tr>        
                <td class="graycolor_center_align" width = "100"> 구매자 조회</td>
                <td class="content_left_align" width = "100"> <INPUT maxlength="200" name="txtBuyerID" size="15" style="font-size:12px;" type="text" value=""> </td>
                <td class="content_left_align"> &nbsp; <input name=button type=submit value="조회">     </td> 
            </tr>      
        </table>         
        <br>
 
<%   
' 쿼리 실행
Set rsList = objCnn.Execute(strSQL1)
%>  
 
        <table width="600">   
            <tr>
                   <td class="graycolor_center_align" width = "100">손님</td>
                 <td class="graycolor_center_align" width = "100">구매물품</td>
                 <td class="graycolor_center_align" width = "100">구매수량</td>
                 <td class="graycolor_center_align" width = "100">구매일</td>
                 <td class="graycolor_center_align" width = "100">단가</td>
                 <td class="graycolor_center_align" width = "100">총 금액</td>         
              </tr>    
<%
'결과가 끝이 아닐 라면, 
Do while Not rsList.EOF 
%>
            <tr>
                 <td class="content_center_align" width = "100"><%=rsList("memberid")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("Foodname")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("buycount")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("InsDate")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("price")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("total_price")%></td>
              </tr>
                        
<%
   rsList.MoveNext
 
Loop
%>
          </table>
                                       
     </form>
</body>          
 
<%
'열었던 연결 닫기
objCnn.Close
Set ObjCnn=Nothing
%>
cs

 

  위의 코드를 관리자 권한으로 실행한 메모장(파이썬 18교시 ASP 샘플 실행하는 부분에서 설명함)에 붙여 놓고, "c:\inetpub\wwwroot" 폴더에 "ANSI" 인코딩(한글이 안깨지려면 이렇게)으로 "injection_test.asp" 라고 저장한다.

 

  브라우저 주소창에 "http://localhost/injection_test.asp" 라고 입력하면, 아래와 같이 원하는 구매자를 넣고, 조회를 누르면 해당 구매자가 구입한 물품이 나타나는 간단한 화면이 나오게 된다. 상단에는 인젝션이 일어나는 상황을 보여주기 위해서, 실제 데이터베이스로 날라가는 쿼리를 표시하게 했다. 상단의 쿼리를 보면 위의 ASP 에 있는 쿼리 조합 코드에 의해서 where 조건에 있는 "memberid" 에 사용자가 입력한 "tom" 이 결합 됨으로서, tom 이 구입한 물품을 가져오게 되는 것을 볼 수 있다(이 쿼리가 이해 안간다면 SQL 기초가 부족 한 거니, Select 문, Join 문, Union 문, Order by 문을 공부하길 바란다)

 

 

 

2.4 인젝션 시도해보기

   앞에서 인젝션은 외부에서 들어온 구문을 해석하는 파서나 인터프리터의 문제라고 얘기했듯이, SQL 인젝션은 프로그램에서 던지는 SQL 문과 SQL 서버 사이의 문제이다. 하지만 여기서 순진한 SQL 서버는 보내준데로 실행해 주는 것일 뿐이니 별 잘못은 없다고 볼수 있고, 편파적이긴 하지만 ASP 프로그램이 나쁜 의도의 사용자가 입력한 내용을 잘못 해석해서 SQL 서버로 던져서 일어난다고 보는 것이 맞다. 결국 SQL 인젝션은 프로그램을 잘 속여 SQL 서버로 나쁜 쿼리를 던져 실행시키는 것이 목적이기 때문에, SQL 서버 종류별로 문법이 약간 달라서 악용되는 기법이 조금씩 틀린 부분이 있다. 

 

 

  그럼 모든 SQL 서버들에서 문법 문자로 사용하는 홑따옴표(')나 쌍따옴표(")가 기본적인 SQL 인젝션을 판단하기에 적절하다. 물론 블라인드 SQL 인젝션(Blind SQL Injection)이라는 한 단계 더 꼬여있는 분야도 있지만, SQL 의 미묘한  문법을 사용한다는 측면에서는 동일하다고 봐도 된다. 위의 화면에서 홑따옴표(') 하나만 넣어서 조회를 해보도록 한다.

 

  원래 일반적인 웹 사이트는 에러가 사용자 친화적인 공통 에러 페이지("사용에 불편을 드려 죄송합니다" 같은)로 가도록 설정이 되기때문에, 위와 같은 자세한 에러 페이지를 만나기는 어렵다. 하지만 여기서는 SQL 인젝션의 원리를 설명하기 위해서 일부러 자세한 에러를 보이도록 앞에서 설정했었다. 위의 SQL 서버로 전달되는 쿼리를 보면 o.memberid =''' 식으로 입력되어, 홑따옴표가 3개가 되기 때문에, SQL 문법이 깨지게 된다. 실제 MSSQL 서버에서 반환된 에러메시지도, 따옴표의 짝이 안 맞는다는 에러가 나온다. 현실적으로 저런 경우라면 SQL 인젝션이 된다는 사실을 확인하고 여러 시도를 해볼 수 있게된다.

 

 

  다음엔 입력 인자로 tom' 을 넣어본다. 여전히 쿼리를 보면 'tom'' 으로 홑 따옴표 수가 안 맞아 SQL 문법을 깨뜨리므로 동일한 에러가 나게 된다.

 

 

  그 다음에는 MSSQL 에서 그 뒤에 나온 문장을 주석처리를 해주는 효과를 지닌 -- 문자를 이용해서, tom' -- 라고 넣어보자. 이번에는 -- 표시로 인해서 뒤의 ' order by o.buycount desc 문장이 모두 주석 처리되기 때문에 실제 유효한 쿼리는 ...'tom' 까지가 되어서 문법이 깨지지 않기 때문에 정상적인 쿼리가 되어 결과가 나오게 된다. 대신 주석 뒤의 order by 문장은 무시되어 정렬이 없어져 나온다. 궁금한 사람은 해당 문장을 복사해서, SSMS 에 복사해 실행해도 마찬가지 결과가 나오게 된다.

 

 

  자 그러면 이제부터는 이것저것 문법을 조합하여 자유롭게 쿼리를 만들 수 있게 된다. tom' and o.buycount = 3 -- 값을 넣어서 구매 수량이 3개인 건만 가져올 수도 있다.

 

 

  그러면 이 시점에서 "이게 뭐 어쨌다고?" 라고 생각할 수도 있을 것 같다. 원래 저 페이지는 해당 테이블들을 조회를 하는 쿼리였고, 인덱스를 좀 이상하게 타면 데이터베이스에 부하를 많이 줄 수 있긴 하겠지만(이건 DDOS 공격이 될수도 있겠지만), 보안적으로 정말 위험한 건가 하는 생각을 가질 수도 있다. 사실 이제부터는 case by case 라고 보면 된다. 운이 좋으면 심각한 문제가 일어나지 않을 수도 있지만(하지만 잠재적으로 큰일이 날수 있는 중요한 취약점이라 꼭 바로 수정해야 하긴 한다), 어쩌면 매우 큰일이 일어날 수도 있게된다.

 

  우선 DB 나 보안 관련된 팀이 없는 많은 작은 회사들은, 어플리케이션에서 사용하는 데이터베이스 계정을 기본 계정인 admin 이나 DB Owner 계정으로 사용하고 있을 수 있다(권한을 세분화 하면 관리가 복잡해 지기 때문에). 추가해 해당 계정에 특정한 SQL 명령어 실행 제약이 없을 가능성도 높다(설정에 따라 SQL 문을 이용해 시스템 명령어도 실행할 수도 있다). 엎친데 덮친 격으로 해당 테이블과 같은 데이터베이스 내에 회원의 개인정보 테이블이 있을 경우도 있다(다른 데이터 베이스에 있더라도 접근을 못한다고 하긴 어렵지만 아무래도 환경과 난이도의 영향을 받게 된다).

 

 

  최악의 경우를 상정하기 위해서, 개인정보 테이블을 하나 mytest 데이터베이스에 만들어 보도록 하다. SecretMember 테이블을 하나 만들고, 회원 정보를 하나 넣는다.

1
2
3
4
5
6
7
create table SecretMember(
MembID char(20),
TelNo char(20),
email char(30)
)
 
Insert SecretMember values('admin''010-xxxx-2222''test@test.com')
cs

 

 

 

  그럼 이제 SQL 문법 중에 인젝션에 단골로 사용되는 "union all" 이라는 문법을 써보자. 두 개의 서로 다른 테이블의 내용을 가져와서 select 문 뒤의 컬럼들의 데이터 형만 맞춰 주면, 마치 하나의 테이블에서 가져온 것처럼 결과를 반환하는 연산자이다.. 

 

[w3schools 사이트 - SQL UNION Operator]

https://www.w3schools.com/sql/sql_union.asp

 

 

  그럼 tom' union all select membid + telno + email, '', 0, '2018-01-01', 1, 1 from secretmember(nolock) -- 문장을 넣고 조회를 하게 되면, 기존 프로그램에서 조회하려던 것과 전혀 상관없는 secretmember 테이블 로부터 데이터를 같이 조회해 와서, 화면에 뿌려주게 된다. 만약 해당 테이블에 개인정보가 있다면 SQL 서버는 안전하게 회사 내부에서 보호되고 있더라도, 개인정보가 모두 유출 되는 상황이 벌어질 수 있다(물론 어플리케이션 방화벽 같은 추가적인 방어 장비로 부터도 잘 피해가야 하겠지만. 아마 이런 취약점이 있는 회사는 그런 장비가 없거나 있어도 형식적으로 운영될 가능성이 높을 듯 하다).


 

  한발 더 나아가 만약 별로 털어갈 테이블에 없어서 실망한 공격자가 아래와 같은 쿼리를 날린다고 해보면 tom' ; drop table secretmember -- 만약 해당 어플리케이션의 데이터베이스 계정 권한이 과도하게 있는 경우는 해당 테이블이 삭제되어 버리게 됩니다(";" 는 MSSQL 에서 두개의 명령어의 연결을 나타내서, 두개의 명령어가 같이 실행되어 버린다)

 

 

  SSMS 에서 select top 10 * from secretmember(nolock) 를 해보면 테이블이 사라지고 없다.

 

 

 

2.5 SQL 인젝션 방어 전략

  그럼 나름 무시무시한 SQL 인젝션을 방어하려면 어떻게 하는게 좋을까? 여러 블로그 등에도 많은 가이드가 나오지만 구글에 "owasp sql injection defense" 라고 검색하면, 아래의 OWASP 에서 가이드하는 내용이 나온다. 개인적으로 볼때 초보자를 위한 세부적 기술에 대한 설명은 부족해 아쉽지만, 내용 자체는 군더더기 없이 잘 정리되어 있다고 생각한다.

 

[OWASP 사이트 - SQL Injection Prevention Cheat Sheet]

https://www.owasp.org/index.php/SQL_Injection_Prevention_Cheat_Sheet#SQL_Server_Escaping

 

 

  내용을 보면 순서대로 Prepared Statements(with 파라매터화된 쿼리), 스토어드 프로시저(Stored Procedures), 화이트 리스트 베이스의 입력 체크(White List Input Validation), 모든 사용자 입력의 이스케이핑 처리(Escaping All User-Supplied Input) 가 있다. 해당 페이지에도 명시됬듯이, 이스케이핑 처리는 정말 다른 방법이 없거나, 레거시 시스템이라서 수정할 범위가 넓어 사이트 이펙이 너무 커서 당장 전체를 수정하기 어려운 경우 임시적 조치로 사용하는 것이 맞는듯 싶고, 화이트 리스트 베이스로 입력 체크를 하는 것은 사실 수많은 쿼리에 대해서 경우의 수도 따져봐야 하고, 범용적인 화이트 리스트를 구축하기도 쉽지 않기 때문에 설명에 나온것처럼 2차적인 방어 요소로 생각하고 사용하면 좋을 듯 싶다. 여기서는 Prepared Statements 와 스토어드 프로시저 2개를 구현하면서 데이터를 어떻게 방어하는지 보도록 하겠다.

 

 

 

2.5.1 방어전략 1 - 스토어드 프로시저의 방어 살펴보기

  우선 스토어드 프로시저가 무언지 알아보자. 스토어드 프로시저는 간단히 설명하면 SQL 서버내에 저장되어 있는 바깥에서 호출이 가능한 함수라고 생각하면 된다(물론 MSSQL 은 스토어드 프로시저와 별개로 진짜 함수라 불리는 문법도 있긴 하지만 개념적으로 외부에서 접근가능한 함수라고 생각하면 편할 듯 싶다). 함수와 마찬가지로 입력과 출력을 가지고 있으며, 인자가 있을 수도 없을 수도 있다. 프로그램에서 인자에 값을 넣어 스토어드 프로시저를 호출 하면, 해당 결과가 SQL 문을 실행 한 것과 마찬가지로 프로그램으로 반환된다. 실제 스토어드 프로시저 내에는 SQL 문법으로 이루어져 있다.

 

  그럼 SSMS 에서 스토어드 프로시저를 하나 만들어 보자. 거의 SQL과 같지만, 앞에 create PROCEDURE 문이 있고 그 담에 입력인자 @memb_id 가 딸려온다. 일반적인 SQL 문의 뒤에 where 조건의 o.member_id = @memb_id 부분에서 스토어드 프로시저로 넘어온 인자를 쿼리와 합쳐 필요한 조건문을 만든다.

1
2
3
4
5
6
7
8
create PROCEDURE dbo.Select_Buy_Item 
    @memb_id char(20
AS 
   select o.memberid, s.Foodname, o.buycount, o.insdate, s.price, (s.price * o.buycount) total_price from order_record o(nolock)
   join supermarket s(nolock)
   on s.itemno = o.itemno
   where o.memberid = @memb_id
   order by o.buycount desc
cs

 

 

  이후 해당 스토어드 프로시저를 호출하는 ASP 페이지를 만들면 아래와 같다. 역시 적당히 흐름만 보자 

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
<%@ Language=VBScript %>
 
<!--METADATA TYPE= "typelib"  NAME= "ADODB Type Library"
      FILE="C:\Program Files\Common Files\SYSTEM\ADO\msado15.dll"  -->
 
<%
' 컨넥션 스트링
strOrderList = "Provider=SQLOLEDB; Data Source=localhost; Initial Catalog=mytest; User Id=pyuser; Password=test1234"
Set objCnn = Server.CreateObject("ADODB.Connection")
objCnn.Open strOrderList 
 
%> 
 
 
<%
' 폼 값 받기
strBuyerID = request("txtBuyerID")
 
if(strBuyerID = ""then 
   strBuyerID = ""
end if
 
' 폼으로 받은 인자를 넘겨서, 스토어드 프로시저 실행
Set objCmd = Server.CreateObject("ADODB.Command")
 
with objCmd
             .ActiveConnection = objCnn
             .CommandText = "Select_Buy_Item"
             .CommandType = adCmdStoredProc
             .Parameters.Append .CreateParameter("@memb_id",adChar,adParamInput,20)
             .Parameters("@memb_id"= strBuyerID 
end with
 
 
Response.Write "<hr>"
%>
 
 
<html>
   <head>
      <title>손님 관리</title>
   </head>
 
 <body>
     <form name = "BuyForm" method="get" action="injection_test_sp.asp">
        <table width = 600>
            <tr>        
                <td class="graycolor_center_align" width = "100"> 구매자 조회</td>
                <td class="content_left_align" width = "100"> <INPUT maxlength="200" name="txtBuyerID" size="15" style="font-size:12px;" type="text" value=""> </td>
                <td class="content_left_align"> &nbsp; <input name=button type=submit value="조회">     </td> 
            </tr>      
        </table>         
        <br>
 
<%   
' 쿼리 실행
Set rsList = objCmd.execute
%>  
 
        <table width="600">   
            <tr>
                   <td class="graycolor_center_align" width = "100">손님</td>
                 <td class="graycolor_center_align" width = "100">구매물품</td>
                 <td class="graycolor_center_align" width = "100">구매수량</td>
                 <td class="graycolor_center_align" width = "100">구매일</td>
                 <td class="graycolor_center_align" width = "100">단가</td>
                 <td class="graycolor_center_align" width = "100">총 금액</td>         
              </tr>    
<%
'결과가 끝이 아닐 라면, 
Do while Not rsList.EOF 
%>
            <tr>
                 <td class="content_center_align" width = "100"><%=rsList("memberid")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("Foodname")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("buycount")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("InsDate")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("price")%></td>
                 <td class="content_center_align" width = "100"><%=rsList("total_price")%></td>
              </tr>
                        
<%
   rsList.MoveNext
 
Loop
%>
          </table>
                                       
     </form>
</body>          
 
<%
'열었던 연결 닫기
objCnn.Close
Set ObjCnn=Nothing
%>
cs

 

 

  다른 부분은 앞의 예제와 비슷하고, "폼으로 받은 인자를 넘겨서, 스토어드 프로시저 실행" 주석 부분의 코드만 조금 다르다. 아까 SSMS 에서 만들었던 스토어드 프로시저인  Select_Buy_Item 를 호출 하면서 @memb_id 인자를 넘긴다. 그럼 c:\inetpub\wwwroot 에 ANSI 인코딩 형식으로 injection_test_sp.asp 라고 파일을 저장한다. http://localhost/injection_test_sp.asp 페이지를 로드해서 똑같이 tom' -- 을 입력해 보도록 해보자. 그럼 아무 에러도 나지 않고, 결과가 없는 화면이 나타나게 된다.

 

 

  스토어드 프로시저의 효과는 프로시저를 이용해 안전하게 쿼리를 작성 한다면 Prepared Statement 하고 거의 비슷한 효과를 준다고 보는데, Prepared 라는 말은 쿼리의 실행계획을 세운 다는 의미라고 보면 될것 같다. 쿼리의 실행 계획(Query Plan)이라는 것은 비유하자면 우리가 프로그램을 컴파일하는 행위와 같다고 보면 된다. 우리가 프로그램 소스를 컴파일하고 빌드 할때 컴파일러가 가장 최적의 어셈블리코드를 설계하여 바이너리 파일로 만들어 주듯이, SQL 서버도 내부적으로는 쿼리에 최적화된 형태의 파일이 관리되는 프로그램이기 때문에, 사용자가 원하는 쿼리에 대해서, 어떤 전략을 가지고 결과를 얻기위해 실행할 것인지를 결정하는 과정이 있게 된다. 

 

  이렇게 전략이 결정되게 되면, 다음에 다시 전략을 계산하기 전에는 컴파일된 바이너리와 같이 고정된 전략을 가지게 된다. SQL 문에서 전략에 영향을 미치는 요소가 join, union 등의 연산자나 where 문에서 언급하는 컬럼이름 등의 구조에 영향을 주는 문법 요소들이기 때문에, 결국 SQL 문이 입력되는 상수들 빼고는 내부적인 문법 구조가 고정되어 버리는 효과를 가지게 된다.

 

  그래서 바깥에서 아무리 문법 문자에 해당하는 문자열들을 내부에 인젝션 하더라도, 이미 고정된 구조는 유지한채 문법 요소에 영향을 주지 못하고, 입력되는 상수로만 취급 되게 된다. 사실 이건 최초에는 시간이 많이 걸리는 쿼리 실행 계획을 재사용 함으로서 성능적인 이점을 가지게 되는 데서 출발한것 같은데, 해당 방식이 SQL 인젝션을 근본적으로 예방 가능한 특징을 가지고 있기 때문에, SQL 인젝션의 1번째 방어 권고 요소가 된듯도 싶다(소발에 쥐 잡았다고 봐도 되려나...)

 

  다만 스토어드 프로시저는 내부에서 받은 인수를 이용해 문자열을 합치는 식으로 조합해서(예를 들어 해당 인자를 스토어드 프로시저 내부에서 where 문장 뒤에 명시적으로 붙인다던지...) 새로운 쿼리를 만드는 방식으로도 사용할 수 있기 때문에, 내부에서 상수 형식으로 사용될 때에만 Prepared Statement 와 동일한 효과를 가지는 것 같다고 생각된다. 사실 위와 같이 쓰는 것은 스토어드 프로시저를 사용하는 의미를 잃게 하는 행위인듯도 싶다.

 

 

  그럼 위의 화면의 결과만 가지고는 정말 내부에서 들어간 인자가 문법 문자가 아닌, 일반 문자열로만 취급됬는지 증명하기 힘들기 때문에, 페이지를 조금 변형하여 입력한 문자를 테이블안에 넣어 실제 어떤 형식으로 들어가는지 확인해 보도록 하겠다(개인적으로 첨에 정말 그런지 궁금해서 실험을 위해 해본 행동이다). 먼저 escape_test 라는 테이블을 하나 만들고 hey, hey', hey'' 3개의 문자를 입력해 보도록 하겠다. 여기서는 처음에 create table 을 먼저 선택해 실행 하고, 그 뒤에 insert 문을 하나씩 선택해서 실행해 본다. 실행 하다보면 2번째 insert 문은 hey 뒤에 홑따옴표가 두개 있어('') 에러가 나고, 3번째 insert 문은 세개의 홑 따옴표중 앞의 2개가 ' 문자를 나타내는 이스케이프 문자로 처리되어 hey' 문자가 입력이 된다.

1
2
3
4
5
6
7
8
9
10
create table escape_test
(
   mytext char(30)
)
 
insert into escape_test values('hey')
 
insert into escape_test values('hey'')

 

insert into escape_test values('
hey''')
cs

 

 

 

  실제 들어간 데이터를 셀렉트 해보면 확인을 할 수 있다.

1
select * from escape_test(nolock)
cs

 

 

 

  그럼 해당 테이블에 인자를 받아서 insert 하는 스토어드 프로시저를 하나 만들어 보겠다

1
2
3
4
5
create PROCEDURE dbo.insert_escape_text 
    @mytext char(30
AS 
   insert into escape_test
   values (@mytext)
cs

 

 

 

  이후 해당 스토어드 프로시저를 호출하는 ASP페이지를 하나 만든다. 앞의 페이지와 거의 동일하고, 필요없는 코드 제거와, 스토어드 프로시저 호출 부분만 조금 바뀌었다. 역시 문법은 대충 보자.

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
<%@ Language=VBScript %>
 
<!--METADATA TYPE= "typelib"  NAME= "ADODB Type Library"
      FILE="C:\Program Files\Common Files\SYSTEM\ADO\msado15.dll"  -->
 
<%
' 컨넥션 스트링
strOrderList = "Provider=SQLOLEDB; Data Source=localhost; Initial Catalog=mytest; User Id=pyuser; Password=test1234"
Set objCnn = Server.CreateObject("ADODB.Connection")
objCnn.Open strOrderList 
 
%> 
 
 
<%
' 폼 값 받기
strBuyerID = request("txtBuyerID")
 
if(strBuyerID = ""then 
   strBuyerID = ""
end if
 
Set objCmd = Server.CreateObject("ADODB.Command")
 
with objCmd
             .ActiveConnection = objCnn
             .CommandText = "insert_escape_text"
             .CommandType = adCmdStoredProc
             .Parameters.Append .CreateParameter("@mytext",adChar,adParamInput,20)
             .Parameters("@mytext"= strBuyerID
end with
 
 
 
Response.Write "<hr>"
%>
 
 
<html>
   <head>
      <title>손님 관리</title>
   </head>
 
 <body>
     <form name = "BuyForm" method="get" action="injection_test_insert.asp">
        <table width = 600>
            <tr>        
                <td class="graycolor_center_align" width = "100"> 구매자 입력</td>
                <td class="content_left_align" width = "100"> <INPUT maxlength="200" name="txtBuyerID" size="15" style="font-size:12px;" type="text" value=""> </td>
                <td class="content_left_align"> &nbsp; <input name=button type=submit value="입력">     </td> 
            </tr>      
        </table>         
        <br>
 
<%   
' 쿼리 실행
Set rsList = objCmd.execute
 
'열었던 연결 닫기
objCnn.Close
Set ObjCnn=Nothing
%>
cs

 

 

  관리자 권한으로 연 메모장에 내용을 붙여 넣고, c:\inetpub\wwwroot 폴더에 ANSI 인코딩으로 injection_test_insert.asp 이름으로 저장한다. tom' -- 를 넣고 입력 버튼을 누른다. 이후 SSMS 에서 escape_test 테이블을 조회하여 어떻게 내용이 들어갔나 확인해 본다.

 

 

  실제로 들어간 내용을 보면 우리가 입력한 tom' -- 값이 insert 문법을 깨뜨리지 않고 문자열로 들어가(아마 입력한 그대로 홑따옴표가 들어간거 보면 내부 전달과정에서 이스케이프 처리도 같이 해주는 것으로 추측된다) 있는 것을 볼 수 있다. 이 과정을 통해 고정된 실행계획을 가지도록 정적 쿼리로 설계된 스토어드 프로시저가 Prepared Statements 비슷하게 입력한 문자열을 문법 요소가 아닌 상수로 취급하다는 것을 간접적으로 살펴볼수 있지 않았나 싶다.

 

 

 

 

2.5.2 방어전략 2 - Prepared Statements 방어 살펴보기

  ASP 도 preapared statements 를 지원하는 것은 같지만 나중에도 사용하게 될 php 와 mysql을 가지고 해당 부분을 살펴보려고 한다. 그렇게 하려니 우선 mysql 와 php 를 설치해야되는 인형눈 끼는 작업이 필요하다. 해본적이 없는 사람들은 한번쯤 해볼만한 귀찮은 작업이다.

 

 

 

2.5.2.1 MySQL 설치

  우선 mysql 먼저 설치한다. mysql 은 앞에 설치한 MSSQL 과 과정이 비슷하다.


   MySQL 도 MSSQL처럼 서비스 기반의 데이터베이스이기 때문에 설치를 해야 한다. 구글에서 “mysql community windows installer “ 라고 검색을 한다(버전업이 참 빨리 되는 듯 하다)

 

 

[Download MySQL Installer – MySQL 공식 페이지]
https://dev.mysql.com/downloads/installer/

 


  페이지 하단의 다운로드 경로에서 370메가 정도 되는 풀 설치 버전을 다운로드 하면, 아래와 같이 오라클 회원에 가입하라는 버튼이 보인다.  굳이 회원 가입까지는 필요 없고, 아래의 작은 링크인, “No thanks, just start my download” 를 클릭하면 로그인 없이 다운받을 수 있다.

 


  다운 받을 파일을 실행하여 설치로 들어간다. 우선 라이선스에 동의 체크박스를 체크 후, “Next” 버튼을 누른다.

 

 

  이후 설치 타입으로 “Developer Default” 라디오 박스를 선택 하고 “Next” 버튼을 누른다.

 


 

  필수 프로그램을 체크하는 Check Requirement 항목에서는 그냥 “Next” 버튼을 누른다. 계속 진행 하겠냐고 묻는 창이 뜨면, “Yes”를 클릭한다.

 

 

  설치될 프로그램 리스트 들이 나타나면, “Execute” 버튼을 클릭한다. 조금 시간이 걸리면서 각 요소들이 설치되게 되고, 설치가 완료 되면, “Execute” 버튼이 사라지며 “Next” 버튼으로 바뀌어 표시되면 클릭한다. 

 

 

 이제 세팅 화면이 시작 된다. 리눅스로 따지면 config 파일 수정하고 하는 작업 들이다.  Product Configuration 창에서 “Next” 버튼을 누른다.

 

 


  네트워킹 타입을 설정하는 창이며, 테스트 용도로 하나의 서버로 운영할 예정이므로 기본 값인 “standalone” 으로 두고, “Next” 버튼을 누른다.  

 

 

  다음 화면에서도 MySQL 디폴트 포트인 3306 상태로 그대로 “Next “버튼을 누른다.

 

 

  하위 버전에는 없던 패스워드를 SHA256으로 저장하겠냐는 화면이 나오는데, 2018년 6월 현재 PHP 의 mysqli 모듈을 사용하여 쿼리를 날릴시, 위의 Sha256 옵션을 선택하면 에러가 난다(PDO 방식은 괜찮다는 얘기가 있지만, 지금 다시 해당 스타일로 소스를 고치기도 번거롭기도 해서 예전 옵션으로 가려고 한다^^). 아래의 "Use Lagacy ..." 선택하고 "Next" 버튼을 누른다(많이 쓰는 모듈이니 아마 한 두달 후쯤엔 지원하게 되지 않을까 싶다.).

 


  이후 계정을 설정하는 화면에서 루트 계정의 패스워드를 설정하는 “MySQL Root Password”, “Repeat Password” 항목에 “test1234” 라고 적는다. 해당 값을 입력하면 패스워드 강도가 낮다고 나오는데 물론 실제 운영 시에는 충분히 복잡한 패스워드로 설정해야 한다. 오른쪽 하단의 “Add User” 버튼을 클릭 후, 사용자 등록 창이 나오면 Username 에 “pyuser”, Password 와 Confirm Password 에 “test1234” 를 동일하게 넣는다. 역할은 데이터베이스 관리자(“DB Admin”)이고, 모든 곳에서 연결할 수 있게 한다(“All Hosts”).. 이후 “OK” 버튼을 눌러 유저를 등록 시키고, “Next” 버튼을 누른다. 

 


  계속 실행 하기 위해서 윈도우 서비스로 등록하고, 부팅 시 시작하도록 옵션이 되어 있다. 웹 서버의 실행 계정은 기본 시스템 계정으로 돌아가게 되고, 그냥 “Next” 버튼을 누른다.

 

 

  플러그인이나 확장 패키지는 설치할 필요가 없으므로, “Next” 를 누른다.

 

 

  “Execute” 버튼을 눌러서 설정한 내용들을 적용 시킨다. 이후 버튼이 “Finish” 로 바뀌면 클릭 한다.


 

  라우터 설정이 시작된다(길긴 길다 --;). ‘Next’ 버튼을 누른다. 

 


  라우팅 설정은 현재는 상관없으므로, “Finish” 버튼을 누른다.

 

 

  연결 상태를 체크하는 최종 단계가 시작된다. “Next” 버튼을 누른다.

 


  아까 입력한 root 패스워드인 “test1234” 를 넣고, “Check” 버튼을 클릭한다. 이후 잘 연결이 되면 “Next” 버튼이 활성화 된다. “Next” 버튼을 누른다.

 

 

  최종으로 “Execute” 버튼을 누르고, 모든 설정이 적용된 후 “Finish” 버튼이 나오면 클릭한다.

 


  완료 단계로, “Next” 버튼을 누른다.

 

 

  쉘하고 워크벤치 실행이 체크된 상태에서 "Finish" 버튼을 누른다.

 

 

 

 

2.5.2.2 MySQL 테이블 및 데이터 설정
  MySQL 을 관리할 수 있는 쉘 창과, 워크벤치(MSSQL SSMS 비슷한 클라이언트 이다)가 완료 후 실행 된다. 예전에는 HeidiSQL 같은 외부 클라이언트를 사용했는데, 새롭게 생긴 워크벤치도 괜찮아 보이니, 여기서는 워크벤치를 이용해서 데이터베이스 및 테이블을 설정하도록 하겠다.
 
  설치 완료 후 실행된 MySQL워크벤치에서 “Database > Connect to Database” 메뉴를 선택 한다. 아래와 같이 연결 창이 나오면 Username 란 에 “pyuser” 를 입력하고 “Store in Vault” 버튼을 눌러서, Password란에 “test1234” 를 입력 하고 “OK” 버튼을 눌러서 패스워드 입력 창을 닫는다. 이후 “OK” 버튼을 눌러서 데이터베이스에 연결을 한다.

 


  연결이 되고 나면, 아래와 같은 쿼리 입력이 가능한 창이 생기는데, 밑의 MySQL 에 맞춘 쿼리 내용을 복사해서, 쿼리 입력 창에 붙여 넣기 한다. 이후 해당 쿼리를 MSSQL 때처럼 마우스로 드래그 하여 선택하고, “CTRL+SHIFT+ENTER” 키를 누르거나, 상단 메뉴에서 “Query > Execute (All or Selection)” 메뉴를 실행 한다. 그러면 하단 아웃풋 창에 반영되었다는 결과 메시지가 나오게 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE DATABASE mytest;
 
use mytest;
 
CREATE TABLE supermarket(
    Itemno int NULL,
    Category char(20NULL,
    FoodName char(30NULL,
    Company char(20NULL,
    Price int NULL
);
 
insert into supermarket
values (1'과일''자몽''마트'1500);
insert into supermarket
values (2'음료수''망고주스''편의점'1000);
insert into supermarket
values (3'음료수''식혜''시장'1000);
insert into supermarket
values (4'과자''머랭''조각케익가게'3000);
cs


 
  반영된 것을 확인 하기 위해서 아래의 쿼리를 복사하여 붙여 넣고, 다시 셀렉트 한 후 실행 해 본다. 아래와 같이 결과가 정상적으로 나오게 되면 된다.

1
select * from supermarket;
cs

 

 

 

 

 

2.5.2.3 PHP 설치(with Apache)

   처음 PHP를 설정하는 사람들은 조금 설정이 복잡하다는 생각을 할 수 있다. 왜냐하면 PHP 만 설치하면 되는게 아니고, 아파치(Apache) 서버와 PHP 서버를 각각 설치하고, 여러가지 세팅들을 해야하기 때문이다.

 

  파이썬 글의 플라스크와 장고 파트에서 얘기는 하긴 했지만 웹서버는 보통 2개의 역할로 나누어지게 된다. 사용자의 웹 요청 자체를 처리하는 순수 웹 서버 부분이 있고, 해당 사용자의 요청을 프로그래밍 로직에 맞춰 해석하여 결과를 생성해 반환하는 부분이 있다. 아파치는 웹서버 역할을 하고, PHP 언어가 해석기 역할을 하게 된다(비슷한 예로 JSP는 아파치 웹서버위에 올라가 있는 톰캣이란 모듈로 서비스가 된다. 반대로 PHP 를 앞에서 ASP 에서 사용했던 IIS 와 연결할수도 있다. IIS에 플라스크를 얹을 수도 있고 말이다).

 

  그럼 우선 PHP 프로그램을 세팅하도록 해보자 구글에서 "php download"를 검색해서 공식 다운로드 페이지로 이동한다.

 

[PHP 다운로드 페이지 - PHP 공식 페이지]

http://php.net/downloads.php

 

 

  "Windows Download" 링크를 클릭하고, "VC15 x64 Thread Safe" 버전(x64가 64비트 윈도우를 얘기하고, Thread Safe 버전을 선택해야 현재 새팅에 필요한 DLL 이 있다)을 다운로드 받는다. 그리고 c:\PHP7 폴더에 압축을 푼다(압축 해제시 아래와 같은 폴더 구조가 되어야 한다)

 

 

  c:\PHP7\php.ini-production 파일을 php.ini 으로 이름을 변경 한후, 맨 뒤에 아래의 내용을 추가한다(헷까리는 사람들을 위해 이 글의 맨뒤에 PHP와 아파치 설정 파일 두개를 zip으로 첨부했다). 내용을 보면 우리가 사용할 mysqli 모듈을 읽어오는 부분이다(파이썬에서 import 하는 거랑 비슷한 행위라고 보면 될것 같다)

1
2
extension_dir = "C:\PHP7\ext"
extension=php_mysqli.dll
cs

 

 

 

 

2.5.2.4 아파치 세팅과 PHP 연결하기

  이렇게 하면 현재 시간에 보여주기 위한 용도로는 PHP 세팅이 완료되었고, 아파치 서버 세팅을 하도록 하겠다. 우선 구글에 "apache windows binary" 를 검색해 아래 페이지로 이동한다.

 

[아파치 바이너리 다운로드 - Apache Lounge 사이트]

https://www.apachelounge.com/download/

 

 

  "Apache 2.4.33 Win64" 링크(버전은 시간이 지나면 변할 수가 있다)를 클릭하여 zip 파일을 다운로드 받는다. c:\Apache24 에 압축을 푼다(아래와 같은 폴더 구조가 되어야 한다)

 

 

  PHP와 마찬가지로 설정 파일을 수정해야 한다. c:\Apache24\conf\httpd.conf 파일을 열어 아래의 몇가지 설정을 확인하거나 추가한다.

 

1) ServerRoot 항목에서 서버 루트 경로를 체크한다. 이미 "c:\Apache24" 로 세팅되어 잇을 것이다(버전이 바뀌면 경로에 맞춰서 바꿔줘야 할수도 있다)

1
ServerRoot "c:/Apache24"
cs

 

2) DocumentRoot 항목에서 아파치의 웹 루트 경로를 체크한다. 역시 마찬가지로 세팅되어 있으며, htdocs 는 아파치에서 사용하는 디폴드 웹루트 경로라고 보면 된다(IIS 에서 wwwroot 를 사용하는 것과 비슷하다고 보면 된다).

1
DocumentRoot "c:/Apache24/htdocs"
cs

 

3) Listen 항목(사용자의 웹 요청에 귀기울이고 있다는 의미로 보면 된다)을 확인하면, 기본으로 80 포트를 사용하게 되어 있지만, 현재 ASP 서비스 하는 IIS 에서 80을 이미 사용하고 있기 때문에 충돌나지 않도록 9999 로 바꾼다(서로 다른 프로그램이 하나의 포트를 공유해 사용할 수는 없기 때문이다).

1
Listen 9999
cs

 

4) 마지막으로 앞에 얘기한 PHP 와 아파치를 연결하는 작업을 해야한다. 좀 복잡해 보일지도 모르지만, 아파치에서 PHP 가 어디 있는지 알려주고, PHP7 모듈을 읽어오고, PHP 핸들러를 만든 후, .php 확장자를 해당 핸들러에 연결해 주는 자연스러운 과정이다. 이렇게 되면 웹서버에서 .php 파일이 호출 되면, 아파치가 PHP7 에게 해당 요청의 처리를 위임하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# PHP 설정 파일 경로
PHPIniDir "C:/PHP7"
 
# 사용할 PHP 모듈 읽기
LoadModule php7_module "C:/PHP7/php7apache2_4.dll"
 
# 웹서버가 받은 요청에 대해 해석을 할 핸들러(해당 데이터를 적절히 처리 한다는 의미에서)
AddHandler application/x-httpd-php .php
 
# .php 확장자가 들어오면 앞에서 설정한 PHP7 핸들러에게 전달함.
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>
cs

 

 

   이후 매번 아파치 서버를 실행 시키지 않고 서비스로 등록하기 위해서, 관리자 권한으로 커맨드 창을 먼저 띄운다. “윈도우+x” 키를 눌러서, 왼쪽 메뉴에서 “검색”을 선택한다. 검색 창이 나오면 “CMD” 이라고 찾은 후, “명령 프롬프트” 아이콘이 나오면 마우스 오른쪽 버튼을 눌러서 컨텍스트 메뉴를 띄워 “관리자 권한으로 실행”을 선택한다

 

 

  커맨드 창이 나오면 "cd c:\apache24\bin" 을 입력해 아파치 실행 파일 경로로 이동 후,  "httpd.exe -k install" 을 입력해 아파치 웹 서버를 서비스로 등록한다(이후 부터는 재부팅이 되도 항상 IIS 처럼 실행되어 있게 된다)

 

 

  이후 아파치를 기동 시키기 위해서 탐색기에서 "c:\apache24\bin\ApacheMonitor.exe" 파일을 실행 하면, 트레이 아이콘에 아파치 서비스 모니터링 아이콘이 생긴다(아파치 서버를 시작하거나 멈추거나, 재시작 할때 쓰는 프로그램 이다). 해당 아이콘을 더블 클릭한 후, "Apache Service Monitor" 창이 뜨면 "Start" 버튼을 누른다. 

 

 

  브라우저를 열어 "http://localhost:9999/" 를 입력하면 아래와 같은 정상적인 기동 화면이 나오게 된다(MySQL PHP, 아파치 설정이 끝났다)

 

 

 

2.5.2.5 SQL 인젝션 취약점이 있는 PHP 페이지 살펴보기.

  이제 세팅이 끝났으니 PHP 페이지를 만들어 보려 한다. PHP 쪽에서 유명한 SQL 관련 모듈은 mysqli 와 pdo 두개가 있는 것 같은데, 여기서는 mysqli 를 이용한다. 바로 샘플을 보도록 하겠다.

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
<?php
// 에러나면 표시하게 설정합니다.
error_reporting(E_ALL);
ini_set("display_errors"1);
 
// 연결 문자열
$servername = "localhost";
$username = "pyuser";
$password = "test1234";
$dbname = "mytest";
 
// 연결을 합니다. 
$conn = new mysqli($servername$username$password$dbname);
 
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
 
// 인자가 있으면 받아서
if(isset($_GET["name"])){
    $foodname = $_GET["name"]; 
else {
    $foodname = "";
}
 
// 쿼리에 조합합니다.
$sql = "SELECT Itemno, Category, FoodName, Company, Price FROM supermarket where FoodName=\"" . $foodname . "\";";
 
print "<h3>쿼리 결과</h3>";
print $sql;
print "<hr>";
?>
 
<html>
<body>
 
<form action="mysql_test.php" method="get">
 FoodName: <input type="text" name="name"><br>
<input type="submit">
</form>
 
</body>
</html> 
 
<?php
// 쿼리 결과를 받습니다.
$result = mysqli_query($conn$sql);
 
// 결과가 없을 때까지 루프를 돌리면서
if (mysqli_num_rows($result> 0) {
    while($row = mysqli_fetch_assoc($result)) {
        echo "Itemno: " . $row["Itemno"]. ", Category: " . $row["Category"]. ", FoodName: " . $row["FoodName"]. "<br>";
    }
else {
    echo "0 results";
}
 
mysqli_close($conn);
 
?> 
cs

 

  전체 구조는 대충보면 ASP 와 비슷하다(사실 ASP 코드를 생각하면서 구글을 찾아가며 만들어 봐서 그럴지도 --;). 연결 문자열을 세팅하여 연결하고, 퀴리를 조합하여 DB 쪽으로 쿼리를 보내고, 결과를 받아서 화면에 뿌린다. 역시 취약점이 있었던 ASP 와 비슷하게 쿼리를 직접 조합해서 만들게 된다. "c:\Apache24\htdocs\" 폴더에 UTF-8 인코딩(한글 입력시 에러가 안나게 된다. PHP 도 파이썬 같이 UTF-8이 기본 인코딩 인가 보다)으로 "mysql_test.php" 라고 저장한다.

 

  브라우저를 열어 "http://localhost:9999/mysql_test.php" 를 호출하고, 입력 란에 "자몽"이라고 넣고, "쿼리 전송" 버튼을 누른다. 위쪽 쿼리를 살펴보면 SELECT Itemno, Category, FoodName, Company, Price FROM supermarket where FoodName="자몽"; 문이 실행 되어, mysql 에 넣은 4개의 데이터 중, "FoodName" 이 "자몽"인 데이터 한 건만 나오게 된다.

 

 

  홑따옴표나 쌍따옴표 기호를 넣어보면 ASP 와는 조금 다르게 명시적으로 쿼리가 깨졌다는 에러는 안나오기 때문에(직접 한번 해보면 쌍따옴표는 그래도 문법이 깨졌다고 판단해 그런지-아마도 문법이 깨지는 쿼리를 날렸기 때문에 리턴값이 안 넘어왔을 같다-Warning 이 나오긴 한다), 조금 다른 쿼리를 만들어 쿼리가 동작한다는 것을 증명해 보도록 하겠다. 입력 창에 자몽" or "x"="x 라고 넣어본다. 

 

  위의 쿼리를 다시 보면 where 조건에 FoodName="자몽" or "x"="x" 가 입력 되어, "x"="x" 는 항상 참이기 때문에 FoodName="자몽" 조건에 관계 없이 전체 결과를 가져오는 SELECT Itemno, Category, FoodName, Company, Price FROM supermarket; 와 같은 쿼리가 되어 버린다. 이렇게 되면 SQL 문법의 구조를 바꾸는 인젝션 취약점이 존재한 다는 것을 확인 할 수 있게 된다.

 

 

 

2.5.2.6 Prepared Statement 방식으로 호출하기

  모든 언어가 마찬가지지만 해당 방어 방식을 사용하려면 결국 사용하는 라이브러리에서 지원을 해 주어야 한다. 구글에서 mysqli 의 Prepared Statement 호출 방법을 찾아 구현한 코드가 아래의 코드이다. 전체적인 구조는 앞의 코드와 비슷하지만, $conn->prepare, bind_param 같은 Prepared Statement 호출을 이용하는 코드들이 보인다.

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
<?php
// 에러나면 표시하게 설정 합니다.
error_reporting(E_ALL);
ini_set("display_errors"1);
 
// 연결 문자열
$servername = "localhost";
$username = "pyuser";
$password = "test1234";
$dbname = "mytest";
 
// 연결 합니다.
$conn = new mysqli($servername$username$password$dbname);
 
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
 
// 인자가 있으면 받아서 변수에 세팅을 합니다.
if(isset($_GET["name"])){
    $foodname = $_GET["name"]; 
else {
    $foodname = "";
}
 
?>
 
<html>
<body>
 
<form action="mysql_prepared.php" method="get">
 FoodName: <input type="text" name="name"><br>
<input type="submit">
</form>
 
</body>
</html> 
 
<?php
// Prepared Statement 방식으로 쿼리를 호출해 결과를 받습니다.
$arr = [];
$stmt = $conn->prepare("SELECT Itemno, Category, FoodName, Company, Price FROM supermarket where FoodName = ?");
$stmt->bind_param("s"$foodname);
$stmt->execute();
$result = $stmt->get_result();
 
// 결과가 없을 때까지 화면에 뿌립니다.
if (mysqli_num_rows($result> 0) {
    while($row = $result->fetch_assoc()) {
        echo "Itemno: " . $row["Itemno"]. " - Category: " . $row["Category"]. " " . $row["FoodName"]. "<br>";
    }
else {
    echo "0 results";
}
 
$stmt->close();
 
?> 
cs

 

  마찬가지로 "c:\Apache24\htdocs\" 폴더에 UTF-8 인코딩으로 "mysql_prepared.php" 라고 저장하고 브라우저에 "http://localhost:9999/mysql_prepared.php" 를 호출한 후, 입력란에 아까 SQL 인젝션을 확인 했던, 자몽" or "x"="x 를 넣어서 전송해 본다. 앞의 스토어드 프로시저에서 설명했던 것처럼, 문법 요소가 아닌 상수로 취급되어 에러가 나지 않고 결과만 없게 된다. 아까 Warning이  났던 쌍따옴표를 전송해 봐도, 마찬가지로 괜찮다. 

 

 

  좀 더 실험을 해보고 싶은 경우는 해당 페이지를 수정해서, 앞의 스토어드 프로시저 때 처럼 데이터를 넣어보는 샘플을 만들어 보면 더 확실하게 알 수 있을 것 같다. 예전 버전의 PHP 에서 Magic Quotes 라는 사용자가 입력한 문자 중 좀 위험해 보이는 문자들 앞에 백슬러시를 붙여주는 방어 옵션을 제공하였 었는데, 아무래도 강제로 모든 입력에 대해 이스케이핑 하는 경우는 호환성 같은 사이드 이펙을 많이 발생시켰을 것 같다. 현재 버전에서는 기본 옵션에서 빠져 있는 듯 하며, 자세한 부분은 아래의 글에 나와있다. 

 

[why-magic-quotes-are-gone-in-php7 - thePHP.cc 사이트]

https://thephp.cc/news/2017/08/why-magic-quotes-are-gone-in-php7

 

 

 

 

2.5.3 SQL 인젝션 방어전략 정리

  여기까지  결국 스토어드 프로시저와 Parepared Statement 를 살펴보고 데이터가 어떻게 악용되게 되고, 어떻게 방어가 되는지를 살펴봤다. 사실 인젝션이나, XSS 같은 문제들은 디비나 브러우저 종류에 따라서 공격 케이스가 달라지기 때문에, 경우의 수를 모두 고려해서 개발자나 보안 인력이 고안한 로직으로 필터링 하기에는 복잡한 요소들이 많다.(해당 인젝션의 대상이 되는 서로 다른 밴더의 파서의 동작을 세밀한 부분까지 잘 이해해야 하기 때문에) 그러므로 앞에서 보인 안정되고 공식적인 방어기법에 의존하는 방식이 좋은 것 같다.

 

  다만 앞에서 얘기했듯이, 왜 방어가 되는지에 대해서 데이터 관점에서 이해해야지만, 혹시나 예외적인 상황이 생겼을 때, 데이터를 추적하고, 해당 이유를 찾아 적절한 방어를 할 수 있지 않을까 싶긴하다.

 

  또한 직접적인 방어 말고도 인젝션에 추가로 영향을 미치는 요소들이 추가로 있다. 어플리케이션 실행 계정에 대한 필요한 최소 권한 관리나(필요한 테이블만 조회한다든지, 스키마 변경이나, 시스템 함수 기능등의 실행은 막는 다는지... 이 부분은 프로그램 뿐만 아니라 조회 권한을 가진 사람들의 악용까지도 최소한으로 만들게 된다), 각 입력 변수에 대한 정확한 설계 및 검증(예를 들어 숫자만 들어오는 변수는 숫자형으로만 만든다든지)등이 함께 이루어지면 더더욱 안전한 설계가 될듯 싶다. 해당 부분은 defense in depth 라는 개념에서 얘기하 듯 하나의 방어가 혹시나 뚤리더라도, 다른 방어가 막게되는 안전한 시스템을 지향하게 된다. 물론 이런 부분이 실제 사용성이나, 설계 변경의 용이성과도 출동이 나는 경우는 있기 때문에, 여러 부서들의 이해관계를 기반으로 적절한 선택을 해야할 듯도 싶다.

 

[Defense in depth]

https://www.owasp.org/index.php/Defense_in_depth

 

  마지막으로 어느정도 규모가 있는 회사에서는 법적인 사항때문이라도 어플리케이션 외부의 네트워크 구간에서  데이터베이스에 연결하는 모든 쿼리를 감사하고 통제하는 디비 접근제어 솔루션을 사용하는 경우도 있다. 뭐 여하튼 어떤 경우라도 하나가 모든걸 다 해결해 주진 않는 건 사실이다. 

 

2.5.4 Prepared Statement 사용 예외 사항

  이 부분 다른 일을 하다가 생각이 나서 추가하게 됬다. 알다시피 Prepared Statement 가 좋은 SQL 방어 도구 이긴 하지만 앞서 말했듯이 원래 SQL 방어용이 아니고 정형화된 쿼리를 날리는 용도 이기 때문에 적용이 안되는 경우가 있다. 

 

  예를들어 SQL 서버 쪽에 입력 받은 테이블이나 컬럼명에 따라서 동적으로 쿼리를 날려주는 관리 툴을 만들 경우는 쿼리 요소 중 개념적으로는 테이블 명 이나 컬럼이 인자가 되게 된다. 하지만 이 경우 쿼리 플랜이라는게 테이블 구조나 컬럼 등에 따라서 달라지기 때문에 테이블이나 컬럼이 변경되는 쿼리인 경우 Prepared Steatement 를 사용해서 Parameterized 된 쿼리를 날릴수는 없다. 

 

  이 경우는 인자로 넘어오는 테이블이나 컬럼 변수를 입력값 체크를 통해 위험한 문자들을 제거하는 방식으로 할수 밖에 없을 듯 하다(개인적이라면 테이블 컬럼 생성 표준에 따라 허용되는 문자 -> 아마 거의 "_" 정도)와 아스키 문자, 숫자 정도만 whitelist 로 허용하는 건 어떨까 싶다. 뭐 한글이름으로 테이블을 만들거라면 그것도 추가하고 --;)

 

 

 

 

3. 그 외의 Injection 들

  여기서 모든 인젝션을 찬찬히 다루어도 좋겠지만, 처음에 얘기했듯이 인젝션의 범위는 크게 보면 어플리케이션으로 들어오는 모든 데이터에 대한 이야기기 때문에 범위가 너무 넓기도 하고, 일부는 뒤에서 다른 주제를 얘기할때 다루게 될 것 같다. 아래의 OWASP 페이지를 보면 다양한 타입의 인젝션을 언급 하고 있다(가볍게 한번 읽어보기를...),

 

[Injection Theory - OWASP 사이트]

https://www.owasp.org/index.php/Injection_Theory

 

[Injection Prevension Cheat Sheet - OWASP 사이트]

https://www.owasp.org/index.php/Injection_Prevention_Cheat_Sheet

 

  해당 페이지에서는 SQL, LDAP, 시스템 명령어, HTML, XML, JSON, HTTP Header, File Path, URL, 프로그램에 따라 다른 특수한 스트링 포맷 등 여러가지를 얘기하고 있으며, 해당 요소들이 의미가 있는 이유는 보통 프로그램들이 내부나 외부에서 데이터(메시지)를 교환할때 위와 같은 포맷을 통해서 데이터를 전달해서, 내부에서 파싱해 처리하는 부분이 많기 때문이라고 생각하면 될듯 하다.

 

  해당 부분을 방어하기 위해서는 역시 SQL 인젝션 방어로 사용된 스토어드 프로시저나 Prepared Statement 와 비슷하게 각 인젝션 타입에 특화된 방어 방식이 있다면 해당 방어 모듈, 프레임워크 등을 채용하고(1순위), 화이트 리스트를 기반으로 검증된 데이터만을 통과시키거나(2순위), 검증된 이스케이프 함수(URL encoding, HTML encoding 등)를 사용하거나(3순위), 마땅한 함수가 없거나 데이터의 자유도에 대한 요구사항이 높아서 화이트 리스트 방식을 사용하기 힘들다면 문법을 깨뜨릴 수 있는 문장이나 특수 문자를 삭제하는 커스텀 필터를 만들거나(4순위)하는 게 맞지 않을까 싶다.

 

  그러한 바탕위에 다른 측면에서 SQL 인젝션에서 얘기한 권한 최소화(웹서버 실행 권한은 필요한 권한만 가지도록 설계한 전용 계정으로 실행하거나, 명령어 실행이 필요할 시 범용 함수보다는 디렉토리 생성이라든지 하는 특정한 용도로만 실행하는 함수를 사용한다거나)의 여러 측면을 설계와 운영 부분에 적용하는 것이 맞지 않을까 싶다. 

 

  SQL 인젝션 예제를 통해서 인젝션의 일반적인 특성에 대해서는 설명되었다고 생각되고, JSON 이나 HTTP Header, File Path 등에 대해서는 다른 시간에도 비슷한 관점으로 다루게 될 것이기 때문에, 여기에서 내용을 마치려고 한다(약간 늪에 빠지기 전에 도망치는 느낌은 있다^^).

 

 

 

4. 마무리 하면서

  사실 인젝션을 설명하다보면 약간은 모순된 느낌에 빠지게 된다. 한 측면에서 보면 인젝션이 보안의 모든 요소에 영향을 주는 아주 중요한 위치를 차지하고 있지만, 사실 인젝션을 이해하는 중요한 열쇠는 보안적인 패턴 보다는 각 인젝션의 목적이 되는 데이터베이스, 대상 시스템 같은 도메인이나, 해당 현상이 일어나는 실제 장소인 프로그래밍 언어 환경에 있다고 본다. 만약 그러한 대상이 되는 기술 자체를 이해하지 못하는 상태에서 인젝션에 대해서 논하는 것은 (아마도) 탁상공론이 되기 쉬울 것이다.

 

  또 다른 측면에서 보게되면 세부적인 보안 방식은 대상이 되는 기술에 종속적이기는 하지만, 전체적인 맥락에서 인젝션이라는 것은 데이터가 외부로부터 프로그램 안으로 들어가는 모든 행위를 나타내기 때문에, 프로그래밍 언어 그 자체의 이슈 인것 같은 부분도 있다. 어째든 처음엔 어려워 보이는 대상이 잘 알게 되면 생각보다 별 것 아닌 경우도 많고, 그렇게 이해하여 별 것 아니게 보였던 대상도 다시 자세히 따져 보려하면 어려워 보이는 경우가 종종 있는듯 싶다. 그 둘 사이에서 균형을 잘 유지하는 것도 중요할 것 같다.

 

 

[PHP7, 아파치 컨피그 파일 첨부]

config_files.zip

 

 

 

2018.6.17 by 자유로운설탕
cs

 

posted by 자유로운설탕
2018. 3. 2. 23:37 프로그래밍

   안녕하세요. 블로그에 연재했던 "구글로 공부하는 파이썬" 글이 출간 제의를 받아 책으로 만들어 졌습니다.

 

  책의 경우 조금은 장점을 더해야 할 듯 해서, 아래의 내용들이 추가 및 수정 되었습니다.

  • 현재 최신 버전인 파이썬 3.6.4 로 업데이트 하여 진행(5월인가 3.7 정식 버전이 나오는 것으로 알고 있습니다. 그때까지는 일단 최신이겠네요;)
  • 작년말 업데이트 된 장고 2.0 으로 진행(장고 1.11 과 비교하면 라우팅 부분의 코드가 조금 변경되었습니다)
  • 4장 SQL 부분에 MySQL, 몽고디비, 오라클, Sqlite3 설치 및 세팅, 조회 예제 추가
  • 각 장의 뒤에 예제에 나온 문법 요소들을 설명하는 미니문법 섹션 추가
  • 3.6 버전 pymssql 한글 깨짐 현상에 의해 pyodbc 사용 예제로 변경
  • 자잘한 예제 및 구성 변경(Plotly 오프라인 예제, 작업 자동화 예제 간략화, d3.js 에러에 대한 장고 어플리케이션 측면에서의 해결 코드 추가 등)
  • 3가지 무료 편집기(PyCharm, Visual Studio Code, Atom) 설치 및 예제 실행을 위한 세팅 방법 부록으로 추가
  • 모호한 내용을 다듬거나, 링크로 소개했던 부분을 내용으로 구성하여 업데이트 함(잘못 설명한 내용들은 블로그에도 곧 업데이트 할 예정 입니다)

 

   계약 관계로 책의 내용을 블로그에 업데이트 하진 못하지만, 답변에는 제약이 없도록 출판사 쪽에 양해를 얻은 상태 입니다. 해당 부분 이해해 주시길 바라며, 혹시 블로그를 참조해서 진행하시다가 막히시는 경우 언제라도 댓글로 문의해 주세요^^.

 

   그럼 이런 글을 올리는게 좀 민망스럽긴 하지만, 모쪼록 이해 부탁 드립니다.

 

 

posted by 자유로운설탕
2018. 1. 1. 19:15 보안

  이번 시간에는 코드 읽기라는 주제로, 어플리케이션 보안에 있어 코드를 읽는 부분이 어떤 의미를 가지고 있는지에 대해서 이야기 해보고, 현실적으로 코드 읽기를 적용하려 할때 어떤 문제가 있는지에 대해서도 잠시 얘기해 보려 한다. 개인적으로는 일반적인 테스트든, 보안에 관한 테스트든 가능하면 블랙이나, 그레이 박스 관점에서 진행하는 것에 추가해 소스를 이용하는 것을 선호하는 편이다. 그렇게 함으로써 미지의 범위에 대한 불안감도 많이 덜어주고, 적절히 사용하면 효율성 면에서도, 원래대로 라면 당연히 해야 되는 단순 작업을(인형눈 끼우기 같은...) 많이 줄여주기 때문이다.

 

 

[목차]

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

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

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 

 

 

1. 들어가면서

  만약에 아래와 같은 단순한 터치스크린 화면 기반의 프로그램에 대한 보안 테스트를 한다고 가정해 보자. 로직은 단순해서 금액을 넣을 경우 돈이 인출되는 프로그램이다(단순화를 위해 인증이나, 비밀번호 등의 체크는 앞에서 이미 진행했다고 가정).

 

 

  그럼 일반적인 테스트나 보안 테스트 요소를 적당히 리스트업 해 보면, 아래와 같은 여러가지 케이스 들이 생길 수 있다.

1) {-1}, {0, 1, 2}, {999, 1000, 1001} - 마이너스 값, 경계 값 
2) {10000}, {32768} - 아주 큰 값
3) SQL 등 Injection 에 관련된 문자나 특수 문자 

4) show me the money 같은 치트키 입력

5) 그 밖의 예상 못한 결과들(버튼을 빠르게 두 번 누르거나, 화면의 구석에 "Hi" 라고 그리면, 비밀 메뉴가 열린다 든지)

 

 

  위의 케이스 중에서 1~3번 정도는 어떻게든 커버할 수 있겠지만, 4~5의 영역은 블랙박스(외부의 UI만 보고 하는)나 그레이 박스(외부의 UI 와 일부 내부 테이터를 보면서 하는) 테스트 기법을 가지고는 사실 커버하기가 힘들다. 4번 같은 경우는 잘 악용되는 문구의 사전식 대입 정도가 최선일 것이다(보안에서 스캐너가 어드민 페이지 경로를 찾을때 하듯이 말이다). 결국 보안 테스트에 대한 케이스는 요즘 같은 복잡한 어플과 인터페이스 및 서비스 들이 엮어 있는 상태에서는 사실 거의 무한대라고 봐도 될 것이다.

 

 

  그런데 만약 우리가 해당 화면에 대한 소스를 볼 수 있어서, 입력 박스가 "intMyMoney"라는 이름을 가지고 있고, 인출 버튼이 "btnWithdraw" 라는 이름을 가진것을 알수 있고,

 

 

  버튼을 눌렀을 때의 실행되는 코드가 아래와 같이 이루어 졌다는 것을 볼 수 있다면 어떻게 될까?(참고로 아래의 코드는 예를 들기 위한 가상의 코드이다)

1
2
3
4
5
6
7
8
On_btnWithdraw{
 
If(isNumeric(intMyMoney) & intMyMoney >= 1 & intMyMoney <= 1000)
    { 인출 하는 함수 }
Else 
    { error 를 처리하는 함수 } 
 
}
cs

 

 

  충분히 코드의 관리나 빌드 및 배포가 안전한 프로세스 상에서 이루어지고 있다는게 보장되고 있는 상태라면, 우리가 앞에 리스트업 했던 여러 상상의 테스트 케이스 들은 현실적인 케이스로 좁혀질 수 있게된다. 예를 들어 아래와 같은 코드라면 isNumeric 함수가 잘 동작되는지 보기위해 문자, 특수문자 정도를 넣어서 예외처리가 잘되는지 보고, 1과 1000 사이의 경계값 정도를 체크해 보면 테스트가 마무리 될 것 같다.

 

  다만 한가지 눈에 코드가 보인다고 해서, 해당 코드의 동작을 증명하는 실제 테스트 조차 안 하는건 아주 위험한 행위이다. 사람이란 언제나 편견에 의한 착각과 실수를 하기 때문에 눈으로 보기에 확실해 보이는 것도, 실제로는 아닌 경우가 많기 때문이다(개인적으로 자신있게 그러다가 몇번 돌을 맞은 후 얻게 된 교훈으로, 개발자와 같이 동시에 보더라도 서로 의견을 나누다가 편견이 생기기도 한다. 만약 두사람이 라면 서로 상대방의 가정을 모르는 상태에서 보는게 더 날것 같다-비슷한 경험으로 백화점 알바할때 재고 숫자를 세기 위해서 두 명의 알바가 따로 따로 세서 서로 숫자가 맞으면 넘어갔던 적이 있다).  

 

  게다가 만약 위의 코드에 개발자가 테스트를 위해서(또는 나중에 이익을 얻기 위해서) 아래와 같은 코드를 심어 놓았다면 어떻게 될까?  물론 저런 사고를 막기위해서, 개발자 간 피어 리뷰 또는 커밋이나 빌드 전 커미터라고 불리우는 시니어들의 최종 코드 리뷰를 하기도 하고, 개발 환경에서만 유효한 안전한 디버킹 코드를 넣는 방식을 가이드 하기도 한다지만 해당 부분은 한쪽으로 밀어놓고 무시해 보자.

1
2
3
4
5
6
7
8
9
10
11
On_btnWithdraw{
 
If(intMyMoney == "Show Me the Money")
    { 무한대 인출 가능 함수 }
 
If(isNumeric(intMyMoney) & intMyMoney >= 1 & intMyMoney <= 1000)
    { 인출 해줘 함수 }
Else 
    { error 처리 함수 } 
 
}
cs

 

  아마 블랙박스 스타일의 검증으로는 위의 부분을 절대 잡을 수 없을 것 이다. 실제 저런 일은 드물거나, 어딘가 안 들키고 조심스럽게 숨겨져 있겟지만, 비슷한 느낌의 사건이 맥 OS 쪽에서 발생했었다(아마 해당 경우는 최초 root 계정이 비활성화 된 상태에서, 패스워드를 지정하면서 명시적으로 활성화 할수 있는데, 어떤 이유인진 모르지만 패스워드가 없는 상태에서 디폴트로 활성화 된것으로 추정되긴 한다).

 

  여튼 위와 같은 방식의 코드는 아니겠지만, 백도어 기능이 생겨버렸던 것도 맞고, 리그레이션 케이스(새로운 빌드가 나왔을 때 기존의 주요 취약점이나 버그등을 확인하는 것)로도 잡기는 힘들었을 것이다(왜냐하면 나름 저런 희귀 케이스들은 보통 사건이 일어난 후 리그레이션 테스트에 추가되기 때문이다). 저런 사실을 해커가 아는 상태에서 모르는 척 사용한다면 얼마나 큰일이 될까 생각해보면, 화이트 박스 개념의 취약점 체크가 블랙 박스 만큼 중요한 의미를 가진다는 것을 조금은 어필이 되지 않을까 싶다.

 

 [애플, 하이 시에라 ‘루트’ 취약점 수정한 보안 업데이트 배포 - IT World]

http://www.itworld.co.kr/news/107371

 

 

 

 

2. 다른 예 - 복잡성의 단순화

  또는 만약 특정 사이트에 있는 모든 이미지 업로드 기능을 체크 하거나, 이미지 업로드 기능이 많은 관리자 페이지에 대해서 업로드 취약점이 있는지 체크를 한다고 할 경우, 만약 소스의 내용을 전혀 파악할 수 없는 상태라고 해보자. 이 경우 모든 페이지를 일일히 살펴보면서 체크하거나, 시간이나 리소스가 충분하지 않다면 업로드 로직 호출 패턴이나, 감에 의해서 같아 보이는 종류로 묶어서 유형별로 한개씩만 테스트 하고 무사하길 빌 수 밖에 없다(물론 운이 좋은 경우는 공통 업로드 URL 이 존재해서, 해당 URL만 호출 할 경우도 있을 수는 있을 것 같지만...). 

 

 

  이렇게 내부적으로 공통적인 로직이 들어있는지 알 수 없는 경우 막막하게 느껴지지만, 반대로 소스를 볼수 있을 경우에는 문제가 달라지게 된다. 소스내에서 공통된 업로드 처리 함수가 있는지 체크하고, 해당 함수내에서 외부에서 들어온 업로드 관련 인자들을 어떻게 처리하는지, 어느 폴더에, 어떤 이름으로 저장하는지를 체크 후, 실제 존재하는 업로드 관련 페이지들이 해당 안전한 공통 로직을 이용하는지만 여러 방법으로 찾아 증명만 하면 된다(해당 부분은 나중에 파일 업/다운로드에 대한 시간에 자세히 살펴 보려고 한다).

 

  최악의 경우 개발자 들이 각각 공통 함수를 무시하고, 자신만의 로직으로 업로드 코드를 개발하였다고 하더라도 어떤 부분이 공통이 아니여서 좀더 세심하게 들여다 볼 필요가 있는지를 정확하게 알 수 있게 된다. 물론 이런 작업을 페이지 소스를 살펴보면서도 할 수 있지만, 들이는 에너지와 정확도 면에서 소스를 보는 쪽이 휠씬 유리하다.

 

 

 

 

3. 일반 언어와의 비교

  그럼 프로그래밍 언어에만 한정 짓지 말고 일반 언어와 한번 비교해 보려고 한다. 언어에도 읽기, 쓰기, 듣기, 말하기가 있듯, 코드에 대해서도 비슷한 부분이 있다.

 

 

3.1 읽기
  읽기 측면에서 코드를 통해서 여러 점검이 필요한 중요 로직들을 얻어낼 수 있다. 특히 전형적인 취약점이 아닌 설계에 대한 보안 문제는 전체적인 로직을 잘 살펴보아야만 발견할 수 있을 가능성이 높다. 물론 관련 기획 문서를 보거나, 실제 해당 페이지나 기능들을 실행해 보는 것도 효과가 있지만, 소스 읽기는 밖에선 잘 안 보이는 숨겨진 부분들을 다른 관점에서 살펴보게 해준다.

 

  다만 여기서 한가지 짚고 넘어가고 싶은 것은 해당 효과를 꼭 소스 읽기를 통해서만 얻을 수 있는 것은 아니라는 점이다. 기존에 알고 있는 여러 지식으로 부터의 유추나, 관련 개발자와의 대화 및 문의에 의해서도 많은 것을 얻어낼 수 있다. 다만 이 경우도 소스를 어느정도 이해하고 있는 것은 정확한 질문을 던져서 원하는 해답을 얻어낼 수 있을 가능성을 높여주게 된다. 보안과는 조금 먼 관점의 해당 개발자 스타일의 설명을 잘 이해해, 점검에 도움이 되는 형태로 스스로 재가공 할 수도 있다. 경험상 맘에 맞거나, 일하는 스타일이 맞는 개발자 동료가 있는 것은, 구글 검색엔진이 도와주는 것과 비슷하게 든든한 효과를 주게 되므로, 상황이 어쩔 수 없다면 모르겠지만 꼭 혼자서 모든 걸 부딛치려고 하는 것은 좋은 것은 아니다. 

 

  또한 로직을 이해하기 위해 코드를 읽는 것은 개발을 하기위해 코드를 읽는 것과는 조금 다른 측면이 있다. 만들기 위한 코드 읽기는 프로그램의 세부 로직과 문법의 디테일한 구조를 동시에 살펴봐야 하지만, 보안을 위한 코드 읽기는 데이터의 흐름에 따른 요점만을 읽는다고 보면 된다. 예를 들면 입력 인자들이 어떤 외부에서 들어오고, 해당 인자들이 내부 로직에 어떻게 전달되고, 어떤 처리를 거쳐서, 어떻게 안전하게 사라지게 되는가의 라이프 사이클을 살펴보는 작업이다. 또한 현실과 차단되어 코드 만을 보는 것 보다, 블랙박스 같이 바깥 쪽의 흐름도 참고하면서 비교해 보는게 좀더 난듯 하다.

 

 

3.2 코드 쓰기
  현대의 코드 쓰기 작업은 많은 부분이 수정(modify)의 측면에서 사용되는 경우가 많은 듯 하다. 예를 들어 API 를 호출해 결과를 화면에 뿌려줄 경우도 처음 부터 맨땅에서 시작하는게 아니라, API 를 만든 사람이 제공하는 샘플을 기반으로 구현하게 된다. 또 구글링을 통해서 많은 코드를 얻고 있고, 해당 언어에 대한 샘플이 없더라도, 다른 언어로 이루어진 샘플을 보면서도 아이디어를 얻게 된다.

 

  그래서 패턴이 되는 조각 코드들을 적절히 만들거나 참조해 쓸 수 있게 되면(예를 들어 HTTP 요청과 받기, HTTP 소스 분석, 주기적인 처리), 어느 언어를 사용하든 구글을 이용하여 적당히 원하는 동작을 하는 코드를 참조해 만들어낼 수 있다(어떤 사람들은 이런 웹에 돌아다니는 코드는 돌아가긴 하지만 유지보수가 성능이 보장 못 한다고 저평가 하는 측면도 있지만 해당 부분은 관점에 따라 맞는 것도 틀린 것도 같다. 그렇게 따지면 우리가 쓰는 오픈소스도 웹에서 돌아다니는 코드니까 말이다). 이러한 작업이 가능하기 위해서는 정규 표현식, 여러 데이터베이스, 스크립트언어, 빌드 방식 언어, 빅데이터 오픈소스 등등 여러 언어와 배경 지식들을 평소에 습득하되, 특정한 기술에 의존적인 부분 보다는 기반적인 부분에 초점을 익히는 것이 좀더 바람직 하지 않을까 싶다.

 

  또 이러한 "서당개 3년" 스타일의 쓰기는 보안 테스팅을 할때도, 실제 환경상으로 불가능하거나, 실 사이트에 영향을 미쳐 수행하기 힘든 부분을, 코드를 적절히 변경하여 테스트 환경에서 체크함으로서, 최소한의 안전을 보장하게 만들 수도 있다(예를 들어 위의 예를 든 업로드 코드를 실제 운영 환경에서 돌려볼 순 없지만, 방어를 하는 필터 함수만을 잘라내어 체크한다든지 하는 상황이 있을 수 있다). 

 

  그래서 코드를 백지부터 만드는 능력이 떨어져도(요즘 이런 사람이 얼마나 될까는 싶다), 우선은 기존 프로그램들을 조금씩 수정해 보는 모드로 공부하는 것을 추천한다. 언어로 따지면 쓰기 전에 우선 많이 읽어서 여러 패턴을 익힌후, 해당 패턴을 이용해서 더듬더듬 쓴다고 봐도 될것 같다. 프로그램의 좋은 점은 언어의 말과는 달리 잘못 쓰게 되면 아예 안돌아가거나 버그가 바로 나오기 때문에 짧은 한마디를 할때부터 자동 교정의 훈련이 되는 점이다. 다만 충분한 경험이 쌓이기 전에는 이해가 안 되는 에러가 나서 답답함이 많긴 하지만 말이다(그 경우 구글을 도움을 받으면 된다).

 

 

3.3 듣기 & 말하기
  취약점이 발견됬을때 해당 취약점을 올바르게 고치려면 해당 언어와 돌아가는 환경을 올바르게 이해할 수 있으면 좋다. 해당 지식을 기반으로 개발자와 의논하게 되면, 앞에서 얘기한 바와 같이 개발자의 언어로 설명하는 주제들에 대해서도 비교적 쉽게 이해도 할수 있고(또 이러면서 많이 배우게 된다), 적절한 논의 및 보정, 협의를 할수 있게 된다.

 

  또한 이쪽에서 원하는 부분에 대한 빠른 피드백을 얻을 수도 있고, 단순히 어떠한 작업을 해달라고 요구사항 만을 기계적으로 전달하는 것보다는(보통 취약점은 찾을 수 있지만, 대상 코드를 어떻게 수정해야되는지에 대해 정확하게 모르는 경우 이런 태도를 취한다. 극단적으로 보면 "네가 알아서 고쳐" 이런 모드다) 사이드 이펙트가 없는 정확한 코드의 수정이 이루어질 수 있다. 개발 쪽에서는 의외로 보안 쪽에서 당연하다고 생각하는 언어의 보안적 관점에 대해서 모르는 경우가 많기 때문에, 원하는 방식과 결과만을 이야기 하는것만으로 충분하지 못할 때가 많다.(원리와 결과에 이르는 과정을 이해시켜 주는 것이 종종 필요하다).

 

  다만 이 부분은 이상적인 얘기고, 개발자에 따라서는 자신의 코드 영역에 들어오는 것을 싫어하는 사람들도 꽤 많다. 그런 경우 본인의 코드 영역을 간섭 받고 있다고 받아들이게 되어 역효과를 가져올수도 있다. 반대로 적극적으로 코드의 흐름에 대해서 얘기하면서 의논하는걸 좋아하는 개발자도 많이 있다. 상황 상황에 따라 틀리지만, 적절히 상대의 성향에 맞추어서 잘 협상하는 것이 좋다고 생각한다(개인적으로 이런 문제로 부딛쳐 사이가 안 좋아진 경우가 있는데 지금은 좀 후회가 된다. 지나고 나면 좀 부질 없는 일들 같기도 하다)

 

 

4. 보안에서의 코드 읽기의 문제점

  아마 어떤 분들은 위의 글들을 읽으면서 현실과 동떨어진 얘기라고 생각 할수도 있을 것 같은데, 그런 부분들에 대해서 언급해 보려고 한다.

 

 

4.1 현실적으로 어려운 점

  확실히 코드를 읽으면서 보안 설계가 잘 되어 있는지 체크하는 방법은 단점 또한 많다. 첫 번째로 한정된 시간이다. 주어진 시간이 몇 일 밖에 안되는 상황에는 남이 만든 소스를 들여다 보면서 로직을 파악하는 것보다는, 주어진 시간을 효율적으로 쓸수 있는 다른 방법들을 찾아보는 것이 낫다. 시간과 리소스를 고려하여 블랙박스 접근법 만을 쓸지, 화이트 박스 접근법을 같이 쓸지, 아니면 일부만을 보조해서 쓸지를 선택해야 한다.

 

  두 번째는 "1 vs N" 의 문제로써, 인원 비율 상 수 많은 개발자들이 만든 소스를 보통 한 사람이 살펴봐야하기 때문에 적절히 범위를 제한하거나, 요점만을 읽는 속도를 높이는 방법을 계속 고민하고 연습해야 한다.

 

  세 번째는 초기 진입 장벽으로 이것은 영어를 처음 배울때와 비슷하다. 처음에 소스를 읽게되면 하나하나 문법적인 부분도 따져가면서 읽어야 해서 모르는 것도 많고, 느리고, 난해한 해석 과정을 거쳐야 한다. 이 부분은 프로그래밍을 만드는 것을 배우는 것과 비슷하다고 보고, 피할 수 없는 측면이 조금은 있다. 분명한 것은 영어책과 마찬가지로 많이 읽으면 읽을 수록 빨라진다.

 

  네 번째는 회사에서 보안이나, 지적재산권 보호 등의 여러가지 이유로 특정 개발자 이외에는 소스를 보여주지 않는 경우도 있을 수 있다. 이 경우는 세월을 이용한 증명과 설득 밖에 없어 보이는데, 해당 회사에서 꾸준히 근무하여 신뢰를 쌓고, 검증을 위해 코드를 보는 방법이 효율적이고, 안전한 코드를 만드는데 도움이 된다는 부분을 설득해서 권한을 획득하는 방법밖에 없을 것 같다. 어떤 회사는 내부자에게 모두 공개하는 회사도 있으니 복불복일 듯은 싶다. 근데 막상 공개해 줘도 실력이 안되 못 볼 수도 있으니 자기 실력은 잘 객관적으로 파악하고 있어야 하는 듯도 싶다.

 

  마지막으로 세상엔 많은 언어들이 있고, 자신의 일하는 환경에도 많은 언어들이 혼재되어 있을 수 있다는 부분이다. ASP, PHP, C#, 자바, SQL, 자바스크립트, jQuery, Node.js, 스칼라 등등의 수 많은 언어들과 언어의 배경지식들을 적절히 따라잡아야 한다. 또 앞으로 나오는 새로운 기술들도 새 프로젝트에서 기존의 패턴들을 사용하기 위해서 어느 정도의 깊이로는 따라가야 한다고 본다. 또 은근 코드를 볼때, 코드가 저장된 저장소 특성에 따라서, 해당 저장소에 적합한 검색 방식을 습득하거나 궁리해야 하기 때문에 문제가 된다. 낯설지 않은 저장소에 담겨진 소스를 봐야하는 경우는 이전 버전의 비교라든지 하는 작업 부분에서 병목을 가져올수 있게 된다.

 

 

4.1 현실적으로 다행인 점

  위와 같이 난해한 코드 읽기의 세계에서도 다행인 점들이 있다. 

 

  첫째로 패턴화가 가능하다는 것이다. 한 프로젝트 안에는 많은 유사한 구조와 로직들이 있고, 그 구조와 로직은 다른 프로젝트에서도 존재할 가능성이 아주 높다(코드나 구조의 복사는 바이러스와 같고 생각보다 정답은 비슷한 경우가 많다). 또 많은 소스들이 구글 검색이나, 해당 언어 및 라이브러리의 가이드, 다른 시니어, 이전 개발자의 코드 등을 기반으로 만들어졌기 때문에, 이런저런 사유로 유사한 로직과 구조들도 많은 편이다. 또한 웹같은 경우는 동작이나, 디자인 상의 유사적인 부분이 많다(이런 것을 편하게 만들기 위해서 파이썬의 장고나, 자바의 스프링, .NET의 MVC 패턴 같은 프레임워크 들이 있는 것일테고..). 

 

  둘째로 영어, 중국어 등의 언어와 마찬가지로 소스를 많이 보게되면 그만큼 읽는 속도가 빨라지게 된다. 요점만 골라읽는 요령도 조금이나마 늘게 되고 말이다. 또 스스로 로직을 정리하기 때문에 계속적으로 업그레이드가 되는 프로그램 이라면, 향후 추가적인 점검을 진행 할 때 해당 지식들이 좋은 백그라운드를 형성하게 된다(그래서 도메인 전문가가 생기는 걸꺼다..). 그리고 아마도 직접 만드는 만큼은 아니지만, 간접적으로 프로그래밍에 대한 경험적 노하우가 쌓이게 되어 나중에 자동화 등의 분야에서 코드를 만들어 볼때도 도움이 된다.

 

  마지막으로 앞에도 잠시 얘기 했지만 잡(Job), API 같은 UI 적인 형태가 없는 프로그램을 분석하고 테스트 할 때, 프로그램의 형태 및 어떻게 돌아가는지를 이해하기 쉬워 해당 검증 작업을 용이하게 해줄 수 있다.

 

 

 

5. 실행 전략

   조금 앞의 내용과는 겹치는 듯 하지만, 정리하는 의미에서 어떻게 그럼 코드 읽기를 이용할까를 생각해 보자.

 


  첫 번째로, 항상 "시간 vs 선택한 방법의 효율"을 생각해야 한다. 자신이 선호하는 한 두가지 기술에만 집착하면서 진행하다 보면, 오히려 자가당착에 빠지게 된다. 정말로 안으로부터 보는 게 가치가 있는가를 검토해서 확신이 있을 경우만 코드 읽기 방법을 이용해야 한다. 프로그램의 도메인에 따라서 밖에서 보는 것이 더 단순하고 명확할 때도 많다.

 

  두 번째로, 코드를 읽으면서 습득한 지식은 가능하면 문서화를 해두는 것이 좋다. 시간은 기억을 희미하게 만들어서, 다 이해했던 코드도 몇달만 지나면 가물가물 해서 다시보면 이게 뭔가 싶어진다. 이건 매일 코드를 보는 개발자들도 마찬가지라고 한다.

 

  세 번째로, 자신이 선호하는 언어는 있어도 괜찮지만, 보통 주력 언어의 업무를 가능한 고집할 수 있는 개발쪽 과는 달리(물론 새로운 기술의 압박은 계속 되겠지만), QA나 보안 같은 경우는 언제 어떤 언어와 환경을 체크해 해달라고 던져올지 알수가 없는 부분이 있다. 그래서 가능한 언어 자체보다는 언어가 의존하는 여러 배경 지식을 익히고, 언제라도 새로운 언어나 환경에 적응해야 된다는 마음가짐을 가지고 있어야 한다고 생각한다. 다만 다행히 패턴이란게 있고, 새 언어는 기존 언어를 벤치마킹 하는 경향이 있기 때문에, 스크립트 언어를 하나 열심히 경험에 두면 다른 스크립트 언어에 대해 적응이 많이 수월하고, NET, 자바 같은 객체지향의 언어도 마찬가지라고 본다.  

 

  네 번째로, 읽는 속도도 분명이 중요한 요소중 하나라는 것이다. 코드를 읽고, 기능을 분류하고, 케이스를 제외하거나, 뽑아내는 것도 보안 테스팅의 한 요소이긴 하지만, 그 부분에 너무 시간을 쏟다보면, 실제 해당 분석에서 세운 가설들을 직접 해보면서 증명할 시간이 모자랄 수도 있게 된다. 그리고 보통 개발 뒤에 일어나는 검수 업무에 대해서는 생각보다 충분한 시간이 주어지진 않는 경우가 많아서 마린의 스팀팩과 같이 집중해서 속도를 낼 수 있는 훈련을 꾸준히 하는것도 중요한 것 같다(뭐 이렇게 보면 스포츠 경기의 준비 단계와 비슷한 것도 같다). 더 좋은 방법은 분석에 대한 시간을 미리 벌기 위해 개발 단계에서부터 참여가 가능하다면 좋을 것 같다.


   다섯번째로, 역시 앞에서 한 얘기지만 혼자서 모든걸 할수 있다고 생각하는 것 보다, 믿을 수 있는 동료들과의 좋은 네트워크를 가지는 것이 좋다. 일하는 스타일이 맞는 친한 개발자들을 만들고(물론 일을 위해서 억지로 관계를 만들라는건 아니다), 개발자들이 프로그래밍 언어에 접근하는 방식을 계속 관찰하다보면, 단순히 책이나 혼자서 공부하는 과정에서는 발견하지 못하는 사실들을 배울 수 있게 되는 것 같다.

 

  마지막으로 쓰기의 경우는 스스로도 잘 못하는 부분이긴 하지만, 좋은 라이브러리들의 사용에 집중을 하고, 오픈소스들을 이해하고, 실용성에 중심을 두고 접근을 하는게 어떨까 싶다. 앞의 파이썬 글도 사실 그런 바램에서 진행된 활동이라고 보면 된다. 

 

 

 

 

6. 이런 저런 꼬투라기

  꼬리가 길지만 마지막으로 몇가지 생각나는 부분을 적어보면 

 

 

  첫 번째로 화이트 박스 기법을 적용하는데 있어서 가장 중요한 부분 중 하나가 소스 컨트롤이 얼마나 잘 구축되 있으냐 인것 같다. 소스가 자기 멋대로 변경되어 버린다면, 기껏 분석 및 테스트 했던 내용들이 도로묵이 될 수 있으니 말이다. 또는 소스 비교나 변경이력 추적도 분석에 도움이 되는데, 해당 부분이 체계적이고 원활하게 되는 환경이 지원된다면 좀더 유리할 것 같다. 다만 개발쪽에서 편한 툴은 검증 하는 쪽에서는 좀 정보가 부족해 불편한 경우가 많긴 한다.
 

 두 번째로 방법론이 검증하고자 하는 대상을 앞서서는 안 된다. 방법론은 효과적인 점검을 위한 수단이 뿐이지 가장 중요한 것은 대상에 대한 분석적 파악이 우선이다. 이후 어떠한 방법론을 쓸까 하는 것은 분석의 결과지 행동의 목적이 되어서는 안된다.

 

  세 번째로 방어측면 에서 OWASP 같은 데서 제공하는 여러 방어 함수의 효율성과 성능에 대해 꾸준히 관심을 가지는것도 좋은것 같고, 파이썬 등으로 조그마한 툴을 만들어 업무에 사용해 보는 것도 괜찮은 것 같다. 보안 쪽은 의외로 패턴화된 문제들이 많아서 자동화의 ROI 가 좋고 생색내기도 편한 것 같다. 또한 오픈 소스의 사용은 내부 로직을 제대로 이해한 채 사용해야, 필요한 커스터마이징도 할수 있고(외국은 보통 이렇게 쓰는 듯하다), 해당 프로그램의 적절성과 한계를 인식할 수 있다고 생각한다.

 

 

[마무리 하면서]

  스스로 코드 읽기에 대해서 충분히 잘 한다고는 할수 없다 생각되지만, 보안이나 QA에 새로 들어오는 사람들이 직업의 초창기 때부터 의식적으로 노력했으면 하는 부분이라서, 이렇게 얘기를 하게 되었다(개인적으로 더 열심히 할걸 하는 아쉬운 부분이기 때문이기도 하다). 아무쪼록 공감하는 분이 있음 좋겠다.

 

posted by 자유로운설탕
2017. 12. 2. 22:35 보안

  파이썬에 대한 글을 마치고, 이제 두 번째로 보안에 대해서 이야기를 한번 해보려고 한다. 개인적으로 생각했을때 아직 잘 모르는 영역들도 많고, 관심은 있지만 공부를 안해서 발전이 거의 없는 영역들도 많다고 생각한다. 

 

  이 글에서는 보안을 처음 공부하거나, 공부는 하고 있지만 너무 범위가 넓고 어렵게 느껴지거나, 개발이나 시스템, 테스팅 같은 연관이 있는 일을 하는 사람들에게 보안이 생각보다 그렇게 어려운 분야는 아니며, 개발이나 테스팅, 시스템, 네트워크 쪽의 지식처럼 해당 분야의 패턴을 익혀두면 여러 다른 분야에서도 유용하게 쓸 수 있다는 것을 얘기해 보려 한다. 나아가 얼마나 잘 될지는 모르겠지만 보안의 어떤 부분이 보안을 어렵게 생각하게 만들게 되는지도 개인적인 경험에 비추어 설명해보고 싶다. 이전 파이썬 글과 마찬가지로 하나의 접근 방식이라는 관점으로 비판적으로 받아들이기를 바라며 역시 재밌는 시간이 되길 바라면서 글을 시작하려 한다.

 

 

[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 설정, 패치, 하드닝

10. 설계문제

11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터


 

1. 들어가면서

  보안 공부를 어떻게 해야하느냐를 알고 싶어서 검색엔진을 찾아보면 해야될 일이 엄청 많아 보인다. C++와 같은 프로그래밍 언어부터 시작해서, 웹 및 시스템 프로그래밍, 객체지향 개념, 운영체제, 웹서버, 네트워크 장치들, 프로토콜, 암호학, 리버싱, 포렌식, 악성코드 등에 대해 하나하나 이해해야 하고, 나아가 오픈소스, 모의해킹, 보안 관련 툴 들을 이해해야 한다고 한다. 

 

  사실 위의 요소 중 하나로만 범위를 제한하게 되더라도 엄청 많은 하위 카테고리로 확장되게 된다. 예를 들면 웹은 CGI(Common Gateway Interface) 같은 초창기 서비스 구조로부터 ASP, PHP 언어 같은 스크립트 베이스의 언어, JAVA, NET 같은 좀 더 포말한 프로그래밍 형태의 웹으로 확장되고, 여러 타입의 서비스와 API 등도 포함되게 된다.

 

  네트워크도 이름도 낯선 OSI 7 계층 구조부터, TCP/IP, 방화벽, IPS, 스위치, 라우터 등의 여러 이슈들로 나누어지고, 그런 부분도 또 관련 장비의 밴더나 각 진화된 세대에 따라서 서로 다른 영역으로 갈라질 수 있다. 나머지 필수 기술 항목들도 역시 마찬가지로 많은 갈림길이 있다. 웹 서버들도 아파치, IIS, 톰캣 등 조금씩 특성이 다른 여러가지의 웹 서버들이 있고, 각 서버는 각각의 버전마다 설정이나 지원 기능 등에 따라 특성이 또 조금은 다르다고 볼 수 있으며, 다른 웹 언어들과도 연결된다.

 

  게다가 현대의 빠른 기술의 변화에 맞춰서, 빅데이터나 머신러닝등의 새롭게 보안이 필요해지는 영역과 언어들이 계속 쏟아져나오고 있다. 또한 좀 더 넓게 현실적으로 보면 IT 기술의 범위를 벗어나서 물리적인 영역이나, 작업 및 관리 프로세스, 심리적인 부분도 관련 되게 된다.

 

 

  개인적인 관점에서 또 하나 보안을 어려운 분야로 생각하게 하는 부분은, 보안 측면을 바라보는 시각이 매력적인 모의해킹이라는 공격적 측면과, 자격증 획득이라는 측면에 많이 중심이 치우쳐 있기 때문인 것 같다.

 

  보안 지식 전체가 몸을 건강하게 해주는 운동들의 구성이라면, 모의해킹은 프로격투기 선수의 실전 스파링 훈련과 같다고 본다. 모의해킹은 시스템을 구성하는 모든 제반 기술들에 대한 균형있는 검증을 통해서, 안전한 디자인이 이루어졌나를 확인하는 활동이라고 생각한다. 그래서 모의해킹 기법 관련 책에서 얘기하는 많은 백과사전 방식의 기법을 익혀서 대상에 대해서 테스트를 하고, 취약점을 찾거나 찾지 못한 결과에 따라 대상이 불안하거나, 안전하다고 판단하는 것은, 사실 그렇게 많은 의미는 없어 보인다. 중요한 것은 그러한 기법들을 보안 세계 쪽에서 정리하고 권장하게 된 배경을 이해하고, 점검 하려는 대상에 대해 기술적으로 정확하게 이해하여 적절한 기법을 적용하여 검증하는 부분인 것 같다.

 

  자격증 또한 해당 분야를 마스터(사실 사람 자체가 불완전 하며, 모든 분야에서 한 개인은 역사를 스쳐가는 디딤돌 같은 역활인지라 어떤 분야를 마스터했다는 말은 신기루 같은 것 같기도 하지만...)한 전문가를 보면서 행동을 따라하는 것과 비슷하다고 본다. 자격증에 나오는 지식은 현실의 많은 보안 지식을 모아서, 전문가들이 정리하고 체계화된 지식에 불과하다고 본다. 해당 지식 체계가 보안의 전체적인 모습을 정리하고, 체계적으로 접근을 하게 해주는 것은 부정할 수 없는 사실이지만, 해당 지식안에는 현실을 객관화한 지식은 있지만, 현실 자체는 없는 경우가 많다.

 

   지식 자체는 손가락이고, 그 손가락이 가르키는 곳을 봐야지 의미가 있지 손가락 자체를 아무리 외어도 의미는 없다고 본다(자격증 책 본문의 시험용 암기 내용보다는, 각 챕터의 뒤에 있는 그 내용이 나오게 된 근거인 참고 서적이나 관련 링크들에 대한 포괄적인 이해가 사실 더 중요한것 같아 보이지만, 시험공부를 위해서 기본적으로 외우거나 이해해야 할 내용이 엄청 많기 때문에 그런 부분까지 차분히 읽을 분위기는 되지 않는다). 훌륭한 전문가의 행동을 따라하는 것보다, 그 행동을 일으키게 한 마음의 흐름을 이해해야지만, 실제 해당 전문가의 노하우의 그림자라도 익힐수 있게 되지 않을까 싶다.

 

 

 

2. 보안이 어렵게 느껴지는 이유

  우리가 어떤 것을 배우려면, 우리가 어떤 것을 배우고 싶어 하는지를 우선 명확히 알아야 하는 아이러니가 있다(파이썬 글 21교시의 5 orders of ignorance 를 참고). 보안이란 분야가 어떤 것 인지에 대해서 많은 사람들이 서로 다른 정의를 가지고 접근하겠지만, 개인적인 관점에서 가장 주요한 부분은 데이터의 흐름을 따라가는 것이라고 본다. 데이터의 관점에서 한정해 보면 개발은 데이터가 설계된 길로 가게하는 것이고, 테스팅은 데이터가 주어진 길로 제대로 가는지를 증명하는 일이며, 보안은 데이터가 주어진 길에서 납치나 회유되지 않는지 살펴보는 일이라고 볼수 있다고 생각한다. 

 

  또한 보안은 일반적인 프로그래밍이나 테스팅 보다는 우리의 일상생활이나 시스템 바깥의 물리적, 프로세스적 환경의 관리까지 생각하기 때문에 조금 더 범위가 넓게 된다. 여담이지만 점점 개인정보보호 분야의 영역과 보안의 영역이 많이 겹치게 되고 있는데, 개인정보는 (개인의) 중요한 데이터를 보호하는 분야이고, 보안은 중요한 데이터를 보호 하는 분야이기 때문에, 어쩔 수 없이 기술적인 영역에 대해서 관심사는 살짝 다르지만 상당 부분 겹치게 되어있다고 본다.

 

 

  그럼 데이터의 흐름을 따라가려면 어떻게 해야 할까? 데이터가 보여지거나, 지나가거나, 처리되거나, 저장되는 모든 영역을 잘 이해해야 한다. 컴퓨터 안의 CPU, 레지스터부터, 메모리, 디스크 내의 바이너리 형태의 데이터, 운영체제, 프로세스, 프레임워크, 응용 프로그램, 웹서버, 네트워크(프로토콜), 서버, 관리프로그램, 사용자 프로그램, 사용자, 외부 장치, 업무 프로세스 또는 은행의 비밀번호를 기억하고 있는 고객들에 이르기 까지 데이터가 현실의 물리적 형태, 0과 1의 형태의 바이너리 형태로 존재하거나, 레지스트 값, 변수, 메모리 값, 프로그램 내의 변수, 설정 값, 입력 값, 전송 값 등 모든 머무는 영역에 대해서 올바르게 이해해야 한다. 결론적으로 얘기하면 앞의 많은 기술의 요소 부분을 이해해야만 보안을 잘 할 수 있는 것이 아니고, 꺼꾸로 보안을 잘하기 위해서, 그러기 위해서 데이터를 이해하기 위해서, 데이터의 흐름을 이해하고 적절히 보호하기 위하여 해당 기술들을 익혀야 한다고 본다. 

 

  이러한 끝이 잘 보이지 않는 넓은 범위의 기술 범위들은, 앞의 파이썬 글에서 얘기했던 프로그래밍의 배경지식과 비슷하다고 본다. 보안은 보안의 원리 자체가 어려운게 아니라(뭐 쉽다는 의미는 아니고, 배경지식을 잘 안다면 생각보다는... 이라는 말이지만... 반대로 얘기하면 배경지식을 모르면 실제보다 과도하게 어려워 보인다) 이러한 배경지식의 균형있는 습득이 어려우며, 프로그래밍에서의 게임 프로그래밍의 위치와 비슷하게, IT계의 종합 예술 영역 같은 성격을 띄고 있다고 생각한다. 그래서 보안은 데이터를 제대로 보고 흐름을 따라가기 위해서 많은 배경 지식이 필요하게 된다. 프로그래밍도 그러한 배경 지식 중 하나이고, 앞서 얘기한 많은 분야들도 그래서 필요하게 된다. 그리고 모든 분야가 마찬가지겠지만 모르는 분야나, 새로운 기술을 계속 따라가야 하는 어려움도 있게 된다.

 

 

  예를 들어 보안을 모의해킹 같은 공격의 관점으로 봐서, 아주 뛰어난 도둑이 있다고 가정을 해보자. 해당 도둑은 사회공학적 기법을 이용해 사람들을 속여 정보를 얻거나, 원하는 행동을 하게 하는 것도 능숙하고, 담을 타거나 빠르게 이동하기 위한 신체능력도 뛰어나며, 일반 자물쇠나, 생체인식등과 같은 현존하는 모든 자물쇠의 이해에 능통한 상태로 그러한 기법들을 이용하여 원하는 목표를 놓친적이 없다고 하자.

 

  그런데 어느날 열쇠의 주인이 아니면 절대 풀수 없다는 양자역학을 이용한 자물쇠(무슨 자물쇠인지는 모르지만 그런 어려운게 생겼다고 해보자)가 발명되면서 주요 보물들이 해당 자물쇠에 의해서 보호되게 되었다. 이제 도둑이 계속 목표를 달성하려면 어떻게 해야될까? 해당 양자역학적 자물쇠를 이해하여 속일 수 있는 방법을 찾는 수 밖에 없다(미션 임파서블 영화를 생각하면 된다. 물론 영화에서처럼 어떤 자물쇠로 잠겼는 지에 상관없이 사람이나 환경을 이용하여 사회공학적 기법으로 푸는 방법도 있긴 하겠지만 말이다. 기술은 기술을 운영하는 사람의 스마트함에 비례해 효과가 있으니 말이다).

 

  또는 반대로 여러분이 자물쇠가 얼마나 안전한지 체크를 해주는 보안 전문가고, 해당 양자역학적 자물쇠를 사용하는 사람으로부터 해당 자물쇠가 얼마나 안전한지 검증을 해달라고 요청을 받았다고 해보자. 새로운 자물쇠의 안전을 검증하려면 해당 자물쇠의 원리를 우선 이해해야만 한다. 물론 해당 자물쇠를 이해 못한다고 기존의 뛰어난 도둑(악의적인 해커)이 뛰어나지 않은 것은 아니고, 뛰어난 보안 전문가가 전문가가 아니게 되는 것은 아니다. 하지만 해당 분야에서 전문가 명성을 계속 유지하기 위해서는 새로운 기술을 이해하여, 자신이 기존 체계로 부터 이해하고 있는 패턴을 적용하여 공략점을 찾거나, 외부의 공격에 안전한 설계인가를 증명하지 않으면 안된다.

 

 

  다른 예로 당신이 모든 사기 유형을 마스터한 유명한 사기꾼이고, 새로운 매력적인 파생 상품이 나와서 해당 부분을 이용해서 사기를 치고 싶을 때도, 마찬가지로 대상에 대한 이해가 필요하게 될것 이다(사기는 상대방에 대한 이해와 공감을 바탕으로 한, 9개의 진실과 1개의 거짓으로 이루어진 행위라고 생각한다).

 

  현실의 보안 영역으로 내려오면, 윈도우 보안의 전문가인 당신이 리눅스, IOS, 안드로이드의 보안에 대해 체크하고, 적절한 가이드를 하려면 해당 OS 환경을 잘 이해하는 수밖에 없다. 마찬가지로 새로운 데이터베이스, 프레임워크나, 빅데이터 기술에 대한 보안을 체크하려면 해당 분야를 잘 이해할 수 밖에 없다(물론 관련 영역에 대한 스캐너 같은 여러 솔루션을 사용해서 해결하는 방법도 있겠지만, 원리와 무슨 일을 왜 수행 하는지 정확히 모르는 블랙박스 형태의 툴에 의존하는 것은 보안 일을 하는 사람으로서는 너무 순진한 행동이라고 본다). 물론 기존의 다른 유사한 분야에 대한 풍부한 이해는 새로운 분야에 대해서도 많은 부분 패턴으로 적용될 가능성이 높지만, 그것은 새로운 분야를 제대로 이해한 후에야 완전하게 발휘될 수 있는 능력일 것이다(물론 대상의 빠른 이해에도 일정부분은 도움은 된다고 본다). 새로운 분야에서 데이터가 어떻게 흘러가는지를 확실히 이해할 수 없다면, 기존에 익힌 보안에 대한 패턴들을 어디에 적용할지 몰라서 손가락만 빨고 있게 될 수도 있다(아마 아는척 사기를 치지 않는 이상은 말이다)

 

[거짓말 잘하는 비결 - 동아사이언스]

http://m.dongascience.donga.com/news.php?idx=6462

 

 

  또한 보안은 언제나 사실에 기반한 마술과 비슷하다. 취약점은 이해하기 어렵거나 신비로울 수는 있지만, 항상 사실(코드나 설계)에 기반하여 일어나는 것임은 분명하다(그래야 방어의 설계도 가능한 것일테고...). 보안 취약점은 기술의 빈 틈을 파고 들어가는것이며, 기술 자체의 안전한 구성원으로 위장하여 들어가, 데이터베이스 등 외부 요소나 기술 자체에 영향을 미치는 것을 의미한다.

 

  웹 보안에서 가장 흔하게 나오는 SQL Injection 같은 부분은, 만약 여러가지 데이터베이스의 명령어나 관리 기술에 두루 능통하다면, 정말 빠르게 공격과 방어에 대한 원리를 이해할 수 있다(A piece of cake라고 하고 싶다) . 그런데 만약 데이터베이스를 모르는 상태에서 SQL Injection 을 이해하려 한다면, 보안 자체의 측면보다는 데이터베이스와 SQL 문장의 이해의 늪에 빠져서 시간도 많이 걸리고 불완전하게 단편적으로 이해하고 넘어갈 가능성이 높다.

 

  비슷하게 자바스크립트를 기반으로 한 XSS 나 CSRF 같은 취약점 패턴 등도 자바스크립트를 얼마나 잘 이해하고 있느냐에 이해의 난이도와 깊이가 달려있고, 커맨드 인젝션이나, XML 인젝션 등도 얼마나 시스템 명령어나, 프로그램의 내부 구조 및 관련 함수들, XML 구조를 잘 이해하느냐에 중요 포인트가 있다(물론 특정 취약점은 프로그램의 다른 요소와 믹스되어서 효과가 증폭되거나 경감되는 부분도 있긴 때문에 한 기술의 타입으로 단순화 하기는 힘들지만 말이다). 해당 부분에 대한 이해가 충분하다면 보안 이라는 패턴을 적용해 어떻게 해당 기술이 악용이 되고, 방지할 수 있는지를 쉽게 이해할 수 있게 된다. 나아가 그 방어 방식의 한계 및 제약과, 한계에 따른 리스크를 다른 수단으로 보완하여 경감해야 된다는 사실도 인지할 수 있게 된다. 해킹이란게 시스템을 속이는 것이라 얘기되지만, 그 앞에는 사실 시스템을 (잘 이해하고) 속이는 것 이라는 중요한 말이 숨어 있다고 생각한다.

 

  그럼 이 글에서는 맘대로긴 하지만 (기술적인) 보안 공부를 다음과 같이 정의해 보도록 하겠다. "데이터의 흐름을 이해하기 위해서, 데이터가 흘러가는 공간을 구성하고 있는 기술에 대해서 이해하는 과정". 앞으로의 시간들은 쓰는 사람의 지식의 한계 때문에, 그러한 부분의 아주 깊은 곳까지는 안내하진 못하겠지만, 데이터의 흐름을 이해하기 위해 이용되는 여러가지 방법들을 살펴보는 방식으로 진행될 것 같다. 하지만 뭐 항상 그렇지만 진행되다보면 원래 의도와 다르게 흘러가기는 한다...

 

 

 

3. 보안의 공격과 방어 측면의 차이

  프로그래머들에게 조금 미안하긴 하지만 개인적으로 IT 쪽의 많은 분야에서 그다지 바람직하지는 못하다고 생각되는 흐름 중 하나는, 프로그래밍 경험이 있느냐에 대한 부가가치를 너무 크게 잡는 다는 부분이라고 본다. 물론 QA나 보안이나, 시스템, 빅데이터 등의 여러 분야에 대해서 프로그래밍 능력이 상당한 도움이 되는 것은 맞다.

 

  하지만 프로그래밍 실력을 미리 갖추고 일을 시작 하는게 중요한 거라기 보다는, 해당 업무의 특정 레벨에 올라가게 될때 자연스럽게 업무의 효율성과 확장성을 위해서 자연스럽게 필요성이 생기게 되는 것이라고 본다. 무언가 귀찮거나 반복되서 의미 없는 일을 줄이기 위해 위해서는 어쩔 수 없이 자동화의 힘을 빌려야 하고, 그러다 보면 조금씩 자연스럽게 프로그램이란 세계에 발을 들이게 된다. 물론 그 전에도 API 사용이나, 명령어 스크립트 작성 등 기본적인 프로그래밍 지식이 있으면 좀더 적응이 용이한 부분들도 분명히 있다. 

 

  개인적으로는 프로그래밍 능력이 있느냐가 원천적으로 있느냐가 중요하다기 보다는, 업무상 자연스럽게 필요하게 되서 프로그래밍 능력을 습득하게 된다는 것이 좀더 맞을 것 같다. 물론 해당 부분에 대한 부분은 금방 휘리릭 습득되는 부분은 아니기 때문에 직업의 초기부터 조금씩 꾸준히 노력하는 것이 맞아보인다. 추가로 분야에 따라 프로그래밍 이란 것도 각 분야에 적절한 스타일로 다르게 접근 되기 때문에 쓰이는 포커스가 다른 경우도 많다. 제일 중요한 것은 해당 분야의 도메인 지식을 기반으로 적절하게 프로그래밍 언어의 능력을 쓰는 것인것 같다.  파이썬 글 17교시의 머신러닝 부분에서 프로그래밍이 어떤 식으로 쓰이고 있는지와 비슷하게 보면 된다고 할까? 표현하긴 좀 어렵지만 프로그래밍은 프로그래머 들에게만 생기는 직업적 기술이라기 보다는 약간 공공재 적인 기술적 영역이라고 보고 싶다. 그래서 두려워 하지 말고 일단 뛰어들어 보는 것을 추천한다. 시간을 들여 익숙해 지면 야매 정도는 되니까...

 

 

 보안 쪽도 비슷한 흐름이 있다고 보는데, 웹어플리케이션 보안 부분을(특히 모의해킹이라고 칭해지는 부분) 특수한 인력이 특수한 절차를 밟아서 획득할 수 있는 완전한 검증 방식으로 생각한다는 부분이다. 모의해킹은 표면적인 효과 이외에 생각할 점이 많은 분야라고 본다. 

 

  우선 수행하는 사람의 능력에 많이 좌우된다. 보안 분야도 개발이나 테스팅과 비슷하게 사람들마다 접근하는 전략과 효율성의 차이가 많이 나고, 기술 및 도메인에 대한 이해도도 틀리며, 그 차이가 외부에서 볼때 확실히 구분되기 힘들다. 인력이나 도메인에 많은 영향을 받아 어떤 기술셋을 가진 사람이 어떤 분야를 점검하느냐도 중요하고, 어떤 언어나, 개발프로세스를 거쳐 만든 도메인을 만나느냐에 대한 운도 작용하는 것 같다. 또 점검 후 점검한 영역에서 실제 사고가 나거나, 비슷하거나 더 상위 수준(또는 비슷하지만 시간에 쫒기지 않은 한가한)의 인력이 충분한 시간을 가지고 크로스 체크 하기 전에는 효과를 증명하기 힘든 경우도 종종 있는 듯 싶다. 또 이해한 만큼만 점검이 가능하기 때문에, 시행자의 대상의 이해 정도에 의해서도 많이 차이가 나고, 일반적으로 리소스나 범위 등의 여러가지 사유로 소스 기반이 아닌 블랙박스나 그레이박스 기반, 그리고 제한된 시간 프레임 내에서 진행되기 때문에, 구성되어 있는 자원이나 소스에 대한 완전한 커버리지를 가지긴 힘든 경우도 많은것 같다.

 

  그리고 대부분의 경우 증명보다는 시나리오 기반으로 진행되기 때문에, 해당 시나리오를 벗어난 전체적인 보안성 측면에 대해서는 보장(Assureance)하지는 못한다고 보는게 맞을 것 같다. 또한 불행하게도 점검한 소스나 환경은 계속 변할 수도 있기 때문에, 해당 점검 시점 후 시간이 지나게 되면 다시 코드의 안전함을 보장하지 못한다는 모순에 빠지게 된다(개발자중 한명이 모의해킹이 완료된 후의 코드 베이스에 취약한 코드를 하나 넣었다면 어떻게 될까?).

 

  마지막으로 시스템이 너무 기초적인 보안 설계가 안 되어 있다면 중요한 취약점들을 다수 발견할 수는 있겠지만, 해당 취약점에 대부분의 시간을 쏟고, 진행이 막히게 되어, 더 미묘한 내부를 들여다 보지 못할 수도 있다. 뭐 중요한 기본 취약점들을 찾았다는 의미는 있긴 하지만, 해당 취약점들에 갈 길이 막혀 깊이 있는 취약점은 찾지 못할 수 있다. 기본적인 기능의 동작도 제대로 안되서 중요도가 높은 버그를 잔뜩 올렸지만, 해당 가능에 대해 깊이 있는 테스트를 하지 못하는 QA 테스트와 비슷한 상황이라고 보면 된다. 해당 경우는 모의해킹이 중요한게 아니라 시스템이나 프로세스를 기초부터 개선 하는게 더 중요할지도 모르는 상황이라고 본다. 분명히 모의해킹은 보안쪽의 중요한 요소이기도 하고, 수술에 꼭 필요한 잘 드는 메스이기는 하지만 은총알은 아니여서, 환자의 상태나 다른 검사 장비들과, 의사에 판단에 의해 잘 조합하여 사용해야 효과가 큰 것 같다.

 

  그래서 모의해킹은 어떻게 보면 공부를 열심히 하고 보게되는 고3의 수능 같은 평가 활동 같다고 보고 있다. 테스팅을 아무리 하더라도 최초부터 없는 품질을 얻을 수는 없는 것과 마찬가지로(중요한 버그들을 발견하고 고치는 행위자체가 반드시 품질을 보장하진 못한다 파이썬 머신러닝 파트에서 얘기했던 garbage in garbage out 과 마찬가지로...), 모의해킹으로 애초부터 들어 있지 않은 보안성을 잡을 수는 없다. 그런 믿음은 이미 다 지어진 집에서 발견된 균열들을 급하게 메우면서 집이 안전하길 바라는 것과 비슷하다. 사실은 설계 부터의 과정에서 뭔가 어긋나서 생긴 문제인데도 말이다. 

 

  보안성은 시험 전에 한참 꾸준히 해왔던 공부 같이, 여러 요구사항의 수집에서 시작하여, 안전한 설계, 개발의 과정 중에 이루어지는 여러 관점의 보안적 리뷰와 체크에서 생기는 것이라고 보는게 좀더 근본적일 것 같다. 모의해킹은 어플리케이션 보안에 대한 오랜 동안의 업계 사람들의 시각과 기술적 노하우, 취약점에 대한 개인의 감, 실제 경험들이 총 집성된 평가 체계라고 생각하지만, 역으로 그런 기본 역량을 충분히 갖춘 사람들이 평가를 해야지 충분한 의미가 있다는 단점도 분명히 있다는 것을 잊으면 안된다(물론 절차적인 면죄부라는 느낌이 좀 있긴 하다...) 

 

 

  그럼 해킹, 모의해킹과 같은 공격과 시큐어 코딩과 같은 방어의 차이는 무엇일까? 우선 둘의 공통점은 공격하고자 하는 대상과 방어하고자 하는 대상이 먼저 얘기한 데이터라는 것이다. 두 가지의 차이점은 , IT를 무협과 비교하는 걸 별로 선호하는 편은 아니지만, 무협만화에 나오는 빠른 검과 느린 도의 대결과 비슷하다고 본다(멋진 캐릭이 나오는 게임 또는 만화의 삽화를 넣고 싶긴 하지만, 저작권 문제로.. 무협 만화 좋아하는 사람은 열혈강호의 담화린(빠른칼)과 도제(느리지만 무거운 도)가 싸우는 장면을 상상해 보면 어떨까 싶다^^). 공격은 시스템의 헛점을 노리고 다양한 영향을 미치려고 계속 변화하면서 수행되는 반면, 방어는 사실 기본기에 충실해서 느리지만 핵심을 지키면서(靜中動) 공격으로 들어온 외부 코드들을 무용지물로 만들게 된다고 본다.

 

 

  그럼 느리지만 무거운 도에 해당하는 방어를 코드로 예를 들면 어떻게 될까? 개인적인 생각에는 아래와 같이 (외부에 입력된 변수로 부터 영향을 받을 가능성이 있는) 내부에서 사용할 변수의 타입과 범위를 체크하는 코드가 아닐까 싶다. 아래와 같이 정확한 정수 타입과 필요한 수의 범위를 제한하게 되면, (아래와 같은 숫자 변수에 대한 코드에 경우에는) 어떠한 인젝션 시도에도 안전한 코드가 될 것 같다.

1
2
3
4
5
6
ext_var = "union all ... --"
 
if type(ext_var) is int and ext_var > 0 and ext_var <= 10:
    print ("use variable");
else:
    print ("not safe");
cs

 

   해당 방어 코드를 모르는 공격자는 공격을 위해서 많은 화려한 기법을 사용할테고, 그 중에 똑똑한 공격자는 금새 잘 막혀 있다는 걸 알아채고, 시간낭비 없이 다른 취약한 변수나 코드들을 찾아 다니게 될 것이다. 좀 더 나아가면 해당 부분은 관련 방어를 지원하는 프레임워크를 쓰거나, 해당 프로그래밍 언어에서 지원하는 범용적인 보안 함수를 사용하는게 더 맞겠지만, 여기서는 방어하는 무거운 도(刀)에 대한 극단적인 이미지를 나타내기 위해서 아래와 같은 코드로 제시해 보았다. 문자열 변수 등과 같이 좀더 상세하게 따져가며 저울질할 부분들은 나중에 해당 주제에 대해서 다루면서 얘기하면 될 것 같다.

 

  파이썬 코드의 실행을 모를 경우 파이썬 2교시 글을 참고하면 된다

c:\Python\code>python variable_check.py
not safe

 

 

  한가지 더 인터넷에서 많이 언급되는 얘기를 생각해 보면, 공격의 대표적인 방식인 모의해킹의 기법을 이해해야만, 코드의 방어를 어떻게 해야 할지 제대로 알수 있다는 부분이 있다. 해당 부분은 맞는 얘기기는 하지만, 다른 한편으로 조금 더 생각해 볼  부분이 있다고 본다.

 

  방어 측면에서의 모의해킹의 이해란, 모의해킹에서 사용하는 기법이나 다양한 툴의 원리와 사용의 이해의 측면 보다는, 해당 공격으로 일어나는 데이터들의 변화에 초점을 맞추는 것이 맞을 듯 싶다(이 부분은 보안 테스팅을 위해 코드를 읽을 때도 비슷하게 적용된다. 물론 취약점의 원리를 이해하면 자동으로 툴이 무엇을 하려 하는지를 어느정도 이해하게 되겠지만 말이다..). 어차피 방어 측면에서는 공격과 같이 현란한 데이터의 변경으로 맞설 수는 없기 때문에(물론 뭐 요즘의 OS 등에서 실행파일이 로드되는 메모리 번지를 계속 변경 시킨다든지, 솔루션 등에서 머신러닝 등을 이용해서 비 정상적인 통계 패턴을 계속 추적한다든지 하는 등은 공격과 비슷한 다이나믹한 방어로 볼수 있긴 하겠지만), 일반적으로는 공격의 원리에 대치되는 배치로 대응하는 수밖에 없다. 공방 게임에서 공격 유닛에 대한 방어는, 해당 유닛에 상응하는 방어 건물의 배치로 이루어지는 것과 비슷하지 않을까 싶다. 그 방어 건물이 우리가 자주보게 되는 "시큐어 코딩 가이드" 같은 거고 말이다.

 

 물론 모의해킹의 아주 미묘한 부분까지 이해하여, 관련 전문가 수준으로 알게되면 분명 더 좋은 방어를 하게 될듯 싶다(물론 공격 뿐만 아닌 방어 관점의 이슈들도 충분히 잘 안다는 가정하에 얘기이다). 

 

  보안에 대한 분야는 사실 너무 넓은 분야이고, 모든 존재하는게 사실 보안과 연관이 있고, 그 안에 서식하고 있는 많은 요소들도, 계속 살아있는 생태계 처럼 변하고 있기 때문이다. 지금 해당 영역에서 완벽하더라도 앞으로 계속 따라기기 위해서 해야 될 공부가 너무 많다. 실제로 외부에서 보기엔 완벽해 보이는 사람들도, 좀 더 자세히 들여다 보면 생각보다는 완벽하지 못한 경우가 많고, 그것은 비단 보안 뿐만 아니라 모든 지식의 영역이 마찬가지인듯 싶다.

 

 

  

4. 마무리 하면서

  앞으로 글을 진행 하면서 앞에서 한 얘기들을 좀더 자세히 설명하기 위해서, 몇 가지 방향을 잡고 진행하려 한다. 우선 공격보다는 방어에 필요한 부분을 위주로 설명할 예정이며, 공격을 얘기할 경우는 방어 측면에서 이해가 필요한 공격의 측면을 중심으로 얘기하려고 한다. ASP, Python, PHP 와 같은 스크립트 언어와, 자세히는 모르지만 .NET 이나, JAVA 같은 주제를 잘 이해시켜 줄 수 있는 언어를 가지고, 가능한 실제 움직이는 코드를 구현하고자 한다. 그리고 피들러와 같은 관찰 및 조작 할수 있는 툴을 통해 데이터가 흘러가고, 변조 되는 부분을 시연하고, 방어를 위한 코드를 제시하며, 해당 방어의 원리와 제한 들에 대해서 얘기하려 한다. 

 

  비교적 잘 설명이 가능할 거리고 생각하는 웹 기술을 중심으로 이야기를 시작하며(사실 웹이나 다른 분야나 넓게 보면 기본적인 보안 패턴은 비슷하다고 생각하지만 아직은 크게 자신은 없다^^), 후반에 일반 어플리케이션에 대한 등에 대한 얘기도 얇은 지식이나마 정리해 볼까 한다. 이 글의 목표는 읽은 사람들이 각 보안 영역에 대해서 어떻게 접근해야 겠다는 스스로의 생각을 가지게 되고, 여러 보안 가이드를 볼 때, 해당 가이드를 작성한 사람이, 어떤 관점에서 그러한 방어 전략을 제시했는지에 대한 이해의 기반을 가지게 하는 것이다. 

 

  물론 앞의 파이썬 글과 마찬가지로 각 주제들에 대해 깊은 부분 까지 설명하기에는 스스로 아는 것이 적다고 생각하고, 시간의 제한도 받기 때문에 "보안에 대한 이런 저런 주제를 가볍게 다루는 기술적 산문집" 이라고 기대하고 읽어주면 좋을 듯하다. 내용 중 잘못됬거나 의견이 다른 부분은 댓글을 달아 알려주심 감사할듯 싶다.

 

그럼 재미있는 보안공부가 되길 바라며...

posted by 자유로운설탕
2017. 8. 19. 23:28 프로그래밍

  이제 개인적으로 소프트웨어를 바라 보는 관점에 영향을 주었던 글을 하나 소개하면서, 구글로 공부하는 파이썬 시리즈를 마무리 하려 한다.

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

[추가]

현재 모바일에서 코드(colorscript 이용)가 나온 후에 글 색깔이 흐려져서 읽기가 어렵네요. 티스토리 개편하고 현재 스킨이 너무 예전것이라서 그런가도 싶긴한데, 수정이 쉽진 않을것 같아서 일단 웹에서는 정상적으로 보이니 웹을 이용해 주세요. 조만간 수정해 보도록 하겠습니다.

감사합니다.

 

 

[The five orders of ignorance]

  저도 몇년전에 누군가에게 소개 받았던 글이긴 하지만, 'Communications of the ACM' 저널에 실렸던 'The five orders of ignorance(무지의 5계층)' 라는 소프트웨어 개발에 대한 글을 소개하려 한다(아래 URL 안의 링크를 클릭하면 pdf 로 된 전체 글을 다운받을 수 있다).

http://www.corvusintl.com/CACM002-5OI.htm

 

  약간의 개인적인 해석을 추가해 요약하자면 아래와 같다.

 

  인류가 가지고 있는 지식을 저장하는 매체는 DNA, 뇌, 하드웨어, 책을 거쳐 현재는 소프트웨어라는 형태가 생겨나게 되었다. 소프트웨어는 뇌처럼 공간이 제한되어 있거나, 쉽게 변하지 않고, 책처럼 수동적이지도 않으며(뭐 읽는 사람에게 감성과 상상력을 일으킨다는 측면에서는 그렇지 않을지도 모르지만...), 하드웨어나, DNA 처럼 느린 변화를 가지거나, 유연성이 부족하지도 않다.

 

  우리가 소프트웨어 제품을 만든다는 것은, 특정한 제품 자체를 만드는것이 아닌 우리가 알고 있는 지식들을 저장하는 매체를 구현하는 행위라는 것이다. 그래서 소프트웨어의 잘 알려진 지식들은 프레임워크나, IDE 를 통해서 자동으로 코드를 만들어 낼 수 있는 반면에, 다른 특정한 부분들은, 직접 해당 지식을 획득하여 구현해 내야 한다.

 

  예를 들어 현재의 파이썬 시리즈를 다 읽은 시점에서, 새로운 파이썬 프로그램을 만든다고 해보자. 그 중에 어떤 부분들은 우리가 이미 알고 있는 지식을 재사용하여 쉽게 구현할 수 있으며, 만약 새로운 프로그램의 주요한 부분들이 기존에 지식들과 유사하다면, 정말 쉽게 짧은 시간에 새로운 프로그램을 만들 수 있을 것이다. 그런데 만약에 우리가 현재 다루지 않았던 기능을 가진 프로그램을 만들려 한다면 해당 분야에 대한 지식을 추가로 획득해야 한다. 추가적인 지식이 필요한 경우인 경우라도 다른 유사한 프로그래밍 경험을 통해서, 해당 지식의 영역(예를 들어 쓰레드 구현)을 경험해 본적이 있다면 프로그래밍 언어의 차이점만을 잘 극복한다면 백지에서 시작하는 것보다 좀더 쉽게 원하는 지식을 획득할 수 있을 것이다. 아마도 최악의 경우는 어떻게 해당 부분을 해결해야 할지에 대한 방법이 전혀 감이 잡히지 않는 경우일 것이다.

 

  이런 관점에서 볼때 소프트웨어 개발은 지식을 획득하는 활동인 동시에, '무지(ignorance)' 를 줄이거나 제거하는 활동으로 바라볼 수 있다. 이 글에서는 이러한 측면에서 무지의 계층을 5단계로 나누어 제시한다.

  • 0 계층 - 무지의 결핍(Lack of Ignorance) : 무언가를 오랬동안 경험해 왔기 때문에 그것에 대해 잘 아는 상태이다. 오랫동안 보트를 타봤다면 어느 정도 항해에 대해서 잘 안다고 얘기할 수 있는 것처럼 말이다.
  • 1 계층 - 지식의 결핍(Lack of Knowledge) : 무언가를 모른다는 것을 정확히 알고 있는 상태이다. 예를 들어 러시아어를 못한다는 것을 안다면, 러시아어를 공부하기 위해 학원을 다니거나 책을 봐야한다는 것을 명확히 알수 있다.
  • 2 계층 - 의식의 결핍(Lack of Awareness) : 무엇을 모른다는 것 자체를 인지하지 못하고 있는 상태이다. 무엇을 모르는지 자체를 모르기 때문에, 무엇을 해야 하는지도 모르는 상태 이다.
  • 3 계층 - 프로세스의 결핍(Lack of Process) : 인지하지 못하는 무지를 알수 있게 해주는 수단 자체가 없는 상태를 얘기한다. 그것은 특정한 프로세스의 부재 일수도 있고, 특정한 환경의 부재일 수도 있다.
  • 4 계층 - 약간 유머 같긴 하지만, 무지의 5계층 자체를 모르는 단계라고 한다. 이 글을 읽게 되면 이 단계에서 벗어나는 셈이라고 한다.

 

  개인적으로는 이 글에 공감이 가서 소프트웨어를 바라보는 관점이 많이 바뀌게 되었고 설명은 하기 어렵지만 왠지 좀더 대상을 편하게 바라보게 되었다. 소프트웨어에 대한 여러 다양한 기술 업무들이 비슷하게 무지를 줄이고, 숨어있는 무지들을 찾아내는 활동이라는 생각이 들었으며, 결국 소프트웨어에 담겨있는 내용은 지식이기 때문에, 소프트웨어이외의 다른 분야들의 좋은 접근 방법이나 개념들도 무형적 지식이라는 측면에서 비슷하게 연결이 되는게 아닐까하는 생각도 들었다. 스타크래프트의 'Black Sheep Wall' 처럼 무지의 영역을 한순간에 밝혀주는 마법의 치트키는 없겠지만, 자신이 모르는 필요한 부분들을 계속 발견하고 해당 영역들을 채우는 것이, 소프트웨어에 관한 직업을 가져가면서 스트레스를 덜 받는 방법이 아닌가 싶다.

 

 

 

 

[마무리 하면서]

  이 글의 목적은 읽는 사람을 파이썬에 대해서 어느 수준까지 끌어가는 것은 아니다(개인적으로 그럴만한 능력도 안된다고 생각하고...). 오히려 프로그래밍이란 생각보다 모호한 일이며, 다른 사람들이 미리 만들어 놓은 수많은 지식과 접근법들을 조합하여 문제를 해결하는, 정답이 여러개이며 완벽한 정답은 없는 선택적 영역이라는 것을 얘기하고 싶었다. 또한 코드를 만드는 과정에서 무언가가 막혔을때, 검색 엔진을 이용해 문제를 찾아서 해결하는 과정에서 일어나는 '헤멤'이라는 활동을 실제로 보여주고 싶기도 했다(뭐 다른 사람은 다른 방식으로 헤멜 수는 있을 것 같다). 그리고 조금 욕심을 내자면 각 주제 영역에 대해서 스스로 공부할 수 있게, 조금 먼저 길을 걸어본 입장에서 해당 지식들에 대한 접근 방법을 안내를 하고 싶었다. 얼마나 해당 부분을 충족 시켰는지 자신은 없지만, 목표로 했던 이야기들은 다 한 것 같아서 여기서 일단 글을 멈추려고 한다. 다들 너무 부담 갖지는 말고 천천히, 하지만 꾸준히 공부 하시기를 바란다.

 

 

 

 

2017.8.20 by 자유로운설탕
cs

 

  

 

 

posted by 자유로운설탕
2017. 8. 6. 00:30 프로그래밍

  이번 시간에는 앞 시간의 플라스크(flask) 시간에 이어서, 파이썬을 대표하는 웹 프레임워크로 알려져 있는 장고(Django) 를 살펴보는 시간을 가지려고 한다. 플라스크를 살펴볼 때와 비슷한 방식으로 공식 메뉴얼을 기준으로 전체적인 장고의 구조에 대해서 살펴보고, 플라스크에서 구현했던 2개의 예제(MSSQL 테이블 표시, d3.js 그래프 그리기)를 장고 환경에서 똑같이 구현하는 과정을 보여 줌으로서, 플라스크와는 어떤 다른 측면들이 있는지를 설명하려고 한다. 

 

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

 

[들어가면서]

  우선 시작하기 전에 혹시 19교시의 플라스크(flask)에 대해서 보지 않은 상태라면, 먼저 해당 내용을 보고 오기를 바란다. 장고를 살펴본 결과 플라스크와 대비되는 부분을 짚어가며 설명하면 효율적이라는 생각이 들었다. 플라스크에서 개념을 설명했던 url routing(url 라우팅), static files(정적 파일), rendering templates(템플리트 랜더링)의 개념은 거의 동일 하게 사용되기 때문이다. 또 템플릿 소스도 일부만 수정해 그대로 사용할 계획이다. 이번 시간에는 ORM, MVT(MVC), ADMIN 등 장고에 특화된 개념만 추가로 설명할 계획이다. 개인적으로 플라스크를 살펴볼때 보다 2~3배 정도의 시간이 장고에 소요됬었고, 플라스크를 이해한 상태라면 장고도 비교적 접근하기 수월하다는 생각이 들었다.

 

 

[한글 문서 여부]

  역시 플라스크와 비슷하게, 구글을 찾다보면 아래의 한글 페이지가 있긴한데, 제목까지만 다 번역되어있고, 실제 내용은 튜토리얼 까지만 번역되어 있다. 다만 튜토리얼에 전체 구조를 이해시키는 핵심적인 얘기들이 많으므로, 튜토리얼을 한글로 보며 대충 돌아가는 상황을 파악후, 영문 문서들을 그담에 보길 권장한다.

https://django-document-korean.readthedocs.io/ko/master/

 

  또 다른 튜토리얼 레벨의 괜찮은 한글 문서는 구글에서 'django sample' 로 찾으면 나오는 아래의 장고걸 사이트 문서이다. 개인적으로 봤을때 체계적으로 잘 정리되어 있어 처음에 개념을 잡는데 도움이 됬었다. 파이썬 버전도 3.5 버전 기준이라 문법에 위화감도 없다.

https://tutorial.djangogirls.org/ko/django_start_project/

 

  그리고 마지막으로 영문 예제 사이트는 아래 사이트가 괜찮았다. 여기도 마찬가지로 3.5.x 버전이고, 위의 3개 사이트의 내용을 보면 대충 장고 사이트가 어떻게 돌아가는지에 대해 전체적으로 감이 잡히기 시작할 것이다.

https://scotch.io/tutorials/build-your-first-python-and-django-application

 

 

[Django Documentaton 보기]

  구글에서 'django document' 라고 검색하면 아래의 문서가 나오는데, 현재 최신이 1.11 버전(이게 '일점 일일' 버전이 아니라 '일점 십일' 버전이다. 첨에 이것땜에 구글을 검색하때 1.7, 1.8 같은 미래 버전이 왜 있지 하고 좀 헷깔렸다--;)의 공식 메뉴얼 페이지를 볼수 있다.

https://docs.djangoproject.com/en/1.11/

 

  'First steps' 안에 있는 Overview, Installation, Tutorial 에는 장고를 이용해 사이트를 세팅하는 것부터, 라우팅(URLconfs. 라고 보통 말한다)을 설정하는 법(urls.py), 모델을 만들어 해당 구조를 데이터베이스 및 어드민 기능과 싱크(migration) 시키는 법, 뷰와 템플릿을 사용하여 표시하고, 폼을 전송하여 받아 처리하고, 정적(static) 파일들을 설정하고 접근하는 법에 대한 전체적인 흐름을 보여준다.

 

  'The model layer' 에서는 모델을 상세하게 다루어, 모델에 대해 어떻게 클래스로 정의하고, 정의된 모델들을 지원하는 메쏘드들을 이용해 쿼리를 요청해 값을 가져오고(QuerySet), 데이터베이스와 모델을 어떻게 싱크시키고(Migrations), 모델을 벗어난 커스텀 쿼리를 데이터베이스에 어떻게 날리며(Raw SQL), 데이터베이스별로 모델을 적용하는데 필요한 여러가지 참고사항과 주의사항들을 얘기한다.

 

  'The view Layer' 에서는 어떻게 라우팅을 구성 하며(URLconfs), 어떻게 요청(request)을 하고받아서, 모델에 대한 검색을 지원하는 QuerySet 를 이용하여 결과를 가져와서, 어떻게 응답을 하는지(Requsest and response objects), 파일 업로드를 구현하는 법(File upload), 내장된 뷰(generic view)를 이용하는 방법(Built-in display view) 등을 다룬다.

 

  'The template layer' 에서는 템플릿에서 여러가지 장고에서 지원하는 템플릿 지원 로직들을 사용하여 표현하는 방법을 얘기한다(for 라든지 url 이라든지 여러 문법들을 사용하는 방법을 얘기하는데, 앞의 플라스크와 비슷하지만 조금 더 확장된 기능이라고 보면 될듯 하다) 
 

  이후 'Forms' 에서 폼을 넘기고 처리하는 부분을, 'The Admin' 에서 모델과 연관되어 자동으로 업데이트 되는 관리 페이지를 커스터 마이즈해 사용하는 부분을, 'Security' 에서 잘 알려진 보안 이슈들에 대응하는 설계를 구현하는 방법을, 'Common Web application tools' 에서 웹사이트를 개발하면서 주로 만나게 되는 세션, 캐싱, 스태틱 파일, 사이트맵 등등에 대한 구현을 지원하는 기능들을 얘기한다. 그 외의 섹션에서는 유니코드라든지, 로케일이라든지, 개발, 테스팅 방법이라든지 하는 여러 내용들을 다루고 있다.

 

 

  위에서 얘기했던 내용들을 이해한대로 그려보면 아래의 그림과 같다. 브라우저가 폼이나 API를 통해 요청을 하면, 장고의 웹 모듈이(WGSI - 메뉴얼에 이 기능은 테스트에만 사용하고 운영시에는 아파치 등을 연동해 쓰라고 명시되어 있다) 요청을 받아, 라우팅(URLConfs) 기능을 통해 해당되는 뷰의 콜백 함수에 전달한다.

 

  모델은 ORM(Object-relational mapping)이라는 패턴 기법을 이용해 데이터베이스를 가상의 프로그래밍적 객체로 모델링하여 정의하고, migrate 명령어를 이용해, 실제 데이터베이스에 테이블을 만들거나, 수정하여, 장고와 데이터베이스 사이의 구조를 싱크(뭐 migration 이 한 방향의 의미긴 하지만 넓게 보면 싱크 개념인것 같다) 시킨다. 또한 데이터 베이스에 SQL 문을 날리듯, 모델에서 QuerySet 이라는 검색용 메써드를 제공해, ORM 객체로부터 데이터를 조회해오게 한다(select, where, order by, join 등을 실제 비슷하게 구현한다. 앞에 진행했던 시간을 생각해 보면, 'Panda' 가 가상의 메모리 객체(dataframe)를 만들어서 비슷한 행동을 했었다).

 

  양 쪽을 싱크하는 과정에서 어드민에서 필요한 몇몇 테이블도 데이터베이스 안에 들어가게 되고, Admin 쪽에는 모델에서 정의한 ORM 객체들을 살펴보고, 데이터를 넣거나, 수정하거나, 지우거나 하는 관리 행동을 할수 있는 기본적인 인터페이스가 자동으로 싱크되어 구현된다.

 

  이러한 모델에서 만들어진 ORM 개체들은 뷰에서 QuerySet 을 이용하여 호출되어, 사용자가 요청한 조건에 적합하도록 가공되며, 템플릿과 조합되어 동적인 UI를 생성해 제공되거나, Json 응답 등으로 템플릿과 상관없이 독립된 형태로 사용자에게 응답을 줄 수 있다. 템플릿은 기본적으로 HTML 베이스로 구성된 응답(response)을 위한 기본 문서이며 여러가지 장고에서 제공되는 템플릿용 지원 기능 들과 CSS, Javascript 같은 스태틱 파일들을 이용하여 적절한 UI 를 구성하여 사용자에게 처리결과를 보여 주게 된다. 

 

  위의 그림중에 대부분의 요소들은 앞의 레거시 웹이나, 플라스크(flask) 살펴보기 시간에 얘기했던 주제들이거나, 추후 메뉴얼을 보면서 자세히 항목들을 살펴봐야 될 주제들 같아서, 이 시간에는 ORM 과 그에 연관된 주제들(QuerySet, Migration), 그리고 장고의 뼈대를 구성하는 MVT 라는 패턴 구조에 대해서 설명한 후 나머지 플라스크와의 자잘한 차이는, 실제 샘플을 구현하면서 중간 중간 얘기하려 한다.

 

 

[ORM]

  ORM 은 앞에서도 얘기했지만, 데이터베이스의 테이블의 구조와 관계를 클래스와 프로퍼티(속성), 메쏘드를 이용해, 객체의 관계로서 모델링 하는 기법이다. 구글에서 'orm pros and cons' 나 'orm 장단점' 이라고 검색하면, 아래와 같은 많은 페이지가 나오면서 난상 토론을 보게 되는데,

 

https://stackoverflow.com/questions/494816/using-an-orm-or-plain-sql

https://stackoverflow.com/questions/35955020/hibernate-orm-framework-vs-jdbc-pros-and-cons

https://gs.saro.me/#!m=elec&jn=718

http://layered.tistory.com/entry/ORM%EC%9D%80-%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%8B%A4-ORM-is-an-antipattern

https://okky.kr/article/286812


 

  ORM 사용에 대한 판단은 각각 하는게 맞겠지만, 몇 가지 생각해 봐야할 것 같은 부분들이 있다. 우선 1번째는 'ORM' 은 'ODBC' 나 Selenium 의 'webdriver' 처럼 원소스 멀티유즈를 표방한다. 그러다 보니, 같은 모델이 여러 데이터베이스와 100% 호환된다는 보장은 사실 힘들것 같다(메뉴얼의 'The model layer' 의 뒷 부분이 이런 차이들과, 모델로서 해결하기 힘든 경우 raw sql 을 사용하는 부분들을 가이드 하고 있다). 데이터베이스마다 미묘한 문법차이나, 설계 차이가 있을 수 있고, 데이터베이스 버전 별로 100% 호환이 되도록 충분히 지원하고 테스트 되었다고 보기 힘들 수 있다. 마치 셀레늄에서 webdriver 의 종류에 따라 서로 다른 브라우저 끼리의 동작이 미묘하게 달랐던 것 처럼 말이다. 그래서 가능한 ORM 을 쓸 경우, 해당 프레임워크가 가장 기본으로 지원하는 데이터베이스를 사용하는게 업데이트도 계속 지원되고, 호환이 안되어 곤란한 일이 안 생길 것 같다(예를 들어 Django 는 PostgreSQL, MySQL, Oracle, Sqlite3 를 공식적으로 메뉴얼에서 다룬다).

 

  2번째는 ORM 이 데이터베이스에서 구조와 성능을 위해서 지원하는 주요 기능 요소들을 충분히 모델상에서 포함하고 있는지에 대해서이다. 장고에서 기본적으로 하나의 모델 객체는 하나의 실제 테이블에 마이그레이션이되어 매칭 되는 구조인데, 실제 복잡한 사이트에서 효율적인 설계가 해당 방식으로 100% 이루어질 수 있는지를 체크해 봐야한다(현실에서는 수많은 테이블이 서로 join 등으로 엃혀있는 관계를 가진 경우가 많아서, ORM 모델로 구현시 가독성이나, 복잡도가 감당할만 한지 등을 따져봐야 할 것 같다)

 

  3번째는 레거시 데이터베이스를 사용하는 경우이다. 관계형 데이터베이스의 특성상 많은 테이블들이 서로 연관을 가지면서 파편화 되있을 가능성이 많은데, 해당 부분을 현재의 모델로 흡수하여 구현하게 되면, DB프로시저 등을 생산해서 추가적인 중계가 필요할 수도 있고, 그런 경우엔 장고의 ORM 에서 구현한 장점 중 하나인 마이그레이션 기능을 원활히 사용할수 없을 가능성이 높아진다. 

 

  4번째는 DBA 인력들과의 협업이다. 개발 쪽에서 독자적으로 데이터베이스를 관리하고 책임지는 구조라면 모르겠지만(뭐 요즘 유행인 DevOps 등의 조직에선 모르겠다), DB 관련 팀 쪽에서 데이터베이스의 성능, 테이블 스키마 관리 등을 책임 지고 있다면, 만들어진 모델을 검증하기 위해서, DBA 인력이 장고의 ORM 을 구현한 클래스들과 QuerySet 구성을 이해하고, 해당 부분이 실제 현재의 데이터베이스에 어떤 영향을 주는지 모델의 변경이 생길때마다 매번 검증을 해야하는 상황이 생기게 되는데, 이런 방식이 가능한 시나리오인지는 의문이 생긴다.

 

  마지막으로는, 장고 쪽에 마이그레이션 기능을 위해, 데이터베이스의 스키마를 자동으로 바꿀 수 있는 권한을 주는 부분이 보안적으로 적절한가에 대한 고려와, Admin 의 요구사항이 경험적으로 단순히 모델 객체에 데이터를 넣거나 편집하는 것으로 단순하게 이루어지진 않기 때문에 자동화된 어드민이 실제 얼마나 유용할까에 대한 의문이다.

 

  결론적으로 적절히 우호적인 환경에서는 장고의 ORM 을 사용해 데이터베이스를 마이그레이션 하여 관리하는 것도 좋지만, 해당 경우 발생할 여러가지 반대 급부를  생각해 봐야 하며, ORM 을 사용하는 것 자체가 개발자에게 데이터베이스를 덜 이해해도 된다는 면죄부가 되는 것은 아니라는 것을 얘기하고 싶다. 오히려 개인적인 생각에는 ORM 을 사용해 모델을 설계해서 테이블과 싱크 시키고 싶다면, 장고의 모델과 QuerySets, 마이그레이션 쿼리, 객체지향 설계를 잘 이해하고, 동시에 사용하는 데이터베이스의 여러 성능 요소를 결정하는 미묘한 특징들에 대해서 잘 이해해야지만 충분히 규모가 커져도 유지보수가 가능한 좋은 설계가 나오지 않을까 싶다. 뭐 그냥 개인적인 생각이라는 것을 꼬리로 단다.

 

 

[MVT]

  MVT(Model, View, Template) 패턴은, 기존에 많이 쓰이는 용어가 MVC 이니 두 개를 비교해 보면서 살펴보도록 하자, 구글에서 'mvc vs mvt' 로 검색하면 아래의 페이지가 나온다.

https://stackoverflow.com/questions/6621653/django-vs-model-view-controller

 

If you’re familiar with other MVC Web-development frameworks, such as Ruby on Rails, you may consider Django views to be the controllers and Django templates to be the views.

 

  해당 내용 중 위와 같은 내용이 있는데, MVC 의 model 은 장고에서 설명한 구조와 거의 같이 데이터베이스와 매핑되는(사실 어떻게 보면 웹 프로그램이라서 그렇지 원래 ORM 은 굳이 매핑되는 대상이 데이터베이스일 필요는 없는것 같다) 부분을 얘기하고, 컨트롤러(controller)는 장고나 플라스크의 url 요청을 받아 해당되는 함수에 연결해 주는 라우팅 부분을 얘기한다. 그리고 뷰(View)는 실제 모델로부터 데이터를 받아서 보여주는 역활을 한다.

 

  그래서 장고의 경우 위의 그림에서 그렸듯이, 뷰에 URLconfs 기능이 있기 때문에 MVT의 뷰는 컨트롤러 개념을 가지고 있다고 하고(사실 뷰에서 템플릿을 사용안하고 바로 응답값을 줄수도 있기때문에, 템플릿과 뷰 개념을 같이 가지고 있다고 보는것도 맞을듯 싶다), 실제 상으로 템플릿에서 모든 결과를 보여주기 때문에 MVT 의 템플릿은, MVC 의 뷰와 같다고 말하는 것이다. 사실 좀 말장난 같은 요소가 있으며, 기능이 어느 편에 붙었는지에 상관없이 전체적인 기능 요소들은 거의 동일 하므로, 두 개가 사실상 같은 개념이라고 봐도 무방할듯 싶다(언어로 따짐 사투리 관계인데 뭐가 표준어인지는 모르겠다^^;).

 

 

[사전 준비 - Django 설치]

  파이썬 3를 지원하므로 pip 명령어로 설치하면 된다.

c:\Python\code>pip install django
Collecting django
...
Successfully installed django-1.11.4

 

 

 

 

[DB 에서 데이터 불러와 HTML 테이블로 보여주기]

  그럼 본격적으로 장고를 이용해서 앞의 시간에 플라스크로 구현해 봤던, 4교시에서 만들었던 예제를 응용한, MSSQL 데이터베이스에서 데이터를 불러다 HTML 테이블 형태로 웹에 표시하는 부분을 구현해 보기로 하자. ORM 모델을 무시하고, 4교시와 비슷하게 pymssql 모듈을 사용하여 모델을 무시하고 호출하는 것도 가능하겠지만, 그러면 장고의 모델 부분을 살펴 볼수 있는 기회가 없어지기 때문에 ORM 을 최대한 이용하도록 구현을 해보려고 한다.

 

 

[장고의 모델에서 MSSQL 을 지원하게 해주는 모듈 찾기]

  우선 공식 메뉴얼에는 MSSQL 지원에 대해서 언급된 내용이 없으므로, 구글에서 'django mssql' 이라 검색해서, 아래의 페이지를 찾는다.

https://django-mssql.readthedocs.io/en/latest/

 

  스토어드 프로시저(stored procedure)도 호출할 수 있고, 메뉴얼이랑 사용법등이 그런데로 괜찮은듯 한데, 지원하는 버전을 보니, django 1.8 까지 지원한다. 적용하면 돌아갈것 같긴한데 이젠 지원이 끊긴듯 해서 찜찜하다. 그래서 다시 다른 페이지를 찾다보니 아래 레딧 페이지에 'django-pyodbc-azure' 를 쓰라는 조언이 있다.

https://www.reddit.com/r/Python/comments/4iq7zb/django_19_with_mssql_as_backend/

 

  그래서 다시 구글을 검색해, 해당 모듈의 깃허브 페이지로 가니, 장고 1.11 최신버전과 MSSQL 2016 까지 지원해 주는, 현재 활발히 유지되고 있는 모듈이 있다.

https://github.com/michiya/django-pyodbc-azure

 

 

  그럼 설치 가이드에 있는 것처럼, pyodbc 와 django-pyodbc-azure 를 각각 설치해 보자(ODBC 개념은 4교시때 간단히 설명했다)

c:\Python\code>pip install pyodbc
Collecting pyodbc
...
Successfully installed pyodbc-4.0.17


c:\Python\code>pip install django-pyodbc-azure
Collecting django-pyodbc-azure
...

Successfully installed django-pyodbc-azure-1.11.0.0

 

 

 

[이미 만들어진 스키마를 장고 모델로 가져오는 방법 찾기]

  이제 모듈이 설치되었는데, 모듈이 잘 동작하는지 보려면, 장고 모델을 만들어서 마이그레이션으로 MSSQL 데이터베이스와 싱크를 해야될 것 같다. 그런데 다시 supermarket 테이블에 대한 모델을 만들어, 마이그레이션을 시키고 다시 장고의 어드민이나 SQL Management Studio 를 통해 데이터를 넣고 하는게 귀찮기도 하고, 기존 레거시 데이터베이스를 사용하는 경우 모델을 어떻게 구성하나도 궁금하기도 해서 구글에 'django pre existing database' 라고 검색해 아래의 메뉴얼 페이지를 찾았다. 근데 다행히도 설정파일에 연결문자열만 맞춰 놓으면, manage.py 의 inspectdb 명령어를 사용하면, 데이터베이스의 테이블을 자동으로 읽어와서,  해당 되는 테이블 구조에 맞는 model.py 파일을 만들어 준단다. 그럼 조금 뒤에서 모델을 만들때 이 기능을 사용하기로 해본다.

https://docs.djangoproject.com/en/1.11/howto/legacy-databases/


 

[프로젝트 만들기]

  일단 앞의 플라스크와 마찬가지로 가상환경을 쓸 필요는 없을 듯해 virtul env 설정은 생략하고, 앞의 장고걸의 예제들을 따라해 보다보니 설정 문제인지 현재 환경에서(windows 10) 일부 명령어 실행에 에러가 나서, 첨에 소개한 아래의 영문 설명 페이지를 기준으로 virtual env 설정 하는 부분과 migration 하는 부분만 제외하고 적당히 따라해 보기로 했다.

https://scotch.io/tutorials/build-your-first-python-and-django-application

 


  먼저 프로젝트를 생성한다. 아래의 명령어를 치면, c:\python\code 밑에 djangoweb 이라는 프로젝트를 만든다. 뭐랄까 GUI 메뉴를 사용하는 방식은 아니지만, 이제 부터는 비주얼 스튜디오나 이클립스 등으로 프로젝트를 만드는 느낌으로 따라오면 된다.
c:\Python\code>django-admin startproject djangoweb

 

  이렇게 되면 메뉴얼에도 나오지만 아래와 같이 c:\python\code 폴더 아래 djangoweb 이란 폴더가 생성되면서, 아래와 같은 서브폴더와 파일 구조를 가지게 된다.

1
2
3
4
5
6
7
djangoweb/
    manage.py
    djangoweb/
        __init__.py
        settings.py
        urls.py
        wsgi.py
cs

 

  뭐 각각의 파일이 모두 나름의 역활은 있지만, 자세한건 메뉴얼을 보고, 현재 시간에 의미 있는 요소들만 얘기하면,

1) mamage.py - 어플리케이션(프로젝트 안에서 실제 돌아가는 프로그램 모듈 1개를 얘기한다)을 만들거나, 데이터베이스에 모델 정보를 마이그레이션 하거나 하는 등의 여러 프로젝트의 관리에 필요한 기능들을 모아논 장고 전용의 작은 파이썬 프로그램이라고 보면 된다.

2) settings.py - 프로젝트 전반에 필요한 설정들을 저장한 파일. 데이터베이스 연결 문자열, 사용하는 어플리케이션들의 정의, 로케일, 스태틱 파일 경로, wsgi(web server gateway interface - 사용자로부터의 웹 요청을 처리하는 모듈) 지정 등등이 들어가 있다고 보면 된다/

3) urls.py - 1차 라우팅 설정이 들어 있다. 1차라고 하는 이유는 나중에 어플리케이션을 만들면 또 그 안에 라우팅 설정이 또 있다. 들어온 요청을 각 어플리케이션에 분배하는 1차 라우팅 단계라고 보면 된다.

 

 

[프로젝트 안에 어플리케이션 만들기]

  그 담에는 어플리케이션을 만들기 위해서, 'cd' 명령어를 이용하여 새로 만들어진 'djangoweb' 폴더 안으로 이동하여 'supermarket' 테이블에서 조회해서 보여줄 어플리케이션을 생성하는 명령어를 실행 한다(뭐 복잡해 보인다고 생각할지 모르지만, 어차피 장고를 사용하려면 정해져 있는 방식이라서 그대로 따라해야 되는 부분이다).

c:\Python\code>cd djangoweb
c:\Python\code\djangoweb>python manage.py startapp supermarket

 

  이 후에는 내부의 폴더에 supermarket 폴더가 생기며 안에 담긴 파일들은 아래와 같다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
djangoweb/
    manage.py
    djangoweb/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    supermarket/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        views.py
cs

 

  역시 이번 시간에 주로 의미가 있는 파일들만 언급하면,

1) models.py - ORM 모델들이 클래스로 정의되어 있는 파일이다. 현재는 빈 어플리케이션을 만들었기 때문에 안을 보면 내용이 비어있다.

2) views.py - 뷰가 정의되어 있는 파일이다.

-> 위의 생성된 리스트를 보면 좀 이상하다라고 생각할수 있는데, 'templates' 폴더나, 어플리케이션 용 'urls.py' 파일은 안보인다. 뒤에서 보겠지만, 해당 파일은 수동으로 생성해 주어야 된다.

 

 

[프로젝트 파일 설정에 어플리케이션 추가]

  가이드에 나와 있는데로, 프로젝트의 셋팅 파일에, 새로 만든 supermarket 어플리케이션을 인식할 수 있도록 추가해 주자. c:\Python\code\djangoweb\djangoweb\settings.py 파일에서 아래 부분을 추가해준다. 

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'supermarket',
]
cs

 

 

[프로젝트 라우팅에 어플리케이션 쪽 라우팅 추가]

  그 다음엔 '프로젝트의 urls.py' 를 수정해 요청이 들어왔을때, 요청의 처리를 'supermarket 어플리케이션' 쪽에 위임하도록 해보자. c:\Python\code\djangoweb\djangoweb\urls.py 파일에서 아래 import 문과, supermarket.url 항목 부분을 추가해 준다. url 쪽이 플라스크와 하나 다른건 8교시때 배웠던 정규표현식으로 파싱을 한다는 것이다. 플라스크 처럼 사용하려면 '^원하는문자열$'(시작과 끝 사이에 원하는 문자열만 있음) 패턴으로만 url 을 정의해 쓰면 될듯 하다. 아래에서는 ^(시작하자마자) 후에 바로 supermarket.urls 로 라우팅 되기 때문에 결국 supermarket 쪽으로 전체 라우팅 제어권을 넘기는 것과 마찬가지가 된다. 

1
2
3
4
from django.conf.urls import include
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('supermarket.urls')),
cs

 

 

[어플리케이션 라우팅 파일 만들기]

  자 그럼 이제 프로젝트에서 supermarket 어플리케이션으로 던진 경로를 해석해주는 어플리케이션 라우팅 부분이 있어야 한다. c:\Python\code\djangoweb\supermarket\urls.py 을 메모장에서 만들어서 아래의 내용을 넣어 utf-8 인코딩으로 저장한다(혹시 한글 주석을 달려면 utf-8로 파일이 저장되어야 장고 실행시 에러가 안 난다). 안의 내용은 일단 아직 뭐가 뭔지 잘 모르기 때문에, 위의 튜토리얼의 예제 내용을 그대로 넣어보자. 웹 서버 루트 경로(^$-시작과 끝 사이에 아무 값도 없음)를 호출했을 경우, 앞서 잠시 얘기한 장고에서 제공하는 generic view 를 사용하여 보여주는 형태 이다(뭐 장고가 잘 돌아가는지만 보려 하는거니 상세한 문법은 넘어가기로 하자)

1
2
3
4
5
6
from django.conf.urls import url
from supermarket import views
 
urlpatterns = [
    url(r'^$', views.HomePageView.as_view()),
]
cs

 

 

[어플리케이션 뷰 파일에 샘플 뷰 추가하기]

  자 그럼 이제 어플리케이션에서 해당 되는 라우팅에 대한 샘플 뷰를 만들어 보자. c:\Python\code\djangoweb\supermarket\views.py 파일에서 아래의 내용을 추가한다. 대충 문법을 보면 generic view 중 TemplateView 라는 것을 이용해 index.html 이라는 템플릿 파일을 지정했다.

1
2
3
4
5
6
7
from django.shortcuts import render
from django.views.generic import TemplateView
 
# Create views
class HomePageView(TemplateView):
    def get(self, request, **kwargs):
        return render(request, 'index.html', context=None)
cs

 

 

 

[어플리케이션 뷰 파일에 해당 되는 템플릿 만들기]

  마지막으로 템플릿 파일을 만들면 된다. c:\Python\code\djangoweb\supermarket\ 폴더에 templates 폴더를 생성한다(c:\Python\code\djangoweb\supermarket\templates\). 이후 해당 폴더안에 c:\Python\code\djangoweb\supermarket\templates\index.html 을 만들어 저장한다. 역시 한글 주석을 위해서는 utf-8 인코딩으로 저장하자.

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>장고샘플</title>
    </head>
    <body>
        <h1>동작함</h1>
    </body>
</html>
cs

 

 

[샘플 페이지 실행]
  그럼 다시 c:\Python\code\djangoweb 경로에서 아래의 명령어를 실행해 보자(manage.py 를 실행 시켜야 해서 실행 경로가 맞아야 한다)

c:\Python\code\djangoweb>python manage.py runserver
Performing system checks...
You have 13 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
...

Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
[05/Aug/2017 17:30:55] "GET / HTTP/1.1" 200 222

 

  뭐 기본 기능에 대한 마이그레이션이 안됬으니 어쩌니 얘기가 나오긴 하지만, 해당 부분은 무시하고 일단 웹페이지를 띄워, http://127.0.0.1:8000/ 을 호출하면 아래와 같이 웹페이지가 동작함을 볼 수 있다.

 

 

  참고로 현재 까지의 파일 구조는 아래와 같다. 추가된 파일과 폴더들은 색으로 표시했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
djangoweb/
    manage.py
    djangoweb/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    supermarket/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        templates/
            index.html
        urls.py
        views.py
cs

 

 

[MSSQL 연결 문자열 추가하기]

  MSSQL 연결 문자열을 추가하기 위해(앞의 'django-pyodbc-azure' git 페이지의 문서를 참고했다), 프로젝트 내의 c:\Python\code\djangoweb\djangoweb\settings.py 을 열어 DATABASE 설정 부분을 디폴트인 sqlite3 에서 MSSQL 로 수정한다.  
<원본>

1
2
3
4
5
6
DATABASES = {
    'default': {
        'ENGINE''django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
cs

 

<수정>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DATABASES = {
    'default': {
        'ENGINE''sql_server.pyodbc',
        'NAME''mytest',
        'USER''pyuser',
        'PASSWORD''test1234',
        'HOST''localhost',
        'PORT''1433',
 
        'OPTIONS': {
            'driver''ODBC Driver 13 for SQL Server',
        },
    },
}
cs

 

 

[모델 자동으로 만들기]

  앞에서 봤던 레거시 테이블을 자동으로 만드는 inspectdb 명령어를 이용하여 model.py 파일을 만들어 본다.

https://docs.djangoproject.com/en/1.11/howto/legacy-databases/

 

 

  일단 제대로 연결 문자열이 설정됬나 보기 위해서 아래의 명령어를 실행해 본다. 잘은 모르겠지만, 대충 출력된 내용을 보니 해당 데이터베이스안에 기존에 만들어 놓은 'Play' 와 'supermarket' 테이블을 가져오고, 필드 이름들도 정상적으로 파싱해 가져오는거 같다.
c:\Python\code\djangoweb>python manage.py inspectdb
# This is an auto-generated Django model module.
....

class Play(models.Model):
...

    class Meta:
        managed = False
        db_table = 'play'


class Supermarket(models.Model):
    itemno = models.IntegerField(db_column='Itemno', blank=True, null=True)  # Field name made lowercase.
...
    price = models.IntegerField(db_column='Price', blank=True, null=True)  # Field name made lowercase.

    class Meta:
        managed = False
        db_table = 'supermarket'

 

 

 그럼 models.py 파일을 만들어 내기 위해서, 아래의 명령어를 실행한다. inspectdb 를 하여 출력된 내용을 리다이렉션('>') 을 이용해서 기존의 자동 생성된 models.py 파일을 덮어쓰기 한다.

c:\Python\code\djangoweb>python manage.py inspectdb > supermarket\models.py

 

 

  확인을 하고 싶으면 c:\Python\code\djangoweb\supermarket\models.py 파일을 열어보면, 테이블의 구조가 아래와 같이 model 파일로 자동으로 만들어 진다. 내용을 보면 우리가 클래스는 배운적이 없지만 걍 내용을 담는 박스라고 생각하자. 클래스 안에 각 디비의 컬럼들이 정의 되어 있는데, 앞의 소문자로 된 itemno, categoty 등이 나중에 QuerySet 등에서 명시되어 사용되게 된다. 

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
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey has `on_delete` set to the desired behavior.
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from __future__ import unicode_literals
 
from django.db import models
 
 
class Play(models.Model):
    original = models.CharField(max_length=30, blank=True, null=True)
    encrypted = models.CharField(max_length=200, blank=True, null=True)
    decrypted = models.CharField(max_length=30, blank=True, null=True)
 
    class Meta:
        managed = False
        db_table = 'play'
 
 
class Supermarket(models.Model):
    itemno = models.IntegerField(db_column='Itemno', blank=True, null=True)  # Field name made lowercase.
    category = models.CharField(db_column='Category', max_length=20, blank=True, null=True)  # Field name made lowercase.
    foodname = models.CharField(db_column='FoodName', max_length=30, blank=True, null=True)  # Field name made lowercase.
    company = models.CharField(db_column='Company', max_length=20, blank=True, null=True)  # Field name made lowercase.
    price = models.IntegerField(db_column='Price', blank=True, null=True)  # Field name made lowercase.
 
    class Meta:
        managed = False
        db_table = 'supermarket'
 
cs

 

 

[마이그레이션 - 일단 생략]

  굳이 여기서 쓸것도 아니고, 모델과 데이터베이스를 계속 싱크 시킬 것도 아니기 때문에 마이그레이션 명령을 돌리는건 생략 하지만, 저도 에러 땜에 이것저것 찾아보다 한번 돌려봤다. 마이그레이션 명령어를 실행하게 되면 아래와 같이 어드민 관련 테이블이나, 장고에서 지원되는 세션 같은 기능에서 사용하는 기본 테이블들이 데이터베이스안에 만들어지게 된다. 실제로 안돌려도 되니 참고만 하자(뭐 돌려도 상관은 없지만 말이다).

c:\Python\code\djangoweb>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying admin.0001_initial... OK
....
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK

 

 

[inspectdb 를 통한 마이그레이션 성공 여부 확인 하기]
  실제 페이지를 모두 만들어서, 만들어진 모델의 Queryset 이 정상으로 돌아가는지 확인 하려면, 만드는 도중에 또 다른 실수를 할지 모르므로, 실제 사이트를 만들기 전에 아래의 장고걸 사이트에서 나온데로, 장고쉘 기능을 이용해서(파이썬 쉘과 비슷하다고 보면 된다), 모델이 잘 생성됬는지 확인해 본다.

https://tutorial.djangogirls.org/ko/django_orm/

 

 

일단 쉘을 실행 하자.

c:\Python\code\djangoweb>python manage.py shell

 

  잘 실행되서 '>>>' 프롬프트가 나오면 아래의 명령어를 넣어 슈퍼마켓 모델을 가져오고, 슈퍼마켓 모델에서 모든 내용들을 다 가져와 보자

>>> from supermarket.models import Supermarket
>>> Supermarket.objects.all()
  File "C:\python\lib\site-packages\sql_server\pyodbc\base.py", line 545, in execute
    return self.cursor.execute(sql, params)
django.db.utils.ProgrammingError: ('42S22', "[42S22] [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]\ufffd\ufffd \ufffd\u0338\ufffd 'id'\ufffd\ufffd(\ufffd\ufffd) \ufffd\u07f8\ufffd\ufffd\u01fe\ufffd\ufffd\ufffd\ufffd\u03f4\ufffd. (207) (SQLExecDirectW)")

 

 

  흠 그런데 이상한 SQL 에러가 난다. 근데 에러 파일의 위치를 보면('pyodbc\base.py') 장고쪽은 아니고, pyodbc 쪽 에러같아서 mssql 모듈쪽은 이상없는것 같아 일단 안심은 된다. 어떤 에러인지 찾기 위해서 구글에서 'pyodbc 42S22 error' 라고 검색해서 아래 페이지를 찾아서 보니, 컬럼이 없는 경우의 에러인 것 같다. 그러고 보니 위의 에러에서 'id' 라는 컬럼이름 같은 항목이 보인다. 

https://stackoverflow.com/questions/36202976/pandas-with-pyodbc-nan-error-42s22-error-attribute-qnan-not-found-31

 

 

  그런데 supermarket 테이블에는 'id' 라는 컬럼 자체를 만든적이 없으니 이상하다고 생각하던 중, 예전에 장고 메뉴얼을 읽다가, 마이그레이션 설명 쪽에서 모델에 primary 키(겹치지 않는 유일한 값을 가진 필드 속성)가 없으면, 자동으로 테이블에 'id' 라는 이름으로 자동으로 숫자가 증가하는 primary 키를 만든다는 내용을 스쳐가듯 본 듯한 기억이 났다. 해당 테이블에는 당연히 primary 키 설정이 없었었고, 그래서 장고가 마이그레이션한 테이블처럼 id 필드가 무조건 있으리라 생각하고, 데이터베이스에 select 쿼리를 보낼 때 'select id, ...' 를 하여 에러가 났나보다.

https://docs.djangoproject.com/en/1.11/topics/db/models/

https://docs.djangoproject.com/en/1.11/topics/db/models/#automatic-primary-key-fields

 

 

  그럼 2가지 해결책이 있을 듯 하다. 하나는 현재의 supermarket 테이블에, 자동으로 숫자가 증가되는(auto increment) id 필드를 추가하는 방법이 있고, 다른 하나는 기존 supermarket 테이블을 삭제하고 다시 만들면서, 특정한 필드를 primary 키로 지정하여 생성하는 것이다(개인적으로 두 방식 모두 해봤는데 둘 다 잘 해결되긴 했다). 여기선 간단하게 가기위해서, supermarket 테이블에 id 컬럼을 하나 추가하자. 구글에서 'mssql add column autoincrement' 라고 검색하여 아래 페이지를 찾았다.

https://stackoverflow.com/questions/4862385/sql-server-add-auto-increment-primary-key-to-existing-table

 

     ALTER TABLE dbo.YourTable
     ADD ID INT IDENTITY

 

  아래의 명령어를 4교시에 설명한 MSSQL Managment Studio 를 실행해 쿼리 입력 창에서 mytest db 를 대상으로 실행한다(SSMS 사용법이 기억이 잘 안나면 4교시를 참고한다)

1
2
3
4
5
6
7
8
9
10
use mytest
go
ALTER TABLE dbo.supermarket
   ADD id INT IDENTITY
 
ALTER TABLE dbo.supermarket
   ADD CONSTRAINT PK_supermarket
   PRIMARY KEY(id)
 
select * from dbo.supermarket(nolock)
cs


  명령 실행후 supermarket 테이블을 셀렉트 한 내용을 보면 아래와 같이 숫자가 증가하는 id 값이 추가로 생겼다.

 

 

  이후 다시 장고 쉘에서 같은 명령어를 입력해 보면 정상적으로 결과를 가져온다. 값이 아니라 오브젝트 자체를 가져오기 때문에 안의 내용은 표시되진 않지만, 설치한 mssql 용 장고 모듈이 잘 동작되는 것을 확인 했으니 이제 실제 코드를 만들면 될듯 하다.
>>> from supermarket.models import Supermarket
>>> Supermarket.objects.all()
<QuerySet [<Supermarket: Supermarket object>, <Supermarket: Supermarket object>, <Supermarket: Supermarket object>, <Supermarket: Supermarket object>]>

 

 

  ※ 참고로 기존 테이블에 id 를 추가하는 게 싫어서 테이블을 재생성 하고 싶으면, 아래와 같은 itemno 에 primary 키 속성이 있는 테이블을 만들고, 데이터를 다시 채워 넣음 된다(물론 그전에 'drop table' 명령어로 기존 supermarket 테이블은 지워야 한다)

1
2
3
4
5
6
7
CREATE TABLE [dbo].[supermarket](
    [Itemno] [intNOT NULL PRIMARY KEY,
    [Category] [char](20NULL,
    [FoodName] [char](30NULL,
    [Company] [char](20NULL,
    [Price] [intNULL
)
cs


 

[supermarket 용 url 만들기]

  아까 샘플 url 을 만든것 처럼,  c:\Python\code\djangoweb\supermarket\urls.py 파일에 아래와 같이 supermk url 을 추가 한다.

1
2
3
4
5
6
7
from django.conf.urls import url
from supermarket import views
 
urlpatterns = [
    url(r'^$', views.HomePageView.as_view()),
    url(r'^supermk$', views.supermk),
]
cs

 

 

[supermarket 용 view 만들기]

  마찬가지로 c:\Python\code\djangoweb\supermarket\views.py 파일의 기존 내용의 마지막에, 아래의 supermk 뷰를 추가한다(따라오시다 헷깔리시는 경우는 나중에 전체 소스를 맨뒤의 부록 섹션에 첨부할테니 그걸 참고하시기 바란다). 예전 플라스크에서 구현한 것과 비슷한 구조로 Supermarket 모델에서 모든 값을 가져오고(supers = Supermarket.objects.all()), 이후에 가져온 데이터를(supers)을 super.html 과 같이 랜더링 한다.   

1
2
3
4
5
from .models import Supermarket
 
def supermk(request):
    supers = Supermarket.objects.all()
    return render(request, 'super.html', {'supers': supers})
cs

 

 

[supermarket 용 template 만들기]

  마찬가지로 c:\Python\code\djangoweb\supermarket\templates\super.html 파일을 메모장으로 utf-8 인코딩으로 저장해 만들면서 아래의 코드를 넣는다. 역시 플라스크와 비슷하게

'{{ }}' 와 '{% %}' 를 사용하여 루프를 돌리면서 <td> 태그 안에 각 컬럼 값을 넣게 된다(템플릿 엔진이 같은가도 싶다).

1
2
3
4
5
6
7
8
9
10
11
<table border="1" cellpadding="5" cellspacing="5">
{% for super in supers %}
   <tr>
      <td>{{ super.itemno }}</td>
      <td>{{ super.category }}</td>
      <td>{{ super.foodname }}</td>
      <td>{{ super.company }}</td>
      <td>{{ super.price }}</td>   
   </tr>
{% endfor %}
</table>
cs

 

 

[supermarket 페이지 결과 보기]

  아까 runserver로 웹서버를 실행한 상태로 두었다면 소스의 변경사항이 자동으로 반영되었을 테고(플라스크도 이랬다), 종료했다면 아래의 명령어를 다시 쳐서 실행한다) 

c:\Python\code\djangoweb>python manage.py runserver

 

  이제 브라우저를 띄워 http://127.0.0.1:8000/supermk 를 실행 하면, 아래와 같이 플라스크로 구현한 것과 비슷한 화면을 볼수 있다.

 

  현재까지의 트리는 아래와 같다. 추가된 파일은 색으로 표시했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
djangoweb/
    manage.py
    djangoweb/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    supermarket/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        templates/
            index.html
            super.html
        urls.py
        views.py
cs

 

 

 

 

[D3.js 에서 Json 데이터 URL 를 호출해 웹으로 그래프 보여주기]
  이 예제를 만들기 위해 플라스크에 있는 예제를 전환하면서 또 한바탕 헤메긴 했었다. 헤멘 부분이 플라스크와 장고의 차이를 보여주기 때문에 일부는 말로 설명하고 일부는 해결 과정을 보여주려고 한다.

 

 

[d3.js 샘플 페이지 찾아보기]

  일단 플라스크 때 구현한 코드를 장고로 옮기려면 두 가지가 필요하다. 첫번째는 d3.js 를 사용하는 템플릿 코드의 일부를 장고에 맞추어 변경해야 할 것 같고, 또 d3.js 에서 호출했던 json 데이터를 반환하는 URL 을 장고로 구현해야 한다.

 

  구글에서 'django d3.js json' 을 찾아서 아래 페이지를 보니 2개의 힌트가 있다. 첫번째는 d3.js 에서 url 을 호출 하는 방식이다. urlconfs 파일에서 url 을 정의 할때 'name' 속성을 이용해 정의하고, d3.js 에서 'url' 문법을 이용해 경로를 호출 한다. 또 json 데이터를 만들어 내는 play_count_by_month 함수는 QuerySet 을 이용해 결과를 가져와서 JsonResponse 함수를 이용해 json 응답을 생성한다(JsonResponse(list(data), safe=False)). 'safe=False' 옵션이 있는 이유는, JsonResponse는 dictionary 형태의 데이터만 기본적으로 중계하고, 다른 데이터 형일 경우는 'safe=False' 옵션을 넣어야만 형변환 에러가 안난다. 

https://stackoverflow.com/questions/26453916/passing-data-from-django-to-d3

 

 

  좀더 자세히 json 을 반환하는 것을 살펴 보려고 추가적인 페이지도 찾아보았다.  JsonResponse 를 사용하는게 1.11 버전에서는 적절해 보인다.

https://simpleisbetterthancomplex.com/tutorial/2016/07/27/how-to-return-json-encoded-response.html
https://docs.djangoproject.com/en/dev/ref/request-response/#jsonresponse-objects

 

 

[urlconf 파일 수정]

  우선 c:\Python\code\djangoweb\supermarket\urls.py 파일에 아래와 같이 'data' 와 'd3sample' 경로를 추가한다. 'data' url에는 아까 샘플에서 봤듯이 name 속성이 추가됬다.

1
2
3
4
5
6
urlpatterns = [
    url(r'^$', views.HomePageView.as_view()),
    url(r'^supermk$', views.supermk),
    url(r'^data$', views.data, name='data'),
    url(r'^d3sample$', views.d3sample),
]
cs

 

 

[view 파일 수정]

  다음으로 c:\Python\code\djangoweb\supermarket\views.py 파일을 수정한다. data 함수 안의 내용은 거의 플라스크때 json 형태로 만든 샘플 데이터를 최종으로 JsonReponse 에 넘기는 변경만 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import json
import numpy as np
from django.http import JsonResponse
def data(request):     
    x = np.array(['2017-07-10''2017-07-11''2017-07-12''2017-07-13''2017-07-14'])
    y = np.array([58.1353.9867.0089.7099.00])
 
    myData = json.dumps([{"date": x[i], "close": y[i]} for i in range(5)])
    return JsonResponse(myData, safe=False)
 
 
 
def d3sample(request):
    return render(request, 'd3sample.html', context=None)
cs

 

 

[templete 파일 생성]

  메모장을 열어서 예전 플라스크 때의 코드를 복사해 json URL 호출 하는 부분만 샘플에서 참고한 내용을 기준으로 변경 후에, 인코딩을 utf-8 로 하여, c:\Python\code\djangoweb\supermarket\templates\d3sample.html 파일로 저장한다. 

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
<!DOCTYPE html>
<meta charset="utf-8">
<style> <!-- 그래프 요소들의 스타일 지정 -->
body { font: 12px Arial;}
path { 
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
}
.axis path,
.axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
}
 
</style>
<body>
 
 
<!-- 라이브러리 로딩. 내부에서 돌리려면 다운받아서 static 폴더에서 읽어와야 할듯 -->    
<script src="http://d3js.org/d3.v3.min.js"></script>
 
<script>
// 그래프 좌표 공간 설정
var margin = {top: 30, right: 20, bottom: 30, left: 50},
    width = 600 - margin.left - margin.right,
    height = 270 - margin.top - margin.bottom;
 
// 그래프 범위
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
 
// 축 정의
var xAxis = d3.svg.axis().scale(x)
    .orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
    .orient("left").ticks(5);
 
// 그래프 선 정의
var valueline = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });
    
// 캔버스 객체 생성
var svg = d3.select("body")
    .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
    .append("g")
        .attr("transform"
              "translate(" + margin.left + "," + margin.top + ")");
 
// 2017-07-01 식으로 데이터를 해석하게 지정함
var parseDate = d3.time.format("%Y-%m-%d").parse;
 
// 전달 받은 데이터를 이용해서 그래프를 그린다.
var callback = function (data) {
 
    data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.close = +d.close;
    });
 
    // 실데이터에 맞춰 그래프 범위 지정
    x.domain(d3.extent(data, function(d) { return d.date; }));
    y.domain([0, d3.max(data, function(d) { return d.close; })]);
 
    // 선 그리기.
    svg.append("path")
        .attr("class""line")
        .attr("d", valueline(data));
 
    // x축 그리기?
    svg.append("g")
        .attr("class""x axis")
        .attr("transform""translate(0," + height + ")")
        .call(xAxis);
 
    // y 축 그리기?
    svg.append("g")
        .attr("class""y axis")
        .call(yAxis);
 
};
 
 
// django 에서 만든 http://127.0.0.1/data 를 호출하여 json 데이터를 가져와 callback 함수 호출
d3.json("{% url "data" %}",  callback);
 
</script>
</body>
cs

 

 

[에러를 만나다]

  그런데 브라우저를 열어 http://127.0.0.1:8000/d3sample 을 호출하여 보니 그래프가 표시되지 않는다. 피들러를 띄워 확인해 보니 d3sample url 이 호출되고, 이후 /data url 까지 잘 호출한다.

 

 

 

  그래서 json 형태로 넘어오는 데이터를 확인하기 위해 브라우저에서 http://127.0.0.1:8000/data url 을 호출해보니, 파일을 저장하라는 창이 뜬다. 뭔가 이상하긴 하다. 해당 json 파일을 c:\python\code 폴더에 저장해서 메모장으로 열어 보니, 아래와 같이 (") 문자앞에 역슬래시(\) 가 들어가 이스케이프 처리가 되어있다(아마 여기서는 안전하지 못한 코드의 경우 JsonResponse 메써드가 방어를 하는 차원인거 같긴하다). 

1
2
3
4
5
"[{\"close\": 58.13, \"date\": \"2017-07-10\"}, 
{\"close\": 53.98, \"date\": \"2017-07-11\"}, 
{\"close\": 67.0, \"date\": \"2017-07-12\"}, 
{\"close\": 89.7, \"date\": \"2017-07-13\"}, 
{\"close\": 99.0, \"date\": \"2017-07-14\"}]"
cs

 

 

  그래서 브라우저에서 F12키를 눌러서, 예전에 배운 IE 개발자 도구를 열은 후, 'F5'키를 눌러서 http://127.0.0.1:8000/data 페이지를 다시 로딩 한다.  개발자 도구창에서 '콘솔' 탭을 클릭한 후 동그란 !표 아이콘을 클릭해 보니, 아래와 같이 개체가 상이하여 루프 메서드를 실행할 수 없다는 자바스크립트 에러가 난다.

 

 

  에러 메시지 밑의 라인 링크('d3smaple (75,5)')를 클릭하면, 해당되는 소스 라인에 가게 되는데, 아무래도 json 데이터를 받아온 내용이 담겨있는 data 변수가 기존 플라스크 때와는 다르게 json 형식으로 잘 해석이 안 된것 같다(아마 위의 \" 이스케이프 처리 때문인 것 같긴하다)

 

 

  그래서 자바스크립트 안에 'document.write(data)' 구문을 넣어 data 변수를 뿌려보니 아래와 같이 파일로 저장했을때는 보였던 escape 문자(\) 는 d3.json 함수를 통과하면서 없어진듯 하고, 정상적으로 뿌려짐을 볼수 있다(해당 디버그 코드들은 최종 샘플에 주석처리해 놓았으니 참고만...). 문법적으로 json 형태는 맞는거 같아, 그렇다면 왠지 이스케이프 문자를 d3.json 이 처리는 해줬지만, json 데이터라고 생각을 안해 변환을 안해 준거 같다는 생각이 든다. 

1
2
// d3.json 으로 받아온 값을 담고 있는 data 변수의 값을 찍어봄
document.write(data)
cs

 

 

 

  그래서 data 변수의 타입을 확인해 보기로 했다. 구글에서 'javascript print typeof' 로 검색하니, 아래의 페이지에서 type 을 문자열로 반환하는 함수를 얻게 된다.

https://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable

1
2
3
4
5
6
7
8
// 데이터 타입을 string 형태로 반환하는 함수
function typeOf (obj) {
  return {}.toString.call(obj).split(' ')[1].slice(0-1).toLowerCase();
}
 
// 에러추적 2 : data 의 형태를 뿌려보니,string 이라고 나옴
var datatype = typeOf(data);
document.write(datatype);
cs

 

 

  데이터 타입을 확인하니 'string' 이라고 나온다.

 

 

   JsonResponse 에서 " 문자를 이스케이프 처리하는게 확실히 문제는 문제인거 같다. 처음에는 json object 를 dict 로 변환해 볼까를 이리저리 궁리했지만 샘플의 json object 는 2차원이고, dict 는 1차원이라서 뭔가 억지로 변환하여 문제를 풀면 d3sample.html 코드도 이것저것 바꿔줘야 할 것 같아서 망설이다가 data 변수의 타입이 string 이지만 실제 내용은 json 데이터 형태인건 맞으니, string 을 json 객체로 변환하면 결과대로 나오지 않을까 하는 생각이 들었다. 구글에 'javascript string to json' 이라고 찾아서, 아래의 페이지를 찾는다. 해당 코드를 이용하여 변환 후에, 데이터 타입을 화면에 뿌려본다.

http://jekalmin.tistory.com/entry/string%EC%9D%84-json%EC%9C%BC%EB%A1%9C-json%EC%9D%84-string%EC%9C%BC%EB%A1%9C-%EB%B3%80%ED%99%98

1
2
3
4
5
// string 을 json 형태로 변환하여 뿌려보니, list 라고 나옴
// 확인 후 변환 코드는 남겨두어, 넘어온 data 값을 변환하여 사용함.
data = JSON.parse(data);
var datatype = typeOf(data);
document.write(datatype);
cs

 

  위와 같이 변환한 후 데이터 형을 다시 확인해보니 array 라고 나온다. 지금 보니 이래서 구글에의 샘플 페이지의 view 함수의 JsonResponse 호출부분에서 데이터를  list 형태로 바꾼건가 싶기도 하다. 뭐 여튼 변환하는 코드만 주석 처리 하지 않고 남겨두어 사용해 본다.

 

 

   최종 코드는 아래와 같고, 디버깅 코드 등으로 변경한 내용은 다른 색으로 표시해 놓았다. 브라우저의 개발자 도구를 잘쓰면 이렇게 덕지덕지 덜 넣어도 될듯하긴 하다. 보시면 결국 넘어온 데이터를 'data = JSON.parse(data);' 를 이용해 json array 형태로 바꾸어줌 간단히 해결나는 상황이다(밑의 typeof 함수는 디버깅 용이다). 해당 내용으로 d3sample.html 내용을 수정해 준다.

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
<!DOCTYPE html>
<meta charset="utf-8">
<style> <!-- 그래프 요소들의 스타일 지정 -->
body { font: 12px Arial;}
path { 
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
}
.axis path,
.axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
}
 
</style>
<body>
 
 
<!-- 라이브러리 로딩. 내부에서 돌리려면 다운받아서 static 폴더에서 읽어와야 할듯 -->    
<script src="http://d3js.org/d3.v3.min.js"></script>
 
<script>
// 그래프 좌표 공간 설정
var margin = {top: 30, right: 20, bottom: 30, left: 50},
    width = 600 - margin.left - margin.right,
    height = 270 - margin.top - margin.bottom;
 
// 그래프 범위
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
 
// 축 정의
var xAxis = d3.svg.axis().scale(x)
    .orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
    .orient("left").ticks(5);
 
// 그래프 선 정의
var valueline = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });
    
// 캔버스 객체 생성
var svg = d3.select("body")
    .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
    .append("g")
        .attr("transform"
              "translate(" + margin.left + "," + margin.top + ")");
 
// 2017-07-01 식으로 데이터를 해석하게 지정함
var parseDate = d3.time.format("%Y-%m-%d").parse;
 
// 전달 받은 데이터를 이용해서 그래프를 그린다.
var callback = function (data) {
 
    // 에러추적 1 : 데이터를 뿌려보니 \ 문자도 제거되고, 정상적인 json 문법인거 같음
    // document.write(data)
    
    // 에러추적 2 : data 의 형태를 뿌려보니,string 이라고 나옴
    // var datatype = typeOf(data);
    // document.write(datatype);
 
    // string 을 json 형태로 변환하여 뿌려보니, list 라고 나옴
    // 확인 후 변환 코드는 남겨 넘어온 data 값을 변환하여 사용함.
     data = JSON.parse(data);
    // var datatype = typeOf(data);
    // document.write(datatype);
 
    // 첨에 이런 에러가 남 (개체가 'forEach' 속성이나 메서드를 지원하지 않습니다.)
    data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.close = +d.close;
    });
 
    // 실데이터에 맞춰 그래프 범위 지정
    x.domain(d3.extent(data, function(d) { return d.date; }));
    y.domain([0, d3.max(data, function(d) { return d.close; })]);
 
    // 선 그리기.
    svg.append("path")
        .attr("class""line")
        .attr("d", valueline(data));
 
    // x축 그리기?
    svg.append("g")
        .attr("class""x axis")
        .attr("transform""translate(0," + height + ")")
        .call(xAxis);
 
    // y 축 그리기?
    svg.append("g")
        .attr("class""y axis")
        .call(yAxis);
 
};
 
 
// flask 에서 만든 http://127.0.0.1/data 를 호출하여 json 데이터를 가져와 callback 함수 호출
d3.json("{% url "data" %}",  callback);
 
// 데이터 타입을 string 형태로 반환하는 함수
function typeOf (obj) {
  return {}.toString.call(obj).split(' ')[1].slice(0-1).toLowerCase();
}
 
</script>
</body>
cs

 

 

  이후 브라우저를 열어 http://127.0.0.1:8000/d3sample 를 호출하면 플라스크 때와 같이 그래프가 정상적으로 출력된다.

 

 

 참고로 앞의 코드에서 d3sample.html 이 아닌, 뷰 파일 쪽을 고치려면, 딕션너리 데이터가 리스트 안에 담긴 형태로 만들어서, JsonResponse 함수로 전달하면 됩니다.

 

 

[마무리 하면서]

  이렇게 해서 플라스크에서 구현했던 예제들을 장고에서 구현해 보면서, 장고란 프레임워크의 여러가지 면들에 대해 살펴보았다(덤으로 javascript 도 약간 배웠다). 생각보다는 장고는 DB에 마이그레이션도 해주고, 어드민도 자동 생성하는 듯, 마법사 모드에 가까운 부분도 있는것 같으며, 확실히 플라스크 보다는 체계적인 구조를 갖춘 듯 하다. 좀 뭐랄까 사용하는 규칙이 엄격하다고 할까... 장고를 사용하는게 괜찮아 보인다면 이제부터 메뉴얼을 찬찬히 훝어보거나, 관련 책을 하나 사서 보거나, 구글을 검색하면서 필요한 부분을 찾아 공부하면 될듯 싶다. 3섹션동안 웹 쪽을 다루긴 했지만 파이썬 기능에 초점을 두었기 때문에, 웹 프로그래밍 책들에서 많이 다루는 게시판이나, 로그인, 파일 업로드, CSS 등으로 화면 꾸미기와 같은 주제들은 다루지 않았기 때문에, 초보시라면 공부해야 될 부분들은 앞으로도 많을 것이다. 여기서는 legacy web, flask, django 세 가지가 각각 나름대로 비슷하면서도 다른 배경을 가지고 살아왔다는 것을 이해 한다면 성공일 것 같다. 이렇게 해서 legacy web 에서 시작하여 flask 와 django 를 살펴본 파이썬의 웹 프로그래밍에 대한 여정을 마치려 한다. 나름 처음 생각 했던것 보다는 잘 정리된 듯도 싶다 --;

 

 

 

[부록]

  전체 코드의 트리는 아래와 같고 소스들은 따라하시다 꼬일때를 대비해서, 참고하시라고 압축 파일로 첨부했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
djangoweb/
    manage.py
    djangoweb/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    supermarket/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        templates/
            index.html
            super.html
            d3sample.html
        urls.py
        views.py
cs

 

 

[첨부 zip 파일]

djangoweb.zip

 

 

 

 

 

2017.8.7 by 자유로운설탕
cs

 

  

 

 

 

 

 

posted by 자유로운설탕
2017. 7. 20. 15:44 프로그래밍

  이번 시간에는 파이썬 웹 프레임워크로 많이 쓰이는 경량화 웹 프레임워크라고 불리우는 플라스크(flask) 를 살펴보는 시간을 가지려고 한다. 메뉴얼을 기반으로 전체적인 플라스크의 구조에 대해서 살펴보고, 지난 legacy web 시간에 구현했었던 DB의 테이블을 조회해 HTML 테이블로 표현하는 예제, Javascript 계의 matplotlib 이라 할수 있는 D3.js 와 결합하여 json 데이터를 가져다가 그래프를 그려주는 예제, 마지막으로 matplotlib 을 이용해서 파이썬 코드 기반의 그래프를 생성하여 HTML 문서에 포함 시키는 총 예제 3가지를 소개하려고 한다. 

 

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

 

[들어가면서]

  우선 시작하기 전에 얘기하고 싶은 것은, 프레임워크란 부분에 너무 많은 기대는 하지는 말라고 하고 싶다. 프레임워크란 해당 분야의 좋은 관행(best practice)들과, 지원용 라이브러리들을 모아 놓은 범용적인 틀 같은 것이여서 개발에 대한 방법론, 자주 쓰는 기능 라이브러리, 안전한 설계 이슈 등 많은 부분에 도움을 줄 수 있겠지만, 그 범용적인 부분이 현재 자기가 만들고자 하는 특정한 프로그램에 적합 하다는 보장은 못해주며, 자신이 만드려는 프로그램에 맞게 커스터마이즈 하기 위해서는 많은 노력이 추가로 들어가게 될 것 이다. 마치 집을 짓는데, 기본 재료와 반듯한 땅, 숙련된 기술공을 제공해 준다 해도, 해당 지원 부분이 내가 원하는 집을 짓는데 필요한 디테일의 전부는 절대 못 된다는 것이다. 해당 프레임워크가 지원하는 언어를 충분한 깊이로 이해하고, 해당 분야(웹, 디비, 시스템, 빅데이터 등등)에 대해 충분히 이해한 상태에서 사용해야, 사용하려는 프레임워크에 대한 이해도도 깊어지고, 적절하게 스스로나 해당 프레임워크가 의도한 대로 사용이 가능할 것이다. 추가로 비슷하거나 상이한 다른 프레임워크들에 대해서도 핵심을 잘 이해하고 있다면, 선택한 프레임워크의 장, 단점에 대해서 객관적으로 바라보고 선택에 대한 트레이드오프를 잘 따져볼 수 있을 것 같다

 

 

[한글 문서 여부]

  Flask 에 대한 메뉴얼 문서는 구글을 찾다보면 한글문서가 있긴 한데, 현재 0.12 버전이 나온상태에서, 파이썬 3 지원이 안됬던 0.11 개발기간 당시의 버전 문서의 번역본이고, 번역이 완전히 다 되진 않은 상태라서, 처음 접할때 전체적인 맥락에 대한 살펴보기 용으로만 쓰라고 권하고 싶다. 또 구글에서 검색하다보면 나오는 간단한 예제들을 구현해 놓은 여러 한글 블로그들도 많으니, 본격적으로 영문 문서를 보기 전에 미리 사전 지식을 쌓아놓으면 좀 더 읽기 수월해 질 듯 하다.

http://flask-docs-kr.readthedocs.io/ko/latest/index.html

 

 

[Flask vs Django]

  그럼 우선 파이썬에서 보통 많이 얘기되는 두 개의 웹 프레임워크를 한번 비교해 보자. 구글에서 'flask vs django' 라고 넣고 검색해 보면 아래와 같은 여러 비교한 페이지들이 나온다.

 

[플라스크와 장고 비교 글 - 이 사람은 플라스크를 더 편하다고 생각한다.] 

https://www.codementor.io/garethdwyer/flask-vs-django-why-flask-might-be-better-4xs7mdf8v

[플라스크, 장고, 피라미드 비교 - 밑에 것은 번역 글]

https://www.airpair.com/python/posts/django-flask-pyramid

http://kmc5500.tistory.com/162

 

  대충 요약해 보면, flask 는 웹 프레임워크가 필요한 최소한의 기능만 제공 하고, 나머지는 외부 모듈이나, 개발자에게 구현을 하도록 유도하는 편이라고 하고, Django 는 중, 대규모의 사이트를 목적으로 만들어져 일반적인 웹사이트 개발에 필요한 풀 패키지를 지원하는 편이라고 한다. 글 들에서도 나오지만 판단은 각자 스스로 하는게 맞을 것 같고, 이번 시간에는 flask 가 표방하는 최소한의 웹 프레임워크가 어떤 의미인지 살펴보도록 하겠다.

 

 

[Flask Documentaton 보기]

  구글에 'flask documentation' 이라고 검색하면 아래의 최신 0.12 버전의 프레임워크에 대한 설명이 있는 링크를 얻을 수 있다.

http://flask.pocoo.org/docs/0.12/

 

  예전 수학 라이브러리 볼때 처럼, 쭉 목차를 살펴 보면, 먼저 'User Guide' 에는 Installation(설치) 부분이 있고(개인적으로 여러 버전을 운영할 일은 없을 듯 해서 vitualenv는 고려하지 않았다),  Quick Start 섹션에 기본적으로 기본적인 flask 웹 기동이나, url 을 파싱해 해당되는 파이썬 함수에 전달하는 라우팅, css 나 img 같은 정적인 파일에 접근하는 방법, 템플릿을 꾸며서 원하는 웹 화면을 보여주는 방법, request 된 데이터를 받아 처리하는 방법 등이 설명되어 있다. 그리고 Tutorial 에는 세팅 방법과, DB 를 조회해 화면에 보여주는 예제가 있는 것 같고, Templates 에는 여러가지 템플릿 제어 방법에 대한 설명, 한참 아래의 Patterns for Flask 에는 여러가지 플라스크로 웹을 구성하는 권장 기법들이 있다. 그 밑의 'API Reference''Additional Notes' 는 위의 사용자 가이드가 익숙해 졌을 때 추가적인 정보를 보기 위해 살펴보는게 맞을 거 같다(개인적으로 flask 를 둘러보면서 예제를 구현해 보려고 했을때 진행했던 흐름이다).

 

 

[Routing]

  실용적으로 보이는 프레임워크긴 하지만, 실제 웹코드 구현에 들어가기 전에 legacy 하고 차이가 나는 부분에 대해서 설명 후에 들어가려 한다(혹시 웹 프로그래밍에 대해서 익숙하지 않은 상태라면 앞 교시인 Legacy Web 파트를 꼭 읽어 보시고 오길 권한다). 먼저 routing 와 static files 에 대해서 생각해보자. 현재 가상현실, 증강현실이 주목받는 세상이지만, 컴퓨터 자체가 어느 정도는 해당 개념들의 표본이 아닌가 싶다. 컴퓨터 내의 파일이라는 개념에 대해 우리는 동영상, 그림파일, 문서파일, 음악파일 등을 자연스럽게 사용해와서 실체화된 것이라고 생각하지만, 실제로는 메모리나 디스크 상에 구분된 숫자에 불과하다는 사실은 맞을 것이다. 그 정보를 운영체제가 해석을 해서, 폴더나 파일로 구분하여 인식하고, 응용 프로그램이 해당 데이터를 전달 받아 우리가 볼 수 있도록 화면에 출력하거나, 소리로 출력하여 노래를 듣거나, 문서를 보거나 하는 것일 것이다. 일례로 오피스가 설치되 있지 않은 컴퓨터라면 doc, xls 파일은 아무 의미가 없는 파일일 것이다. 예컨데 특정한 처리를 해주는 로직을 만나야지만 우리가 파일이라고 믿는 것들이 의미가 생긴다는 것이다.

 

  비슷하게 우리가 legacy web 에서 'test.asp', 'hey.php' 같은 파일들이, 웹 프로그램 확장자를 가진 파일이여서, IIS, Apache 같은 웹 서버에서 해당 파일이 실행이 된다고 믿어왔던 것도 어떤 측면에서는 관념적인 것에 불과할지도 모른다. 역으로 얘기하면 어떤 이름과 확장자를 가진 URL 의 호출이 웹서버에 주어질 때, 그것을 어떻게 해석하냐는 폴더와 파일이라는 물리적인 요소에 달려있는게 아니라, 웹 서버가 요청을 어떻게 해석을 하냐는 논리적인 요소에 달려있다고 볼수 있다 

 

  이 부분이 최근의 웹 서버 모듈에서 볼수 있는 'routing' 이라는 개념이다. 클라이언트가 url 주소에 hey.php 를 요청하든 hey 를 요청하든, 웹 서버 모듈만 지원을 한다면, 해당 파일이라는 형태에 국한되지 않고 해석을 해,  파일 경로를 찾은 후 처리하는 대신에 특정한 로직(함수)으로 직접 전달할 수 있다. 즉 파일과 디렉토리 기반으로 움직이던 웹 서버의 동작을, URL 경로 규칙에 기반한 함수와의 연결 로직으로 추상화(혹은 일반화) 시켰다고 봐도 될 듯 싶다(그래서 개인적으로 확장자를 가진 웹에만 익숙하다가, 어느날 확장자가 없는 처음 웹을 만났을 때 웹서버 내에서 url 에 해당하는 실제 파일의 경로를 찾을 수가 없어서 당황했었던 기억이 난다). 결국 이렇게 되면 기존 웹에서 의미가 있었던 디렉토리와 파일명, 확장자는 모두 의미가 없는 껍데기가 된다. 이러한 URL 과 내부 기능과의 직접적인 연결을 하는 방식을, 네트워크에서 패킷을 적절한 경로로 안내하는 라우터의 역활을 차용해서 routing 이라고 명명한 것 같다.

 

 

[Static Files]

  같은 맥락에서 보면 이제 프로그램 확장자 파일이 아닌 css, jpeg 같은 정적인 파일에 대해서도 기존 웹과 같이 url 기반의 디렉토리와 파일이름 경로로 접근하기는 좀 힘들어 지게 됬다. 왜냐면 이젠 웹서버 모듈은 기존 웹과 같이 웹루트 폴더 기준의 트리 구조로 된게 아니라, 기본으로 라우팅이 되는 논리적 레벨의 매핑이기 때문이다. 그러한 논리적 레벨의 매핑을 특정 디렉토리를 기반으로한 물리적(이것도 앞에서 얘기했듯이 넓게보면 가상이긴 하지만) 매핑으로 잠시 변환하는 기능이, static files 이라고 보면 될것 같다. (flask 에서는 dynamic web, static web 이라는 표현으로 설명한다)

 

 

[Rendering Templates]

  예제를 구현하기 필요한 마지막 개념은 Rendering Templates 섹션이다. 사실 template 란건 특정 UI 의 재사용을 위한 개념으로 많이 사용되기는 하는데, flask 에서는 꼭 재사용이 아니더라도 UI 를 표현하기 위해선 하나의 템플릿(UI 를 표현한다는 측면과 재사용 적인 측면의 의미를 동시에 가졌다고 볼 수 있을 듯 싶다)을 사용해야 된다고 생각하면 된다. dynamic web 인 flask 의 어플리케이션 모듈 쪽에서 표현에 사용할 데이터를 준비한 후, 해당 데이터를 지정한 템플릿에 연관해서 처리를 한다. 이러한 구조가 HTML 과 어플리케이션 로직을 분리하기 위해서 라고 볼 수도 있겠지만, 사실 웹 자체의 베이스가 동적이여서 페이지 개념이 없어졌기 때문에, 최종적으로 페이지를 동적 프로그램 코드내에서 생성하게 되면, 이번엔 반대로 프로그램 코드에 UI코드가 섞이게 되어 의미가 없게 되니, 어쩔수 없이 최종으로 UI 를 표시하는 부분을 템플릿이라는 개념으로 다시 떼어낸 것도 같다. 마치 Javascript 와 HTML 을 event 속성이 연결하였듯이, 템플릿 또한 순수한 html 코드는 아니고, 앞의 ASP 의 <% %> 코드와 비슷하게, 전달된 데이터들을 템플릿 사이에 적절히 끼워주는 방식으로 구현된다(해당 작업을 렌더링이라고 표현하는 듯 하다).

 

 

[사전 준비 - Flask 설치]

파이썬 3를 지원하므로 pip 명령어로 설치하면 된다.

c:\Python\flaskweb>pip install flask

Collecting flask
....
Installing collected packages: flask
Successfully installed flask-0.12.2

 

 

  c:\python 폴더에 flaskweb 폴더(이 폴더 이름은 다른 아무 이름이나 된다)를 만든다. 다시 flaskweb 폴더내에 templates 폴더(이 폴더 이름은 약속된 이름이기 때문에 꼭 이 이름으로 만들어야 된다)를 만든다.

 

 

 

 

[DB 에서 데이터 불러와 HTML 테이블로 보여주기]

  첫번째 예제로 4교시에서 만들었던 예제를 응용해, MSSQL Server 에서 데이터를 불러 HTML 테이블로 출력하는 예제를 보이려고 한다. 여기에서 기본적인 routing, template 를 다루는 코드가 나오니 예제로 개념을 익히면 된다. 18교시와 마찬가지로 4교시에 만든 supermarket 테이블과 python 코드를 그대로 재사용 하려고 한다(4교시를 안해보셔서 환경이 없는 분들은 MSSQL Server 설치와 테이블 생성을 하시고 오셔야 한다).

 

 

[App Code 구현]

  해당 방식 구현을 위해 구글에서 'flask db to html table' 이라고 검색해서, 아래 3개의 페이지를 얻었다.

[DB 데이터를 템플릿에 넘기는 방식을 볼수 있음 - 템플릿 출력 부분 코드가 명확히 표현되 있진 않다.] 

https://stackoverflow.com/questions/29525758/data-from-sqlite-to-an-html-table-in-a-flask-page

[템플릿 출력 하는 코드가 명확히 나옴]
https://stackoverflow.com/questions/42040379/creating-an-html-table-with-database-values-in-flask

[메인 페이지에 SQL 초기화와 조회 관련 코드를 어떻게 배치할까에 대한 힌트]
https://stackoverflow.com/questions/38540256/flask-python-mysql-how-to-pass-selected-data-though-a-for-loop-and-return-it

 

 

  앞의 사용자 메뉴얼의 튜토리얼을 읽어 전체적인 분위기를 파악 후, 아래 3개의 코드들을 참조하고, 4교시에 만들어 놓았던 파이썬 코드를 결합시킨 최종 코드는 아래와 같다. 간단히 설명하면, flask 는 웹서버와 웹어플리케이션 기능을 같이 실행하는데(운영 단계에선 아파치 등과 연계하는게 맞을 듯은 싶다), 127.0.0.1 의 포트 5000번으로 서비스가 된다. 'sqltable' 경로가 호출되어 'showsql()' 함수가 시작이 되면, 지정된 SQL 문을 실행하여, templates 폴더에 있는(flask 의 몇 안되는 미리 약속되 있는 폴더이다) 'myweb.html' 파일과 DB 에서 가져온 전체 결과값(fetchall)을 지닌 'rows' 변수를 (아마 list)를 이용해서 렌더링 하여(rendering) 결과를 표시한다. 아래 코드는 결과를 표현해주는 템플릿 코드가 아직 만들어지지 않았기 때문에 아직은 반쪽의 코드라고 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask
from flask import render_template
import pymssql
 
# MSSQL 연결 하기 
conn = pymssql.connect(server='localhost', user='pyuser', password='test1234', database='mytest')
cursor = conn.cursor()
 
# flask web 실행
app = Flask(__name__)
 
# 'sqltable' 이라는 URL 인자를 'showsql' 이라는 함수로 연결한다. 
@app.route("/sqltable")
def showsql():
    # SQL 문을 실행하여 supermarket 테이블에서 데이터를 가져온다.
    cursor.execute('SELECT Itemno, Category, FoodName, Company, Price FROM supermarket(nolock);')
    # 가져온 모든 데이터를 mytable.html 파일과 함께 랜더링 하여 표현한다.
    return render_template('myweb.html', rows = cursor.fetchall())
 
# 이 웹서버는 127.0.0.1 주소를 가지면 포트 5000번에 동작하며, 에러를 자세히 표시한다. 
if __name__ == "__main__":
    app.run(host='127.0.0.1',port=5000,debug=True)
cs

 

  위의 코드를 메모장에 넣고, 파일형식을 '모든파일', 인코딩을 'utf-8' 로 선택하여, c:\python\flaskweb 폴더에 myweb.py 로 저장한다.

 

 

[Templates Code 구현]

  다음은 myweb.html(이름은 호출하는 .py 파일의 이름과 달라도 무방하다) 템플릿 파일이다. 뭔가 전 시간의 ASP 코드 흐름과 비슷하지 않나 싶다. '<%' 대신 '{%' 로 파이썬 코드임을 표시하고, 안의 문법이 'vbscript' 대신 '파이썬' 문법인 차이같다.  사용자 메뉴얼을 보면 해당 템플릿을 표현하는 방식은 외부 모듈인 'Jinja2' 라는 템플릿 모듈을 차용했다라고 나온다. 대충 로직을 살펴 보면 테이블 외형을 뿌려주고('<table>태그') SQL 조회 결과를 한 줄씩 루프를 돌면서(for row in rows), '<tr>' 태그를 뿌려주고, 그 안에서 다시 해당 줄의 컬럼들을 선택하며 돌면서(for data in row), '<td>' 태그안에 그 값('{{data}}')을 넣어준다. 해당 부분은 ASP 코드랑도 비슷하고, 예전의 7교시 엑셀 시간에 배웠던 엑셀 파일로의 출력과도 비슷한 루프 구조를 가진다.

1
2
3
4
5
6
7
8
9
<table border="1" cellpadding="5" cellspacing="5">
{% for row in rows %}
    <tr>
    {% for data in row %}
        <td>{{ data }}</td>
    {% endfor %}
    </tr>
{% endfor %}
</table>
cs

 

  해당 코드를 역시 메모장에 복사하여, c:\python\flaskweb\templates 폴더에 myweb.html 로 저장한다(html 에 한글을 넣으려면 utf-8 인코딩으로 저장해야 에러가 안난다.)

 

 

  그럼 모든 코드가 구현되었고 c:\python\flaskweb\ 으로 이동하여, 아래와 같이 myweb.py 파일을 실행 한다.

c:\Python\flaskweb>python myweb.py
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 288-594-455
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit) 

 

  이후 브라우저를 열어 http://127.0.0.1:5000/sqltable 이라고 치면, myweb.py 상에서 sqltable 에 해당하는 showsql() 함수를 실행하여(일부러 독립된 요소라는 것을 보여주기 위해 이름들을 다 다르게 했다), 조회한 데이터를 templates 폴더에 있는 myweb.html 와 함께 랜더링 하여 아래와 같이 html 테이블을 보여준다(지금은 자연스럽게 보이지만 여기까지 올때까지의 몇 번의 시행착오 과정은 생략했다). 이 예제를 통해 기존 파이썬 로직들이 flask 라는 웹 서버 겸, 어플리케이션 모듈 프레임워크를 통해서 웹형식으로 표현되게된 흐름(flow)을 캐치하셨음 한다.

 

 

 

 

[D3.js 에서 Json 데이터 URL 를 호출해 웹으로 그래프 보여주기]

  D3.js 는 앞에서도 얘기했지만, 자바스크립트 쪽의 matplotlib 같은 시각화 라이브러리이다. 인기가 많은 편인 듯 해서, 디자인 강화에 중점을 둔 c3.js 등의 d3.js 기반의 라이브러리들도 있는 듯 하다. 해당 라이브러리의 컨셉은 목적상 matplotlib 과 역시 비슷하다(사실 모든 시각화 라이브러리가 비슷 한듯 하다). csv 등의 파일이나, 데이터를 반환하는 api 형태의 url 부터 json 데이터를 가져와서, 데이터 형을 잘 맞춰서, 원하는 그래프를 그려주는 라이브러리 함수에 공급한다. 그럼 보통 svg 형식으로 그래프를 그려(브라우저에서 HTML 문서안에 백터 그림을 나타내는 표준으로, svg와 canvas 두 가지 표준이 있다) 브라우저에 표시해 준다.

 

 

[App Code 구현]

  해당 페이지를 만들어 보기 위해 구글에서 'flask d3', 'd3 simple example', 'd3 simple date' 를 조회해서 아래의 3개의 페이지를 참고했다.

[전체적인 개념]
https://github.com/dfm/flask-d3-hello-world

[실제 동작 하는 코드]
http://bl.ocks.org/d3noob/b3ff6ae1c120eea654b5

[xxxx-xx-xx 형식의 날짜 데이터를 D3 에서 파싱하기 위해서]
https://stackoverflow.com/questions/13654609/draw-d3-simple-line-chart-with-an-array

 

 

  해당 페이지들의 예제들은 데이터 생성 로직이 조금 복잡하여, 주제에 집중하기 위해 17교시 머신러닝 예제 만들때 처럼 아래의 간단한 numpy 데이터를 임의로 만들었다.

1
2
3
   # 데이터 지정
    x = np.array(['2017-07-10''2017-07-11''2017-07-12''2017-07-13''2017-07-14'])
    y = np.array([58.1353.9867.0089.7099.00])
cs

 

 

  위의 예제들을 조합해서 정리한 프로그램쪽 코드는 아래와 같다. routing 경로가 2개로 늘어났다는 점만 제외하면, 나머진 다 앞에서 다루어 봤던 코드들이다. 조금 낯설은 코드는 json.dumps 명령어를 이용해서, json 데이터를 만들어 내는 부분이다.

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
import json
import flask
import numpy as np
 
app = flask.Flask(__name__)
 
# d3sample 을 호출했을때의 템플릿 설정
@app.route("/d3sample")
def showsample():
    return flask.render_template("d3sample.html")
 
# D3에서 가져갈 data url을 호출하면 반환할 json 데이터 만들어 내기 
@app.route("/data")
def data():
     
    # 데이터 지정
    x = np.array(['2017-07-10''2017-07-11''2017-07-12''2017-07-13''2017-07-14'])
    y = np.array([58.1353.9867.0089.7099.00])
 
    # 리스트를 json 데이터로 변환
    return json.dumps([{"date": x[i], "close": y[i]}
        for i in range(5)])
 
# 앞과 비슷한데 조금 틀려만 보임
if __name__ == "__main__":
    port = 5000
    app.debug = True
    app.run(port=port)
cs

 

  위의 코드를 메모장에 넣고, 파일형식은 '모든파일', 인코딩은 'utf-8' 로 선택하여, c:\python\flaskweb 폴더에 myweb_d3.py 로 저장한다(json.dumps 코드의 결과가 궁금 하시면 'http://127.0.0.1/data' 를 브라우저에서 호출해 본다).

 

 

[Templates Code 구현]

  그 다음은 랜더링에 사용할 d3samlpe.html 템플릿 파일이다. 안의 코드는 html 코드 보다는 d3.js 라이브러리를 사용하기 위한 자바스크립트 코드로 가득 차있다. 코드의 흐름을 보면 맨 아래 'd3.json("/data", callback)' 함수에서 'http://127.0.0.1/data' 경로를 호출하여 'json 형태의 데이터'를 얻어와서, 'callback' 함수에 넘겨준다. 'callback' 함수에서는 넘어온 데이터를 'd3.js 라이브러리' 함수에 입력하여 'svg 그래프'를 그린다. 상세한 코드들은 혹시 해당 라이브러리를 이용할 일이 있음(어차피 그래프 종류가 많아 각각 쓰임을 이해해야 한다) 이해하면 되고 여기선 flask 를 설명하는 목적이니 대충 주석과 흐름만 보자.

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
<!DOCTYPE html>
<meta charset="utf-8">
<style> <!-- 그래프 요소들의 스타일 지정 -->
body { font: 12px Arial;}
path { 
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
}
.axis path,
.axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
}
 
</style>
<body>
<!-- 라이브러리 로딩. 내부에서 돌리려면 다운받아서 static 폴더에서 읽어와야 할듯 -->    
<script src="http://d3js.org/d3.v3.min.js"></script>
 
<script>
 
// 그래프 좌표 공간 설정
var margin = {top: 30, right: 20, bottom: 30, left: 50},
    width = 600 - margin.left - margin.right,
    height = 270 - margin.top - margin.bottom;
 
// 그래프 범위
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
 
// 축 정의
var xAxis = d3.svg.axis().scale(x)
    .orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
    .orient("left").ticks(5);
 
// 그래프 선 정의
var valueline = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });
    
// 캔버스 객체 생성
var svg = d3.select("body")
    .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
    .append("g")
        .attr("transform"
              "translate(" + margin.left + "," + margin.top + ")");
 
// 2017-07-01 식으로 데이터를 해석하게 지정함
var parseDate = d3.time.format("%Y-%m-%d").parse;
 
// 전달 받은 데이터를 이용해서 그래프를 그린다.
var callback = function (data) {
    data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.close = +d.close;
    });
 
    // 실데이터에 맞춰 그래프 범위 지정
    x.domain(d3.extent(data, function(d) { return d.date; }));
    y.domain([0, d3.max(data, function(d) { return d.close; })]);
 
    // 선 그리기.
    svg.append("path")
        .attr("class""line")
        .attr("d", valueline(data));
 
    // x축 그리기?
    svg.append("g")
        .attr("class""x axis")
        .attr("transform""translate(0," + height + ")")
        .call(xAxis);
 
    // y 축 그리기?
    svg.append("g")
        .attr("class""y axis")
        .call(yAxis);
 
};
 
// flask 에서 만든 http://127.0.0.1/data 를 호출하여 json 데이터를 가져와 callback 함수 호출
d3.json("/data", callback);
 
</script>
</body>
cs

 

  해당 코드를 역시 메모장에 복사하여, c:\python\flaskweb\templates 폴더에 utf-8 인코딩으로 d3sample.html 로 저장한다(한글 주석이 있어 utf-8 인코딩이 아니면 rendering 과정에서 에러가 난다).

 

 

  그럼 모든 코드가 구현되었고 c:\python\flaskweb\ 으로 이동하여, 아래와 같이 myweb_d3.py 파일을 실행 한다.

c:\Python\flaskweb>python myweb_d3.py
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 288-594-455
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

  이후 브라우저를 열어 http://127.0.0.1:5000/d3sample 이라고 치면, myweb_d3.py 상에서 d3sample 에 해당하는 showsample() 함수를 실행하여, d3sample.html 템플릿을 호출하면, 해당 템플릿 내에서 http://127.0.0.1:5000/data URL 을 호출하여, json 데이터를 받아서 D3.js 라이브러리를 이용해서 브라우저 화면에 아래와 같이 그래프를 출력하게 해준다(뭐 HTML 페이지에서 자바스크립트 d3.js 라이브러리를 이용해서 그래프를 그리는 것도 rendering 이라고 표현해도 된다).

 

 

  앞에서 설명한 방식으로 실제 동작하는지 확인하기 위해 이전 시간에 배운 피들러를 띄워 페이지 호출을 관찰해 보면, 아래와 같이 /d3sample 과 /data 가 차례로 호출되는 것을 볼 수 있다. 앞의 /d3sample 은 브라우저 주소창에서, 뒤의 /data 는 템플릿 랜더링 과정에서 호출한 것이다.

 

 

 

 

[matplotlib 그래프를 웹 페이지에 보여주기]

  마지막 예제는 위와 비슷한 데이터를 matplotlib 으로 그리고, HTML 페이지안에 해당 그림을 <img> 태그 형태로 삽입하는 예제이다.

 

 

[App Code 구현]

  예제를 구현하기 위해 구글에서 몇가지 샘플을 실행해 봤는데, python 2.x 대의 예제라서 라이브러리가 안 맞아 안 돌아 가거나, 3.x 대 예제인데, 실제로 에러는 안 나는데 이미지는 안 나오는(엑박표시) 경우가 많았었다. 몇 개의 예제를 검토해 본 바로는, 보통 두 가지 방식으로 구현 되는데, 첫째는 그려진 이미지를 static 폴더에 img 파일로 실제 물리적으로 저장한 후, html 템플릿 페이지를 띄워, 해당 이미지를 html 템플릿 내에 static 형식으로 포함하는 방법이 있고, 둘째는 <img> 태그 경로에 이미지를 생성하는 flask url 을 지정하여, 이미지를 마임(MIME) 데이터로 전달받아 브라우저에서 표시해 주는 방식이 있다. 여튼 결과적으로 'python 3 flask matplotlib html' 라고 검색해서 아래 2번째 방식으로 구현하는 예제를 찾았다.

http://dataviztalk.blogspot.kr/2016/01/serving-matplotlib-plot-that-follows.html

 

 

  해당 코드를 기반으로 데이터 생성만 간략화한 버전이 아래와 같다.

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
from io import BytesIO
from flask import Flask, render_template, send_file, make_response
import flask
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
import numpy as np
import matplotlib.pyplot as plt
 
app = flask.Flask(__name__)
 
# mypic 을 호출하면 mypic.html 로 렌더링 한다.
@app.route('/mypic')
def mypic():
    return flask.render_template("mypic.html")
 
# matplotlib 그래프 파일을 생성하여 소켓 통신으로 보내준다. 
@app.route('/plot')
def plot():
 
   # 그림판 준비
    fig, axis = plt.subplots(1)
    
   # 데이터 준비
    y = [1,2,3,4,5]
    x = [0,2,1,3,4]
 
    # 그리기
    axis.plot(x,y)
    canvas = FigureCanvas(fig)
    
    # 그려진 img 파일 내용을 html 랜더링 쪽에 전송한다.
    img = BytesIO()
    fig.savefig(img)
    img.seek(0)
    return send_file(img, mimetype='image/png')
 
 
if __name__ == '__main__':
    port = 5000
    app.debug = True
    app.run(port=port)
cs

 

  위의 코드를 메모장에 넣고, 파일형식은 '모든파일', 인코딩은 'utf-8' 로 선택하여, c:\python\flaskweb 폴더에 myweb_mat.py 로 저장한다.

 

 

[Templates Code 구현]

  그 다음은 mypic.html 템플릿 파일이다. 내부 코드는 엄청 간단해서 이미지 태그를 만들면서, 이미지 소스(src) 위치를 '/plot' 으로 지정한다. 그럼 http://127.0.0.1:5000/plot 을 읽어오며 실행되게 되어 해당 데이터가 MIME 데이터로 html 쪽에 전달되어 결합된다(사실 이런 방식은 처음 보는 거라서 좀 신기하긴 하다).

1
2
3
4
5
6
7
8
9
10
<html>
  <head>
    <title>image</title>
  </head>
  <body>
    matplotlib 으로부터 만들어진 이미지
    <p>
    <img src="/plot" alt="Image Placeholder">
  </body>
</html>
cs

 

  해당 코드를 역시 메모장에 복사하여, c:\python\flaskweb\templates 폴더에 utf-8 인코딩으로 mypic.html 로 저장한다.

 

 

  그럼 모든 코드가 구현되었고 c:\python\flaskweb\ 으로 이동하여, 아래와 같이 myweb_mat.py 파일을 실행 한다.

c:\Python\flaskweb>python myweb_mat.py
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 288-594-455
 * Running on
http://127.0.0.1:5000/ (Press CTRL+C to quit)

 

 

  이후 브라우저를 열어 'http://127.0.0.1:5000/mypic' 이라고 치면, myweb_mat.py 상에서 'mypic' url 에 해당하는 'mypic()' 함수를 실행하여, 'mypic.html' 템플릿을 호출하면, 해당 템플릿 내에서 'http://127.0.0.1:5000/plot' 경로를 호출하여, MIME 데이터로 이미지 스트림을 받아서 그림을 표시해 준다. 

 

 

 

 

 

[마무리 하면서]

  이렇게 해서 flask 프레임워크에 대해 간단히 살펴보는 시간이 끝났다. 'routing', 'dyanamic vs static web(static files)', 'rendering templetes' 가 주요 키워드인것 같다. 이전 시간에 legacy web 을 길게 설명하고, 이 시간에 flask 와 legacy web 을 비교하면서 얘기했던 방식이, 쉬운 이해에 도움이 되면 좋겠다. 여러 파이썬 웹 프레임워크에 대한 비교 글들의 끝에서 언급되지만 프레임워크의 선택은 개인의 일하는 스타일의 취향과, 구현하려는 목표에 해당 프레임워크가 추구하는 방향이 얼마나 적합한가에 따라 달라질 것이다. 실제 구현하려다 보면, 전체 구현하려는 대상에 비교해 프레임워크가 지원해 주는 기능은 정말 꼭 필요한 최소한의 기능 밖에 없는 것도 같긴 하다(물론 essential 한 부분이긴 하지만...). 

 

   추가로 d3.js 를 연계한 예제에서 봤듯이 웹의 많은 기능이 자바스크립트 기반에서 움직이기 때문에, 파이썬 로직으로 모든걸 해결하려는 것보다는, 파이썬 웹 어플리케이션 모듈 쪽에서는 데이터를 가공하여 제공하고, 실제 웹 쪽 UI 구현은 자바스크립트 라이브러리를 이용해 구현하면 효율이 좋은 경우도 많을 것 같다(웹의 1/3 쯤은 자바스크립트의 세상이고, d3.js 를 이해하기 위해서는 javascript 와 그래픽 라이브러리에 익숙하면 유리하기 때문에, 웹 전체에 연관되는 분야를 공부해 균형을 맞춰 놓는 것도 중요한 듯 하다). 다행인 점 하나는 'd3.js' 예제에서 봤듯이, 파이썬에서 배웠던 'matplotlib' 같은 목적이 비슷한 라이브러리를 사용해본 경험이, 많은 부분에서 비슷하게 적용 된다. 지금 이 시간이 기반이 되어 다음 시간인 Django 프레임워크에 대해서도 적절하게 설명할 수 있게 되길 바라며, Flask 살펴보기 시간을 마친다.  

 

 

 

 

2017.7.21 by 자유로운설탕
cs

 

  

 

 

 

 

 

 

 

 

posted by 자유로운설탕
2017. 7. 2. 19:54 프로그래밍

  이번 시간에는 파이썬 웹 프레임워크인 Flask, Django 를 살펴보기 전에 웹을 구성하는 기초적인 부분들에 대해서 살펴보려고 한다. HTML, CSS, Javascript, Web Server(IIS), Web Language(ASP), Ajax 에 대해 개념을 간단히 설명하고, 간단한 예제를 만들어 시연해 보면서, 일반적인 웹 환경이 어떻게 구성되어 있는지를 살펴보며, 다음 시간에 얘기할 비교적 최신 개념인 MVC(model view controller)나 Url Rewriting(Routing) 설명을 위한 사전 지식을 쌓아 놓으려 한다. 다만 위 하나하나의 분야는 이렇게 블로그 한 챕터에 담기에는 각각 수 권의 책으로 따로 분리해야 할 만큼 넓은 분야라서, 가볍지만 필요한 개념은 이해할 수 있을 정도로 설명을 진행해 보려고 한다. 만약 기존에 ASP, PHP, JSP 등으로 웹 페이지를 만들어 봤던 분들은 이번 장은 대충 넘겨보거나, 생략하여도 될듯 하다.

 

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

 

[들어가면서]

  왜 파이썬 공부 관련 글에서 하나의 챕터를 따로 빼내 HTML, Javascript, CSS 등의 다른 웹 언어와 지금은 유행이지난 스크립트형 웹 프로그래밍 언어인 ASP 를 언급하려 하냐면, 다음 시간을 위해 Flask 와 Django 를 살펴보다보니, 처음 웹을 접하는 사람들이 접근하기는 꽤 어려운 구조라는 생각이 들었기 때문이다. 아마 Django 같은 프레임 워크를 이해하기 위해서는 기존 Legacy Web 에 대한 지식은 기본으로 갖춘 상태에서, Url Rewriting, MVC 같은 비교적 최신의 개념들에 대한 이해가 추가적으로 선행되야 할 듯 싶다. 물론 파이썬도 어느정도 익숙해 졌다고 가정하고 말이다.

 

  다른 웹 프로그래밍 언어 등에서 기존 웹을 어느정도 경험해 본 사람들은 해당 쪽 방식과 접근 방식이 다른 부분 위주로 비교해 가면서 적응하면 되겠지만, 웹 프로그래밍 세상에 처음으로 들어온 사람들은 해당 프레임워크를 배울때 웹의 일반적인 지식들이 한꺼번에 같이 쏟아져 들어오기 때문에, 아마도 뭐가 프레임위크에 대한 얘긴지, 뭐가 일반적인 웹 기술에 대한 얘기인지 혼란에 빠질 듯 싶다. 또 해당 방식이 기존 방식에 대해 어떤 장단점을 가지고 있는 건지를 알지 못하고 맹목적으로 받아들일 수도 있다. 앞의 시간하고 비교하면 웹 자동화 프레임워크인 selenium 을 사용하고 싶은데 웹 동작을 담당하는 HTML 이나 자바스크립트를 이해 못한 상태에서 막연히 배우는 것보다도 더 힘들지 않을까 싶다. 

 

  또한 구조적인 프레임워크는 분명히 여러 장점을 가지고 있는건 맞지만, 초보자의 입장에서 봤을때는 ASP 와 같은 Legacy Web Language은 아무래도 URL 이 바로 웹 페이지 파일 자체와 일치되는 1:1 관계의 직관성을 제공하여 접근하기가 좀더 쉽지 않을까 싶다. 또한 ASP, PHP 같은 스크립트 언어들은 컴파일 과정이 없이 바로 결과를 볼수 있어서 .NET 이나 JAVA 같은 컴파일 형 언어보다는 에러를 쉽게 만나고 수정해 볼수 있다(개인적으로는 수많은 에러를 만나고 해결하는 과정이 프로그램을 배우는데는 아주 중요한 경험이라고 생각한다). 그래서 flask 나 Django 에 대한 컨셉 설명을 용이하게 해보기 위해 앞서 머신러닝 챕터 진행 전에 수학, 그래픽 라이브러리를 소개하여 분리할 수 있는 개념을 떼어낸 것과 비슷한 일을 시도하려고 한다.

 

 

[IIS, ASP 에 대해서]

  참고로 여기서 잠시 다루는 웹 스크립트 언어인 ASP 는 현재 글을 진행하는 환경인 윈도우즈10 홈 버전이면 프로그램 추가/제거를 이용해서, IIS(Internet Information Server-아파치 같은 윈도우즈쪽 MS 웹 서버임)를 설치해서 비교적 간단히 사용이 가능하다. ASP 는 PHP, JSP 와 거의 기능적으로 비슷하다고 봐도 될듯 하다(뭐 두 언어는 계속 발전해 와서 .NET 으로 전략적으로 마이그레이션 하면서 버려진 ASP 와는 갭이 크다고 말하시는 분들도 있겠지만, 개인적으로 생각하기에는, 서로들 좋아 보이는 점들을 한참 차용했기 때문에 일반적인 기능 범위는 비슷하고 문법 측면만 틀리다고 생각한다. 물론 어떻게든 비슷하게 구현을 할수 있다는 얘기지, 더 이상 새로운 문법 구조나 라이브러리가 지원 되지 않기 때문에 난이도가 같다는 것은 아니다).

 

  지금 생각하면 좀 낯설지만 예전엔 위의 3개 언어가 웹프로그래밍 언어의 패권을 다투기 위해 경쟁하던 시대도 있었었다. 지금은 뭐 더 많은 웹프로그래밍 언어들이 경쟁하는 춘추전국시대에 있는듯 하며, 사실 고수준 언어가 많은 부분을 모듈화 해서 관리해 주지만, 어떤 언어를 쓰냐보다는 어떻게 설계하여 쓰느냐가 더 중요한듯도 싶긴한다.

 

  윈도우즈 7의 경우는 그때의 MS 라이센스 정책의 방향 땜에 홈 버전에서는 IIS 설치가 안되고, 프로페셔널 버전에서만 지원되니, 혹 윈도우즈 7 홈 환경으로 강좌를 따라오고 있는 분이라면, 눈으로 코드 흐름만 살펴 보셔야 할듯 싶다. 개념을 설명하기 위해 관련 코드를 만든것이기 때문에 그러셔도 무방하다(의사코드 대신 ASP 를 사용했다고 봐도 좋을듯 싶다)

 

 

 

 

[웹은 어떻게 동작하는가?]

  웹 브라우징은 기본적으로 아래 그림과 같이 브라우저와 웹 서버가 중심이 되어 일어나는 행위이다. 웹서버는 우리가 많이 아는 IIS(ASP, .NET), 아파치(PHP), 톰캣(JSP) 부터 node.js(Javascript), 파이썬 자체 웹서버 등 다양하다. 브라우저 주소창에 웹페이지 주소를 입력하거나, 또는 결제창에서 결제 버튼을 누르거나, 특정 페이지에서 다음 버튼을 누르거나 할때, 브라우저가 웹 서버에 명시적으로 요청을 보낸다. 해당 요청은 패킷이라는 조그만 신호 단위에 담겨서, 네트워크 카드를 통해서, 인터넷 세상으로 나가게 된다. 

 

  인터넷 세상에서는 라우터와 스위치라는 장치를 통해서 해당 되는 주소(정확하게는 DNS 서버를 통해 얻어온 IP)가 가리키는 사이트로 이동되게 된다. 그럼 해당 서버는 그 요청을 받아서, 포트에 대기(listen)하고 있는 있는 웹 서버(예를 들면 아파치) 프로그램에게 전달하게 된다. 해당 웹 서버 프로그램은 해당 요청의 form 요소등에 대해서 프로그래밍 로직을 적용하여 DB의 내용을 조회하거나, 저장하거나 한후 최종 처리 결과를 HTML 형식으로 꾸며 사용자 브라우저에게 다시 보내준다. 사용자 브라우저는 해당 정보를 구조<tag>에 맞게 적절히 해석하여 사용자에게 그래픽 적인 웹 페이지 화면으로 보여준다. 브라우저에서 특정한 옵션을 설정하는 경우 브라우저와 네트워크 카드 사이에서 웹 프록시 형태의 프로그램이 패킷을 중계하는 일도 있는데, 그게 우리가 사용해본 fiddler 같은 HTTP 패킷을 보는 툴의 동작 원리이다.

 

 

 

 

[웹을 구성하는 언어들]

  웹에서 사용되는 언어들은 보통 어느 측면에서 사용되느냐에 따라 클라이언트 언어(브라우저)와 서버(웹 서버 프로그램) 언어로 나눠볼 수 있다. 클라이언트 언어는 HTML, CSS, Javascript, AJAX 같은 언어로 이루어져 있고, 서버 언어는 JAVA, .Net 같이 컴파일이 되어 동작하는 언어와 ASP, PHP, JSP, Python 같은 스크립트 형태(사실 이것도 실시간 컴파일이라고 봐야된다. 그리고 PHP 도 컴파일 해서 사용이 되는 것 같기도 하고, JSP 도 뒷단은 자바 class 파일을 호출하는 경우도 많은 듯 하니 사실은 구분이 조금 묘하긴 하다)로 이루어진 언어로 이루어져 있다(요즘은 Javascript로 동작하는 서버 환경인 Node.js 가 나오는 등 서버와 클라이언트 언어라는 절대적인 구분이 점점 모호해 져가는 듯은 하다. 파이썬도 웹과 시스템 양쪽에서 쓸수 있듯이 말이다). 또 DB쪽 언어인 SQL(Structed Query Lanauage) 언어도 있다. 브라우저는 클라이언트 언어들을 이용해 사용자의 액션에 반응하거나, 그래픽적인 화면 UI을 보여주고, 사용자의 입력들을 받아 form 이나 json 등에 담아서 서버 쪽으로 전달한다. 서버 언어는 전달된 클라이언트의 데이터들을 미리 작성된 프로그램 로직에 맞춰 처리하여, DB에 저장하거나 하며, 이후 클라이언트 언어 형태로 브라우저에게 적절히 응답을 주게된다.

 

 

  HTML(Hyper Text Markup Language) 은 우리가 매일 클릭하는 링크(hyperlink)와, 페이지 구조를 담고 있는 태그(markup)로 이루어진 언어이다. CSS(Cascade Sytle Sheet)는 초기 HTML 로부터 디자인 속성들을 따로 분리해낸 언어라고 볼 수 있다. Javascript는 초창기의 정적인 HTML위에 event 속성과의 협업을 통한 사용자와의 상호작용으로 생명을 불어 넣어주었다고 할수 있으며, HTML을 개념적으로 구조화한 DOM(Document Object Model) 객체를 이용하여 HTML 요소들을 조작한다. 우리가 웹에서 보는 모든 동적인 동작들이 Javascript 의 출현 덕분에 일어난다고 보면 되며, 파이썬과 비슷할 정도로 다재다능하고 복잡한 언어이며, Node.js 의 출현 덕분에 서버 쪽 언어로도 사용되게 됬다. Ajax(Asynchronous JavaScript and XML)는 자바스크립트로 만들어진, 멈춰진 HTML 페이지 뒤에서 리퀘스트를 날릴 수 있는 라이브러리 묶음이라고 생각하면 될것 같고, HTML 의 <form> 을 이용하지 않고도 json, xml, text 등의 데이터 형태를 이용하여 브라우저 뒤에서 비동기적으로 통신하는 것을 지원한다.

 

  서버쪽의 JAVA나 .NET 등의 컴파일 언어의 경우 사용전 빌드 과정이 꼭 필요하고, 초기 공통 바이너리 로딩 등에 부하가 걸린다고 하지만, 일반적으로 초기 로드 동작이 끝나면 스크립트 형식의 언어보다는, 메모리를 이용해 좀 더 자원을 효율적으로 공유한다고 한다. SQL(Structed Query Language) 은 MSSQL, Oracle, MySQL 등에 쿼리를 날리는 공통 표준으로 실제로는 SQL서버 종류별로 문법이 조금씩 차이는 있으며, 사용자의 요청에 따라 서버 쪽 프로그램에서 데이터를 조회하거나 저장하는 데 사용한다.

 

 

 

 

[HTML 살펴보기]

  HTML 은 아래의 그림 처럼, 하이퍼링크와 태그로 이루어진 언어이다. Markup 은 문서의 활자나 구조를 잡아주는 것을 얘기하는데, HTML 에서도 비슷하게 구조를 잡아주는 요소의 의미를 가지게 된다. HTML 은 밑의 로봇 그림처럼 헤더(header)와 바디(body)라는 것을 가지게 되는데(HTML5 에는 푸터-footer도 있긴 하던데, 어찌 봄 전체 구분 구조자체가 많이 바뀌었으니 여기서는 무시하자), 헤더에는 문서에 대한 여러가지 배경정보(제목, 작성자 등)들이 들어가고, 바디안에 우리가 실제 브라우저에서 보는 화면들이 들어간다고 보면 된다.

 

  그림을 보면 <html> 태그안에 <head> 와 <body> 태그 쌍이 있고, 제목(<title>)을 좀 큰 글자(<h1>)로 보여주고, 한칸을 띈후(<p>), '본문' 이라고 적힌 HTML 문서가 브라우저에서 열리면 해당 정의된 대로 화면에 표시되는 것을 볼 수 있다.

 

 

  브라우저가 HTML 을 해석하는 것은 사실 이미지뷰어 프로그램이나 메모장이 하는 일과 비슷하다. 이미지 뷰어 프로그램이 읽은 이미지에 대해 이미지 종류, 압축방식(jpeg, png 등), 좌표와 색정보에 따라서 화면에 뿌려주거나, 메모장이 텍스트 파일내에 있는 문자, 줄바꿈 기호, 탭(우리 눈에는 글자들이 탭으로 구분된 것으로 보이지만, 실제의 텍스트 파일 내부에는 아스키 코드 09 같은 특별한 기호로 사실 정의되어 있다)을 해석해 우리에게 보여주듯, 브라우저가 HTML 형태의 정보를 받으면 헤더, 바디에 있는 여러 태그 정보들을 분석해서, 화면에 우리가 볼 수 있도록 표시해 주는 것이다(이를 HTML 랜더링 이라고 말한다) 

 

 

 

 

  그럼 HTML의 모든 태그를 다 볼순 없으니 대표적인 몇개만 살펴보자

 

[1- TABLE 태그]

  11교시에서 잠시 다루었지만, 테이블은 아래와 같은 기본 구조를 가진다. 가장 바깥은 <table> 태그로 쌓여 있고, <th> 는 맨위에 있는 제목 필드라고 보면 되고(테이블에서는 옵션 태그라 없어도 무방), <tr> 은 엑셀의 row 같이 테이블의 한 행을 나타내고, <td> 는 하나의 입력 칸인 셀(cell)을 얘기한다. 그래서 테이블의 구조는 <table> 태그 안에 줄을 나타내는 <tr> 태그들이 쭉 있고, 각 <tr> 태그 안에 칸을 나타내는 <td> 태그들이 들어가 있는 단순한 구조이다. 근데 이 단순한 구조로 이것저것 다양한 형태의 테이블을 만들거나 페이지의 구조를 잡다보니 은근 분석하기 어려울 정도로 복잡해 질 때도 많다.

 

 

 

  가장 간단한 구조의 테이블 예제는 아래와 같다. <table> 태그가 맨 밖에 있고, 선(border) 굵기가 1 사이즈를 가진다. <th> 태그안에 제목인 '과자'와 '초콜릿'이  들어있고, 줄을 나타내는<tr> 태그가 두개 있는데, 하나에는 '파이, 카카오45%' 가, 나머지 하나에는 '머랭, 카카오100%'  가 각각 <td> 태그안에 나눠 담겨 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<table border=1>
  <tr> 
     <th>과자</th>    
     <th>초콜릿</th>  
  </tr>  
  <tr>    
     <td>파이</td>
     <td>카카오45%</td>
  </tr>  
  <tr>   
     <td>머랭</td>
     <td>카카오100%</td>  
  </tr>
</table>
cs

 

  c:\python\code 폴더에, 파일형식을 '모든 파일'로 선택하고 table.html (또는 table.htm) 이라고 저장한다. 이후 탐색기에서 해당 파일을 더블클릭해 실행하면 브라우저가 뜨면서 아래와 같이 지정한 테이블이 표시된다.

 

 

  테이블에는 아래와 같은 스타일을 나타내는 속성들이 있다(속성 중 주요한 일부만 표시함). HTML을 복잡하게 생각하지 말고, 우리가 많이 쓰는 워드나, 한글 등에서 작성하는 문서를 브라우저가 이해할 수 있게 태그로 표현한다고 생각하면 된다. 글자를 크게하거나, 오른쪽 정렬을 하거나, 표의 색을 정하거나, 셀의 여백 값을 정하거나 하는 부분들을 아래와 같은 태그 내 속성에 넣어 넣어서 해결한다고 생각해 보면, 속성이 이렇게 많은 이유를 이해할 수 있을 것이다.

 

  그럼 일부 속성을 사용해 보자. 속성 이름들은 <table> 같은 하나의 태그에서만 독점해 쓰이는게 아니고, 비슷하게 속성을 정의할 다른 태그들이 있다면 동일한 이름으로 사용된다. 아래에서는 <th> 태그 내에 배경색(bgcolor : 색은 'yellow' 와 같은 예약된 영어이름이나, '# + 16진수 숫자' 을 이용해 R, G, B 로 표현 가능하다)을 입히고, '파이'와 '머랭'가 들어간 셀의 사이즈를 200 pixel로 늘이고, 특히 '파이'가 들어간 셀은 가운데 정렬을 한다.  

1
2
3
4
5
6
7
8
9
10
11
12
13
<table border=1>
  <tr>
     <th bgcolor=#FF22CC>과자</th>    
     <th bgcolor=yellow>초콜릿</th>
  <tr>    
     <td width=200px align = center>파이</td>
     <td>카카오45%</td>
  </tr>  
  <tr>   
     <td width=200px>머랭</td>
     <td>카카오100%</td>  
  </tr>
</table>
cs

 

  위와 마찬가지로 c:\python\code 폴더에 table2.html 에 저장하여, 브라우저로 실행하면 아래와 같다. 

 

 

  이번엔 맨 마지막에 <tr> 행을 하나 추가하며, 내부의 두개의 셀을 합쳐보자. 해당 역활을 해주는 속성이 'colspan'(column span:컬럼 폭)이다. 이것을 2라고 해주면 위의 테이블을 기반으로 해서 2개의 셀을 세어서, 밑에 하나로 합쳐 표시해 준다(처음 테이블을 만들어 이것저것 해보면 colspan, rowspan{위아래합치기} 개념이 복잡한 테이블에서는 조금 헷깔리긴 했었다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<table border=1>  
  <tr>
     <th>과자</th>    
     <th>초콜릿</th>  
  </tr>  
  <tr>    
     <td>파이</td>
     <td>카카오45%</td>
  </tr>  
  <tr>   
     <td>머랭</td>
     <td>카카오100%</td>  
  </tr>
  <tr>   
     <td colspan=2>비고:살찌는거조심</td>
  </tr>
</table>
cs

 

   c:\python\code 폴더에 table3.html 에 저장하여, 브라우저로 실행하면 아래와 같다. 

 

 

 

[2- FONT 태그]

  다음으로 <font> 태그는 아래와 같다. 해당 태그 사이에 들어가 있는 문장의 색, 크기, 폰트 등을 정의 한다(역시 워드의 글자 스타일을 생각해보면 된다).

1
2
3
4
5
6
<html>
  <body>
    <font size="5" color="blue">첫번째 폰트</font>
    <font face="궁서체" color="green">두번째 폰트</font> 
  </body>
</html>
cs

 

  c:\python\code 폴더에 font.html 에 저장하여, 브라우저로 실행하면 아래와 같다. 

 

 

 

[3- Form 태그]

  세 번째 태그인 폼(<form>) 은 사용자가 입력한 데이터를 서버로 전송하기 위한 요소이다. 우리가 검색 페이지에서 검색어를 넣고 '검색하기 버튼'을 누르거나, 여러 결제 옵션을 선택하고 '결제하기 버튼'을 눌렀을때, 우리가 입력하거나 라디오버튼 등으로 선택한 값들을 서버 쪽으로 묶어 전송하는 역활을 하는 태그가 <form> 이다. 서버 쪽으로 데이터를 날릴때는 HTML 전체 데이터가 아니라 이 <form> 안에 담긴 데이터만 날아간다(물론 이 설명 부분은 요즈음에 와서는 json 이나 xml 을 데이터 형식으로 주로 쓰는 AJAX 와 같은 비동기 방식이나, .net의 viewState 같은 새로운 전송 역활을 하는 형식들이 생겨서 예전같이 절대적이진 않는듯 하다).

 

 

  밑의 그림에 나타난것 처럼 폼은 <form> 이라는 태그로 감싸져 있고, 그 안에 여러가지 사용자의 입력을 받는 태그들이 들어가게 된다(이 부분도 윈도우 GUI 화면 요소들을 떠올리면 쉽게 이해갈 것이다. <form>은 윈도우즈의 다이얼로그 박스와 비견 될듯하다). 폼안에 들어가는 태그는 txt 형태의 필드, 입력값을 와일드 카드로 가려주는 password 형태의 필드, 라디오 버튼, 체크 박스, 셀렉트 박스 등의 다양한 요소 들이 있다. 사용자가 type="submit" 으로 속성이 지정된 버튼을 누르게 되면, 폼의 action 속성에 지정된 URL이 호출되며 폼내 정보들이 전달된다. 

 

  아래의 소스는 위의 그림의 소스를 옮겨놓은 것이다.

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
<form name=“basic" action="http://127.0.0.1/demo_form_action.asp" method="get">
이름: <input type="text" name="myname">
<br><br>
아이디: <input type="text" name="id">
<br>
패스워드: <input type="password" name="pwd">
<br><br>
<input type="radio" name="address" value="seoul">서울
<br>
<input type="radio" name="address" value="busan">부산
<br><br>
<input type="checkbox" name="hobby" value="gym">취미는 헬스
<br>
<input type="checkbox" name="hobby" value="book">취미는 독서
<br><br>
한달 용돈 : 
<select name="pocketmoney">
   <option value="100000">10만원</option>
   <option value="200000">20만원</option>
 </select>
<br><br>
<input type="submit" value="전송"> 
 
</form> 
cs

 

  c:\python\code 폴더에 form.html 로 저장하여 브라우저로 띄워보자. '전송'버튼을 누르면 지금은 없는 페이지를 호출 하기 때문에, 에러가 나긴 할 것이다.

 

  그럼 피들러를 이용해 샘플 페이지에서 전송 버튼을 눌렀을때 폼이 어떻게 날아가는지 실제로 봐보도록 해보자(피들러는 10교시 때 설치 및 기초 사용법을 설명했다). 밑의 피들러 그림을 보면 왼쪽 url 항목에 우리가 지정했던 demo_form_action.asp 파일이 있고(상대 경로로 지정했기 때문에 앞의 도메인과 폴더 부분은 127.0.0.1의 루트 폴더 그대로 이다), 파일이름 뒤에 물음표와 함께 form 안에 담겨있는 태그 요소들이 name 속성을 기준으로 'myname=Hello', 'id=freesugar' 식으로 값이 어사인되어 전송되는 것이 보인다(예전에 웹페이지 파싱 시간에 잠시 얘기했지만 폼 및 폼 내부의 태그들은 name 속성을 기준으로 구별된다).

 

 

  조금 더 보충해서 설명하면, 우리가 주소창에 주소를 입력하여 구글 웹사이트에서 특정 웹 폴더내에 있는, 특정 파일(test.html)을 요청하여 가져오는 것처럼, 페이지내에서 submit 버튼을 눌렀을 때는 폼 태그 내의 action 에 정의되어 있는 URL 경로를 호출하면서 form 안에 지정된 값을 모두 모아서 전송을 한다(해당 부분은 브라우저가 알아서 해준다)

 

 

 

[4-EVENT]

  event 속성의 설명은 여기서 진행하진 않고 Javascript 와 뗄수 없는 관계니 뒤쪽 Javascript 섹션에서 설명 하려고 한다.

 

 

 

[HTML 마무리]

  그럼 이런 다양한 HTML 태그들과 속성들은 어떻게 접근해야 될까? 추천 하는 방법은 '헤드 퍼스트 html' 같은 가벼운 책을 한권 읽어보거나(개인적으로 헤드퍼스트 시리즈가 있으면 워밍업 용으로 먼저 본다. 대신 안의 낱말 맞추기나 퀴즈는 시간도 걸리고 쪽지시험 같아서 잘 안 푸는 편이다. 다만 저자가 다 다르기 때문에 시리즈 별로 품질이 차이가 좀 있다). HTML 책에 돈을 들이기 아까운 분은, 아래의 w3school 사이트의 샘플을 보거나, 구글 검색을 통해 필요한 태그를 조금씩 봐도 된다. 어차피 웹 프로그래밍 공부를 하다보면 태그는 계속 찾아 볼수 밖에 없다.

   https://www.w3schools.com/html/default.asp

 

  하지만 아마도 초보자 분은 w3school 나 구글에서 뭘 봐야할지를 모를 것 같기 때문에, 책이나 웹상의 HTML 관련 블로그나 관련 무료 강의를 보기를 추천한다(대신 이 방식은 a b c 로 진행되는 경우가 많아서 따라가다가 지칠 수도 있다). 그리고 무엇보다 중요한 것은 어느정도 알 것 같은 느낌이 들면, 직접 원하는 UI의 웹 페이지를 만들어 보면서 벽에 부딫치고, 해결해 보는 것이 좋다.

 

  HTML 은 브라우저로 모든 소스를 볼수 있기 때문에, 디자인이 좋은 페이지의 궁금한 요소들을 뜯어 보는 것도 좋다(11교시에서 설명한 브라우저 개발자 도구의 '요소보기'는 웹의 보고싶은 부분을 뜯어보는데 아주 편리한 도구이다). 어느 정도 공부를 해서 잘 안다고 생각해도 막상 웹페이지를 만들어 보거나, 현실 웹의 소스를 보기 힘들도록 꼬아놓은 자바스크립트, CSS, HTML 을 보게되면 한숨이 나올때가 있을 것이다. 쉽게 안되는게 아쉽지만 이런 공부는 'no pain, no gain' 이기 때문에 어쩔수 없다.

 

  HTML 은 4.01 표준과 5.0 이 있는데, 5.0 은 정적인 4.01 환경에서 좀더 동적인 웹을 위한 확장 킷이라고 봐도 될것 같다(개인적으로 게임의 확장팩 같다고 생각한다). 그래서 일단 4.01 위주로 공부한 후 5.0 내용을 보는 것이 좀 더 효율적일 것 하다(초보자분이 HTML 공부한다고 HTML5 책을 덜컥 사버리면 아마 맨붕이 올지도 모른다). HTML 태그가 워낙 잡다한게 많기 때문에(MS워드의 잘 안쓰는 잡다한 기능들과 같다고 보면 된다) 먼저 공부를 추천하는 기초 html 요소들을 아래에 정리해 놨으니 참고 하시길 바란다. 개인적으로 아래 정도만 알고 조금 헤메보면  beautifulsoup 같은 웹 라이브러리를 사용해 일반적인 웹페이지의 HTML을 파싱할 정도는 될 거라고 생각한다 . 물론 다음에 언급할 자바스크립트와 CSS는 HTML과 실과 바늘의 관계라고 볼수 있어 크롤링 등을 위해 페이지에 대한 분석을 잘 하고 싶다면 세 가지 언어를 비슷한 레벨로 수준을 맞춰 놓는게 좋다(어찌봄 원래 하나일 걸 3개로 나눴다고 봐도 된다). 거기다 웹프로그래밍 언어까지 얹어 배우게 되면(아마 자연스럽게 DB도 배우게 될테고) 웹 기술에 대한 전체적인 기초 그림이 완성이 된다고 생각한다.

 

[HTML 추천 태그 및 개념]

  • 기본구조용: <head>, <body>, <br>, <table>, <font>, <a>, <b>, <h1>~<h6>, <hr>, <i>, <p>, <title>, <meta>, <ol>, <ul>, <li>,
  • 프레임 태그: <frame>, <frameset>, <iframe>
  • 이미지 관련: <image>, <map>, <area>
  • 미묘한 구조의 확장: <div>, <span>
  • 외부와의 연결: <object>, 폼: <form>, <input>, <textarea>, <select>, <option>
  • 주석: <!-- -->
  • URL, 절대경로, 상대경로
  • (form 에 관련된) get, post 인자 개념
  • (자바스크립트를 배우는 초입인) event 속성

 

 

 

 

[CSS]

  CSS 는 문법으로 세세히 들어가면 무척 복잡해지는거 같긴 하지만, 간단하게 컨셉만 얘기하면, HTML 에서 각 태그의 디자인 속성들을 독립시켜 읽고 쓰거나, 관리하기 편하게 만든 것이라고 생각한다. 이렇게 무언가를 분리시켜 관리가 편하게 만드는 것은 다음에 나올 MVC 나 객체지향 프로그램, 함수 같은 요소의 공통점인것 같다.  아무래도 복잡히 꼬인 실타래 코드 보다는, 정리되고 분리되어 명확한 코드가 파악도 잘 되고 유지보수도 쉬울 테니까 말이다. 

 

 

  예를 들어 아래와 같은 HTML 코드가 있다면, 단순한 기본 모양의 테이블 이겠지만,

1
2
3
4
5
6
7
8
9
10
<table> 
  <tr>     
     <th>과자</th>    
     <th>초콜릿</th>  
  </tr>  
  <tr>    
     <td>파이</td>
     <td rowspan=2>카카오45%</td>
  </tr>  
</table>
cs

 

  아래와 같은 <style> 태그 안에 담긴 CSS 형식으로 <table>, <td>, <th> 의 디자인 속성을 정의한 파일이 있다면,

1
2
3
4
5
6
7
8
9
10
11
<style>
table, td, th
 {
 border:1px solid green;
 }
 th
 {
 background-color:green;
 color:white;
}
</style>
cs

 

  위의 두 개의 서로 다른 코드를(HTML+CSS) 합쳐서, 아래와 같이 하나의 html 파일로 만들면 서로 독립된 HTML 과 CSS 가 같이 연합해 동작하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<style>
table, td, th
 {
 border:1px solid green;
 }
 th
 {
 background-color:green;
 color:white;
}
</style>
 
<table> 
  <tr>     
     <th>과자</th>    
     <th>초콜릿</th>  
  </tr>  
  <tr>    
     <td>파이</td>
     <td rowspan=2>카카오45%</td>
  </tr>  
</table>
cs

 

  c:\python\code 폴더에 css.html 로 저장하여 브라우저로 열어보자. <style> 태그안에 CSS 형식으로 정의한 스타일들이, 공간적으로는 분리되어 있는 HTML 코드에 적용되어 아래와 같이 꾸며진다(물론 이 부분은 브라우저가 소스를 파싱하여 적용해주는 것이다).

 

 

  위와 비슷한 표현을 하는 HTML 코드 형식으로만 이루어진 아래의 코드와 비교해 보면, 디자인 속성의 분리라는 점이 얼마나 코드를 깔끔하게 정리해 주는지 볼수 있다. 만약 복잡한 HTML 페이지의 디자인을 수정 시 아래처럼 각각 태그마다 디자인이 정의된 코드를 수정하는 것보다는, 위의 CSS 스타일로 분리된 코드를 수정하는 편이 좀 더 쉽고, 편리할 것 같지 않은가 싶다. CSS 는 HTML 의 디자인 작업을 개념적으로 분리시키고, 중복 코드를 제거해 쉽고 명확하게 만들어 준 측면이 있는 것 같다(HTML5 에는 아래 대부분의 디자인 속성을 안 쓰고 CSS 스타일을 사용하게 하는듯 하다).

1
2
3
4
5
6
7
8
9
10
<table border="1"> 
  <tr>     
     <th border="1" bgcolor="green"><font color=white>과자</font></th>    
     <th border="1" bgcolor="green"><font color=white>초콜릿</font></th> 
  </tr>  
  <tr>    
     <td border="1">파이</td>
     <td rowspan=2 border="1">카카오45%</td>
  </tr>  
</table>
cs

 

 

[CSS 마무리]

  CSS 에는 많은 디자인 요소들이 있고, class 를 지정하여 특정한 디자인을 선택해 적용하거나, css selector 같은 주제도 있다.  자세한 부분은 관련 블로그나 책을 한권 훝어 보는 걸 권장한다.

 

 

 

 

 

[Javascript]

  자바스크립트를 설명하려면, HTML 파트에서 설명을 뒤로 미뤘던, HTML 과 자바스크립트를 연결 해주는 요소인 '이벤트(event)' 에 대해 설명해야 한다. 이벤트는 아래 그림과 같이 윈도우즈 프로그램을 움직이게 하는 이벤트 개념이, 브라우저 내의 DOM 객체를 대상으로 구현된 것으로 봐도 될듯 한다. 윈도우즈 운영체제에서 사용자의 키보드, 마우스의 움직임이 어떤 프로그램 창의 어떤 사용자 컨트롤에서 발생했는 지에 따라 이벤트를 발생시켜 처리를 한다면, 브라우저 내에서도 사용자들의 여러 키보드, 마우스 액션이 HTML 페이지내 DOM 의 어떤 요소에서 일어났는지에 따라서, 해당되는 이벤트를 일으켜 자바스크립트를 이용해 처리하게 만드는 구조이다. 아래에 종종 볼수 있는 HTML 이벤트들을 정리해봤다.

 

 

  해당 이벤트가 동적인 웹을 구성하는데 어떤 역활을 하는지 처음 보는 분들은 감이 안 잡힐 듯도 싶어서, 대표적인 적용 예들을 밑에 표시했다(구글이 onchange 인지, onkeyup 일지는 잘 모르겠다^^). 밑의 예에서 유추해 보면 웹에서 UI가 사용자 동작에 따라서 반응하는 부분은 대부분 이런 이벤트+자바스크립트의 도움으로 이루어진다고 보면 될 것이다.

 

 

[자바스크립트 예제 1]

  그럼 간단한 자바스크립트 예제를 2개만 보자. 아래의 코드를 간단히 설명하면 하단에 input box가 두개 있고, 박스 내를 클릭하면 'onfocus' 이벤트가 발생 하며, 위쪽 input box 의 이벤트는 배경을 노란색으로 바꾸어주는 setSytle1 자바스크립트 함수에, 아래쪽 input box 는 파란색으로 바꿔주는 setStyle2 함수에 연결되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
 
<head>
  <script>
     function setStyle1(x)
     {
       document.getElementById(x).style.background="yellow";
     }
 
     function setStyle2(x)
     {
       document.getElementById(x).style.background="blue";
     }
  </script>
</head>
 
<body>
 
<p>Color Change</p>
First name: <input type="text" id="fname" onfocus="setStyle1(this.id)"><br>
Last name: <input type="text" id="lname" onfocus="setStyle2(this.id)">
 
</body>
</html>
cs

 

  위의 파일을 c:\python\code 폴더에, colorchange.html 이라고 저장하고, 더블 클릭해 브라우저로 열어본다. 로컬 파일에서 자바스크립트가 돌아가려 하기 때문에, '차단된 콘텐츠 허용' 경고가 브라우저 하단에 뜰텐데, 이 경우는 딱히 위험한건 아니니 클릭해서 허용을 해줘야 자바스크립트 코드가 동작한다.

 

  처음엔 둘다 하얀 입력 박스인데, 각각 클릭하여 포커스를 주고 나면 아래와 같이 색이 바뀌게 된다.

 

 

 

[자바스크립트 예제 2]

  2번째는 조금 더 복잡한 예제를 해보자. 쇼핑몰 들에서 종종 볼수 있는 메뉴에 마우스를 오버하면 해당 하위 메뉴가 뜨는 예제이다. 이번엔 과정을 보여주기 위해 처음부터 전체 코드를 제시 하지 않고 HTML(원래는 css 와 구분되는게 더 낫겠지만), Javascript, event 각각의 코드를 소개하고 이후 합쳐서 동작을 보려고 한다.

 

  먼저 디자인을 나타내는 HTML 코드이다. 애니매이션이 1초에 수십장의 그림을 사람에게 연속으로 보여줘서 실제 움직이는 것처럼 속이는 것처럼, 자바스크립트도 비슷하게 여러 트릭을 통해 사람을 눈을 피해 표현하는 경우들이 많다(개인적으로 별로 우아하게 느껴지는 코드는 아니다). 아래를 보면 맨 위의 id 가 'mainCate' 인 <td> 태그가 상위 메뉴인 '과자' 를 보여주는 셀이고, 그 안을 보면 id 가 'subCateMenu' 인 '파이와 머랭' 정보를 담고 있는 <div> 태그가 하나 들어가 있는데, 속성들을 잘보면 뒤 쪽에 숨김 속성(display:none;)이 있다. 그래서 첨에는 <div> 태그 안에 있는 '파이와 머랭' 은 안보이고, 보는 사람의 눈에는 상위 메뉴인 '과자'만 보이게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<table width="100" border="0" cellpadding="0" cellspacing="0">   
    <tr><td align="left" id="mainCate">
       <div id="subCateMenu" style="width: 260px; position: absolute; margin-left: 120px; border: 
3px solid rgb(100, 200, 100); padding: 10px; z-index: 10000; display: none; background: rgb(255, 255, 255);">
          <table width="100%" border="0" cellspacing="0" cellpadding="0">
              <tr>
                 <td>                          
                       <div style="width:115px; border-bottom:1px solid;"><a href=“test1">파이</a></div>
                       <div style="width:115px; border-bottom:1px solid;"><a href=“test2">머랭</a></div>     
                </td>
              </tr>
          </table>
         </div>
        과자
        </td>
    </tr>
</table>
cs

 

 

  다음으로 동적인 움직임을 구현해 주는 자바스크립트 코드를 보자. showMenu 와 showSubCateMenu가 함수가 있는데, showMenu 가 하위메뉴가 나타날때, 상위메뉴인 '과자'가 들어있는 셀의 색을 바꿔주는 역활을 하고(backgroundColor), showSubCateMenu 가 숨겨놓은

'파이와 머랭'이 들은 <div> 태그를 보여준다(disaplay=""). 나머지 2개의 hide 계열 함수들 view의 반대의 역활을 해서 원래 상태로 돌려주는 역활을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script language="javascript">
<!--
  function showMenu(td){
    td.style.backgroundColor = "#444444";
    td.style.color="#ffffff";
  }
 
  function hideMenu(td){
    td.style.backgroundColor = "#ffffff";
    td.style.color="#555555";
  }
 
  function showSubCateMenu(i) {
    document.getElementById("subCateMenu").style.zIndex = 10000;
    document.getElementById("subCateMenu").style.display = "";
  }
 
  function hideSubCateMenu(i) {
    document.getElementById("subCateMenu").style.display = "none";
  }
 
//-->
</script>
cs

 

 

  여기까지 오더라도, 마지막에 빠진 고리가 있다. 지금으로서는 HTML 하고 자바스크립트가 서로의 존재를 모른다는 것이다. 이것은 앞에서 얘기했던 'event' 요소가 연결해준다. '과자'가 들어있는 <td> 태그 안에 넣을 이벤트들은 아래와 같다. 이렇게 되면 마우스를 '과자' 셀위에 올리면(onmouseover) show 계열 함수들을 실행해 메뉴를 보여주고 색을 바꾸며, '과자' 셀을 벗어나면(onmouseout) hide 메뉴를 사용해서 원복한다.

1
2
onmouseover="javascript:showSubCateMenu(); showMenu(this);" 
onmouseout="javascript:hideSubCateMenu(); hideMenu(this);"
cs

 

 

  그럼 위의 HTML, Javascript, event 세 가지 코드를 결합한 최종 코드는 아래와 같다.

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
<script language="javascript">
<!--
  function showMenu(td){
    td.style.backgroundColor = "#444444";
    td.style.color="#ffffff";
  }
 
  function hideMenu(td){
    td.style.backgroundColor = "#ffffff";
    td.style.color="#555555";
  }
 
  function showSubCateMenu(i) {
    document.getElementById("subCateMenu").style.zIndex = 10000;
    document.getElementById("subCateMenu").style.display = "";
  }
 
  function hideSubCateMenu(i) {
    document.getElementById("subCateMenu").style.display = "none";
  }
 
//-->
</script>
 
 
<table width="100" border="0" cellpadding="0" cellspacing="0">   
    <tr><td align="left" id="mainCate" onmouseover="javascript:showSubCateMenu(); showMenu(this);" 
onmouseout="javascript:hideSubCateMenu(); hideMenu(this);">
       <div id="subCateMenu" style="width: 260px; position: absolute; margin-left: 120px; border: 
3px solid rgb(100, 200, 100); padding: 10px; z-index: 10000; display: none; background: rgb(255, 255, 255);">
          <table width="100%" border="0" cellspacing="0" cellpadding="0">
              <tr>
                 <td>                          
                       <div style="width:115px; border-bottom:1px solid;"><a href=“test1">파이</a></div>
                       <div style="width:115px; border-bottom:1px solid;"><a href=“test2">머랭</a></div>     
                </td>
              </tr>
          </table>
         </div>
        과자
        </td>
    </tr>
</table>
 
cs

 

  c:\python\code 에 menu.html 로 저장한 후, 브라우저로 열어본다. 아래와 같이 '과자' 가 들어간 셀 부분에 마우스를 올리거나 내렸을때 서브 메뉴인 '파이와 머랭'이 모양이 나왔다 사라지는 메뉴 동작을 볼수 있을 것이다.

 

  그리고 위에서 자바스크립트 코드에 나온 document.getElementById("subCateMenu") 같은 부분들은 자바스크립트가 HTML 코드에 접근할때 쓰는 DOM(Document Object Model)의 개념이 들어가 있다. 우리가 앞에서 beautifulsoup, 이나 selenium 을 사용할때 의식은 안 했지만 자연스럽게 해당 개념을 이용했다고 볼수 있다.

 

 

 

[Javascript 마무리]

  그럼 마지막으로 자바스크립트를 공부하려면 어떻게 할까? 개인적으로 자바스크립트는 깊이 들어가게 되면 파이썬하고 비슷한 깊이의 복잡도를 가진다고 본다(서버 쪽으로 눈을 돌리면 node.js 같은 서버 언어로도 사용되고 있고 말이다). 그래서 되도록 처음엔 너무 깊은 쪽으로는 가지 않도록, 쉬운 책이나 블로그를 보면서 개념을 잡은 후, 이후 웹 프로그래밍 공부를 하면서 궁금한 부분을 만났을때마다 구글 등을 찾아서 개념을 이해하는 것을 추천한다. 자바스크립트 라이브러인 jQuery 를 많이 사용하긴 하나, 실제 웹페이지들은 jQuery 와 일반 자바스크립트 두 가지 코드가 공존하고 있기 때문에 jQuery 는 응용편이라고 생각하고 접근하는 게 나을듯 싶다. 

 

  일단 기초가 잡히면 무엇을 모르는지와 공부해야할 방향을 스스로 알수 있게 되고, 집밥 백선생에서 요리하기 전에 재료를 섞음 어떤 맛이 될지 상상해 보라는 말 같이, 무언가를 덥석 구체적으로 습득하는 것도 좋지만, 그 전에 이것을 습득하게 되면 어떻게 될까를 잠시 생각해 보는 것도 나쁘진 않은듯 하다. 공부할수 있는 시간은 한정되어 있기 때문에, 가야될 방향을 정확하게 잡는것도 중요하다. 사실 이 강의의 의미도 구체적인 구현 지식의 전달 보다는 각 주제에 대한 접근방식과 개념을 전달하는 것이라고 생각하고 있다. 

 

 

 

 

 

[Web Server]

  이제 슬슬 후반부로 들어간다. 웹 프로그래밍을 공부하려면, 기본적으로 웹 서버의 존재를 이해해야 한다. 웹서버는 단순하게 얘기하면 특정 포트로 요청이 오기를 기다리고 있는 서비스 프로그램이다. 사용자(또는 다른 프로그램일수도 있고)로부터 요청이 들어오면, 적당한 프로그램적 처리를 한 후, 다시 요청한 쪽에 HTTP 형태로 결과를 돌려준다. 웹 서버는 사실 2개의 모듈로 나뉘어져 있다고 생각하는 게 좋다. HTTP 통신을 받거나 응답해 주는 순수 웹 모듈과, 받은 데이터를 특정한 언어에 기반해서 처리해 주는 프로그래밍 모듈로 구성되어 있다고 볼수 있다.

 

  예를 들어 순수 아파치 웹서버는 PHP 와 HTML 을 처리할 수 있는 반면, 아파치-톰캣은 아파치에 톰캣 프로그래밍 모듈이 얹어져 있어 JSP 파일의 처리가 가능하다(심지어 성능이 얼마나 나올지는 모르겠지만, IIS 에도 PHP 모듈을 설치해 PHP 웹서버로 사용할 수도 있다). 파이썬도 flask 같은 샘플을 보면 샘플 프로그램을 띄울때 웹서버가 같이 실행되어 프레임워크 뒤로 숨겨져 있어서 잘 안보이이긴 하지만, 웹서버 모듈이 있어야지만 사용자의 요청에 응답할 수 있다. flask 나 Django 도 아파치와 연동해서, 클라이언들과 주고받는 처리는 검증된 아파치 서버가 해주고, flask 나 django 는 뒤에서 웹어플리케이션 모듈로서만 동작하게도 할수 있는거 같다(이렇게 보면 모든 웹서버의 꿈이 어떤 언어라도 연결해 중계해 주고 싶은것인가 싶기도 하다).  

 

 

 

[IIS 설치]

  그럼 다음 섹션에서 ASP 를 돌려보기 위해서 윈도우 10에서 기본으로 지원하는 IIS 를 설치해서 샘플페이지를 하나 호출해 보자.

 

  먼저 IIS를 설치해 보자. '윈도우키+x' 를 누른후 옆 쪽에 나타나는 메뉴에서, '프로그램 및 기능' 메뉴를 선택한다(설치 부분을 설명하기 위해 잠시 전 uninstall 을 했지만, 이전 시간에서 소개한 예전 윈도우 스타일로 시작메뉴를 보여주는 'classic shell' 을 깔았을 경우는 '제어판>프로그램>프로그램및 기능' 으로 가면 된다).

 

  왼쪽에서 '윈도우즈 기능 켜기/끄기' 를 선택한다.  

 

  이후 인터넷 정보 서비스에서 1) 'World Wide Web 서비스' 를 체크하고, 2) 하위 메뉴에서 '응용 프로그램 개발기능>ASP' 를 선택한다. 3) 그리고 웹 관리 도구' 도 체크한다(나머진 그냥 디폴트로 두면 된다). 체크가 다 되었음 확인 버튼을 누른다. 잠시 기다림 IIS 가 설치가 된다.

 

 

 

['Hello ASP' 샘플 페이지 실행]

  이제 메모장으로 샘플 ASP 파일을 하나 만들건데, 기본 사용자 권한으로는 IIS 의 웹루트 폴더인 c:\inetpub\wwwroot\ 폴더에 파일을 쓰지 못하게 되어 있기 때문에, 메모장을 관리자 권한으로 실행해야 된다. '윈도우+x' 키를 눌러서, 왼쪽 메뉴에서 '검색'을 선택한다. 검색 창이 나오면 '메모장' 이라고 찾는다. '메모장' 아이콘이 나오면 마우스 오른쪽 버튼을 눌러서 컨텍스트 메뉴를 띄워 '관리자 권한으로 실행' 을 선택한다. (매번 이게 귀찮다면 wwwroot 폴더 '속성'의 '보안탭'에서 현재 로그인한 사용자에게 '모든 권한'을 주면 파일 저장이 가능해진다) 

 

  이후 메모장에 아래와 같이 입력한다.

1
2
3
<%
   Response.Write "Hello ASP"
%>
cs

 

  해당 내용을 c:\inetpub\wwwroot 폴더에 파일형식을 '모든 파일'로 하여 'test.asp' 로 저장한다. 이후 브라우저를 띄워 주소창에 http://localhost/test.asp 를 입력한다(localhost 나 127.0.0.1 은 현재 컴퓨터의 주소를 나타낸다). 그럼 아래와 같이 ASP 로 만든 간단한 문서가 IIS의 ASP 모듈에게 해석되어 브라우저에게 전달되어 화면에 보이게 된다.

 

 

 

 

[ASP 로 DB 조회하여 HTML 테이블로 출력하는 샘플 만들기]

  그러면 ASP 웹페이지를 하나 만들어 보자. 데이터베이스의 테이블을 조회 해서 결과를 화면에 HTML 테이블 형태로 뿌려주는 페이지를 만드려고 한다. 여러모로 시간을 아끼기 위해서^^; 4교시때 설치 및 세팅해 놓았던 SQL Server 와 그때 만들어 놓은 supermarket 테이블을 그대로 이용한다(4교시를 안보신 분들은 4교시에 소개된 MSSQL Server 설치 및 테이블 생성을 하시던지, 귀찮으시면 의사코드라 생각하시고 눈으로만 보고 동작을 이해하셔도 된다).

 

  4교시에 만든 테이블은 아래와 같은 테이블 이였다. 4 or 7교시 때 해당 테이블의 내용을 가져오는 파이썬 코드를 만들어 봤었는데, 그때 로직을 기억을 더듬어 보면 1) 먼저 DB 정보를 입력해 연결하고, 2) 각 행을 루프를 돌며 하나씩 읽어오면서, 3) 화면에 프린트를 하거나 엑셀로 저장했다. ASP 코드도 사실 이와 엄청 비슷한 흐름을 가진다(어차피 비슷한 스크립트 언어이기도 하고, 언어들은 서로 영향을 받아 유사한 경향이 있기 때문이다. 코드를 비교해 보시면 파이썬 코드쪽이 좀더 현대적이여서 흐름이 세련된 느낌을 받을 것이다). 작성할 ASP 코드에서는 1) DB 정보를 입력해 연결하고 2) 각 행을 하나씩 읽어 오면서, 3) HTML 테이블을 출력하면서 <td> 태그안에 해당되는 컬럼을 하나씩 넣어준다. 

 

 

 

[ASP 샘플 코드 만들어 보기]

  그럼 ASP 를 공부하는 시간은 아니기 때문에 만들어진 예제를 바로 보자. 원래는 웹프로그래밍 예제라고 하면, 사용자가 입력한 내용을 폼으로 전송해, 해당 값을 서버에서 받아 SQL 조건에 조합해 넣어 특정 itemno 의 상품을 조회해 오거나 하는 등의 예제가 좀더 현실적이겠지만, 단순함과, 이전에 만든 파이썬 코드와 비교할 수 있도록 하기 위해 전달 받는 인자가 없이 테이블의 모든 정보를 조회해 오는 방식의 예제를 만들었다.

 

  ASP, PHP, JSP 같은 스크립트 코드를 처음 보시면 낯설수도 있겠지만, ASP 에서는 <% %> 안에 든 내용이 VBscript 문법의 순수 ASP 코드이고, 코드를 보면 거의 파이썬으로 구현했던 예제와 비슷하게 진행이 된다. ASP 를 배울 것은 아니니 코드의 상세 문법을 보지 말고 주석 위주로 흐름만 보길 바란다(참고로 ASP 는 대소문자를 안가린다).

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
<%@ Language=VBScript %>
 
<%
   ' 연결 문자열 정의
   strMyTest = "Provider=SQLOLEDB; Data Source=localhost; Initial Catalog=mytest; User Id=pyuser; Password=test1234"
   Set objConn = Server.CreateObject("ADODB.Connection")
   objConn.Open strMyTest 
 
   ' 실행할 SQL 정의
   strSQL = "select itemno, category, foodname, company, price from supermarket s(nolock)"
 
    ' 쿼리 실행 하여 결과 얻어옴
   Set rtnRow = objConn.Execute(strSQL)
%>
 
 
<html>
   <head>
      <title>supermarket</title>
   </head>
 
   <body>       
      <p>supermarket 상품</p>
      <table border=1>
         <tr>
            <td>번호</td>
            <td>카테고리</td>
            <td>종류</td>
            <td>상품이름</td>
            <td>가격</td>       
         </tr>    
 
<%
   'DB 에서 조회한 행이 끝이 아니라면 루프를 돌리면서 각 컬럼을 <td> 태그안에 끼워 넣는다.
   Do while Not rtnRow.EOF 
%>
 
         <tr>
            <td><%=rtnRow("itemno")%></td>
            <td><%=rtnRow("category")%></td>
            <td><%=rtnRow("foodname")%></td>
            <td><%=rtnRow("company")%></td>
            <td><%=rtnRow("price")%></td>
         </tr>
                        
<%
   'rsList의 내용을 다음 결과 행으로 이동하며 Do 문을 반복한다.
   rtnRow.MoveNext
   Loop
%>
 
      </table>
</body>          
 
<%
   '열었던 연결 닫기
   objConn.Close
   Set objConn=Nothing
%>
cs

 

  그럼 위의 내용을 샘플파일과 비슷하게 관리자 계정으로 실행한 메모장에 붙여넣기 하여 c:\inetpub\wwwroot 폴더에 'supermarket.asp' 이름으로 저장을 하자. 이후 브라우저 주소창에서 http://localhost/supermaket.asp 을 호출한다. 그럼 아래와 같이 DB에서 supermarket 데이터를 가져와서 HTML 테이블로 정리하여 보여주는 페이지가 나오게 된다. ASP 가 처음이신 분은 처음 파이썬으로 DB 를 조회했을 때처럼 조금은 신기한 느낌을 받지 않을까 싶다.

 

 

  참고 1:  여기서 잠시 위의 코드의 구조를 보면, 우리가 배운 HTML 과 ASP 의 프로그램 코드가 하나의 파일에 섞여 있어서, 보기 조금 힘든 것 같지 않는가 싶다(뭐 익숙해짐 편한면도 있긴하다). 나중에 Django, JAVA 나 .NET 같은 언어에서 사용하는 MVC 개념을 통해, 마치 CSS를 통해 디자인 코드를 분리해 낸 것처럼, 이런 레거시 웹 프로그램에 섞여있는 프로그램 코드와 HTML 코드를 개별 요소로 분리해 다루려는 시도를 하게 된다.

 

  참고 2: 현재 기본 설정으로는 에러가 발생시 어떤 에러가 났는지 상세하게 보여주지 않는다. SQL 에러같은 상세한 에러를 보기를 원하는 경우는 아래의 웹사이트를 참고하여 세팅한다.

http://ooz.co.kr/172

 

  참고 3: 그리고 하나 더 노파심에 얘기할 것은, 위의 예제 코드는 소스를 간단히 만들기 위해서, SQL Injection(DB에 공격자가 임의의 쿼리를 삽입할 수 있는 웹취약점) 에 노출되어 있다. 저 방식으로 페이지를 만들어서는 안된다^^; 궁금하신 분은 구글에 'sql injection defense asp' 라고 찾아보심 된다. 요점은 stored procedure 같이 preparedstatement 타입으로 쿼리를 호출하고, 입력 데이터들을 validation 하고, 어플리케이션 DB계정의 권한을 최소화 하고 등등 이다. 현대 웹이 좋은 이유 중 하나는 이러한 패턴을 프레임워크 자체에서 막아주는 경우가 많다.

 

 

 

 

[Ajax]

  그럼 오늘의 마지막 주제인 Ajax에 대해 얘기해 보자. 개인적으로 모든 현대의 세련되고 편리한 웹페이지들은 이 Ajax 의 활약으로 이루어진게 아닌가 싶다. 구글의 검색어 추천이나, 여러 사이트 들의 사용자의 액션에 반응하는 부드러운 UI 의 이면에는, 웹 브라우저의 뒤에서 열심히 데이터를 요청해 나르고 있는 Ajax 가 존재한다. Ajax(Asynchronous JavaScript and XML) 의 비동기적이라는 의미는, 아마도 기존 웹 페이지 간의 명시적인 호출을 동기적이라고 가정했을때, 페이지가 정지해 있는 상태에서 뒷단에서 Ajax 라이브러리를 이용해 데이터를 교환하는 행위를 비동기적이라고 바라보는 관점인 것 같다. Ajax 는 자바스크립로 구현된 라이브러리로 단순하게 봐도 괜찮을 듯 싶다.

 

 

  Ajax 의 간단한 예는 아래 그림의 구글 검색어 추천 기능이다. 사용자가 'python' 이라고 입력하는 동안 계속 추천하는 검색어를 바꿔가면서 보여준다.

 

  해당 추천 검색어가 나오는 과정을 fiddler 로 살펴 보게되면, 아래와 같이 www.google.co.kr 도메인에서 /complete/search? 페이지를 호출하는 6개의 요청('p' 'y' 't' 'h' 'o' 'n' 각각 입력에 따라 총 6개의 요청이 날아간다)을 볼수 있다. 그 중 맨 마지막 요청 항목을 클릭해 보면, 폼안의 'q' 인자 안에 우리가 입력했던 'python' 이라는 글자가 넘어가고, 밑 쪽 보면 결과값으로 'python' 에 해당하는 추천 검색어들이 'json 데이터 형식'으로 담겨 응답으로 오게 된다. 브라우저는 해당 json 내의 값을 적당히 풀러서, HTML 페이지의 DOM 개체에 넣어서, 위의 그림과 같은 추천 검색어 들을 보여주게 된다.

 

 

 

[Ajax 샘플 코드 만들어 보기]

  그럼 실제로 간단한 Ajax 샘플을 하나 만들어보자. 우선 전송되는 데이터 타입은 json 이 아닌 평문으로 간단하게 구현 하려고 한다(ASP 가 좀 구식언어이고, .NET 이 나오면서 업데이트가 없이 버려져서, json 같은 최신 데이터 구조를 파싱하기가 까다로운 점도 이유이다). 아래 코드에 최대한 주석을 달아 놓았다(// 는 자바스크립트 주석이고, <!-- --> 는 HTML 주석이다).

 

  위쪽의 자바스크립트에는 Ajax 리퀘스트가 정의되어 있다(처음 보는 분들은 조금 사용하는 구조가 낯설어 보일테지만, 처음 Ajax 를 만든 사람이 저렇게 사용하라고 정해 놓은거라서, 파이썬에서 SQL 연결 할때 사용하는 모듈의 문법대로 구현 해야하는 것과 같다고 보면 된다. jQuery 등의 라이브러리를 사용하면 또 그 쪽에서 시키는 스타일대로 하면 된다). 하단 HTML 에 정의된 사용자가 입력한 <input> 값을 읽어와서, ajax_sub.asp 페이지를 호출(폼의 action과 비슷하다고 보면 된다)하면서 'no' 인자에 사용자 입력값을 넣어서 보내고, 결과가 반환되어 오면 그 값을 아래 HTML 에 정의된 <span> 태그에 innerHTML 속성을 이용해서 넣는다

 

  하단 HTML 에는 버튼 속성을 가진 <input> 태그가 있는데, 자바스크립트의 getMenu 함수가 onclick 이벤트를 통해 연결되어 있다. 사용자가 번호를 입력하고 버튼을 클릭하면, getMenu 함수가 실행되면서 Ajax 요청을 실행하는 도미노 형태의 구조이다.

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
<%@ Language=VBScript %>
 
<script>
function getMenu() {
   var xhttp;
   // 사용자가 입력한 값을 id 를 통해 가져온다.
   var menuNo = document.getElementById("menuNo").value;
   
   // 새로운 ajax 요청을 만든다
   xhttp = new XMLHttpRequest();
   // 요청에 대해 응답이 정상으로 올때까지 기다려서 
   xhttp.onreadystatechange = function() {
      if (xhttp.readyState == 4 && xhttp.status == 200) {
         // span 태그내에 응답으로 온 텍스트 값을 살짝 끼워 넣는다.
         document.getElementById("menuName").innerHTML = xhttp.responseText;
      }
   }
   // 실제 요청하는 페이지는 ajax_sub.asp 페이지 이고, get 인자로 no 에 사용자가 입력한 값을 넣는다.
   xhttp.open("GET""ajax_sub.asp?no="+menuNotrue);
   xhttp.send();
}
</script>
 
<html>
   <head>
      <title>ajax 샘플</title>
   </head>
<body>
   <table>
      <tr>    
         <td> 메뉴 번호: </td>
         <!-- 사용자가 입력하는 값 -->
         <td width=120> <INPUT id="menuNo" size="10" type="text" value=""> </td>    
           <td width=200> 
               <!-- 버튼을 누르면 getMenu 함수를 실행 한다 -->
               <input type="button" value="해당되는 메뉴 찾기" onclick="getMenu()">
               <!-- 나중에 응답 값을 끼워 넣을 span 태그. 첨에는 아무 내용도 없다 -->
               : <span id="menuName"></span>
           </td>
         </tr>
   </table>         
</body>
</html>
cs

 

  앞의 ASP 예제들과 마찬가지로 해당 코드를 메모장에 붙여놓고, c:\inetpub\wwwroot에 ajax_main.asp 라고 일단저장한다.

 

 

  그 다음은 위의 페이지에서 호출하는 대상인 ajax_sub.asp 페이지를 만들어야 한다. 보통 이 호출 당하는 쪽은 데이터만 받고 보내면 되기 때문에 API 같은 형태로 많이 구현 되어있다. 구현한 코드는 아주 간단하다. 넘어온 no 값을 받아서(사용자가 입력한 값이다), 1 이면 'pizza', 2 이면 'pasta', 그 이외의 숫자면 'drink' 를 반환(response) 해준다(참고로 값이 없을 때의 에러처리는 안되서, 값을 안 넣음 아무 결과도 넘어오지 않는다).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ Language=VBScript %>
 
<%
    ' request 값 받기
   menuNo = request("no")
    
   ' 넘어온 메뉴 번호에 해당하는 메뉴이름을 반환해 준다..
   Select Case menuNo
      Case 1
         Response.Write("pizza")
      Case 2
         Response.Write("pasta")
      Case Else
         Response.Write("drink")
   End Select
%>
cs

 

  해당 코드도 메모장에 붙여놓고, c:\inetpub\wwwroot에 ajax_sub.asp 라고 저장한다. 

 

 

  이후 브라우저를 열어서 http://localhost/ajax_main.asp 라고 주소창에 입력한다. 아래의 화면이 나오면 메뉴번호에 1을 넣고, '해당되는 메뉴찾기' 버튼을 클릭하면, 페이지 뒤에서 Ajax 기능을 이용해 ajax_sub.asp 에 인자를 넘겨 데이터를 조회해서, 'pizza' 를 받아 옆에 표시하게 된다.

 

 

  Ajax 호출하는 과정을 피들러로 살펴보게 되면 앞에 봤던 구글 검색어 추천과 비슷하게, 숨겨진 호출(request)이 보인다. 이제 Ajax 를 통한 비동기적인 호출이 어떤 원리로 일어나고, 어떻게 보이는 건지 대충 감을 잡으시리라 생각한다.

 

 

 

 

[마무리 하면서]

  그럼 이렇게 되어 가벼운 깊이지만 꽤 길게 진행됬던 Legacy Web 편을 마치려 한다(개인적으로 소주제별 균형을 맞추기 가장 힘들었던 시간 중 하나일듯 싶다). 뒤돌아 보면 자잘하게 많았던 HTML(form, event), CSS, Javascript, Web Server, Web Language, Ajax 같은 기초 요소들이 우리가 보고 있는 웹을 지탱하고 있다는걸 볼수 있었다. 이런 'Legacy Web' 에 대한 기초가 잘 잡혀 있다면 파이썬이든, 다른 언어로든 웹프로그래밍을 공부할때 배워야 할 주제들이 한결 가벼워지게 된다고 생각한다. 앞에서 얘기했던 프로그래밍을 배울 때의 외적요소들을 미리 알고 있을 때처럼 말이다.

 

  혹시 위의 주제들을 잘 모르는 상태에서 파이썬으로 만드는 웹을 공부하고 싶다면, flask 나 Django 프레임워크를 공부하는 중간 중간에 꼭 각각의 주제에 대한 쉬운 책 한권 정도는 보기를 추천한다. 개인적으로 프레임워크는 웹의 구성요소들을 잘 배치할 수 있도록 돕는 껍데기 역활에 불과하지 않는가도 싶다. 다음 시간에는 이런 Legacy Web의 요소들이 파이썬의 Flask 라는 프레임워크에 어떻게 녹여져 소개되고 있는지, 가벼운 맘으로 체크해 보도록 해보자.

 

 

 

 

2017.7.15 by 자유로운설탕
cs

 

 

posted by 자유로운설탕
2017. 6. 18. 15:49 프로그래밍

  이번 시간에는 머신러닝에 대한 이런저런 생각들을 얘기하고, 이전 시간에 배웠던 numpy, scipy, 그리고 요즘 주목 받고 있는 텐서플로우(tensorflow) 라이브러리를 이용해서, 머신러닝 계의 구구단 이라고 할수 있는 최소제곱법(Least Square Fit)으로 데이터에 맞는 직선을 추정하는 샘플 3가지를 실행해 보려고 한다.

 

 시작하기전에 먼저 양해를 얻고 싶은 점은 스스로 생각했을 때 머신러닝에 대해서는 아주 조금 밖에는 알고 있지 못하다는 점이다. 비유하자면 수영을 해서 강을 건너야 하는 상황인데, 아직 강가에서 손가락만 살짝 담구고 '물이 얼마나 찬가' 하고 체크하는 레벨이라고 볼수 있다. 그래서 여기서 얘기한 얘기들이 틀릴 수도 있기 때문에, 내용을 최대한 비판적으로 받아 들이라고 권고를 드린다.

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

 

[들어가면서]

  여러 웹사이트와 블로그들을 돌아다녀본 결과, 보통 두 가지 입장이 눈에 많이 뜨이는 것 같다.  한 측면은 수학이나 통계학에 대한 경험을 보유하고 있는 상태에서, R이나 매트랩 등의 연구 목적의 툴을 쓰다가, 좀더 일반적인 머신러닝 프레임윅에 관심을 가져 길을 가다보니, 파이썬 같은 범용 프로그래밍 언어를 해야될 필요성을 느끼게 되어 본격적으로 프로그래밍의 세계에 발을 들여 놓게 되는 경우 이다.

 

  다른 한 측면은 프로그래밍, DB, 보안, 시스템 등의 기술적인 업무 등을 하는 입장에서 머신러닝 책이나 강의를 찾아 보다보니, 원리를 설명하는데 사용하는 선형대수, 통계 그리고 RNN 같은 낯선 이론들을 만나게 되고, 책이나 강의를 이것저것 봐도 머신러닝 라이브러리를 작동 시키는 부분은 대충 따라 할 수 있을 것 같은데, 해당 도메인에 대한 부족한 이해 때문에 전체적인 그림이 그려지지 않고, 어떤 원리로 그런 일들이 가능한지, 어떤 데이터를 가져다 어떻게 가공해야 하는지 대해서는 여전히 모호한 상태임을 느끼게 되어, 수학이나 통계 분야에 대한 공부를 시작하는 경우일 것이다(저같은 경우는 후자 그룹에 속하는 경우인거 같다).

 

  그런데 사실 그렇게 가지지 못한 지식에 대한 대한 니즈나, 동경에서 공부를 시작한다고 해도, 만족할 만한 끝은 좀처럼 잘 보이지 않게 된다. 왜냐하면 그 동안에 관심이 없었던 상대의 영역의 본질에 접근하기는 쉽지 않기 때문이다. 다른 분야의 예를 들자면 캐릭터를 잘 만들거나 그림을 잘 그리기 위해서 3D 모델링 프로그램, 일러스트레이터, 페인터 등의 그래픽 관련 프로그램을 마스터 하려는 것과 비슷한 접근이라고 느껴진다. 아무리 해당 툴의 메뉴얼을 달달 외고, 기능을 잘 이해해서 쓴다고 해도, 그러한 행위의 본질이라고 할수 있는 미학적, 공간적인 감각을 기르는 경험들이 부재한다면, 그러한 노력이 올바른 결실을 맺긴 힘들다고 본다.

  

  마찬가지로 현재 올렸던 글들을 계속 읽어오신 분들은 공감하셨겠지만, 프로그래밍 이란 것은 단지 가시적으로 보이는 코드란 측면에 국한된 지식은 아니다. 외국어를 배울때 그 나라의 문화를 같이 이해해야 자연스럽고 맥락에 맞는 표현을 할수 있게 되는 것처럼, 머신러닝을 받치고 있는 프로그래밍 지식은 파이썬이란 언어 자체에서 출발하여 해당 언어가 동작하는 OS, 네트워크 등의 여러 주변환경과, 데이터를 담는 빅데이터 시스템 등 기술 생태계 전체가 포괄적으로 엮여 있다고 생각한다. 그래서 스펙트럼이 넓을 뿐만 아니라, 인기가 좋아 급속히 성장하며 확장되는 기술 영역들이 의례 그렀듯이 자료들이 초보자들에게 그다지 친절하게 정리되어 있진 않고 계속 변하기 때문에 접근 난이도가 높은 편인 것 같다. 물론 서로 다른 문화가 그렇듯이, 서로 다른 분야 사이에도 자신의 분야의 경험을 기반으로 유추할 수 있는 비슷한 공통 개념들도 많긴 하지만 말이다.

 

  잘은 모르지만 반대로 프로그래밍 영역과 비슷하게 짐작해 보면, 수학이나 물리, 통계 등의 분야도 단순히 보이는 이론과 수학적 지식들이 전부는 아닐 것 같다. 그 학문을 오래 접했던 사람들만이 가질 수 있는 특유의 사고 방식과 문제에 대한 접근법, 데이터와 숫자를 보는 시야나 감을 얻는다는 것은 단순히 통계나 선형대수 책을 공부하는 것과는 좀 많이 다른 일이 아닌가 하는 생각이 든다. 

 

  그래서 어떤 측면에서는 한 사람이 모든 부분을 다 잘 알순 없기 때문에, 효율성을 위해서는 데이터를 다루는 직군, 모델과 알고리즘을 다루는 직군, 프로그램을 관리하는 직군을 따로 나누어 조직해야 한다고 하는 주장도 있지만, 뭐 그런 경우라도 서로서로 상대방이 하는 일을 이해하면서 일을 하면 효율적이기도 하고, 이해 타산을 버리고 순수하게 공부하는 현 시점에서는 무시하기로 하자. 이 시간엔 조금이라도 머신러닝이라는 분야가 정밀한 숫자, 연관된 수학, 복잡한 통계이론을 차지하고도 합리적인 활동으로 보일 수 있는지 생각해보자.

 

 

 

 

[머신러닝이 하는 일에 대해 상상해보기]

  우선 어떻게 기계가 학습을 할수 있다는 것을 직관적으로 이해할 수 있을까? 기계가 학습을 한다는 것은 기계가 사람처럼 알고리즘을 만드는 작업, 즉 프로그래밍을 할수 있다는 얘기다. 예전부터 자동으로 프로그래밍을 짜는 일은 인간의 창조적인 능력의 한 부분이라고 믿어왔고, 여러가지 쉽고 자동화된 프로그래밍 툴을 만들려는 시도가 많이 실패 해 온것으로 알고 있는데, 모두 인간의 자만이였을뿐일까? 개인적인 생각으로는 기계가 학습하여 프로그래밍을 한다고 하는 부분은 사람이 프로그래밍을 하는 부분과는 다르면서도 동일한 모순적인 측면이 있다고 본다.

 

 

  예를 들어 하나의 프로그래밍 로직에 대해 생각해 보기 위해, '1교시 언어를 바라보는 방법'에서 보았던 아래 그림을 다시 한번 보자.

 

  당연한 얘기 같지만 모든 목적을 가진 프로그램은 입력과, 출력을 가지고 움직인다(실행 시점에 인자가 전달되지 않는 프로그램도 실행 시 시스템과 입력과 출력을 하고 있다고 볼수 있을 것이다). 그럼 프로그래머가 작성한 프로그램이 내부적으로 입력에 항상 3을 곱한 후 1을 더 해서 결과를 보여주는 프로그램이라고 해보자. 초등학교 수학식으로 나타내면 'y=3x+1' 일 것이다. 파이썬 식으로 나타내면 아래와 같다.

1
2
3
4
.... 입력으로 x 가 들어옴.
 
= 3x + 1
print (y)
cs

 

  근데 우리가 해당 로직을 이해할 수 없는 상태에서(곱하기가 엄청 고차원 적인 수학이라고 가정해 보자), 사람들이 해당 프로그램을 사용해서 쌓인 충분히 많은 입력-출력 쌍 데이터가 있다고 해보자. 예를 들면 (1, 4), (4, 13), ... (10000, 30001) 같은 형태일 것이다. 해당 데이터를 관찰해 보면 결과 데이터의 경우는 입력 데이터에 사람이 만든 어떤 프로그램 로직(여기서는 x*3 +1 )이 들어가 만들어지는 것이라고 볼수 있을 것이다.

 

  그럼 반대로 만약 어떤 임의의 시스템이 x 입력을 받아 y를 출력을 나타내는 것을, 위의 수집한 데이터에 대해서 최대한 높은 '옳음'으로 처리할 수 있다면, 해당 시스템의 속이 어떻게 생겼는지 상관없이, 해당 시스템은 사람이 작성한 위의 프로그램과 근사적으로(어쩌면 극한적으로) 동일한 자동화 프로그램이라고 할 수 있을 것이다(모든 사칙연산 데이터를 그대로 흉내내는 머신러닝 모델을 상상해 보자. 사람이 만든 계산기와 비슷하다 볼수 있지 않겠는가?).  

 

  즉 데이터 측면만을 보면 충분한 규모의 객관적으로 수집된 입력과 출력 데이터가 있고, 그 안에 현상을 왜곡하는 가짜 데이터가 무시할 만큼만 존재하고(또는 모델내에 그러한 데이터를 걸러내 무시하는 안전장치가 있어도 되고), 그 데이터들이 뭔가 사람이나 현상의 의미있는 활동을 나타낼 가능성이 있다면, 해당 데이터에는 사람이 논리적으로 파악하긴 힘들 수도 있지만, 그 데이터 쌍들을 만들어낸 로직을 포함하고 있다고 볼수 있지 않을까 싶다. 

 

 

  충분한 규모의 객관적인 입력과 출력 데이터 쌍은 비즈니스, 데이터 분석과, 빅데이터의 도움으로 이루어 진다고 보면 될것 같고, 가짜 데이터의 제거 부분은 비즈니스, 데이터 분석, 노이즈에 강한 모델 등을 포함해 모든 동원할수 있는 방법이 다 포함 될수 있을 것 같고, 입출력 데이터들 자체에 숨어있는 로직이나 패턴을 적절한 필터를 써서 참기름 짜듯 뽑아내는 것이 여러 머신러닝 알고리즘과 모델의 앙상블이라고 생각해보면 어떨까 싶다.

 

  그래서 사람이 가진 논리적 기술로 데이터를 가공하지 않고, 머신러닝 알고리즘(물론 이것도 넓은 범위에선 논리인것 같다)이 데이터 안의 로직이나 패턴을 추출해 내는 과정을 '기계학습'이라고 명명 지은게 아닌가 싶다. 로직을 짜내는 과정에서, 해당 머신러닝 필터는 데이터에 커스터 마이즈된 특정한 모양(모델이 데이터에 커스터마이즈 된 부분을 얘기하는지, 데이터에 독립된 필터를 얘기하는지는 좀 아리송 하긴 하긴 하지만. 현재로서는 왠지 모델 자체에 데이터의 숨은 로직의 특성도 포함되는거 같다)을 가진다. 이후 이 특정한 모양으로 커스터 마이즈된 필터를 향후 들어오게 되는 같은 타입의 새 데이터에 적용하게 되면, 사람이 프로그래밍 하지 않아도 기존 결과 데이터와 비슷한 로직의 영향을 받은, 결과 데이터를 만들어 내게 되지 않을까 싶다.

 

  그 후 일반적으로 만들어진 모델을 검증하기 위해서, 보통 전체 데이터셋을 트레이닝셋과 검증셋으로 적당히 나누어 트레이닝셋으로 훈련을 시킨 후에 검증셋으로 실제 경기를 진행 하게 한다. 트레이닝 셋은 일종의 원어민 회화 이고, 검증셋은 실제 다른 외국인에게 배운걸 시도 하는거라고 생각해봄 이상할까--; 

 

 

 

 

[몇가지 문제 들]

  머신러닝이 입력과 결과 데이터 사이에 숨어있는 로직을 뽑아내는 작업이라는 것이 어느정도 맞다는 가정에서, 몇가지 머리가 아픈 모호한 문제들이 발생할 수 있다.

 

 

  1번째 문제는 'garbage in garbage out' 이다. 어떤 우수한 머신러닝 모델이라도 좋은 데이터를 공급해 주어야 좋은 알고리즘을 추출해 준다는 것이다. 정신건강의학과 의사가 상담을 하는데, 환자가 거짓말만 늘어 놓는다면 어떻게 될까? 아무리 실력있는 의사라도 환자의 마음의 병의 원인에 대해서 잘못된 판단을 하게 될것 이다(물론 정말 뛰어나다면 거짓말을 한다는 것 자체를 눈치채고 진실을 얘기하도록 유도할 수도 있겠지만^^). 그럼 이러한 가짜 데이터들은 왜 들어 가게 될까?

 

  우선 충분히 신뢰할 만큼 데이터 양이 많지 않거나 특정 군으로 편향될 수도 있다. 우리가 매번 선거 시기에 듣는 여론 조사용 표본 데이터의 중요성이다. 게임센터를 들어가는 학생 10명의 의견을 듣고 우리나라 전체 학생이 어떤 생각을 가지고 있다라고 말하는 것은 의미 없을 것이다. 근데 사실 현실적으로 충분히 좋을 정도라는 표현은 참 추정하기 힘든 일인거 같다. 충분히 좋을 정도의 개발, 충분히 좋을 정도의 테스트 같이, 잘은 모르지만 통계적으로 의미 있는 충분한 데이터 라는 것도 약간은 바라보는 관점에 따라 애매한 영역이지 않을까 생각해 본다.

 

  다른 원인은 노이즈이다. 만약 누군가 사실을 들키는 것이 창피해서 속마음과는 다른 가짜 선택을 했다면? 데이터를 취합하는 과정에서 잘못된 정보들이 우연히 섞여졌다면? 쇼핑몰에서 연령대별 분석을 하는데 회원들의 아이디들을 가족이 모두 공유해서 사용하고 있다면?(뭐 예를 들어서 회원제인 코스트코 같은데는 지인이 부탁한 물건을 사다주는 사람도 많을거 같다) 해커가 들어와 정보를 본인에게 유리하게 몰래 변경했다면? 해당 데이터의 형태가 애초부터 랜덤적인 선택 요소가 표함되어 있다면? 등등.. 다양한 일들이 생길 수도 있다. 또는 아래의 알렉사 기사처럼 TV 에서 나온 음성 같은 외부 테이터를 잘못 받아들여 엉뚱한 판단을 하게 될지도 모른다.(뭐 홍채나 얼굴 인식이 사진으로 된다든지 하는 것도 비슷한 경우일 것이다) 

http://thegear.co.kr/13718

 

   

  2번째 문제는 데이터 무더기 에서 학습을 위해 제공하기 위해 골라낸 요소들이, 정말 해당 현상을 제대로 설명하는 인자인가 하는 문제이다. 어떤 인자는 혼동만 주는 필요 없는 인자일 수도 있고, 어떤 중요한 인자는 빠져있을 지도 모른다. 최악의 경우는 현재 수집을 안하고 있거나, 현실적으로 수집을 못하는 요소일 지도 모른다. 데이터를 제대로 이해 못하고 머신러닝 모델에게 전달한다는 것은, 사람이 비즈니즈 룰도 이해를 못하고 프로그래밍을 하는 것과 그닥 차이가 없을 것이다. 로직은 돌아 갈테지만 아마도 아무 의미도 없거나 재앙일 것이다.

 

  또는 데이터의 범위를 잘못 잡은 학습을 할수도 있다. 예를 들의 앞의 예가 아래와 같은 로직으로 동작하는데, 현실로 수집가능한 데이터가 10000 까지 였다면, 해당 데이터로 학습된 모델은 10000 을 넘어가는 데이터를 만날때 커다란 재앙을 안겨줄 지도 모른다(버퍼 오버플로우 처럼 말이다).

1
2
3
4
if input <= 10000:
    우리가 수집한 데이터(0~10000)가 적용된 로직
else:
    새로운 타입의 데이터가 생성됨
cs

 

  비슷하게 여론 조사 결과 조작이나, 통계의 여러 부작용 처럼, 데이터의 어떤 집합, 성질을 선택 하느냐에 따라서 의도된 답을 선택하거나 실제와는 다른 결과를 보여줄 수도 있을 듯 싶다. 

 

 

 3번째 문제는 학습된 모델을 실제 현실에 적용할수 있느냐 하는 문제이다. 자율자동차와 같이 운전자의 보호냐 보행자의 보호냐를 선택하는 문제일 수도 있고, 99.9999% 의 정확도라도 false-positive(안전하다고 판단했지만 실제로는 위험함) 가능성이 존재하는 한 실수에 대한 윤리적 문제가 일어날 수 있다. 예로서 머신러닝 기반의 의료검사를 신뢰 하다가 드문 케이스 때문에 암을 놓쳤다고 해보자. 시스템의 정확도가 엄청 높고, 해당 경우의 데이터가 트레이닝 데이터에 포함되지 못한 정말 운이 없는 경우기 때문에 어쩔수 없다라고 할수 있겠는가? 이런 분야라면 의사가 먼저 체크 후 괜찮다고 판단한 환자를 머신러닝으로 2차 체크 하여, 의사의 실수를 줄이는 방식으로 이용하는 등의 사람들이 납득할 수 있는 합리적인 기술 적용 프로세스가 필요하다. 그래서 어떤 사람들은 머신러닝을 기존 기술의 대체제가 아닌 보완제의 관점에서 접근하기도 한다.

 

 

  마지막 문제는 아마도 모델의 오차와 별개로 우리가 가지고 있는 데이터 자체가 제공된 시점부터 이미 실제 진실과는 조금은 차이가 날 수 있을지 모른다는 것이다. 사람들의 해당 데이터를 만들게된 모든 이유를 다 이해한다면 모르지만, 그런 전지전능함을 가지지 못한 우리는 데이터에 담긴 fact 만을 믿어, 중요한 사실을 놓칠지도 모른다. 마치 우리가 매일 만나는 사람이 겉모습과는 다른 속마음을 가질 수 있는 것과 비슷하게 말이다. 또한 해당 진실은 해당 시간대에서만 유효했을 수도 있다. 시간이 흐름에 따라 다른 영향을 받을 수도, 또한 아예 해당 데이터는 시간에 따라 변화를 하는 데이터일 수도 있으니 말이다.

 

 

  그래서 알파고가 바둑이나 스타크래프트 같은 분야를 선택한건 아주 영리한 선택이라고 생각한다. 현실의 데이터가 왜곡되듯이 바둑기사가 불리하다고 바둑알을 속이거나 바둑판을 엎는 일 따위는 없을 테니까 말이다. 또한 바둑과 같은 테이블 게임은 경우의 수는 무한대일지 모르겠지만, 바둑 룰과 선의 공간, 그리고 승리라는 목적으로 닫힌 세상이라는 부분은 그 무한대 성을 많이 제한해 주며, 데이터의 무결성을 보장하는데 오픈된 분야보다 많이 유리하다고 생각한다(게임이란건 사람의 이해 가능성을 전제로 성립될 수 있기 때문에 무한에 가깝다곤 하지만 어째든 제한은 있다고 본다. 다만 해당 부분은 효율적인 승리의 관점에서 제한된다는 거지, 바둑의 철학적 관점에 대한 제한을 얘기하는 것은 아니다). 그것은 다음에 도전할 지도 모른다는 스타크래프트 같이 현재 머신러닝이 강세를 보이고 있는 게임 분야도 비슷할 듯 싶다. 아무리 자유도가 높더라도 게임 분야는 사람이 흥미를 가지도록 방향이 제한되고, 추출해야될 로직들이 머물고 있는 데이터들에게 비교적 안전한 닫힌 세상이라고 생각한다(뭐 그렇다고 쉽다는 얘기가 아니라 real world 에 비해서 상대적으로 그렇다는 의미다. 그리고 부루마블 같은 주사위와 황금열쇠라는 랜덤요소를 가지는 게임 같은건 좀 다른 문제일 것이다). 어떤 기사의 마지막에서 살짝 언급했듯이 알파고의 승리의 숨은 주역은 뒤에 적절한 장르와 데이터를 선택하고, 적절한 알고리즘으로 학습시킨 과학자 들이라는 말이, 물위를 헤엄치는 오리의 바쁜 발을 떠올리게 된다.

 

 

 

 

[기술적인 미래]

  이왕 이것저것 얘기를 한 김에, 앞으로의 머신러닝 기술이 어떻게 될까도 추측해 보자. 빅데이터 분야의 진행을 기반으로 추측해보면, 처음에 나온 하둡이 hive 나 pig 같은 좀더 쉬운 작업을 가능케 하는 프레임윅들로 감싸지듯이, 현재 같이 하나하나 이해하며 초기 값 및 모델, 제어 요소들을 조정해, 좀더 빠르고 높은 정확도의 결과를 얻으려 하는 부분은 점차 통합되고 자동화된 로직으로 대체될 가능성이 높을 듯 하다. 군웅할거중인 머신러닝 프레임윅들도 한두개의 강력한 범용 프레임 윅과, 몇개의 특수 프레임윅들로 정리되지 않을까 생각한다(안 쓰는 애들은 버려지니...). 여튼 머신러닝 기법을 사용하는 방법은 점점 쉬워지며 블랙박스로 감싸질 것 같다. 이런 부분이 많은 강의 하는 분들이, 관련 수학을 깊게 몰라도 머신러닝을 배우는덴 무리가 없다고 강조하는 측면인 듯 싶다. 우리가 기계공학을 몰라도 자동차를 운전할수 있듯이 말이다.

 

  물론 머신러닝 분야의 알고리즘 개발에 발을 담그고 있는 분들이나, 좀더 디테일하며 컨트롤을 원하는 분들은 껍데기 안쪽을 다루기 위해서 좀 더 깊게 이해하려 하는게 맞을 수도 있지만, 어느 정도 깊이로 이해하려 하는지는 좀 생각해 볼 문제 같다. 깊이 문제도 수학식이나 통계 공식의 논리적 정확성의 증명에 너무 집착하다보면 원래 그 식이 얘기하려고 했던 이미지나, 본질을 놓칠수도 있으니까 말이다(공부하기 싫어서 하는 얘긴 아니다^^). 수학은 연산보다는 그 연산이 무엇을 의미하는지를 이해하는게 더 중요한 듯도 싶다. 그리고 아마도 커다란 메이저 회사들이 필요한 프로그래밍 기술들을 머신러닝 프레임윅 안 쪽에 쉬운 인터페이스로 구현해 놓음으로서 현재의 프로그래밍 성격의 장벽은 점점 낮게 될 것 같다. 물론 어려운건 마찬가지 겠지만 현재보다 상대적으로 쉬울수 있다는 얘기긴 하다. 또한 개인이나 자그마한 기업들은 유용한 데이터 자체를 수집하기 힘들수도 있기 때문에 일반적인 분야에 대해서는 1+1 상품처럼 데이터와 학습된 머신러닝을 같이 묶어서 제공하는 부분들도 늘어날 것 같다. 지금 한참 관심을 받고 있는 음성이나 이미지 인식 분야 같은 인류의 공공재 분야에서 말이다.

 

 

  그리고 반대로 생각해 보면 미래에도 개선되기 힘들 것 같은 부분들이 있다. 우리가 비즈니스를 이해하고, 적정한 테이터를 디자인해 수집하고, 학습에 필수적인 값들을 뽑아내고, 머신러닝이 추출해낸 반투명한(뭐 RNN 같은 신경망 이론인 경우는 가중치 들이 왜 그런지 사람은 이해하기 힘들 듯 하다) 로직들이 맞는지를 이해하고, 오차의 본질을 이해해 모델을 조정하는 능력이다(원석을 감정하는 능력이라고 할까..). 또 문제를 파악하고 적절한 기법(알고리즘과 모델)을 선택하는 것도 오랜기간은 수학적, 통계적 기법을 잘 아는 사람들의 손을 대부분 필요로 하지 않을까 싶다. 물론 프레임 윅들이 여러가지 시각화 툴을 통해, 해당 작업을 많이 도와줄 것은 같지만 말이다. 또 아마 지금의 장미빛 전망의 끝이 지나면, 자동화된 프로그램이 못하는 분야에 대해 다른 해결을 원하는 새로운 장르가 생겨날 것도 같다(어차피 현재의 로직으로도 해결 못하는 문제는 많으니까. 왠지 머신러닝은 로직을 벗어난 로직이라 그러한 부분을 일부 해결해 줄것은 같다). 

 

  현재 트레닝셋과 검증 셋으로 나누어, 리그레이션 테스트와 비슷하게 신뢰도를 확인 하는 방법은, 뭔가 약간 너무 낙관주의적인면(출처가 같은 트레이닝과 검증 데이터가 무조건 옳다고 가정하는 측면에서)도 있다고 느껴지기 때문에, 수집을 포함한 전체적인 데이터 케어 관점에서 추출된 로직에 대해 테스트 및 검증 하거나, 구성된 시스템의 여러 부분과 보안적인 측면을 검토하는 검증 활동 영역도 활성화 되지 않을까 싶다. 다만 머신러닝으로 추출된 로직은 프로그래머가 만든 로직과 다르게 명확히 알수 없으므로(그래서 앞에서 반투명이라고 표현했다), 모순적으로 알고리즘과 모델을 이해해야 최소한의 검증이 가능하므로 기존에 일반적인 테스팅이나 보안 활동을 하던 사람들이 접근하기는 장벽이 높은 분야 같기도 하다.


  결과적으로 머신러닝은 데이터를 기반으로 현대의 많은 과학 분야가 그렇듯이 완전한 해에 대한 실용적인 근사로 나아가는 로직이다(퍼펙트할수 있는 예외는 일부 게임과 같이 닫힌 세상의 데이터 일 것이다). 프로그래머들이 프로그래밍 언어를 통해 기호로 분리해 놓은 논리 기호들을 녹여 다시 새로운 기호를 만드는 느낌이라고 할까? 그래서 머신러닝을 바라볼때는 수학이나 통계학을 기호의 학문이 아닌 알고리즘을 구성할 레고의 블록(블랙박스적 도구)으로 바라보는 접근법도 어떨까 싶다. 실제로 우리가 파이썬을 할때도 모든 세세한 라이브러리의 내부 로직까지 모르면서 사용하는 것과 비슷하게 말이다.

 

  왠지 헤메고 있는 사람을 더 복잡하게 만드는 어설픈 얘기들을 늘어놨는지도 모르겠지만^^;, 이런저런 블로그를 돌아다니다가 알게되어 읽어봤던 몇 가지 괜찮았던 머신러닝의 '주변영역'을 건드리는 책들을 소개하면서 생각의 나래를 마무리 하려 한다. 혹시 위의 생각에 조금은 일리가 있다고 생각하시는 분들은, 아래의 책들과 함께, 아래 링크의 유투브의 유명한 '모두를 위한 딥러닝 강좌' 를 보시고, 이후 스스로 자기가 갈 공부 방향을 선택하시면 될것 같다.

https://www.youtube.com/playlist?list=PLlMkM4tgfjnLSOjrEJN31gZATbcj_MpUm

 

  • 틀리지 않는 법 : 수학적 사고의 힘 (현실을 심리학, 경제학으로 해석하는 대신 수학으로 해석해 보며 수학적 접근법의 한계도 얘기 한다. 은근 그 과정에서 머신러닝의 한계 또한 생각해 보게 되는 듯 하다)
  • 화이트헤드의 수학이란 무엇인가(앞의 수학 라이브러리의 복소수 파트에 대해 아이디어를 얻은 책이다. 왜 이런 수학 장르가 생겨나게 됬을까를 찬찬히 생각하게 하는 책 같다. 수학과 별로 안 친한 저와 비슷한 레벨이면 이해가 안가서 살짝 넘겨야 되는 수학적 개념들이 좀 있다)
  • 마스터 알고리즘 (머신러닝에 대한 얘기다. 해당 분야를 오래 경험해온 전문가가 세상에는 5종류의 머신러닝에 대한 접근방식들이 있고, 그들을 조화시킬 궁극의 알고리즘이 있을수 있다는 얘기를 한다. 이 책을 읽게 되면 머신러닝 책들이 왜 그렇게 여러 주제의 잡학 사전처럼 구성되어들 있는지 이성적이면서 감성적으로 수긍이 되게된다)
  • 헤드 퍼스트 통계학 (통계를 직관적으로 설명하려는 시도를 한다. 뒤로 갈수록 직관성이 사라지고 수학만 남는 아쉬움이 있다--;)
  • How to solve it (어떻게 문제를 풀어나가느냐에 대한 책이다. 문제 풀이에 대한 접근 방법을 얘기하는 책이고, 학생들에게 어떻게 수학을 가르치느냐에 대해 얘기하는 책이지만, 어려워서 이해가 안가는 문제들이 종종 있음에도 읽을 가치가 있다. 이 수업의 방향이 이 책 영향을 좀 받은듯 하다^^;) 
  • CODE 코드 : 하드웨어와 소프트웨어에 숨어 있는 언어(이건 프로그래밍 공부가 의미 없게 느껴지는 분들을 위해서 덤으로~ 불빛 신호에서 컴퓨터가 만들어지기 까지의 과정을 설명한다. 중간에서 길을 잃어 필름이 끊기더라도 읽어볼 가치가 있다고 본다. 비 전공자한테는 생각보다 어려울지도 모르지만, 해당 책의 저자가 비전공자도 읽을 수 있도록 노력해 쓴 책이고 파이썬 공부를 하고자 하는 의지 정도만 있음 가능할 듯 싶다.)

 

[몸풀기 - 공학 라이브러리의 푸리에 변환]

   머신러닝을 하다 갑자기 이름도 낯선 푸리에 변환이란 것이 나오게되서 이상하게 생각할지 모르겠지만, 이 라이브러리를 소개하는 이유는, 머신러닝 라이브러리가 특별한 형태의 라이브러리라는 생각을 버리게 하고 싶어서 이다. 그리고 지금 소개하는 라이브러리를 잘 보면 왠지 머신러닝 라이브러리랑 닮아있다는 느낌도 있어서 이다. 그래도 맘에 안든다면 opencv 라는 이미지 라이브러리(정확하게 얘기함 컴퓨터 비전 쪽 라이브러리)를 살펴보는 부록 챕터라고 생각해도 좋다.

 

  푸리에 변환은 보통 전자기파 쪽에서 많이 쓰는 이론으로, 저도 수학적으론 잘 모르지만, 모든 주기를 가지는 신호는 사인과 코사인의 합으로 분리해 나타낼 수 있다는 이론이다(우리가 쓰는 스마트폰의 통신들도 이런 이론들 땜에 가능하게 됬다) 그러다보니 분리된 사인, 코사인 함수들의 주기를 보면, 해당 신호가 어떤 주파수(1초에 몇번 지그재그로 움직이나. 2.6G 와이파이의 경우 1초에 2억 6천번 파형이 움직인다)들을 포함하고 있는지 알게된다. 뭐 주기가 없는 신호도 무한대의 주기를 가졌다고 가정해서 변환하는 신묘함도 있다.

 

  근데 묘하게 이걸 그림에 적용하면, 그림에 분포되 있는 명암 요소에 따라서 주파수 영역으로 변환할 수 있다(사실 정확히 설명하기 위해 이리저리 사이트 들을 찾아봤는데, 그림의 평균적인 명암값 으로부터의 차이를 가지고 판단하는지, 경계 값과의 변화정도 인지 기준을 잘 모르겠다. 아시는 분은 댓글에 좀 --;). 여튼 그렇게 그림에 푸리에 변환을 적용해 정리하면, 그림과 동일한 2차원 영역의 주파수 그림이 나오는데, 중심 부분에 가까울 수록 그림에서 주로 면이라고 얘기하는 색이 비슷한 공간속의 점들의 정보를 가지고 있고, 중심에서 멀어질 수록 그림의 외곽선 이라고 부르는 경계를 구분해 주는 점들의 정보를 가지고 있다. 

 

  그래서 그림을 읽어와 푸리에 변환을 해서 나온 네모난 주파수 평면에서, 가운데 원 영역의 값을 0 값으로 덮어 지워버리면(보통 마스킹 한다고 한다. 밑의 그림 처럼 까맣게 정보를 없앴다), 그림의 외곽선 정보만이 남는 효과를 가져오게 된다(약하게 삭제하면 sharpening effect 가 되는듯 하다). 그것은 뭐 포토샵 같은 전문 프로그램에선 더 정밀한 로직을 쓰겠지만, 이 로직을 적용하면 어떤 그림을 적용하든 동일한 윤곽선 추출 효과를 가져온다(마치 학습된 머신러닝 로직이 데이터에 동일한 효과를 가져오는 것처럼 말이다)

 

 

 

  그럼 예제를 보자(요건 머신러닝 예제가 아니라 구글을 헤맨 과정은 생략한다^^). 일단 컴퓨터에 있는 사진 등을 복사해도 좋고 적당한 이미지가 없다면 무료 사진 사이트인 https://pixabay.com/ 로 가서 이미지를 하나 다운 받아서, c:\python\code 폴더에 mypic.jpg 라고 저장한다.

 

  그리고 opencv 를 설치해야 되는데 불행하게도 python 3.5 버전은 pip 인스톨이 안되는 듯하다. https://www.solarianprogrammer.com/2016/09/17/install-opencv-3-with-python-3-on-windows/ 사이트에서 안내하는 데로, http://www.lfd.uci.edu/~gohlke/pythonlibs/ 사이트로 가서, opencv_python-3.2.0-cp35-cp35m-win_amd64.whl 를 다운받아(가끔 파일이 이름이 바뀌어서 그런데 cp35 가 파이썬 3.5란 의미이고, amd64가 64비트 윈도우이다.) c:\python\code 에 넣고, 아래와 같은 명령어로 whl 파일을 설치했다. whl 파일이 무언가는 예전 시간에 설명했다~

(참고로 어떤 블로그 보다보니 python 3.6 버전에서는 pip 로 그냥 설치가 되는걸 보긴 했다)

 

c:\Python\code>pip install opencv_python-3.2.0-cp35-cp35m-win_amd64.whl
Processing c:\python\code\opencv_python-3.2.0-cp35-cp35m-win_amd64.whl
Installing collected packages: opencv-python
Successfully installed opencv-python-3.2.0

 

 

 그럼 예제를 위해 만든 소스를 구글에서 'python opencv fft low pass filter image' 라고 찾아서, 돌려보다 보니 python 2를 기준으로 만든거라서 나누기 코드 부분에서 문제가 생겨서(나누기 후 결과를 integer 로 취급할거냐 float 로 취급할거냐가 버전이 올라가면서 달라졌다), 아래의 사이트를 찾아, 코드를 조금 수정했다.

[소스 사이트]

http://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_transforms/py_fourier_transform/py_fourier_transform.html

 

[에러 메시지와 해결 사이트]

TypeError: slice indices must be integers or None or have an __index__ method 에러

https://stackoverflow.com/questions/28272322/typeerror-slice-indices-must-be-integers-or-none-or-have-an-index-method

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
import cv2
import numpy as np
from matplotlib import pyplot as plt
 
# 이미지를 읽어옴
img = cv2.imread('mypic.jpg',0)
 
# 푸리에 변환 하고 이런저런 맞춤
= np.fft.fft2(img)
fshift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(fshift))
 
# 작은 마스크를 만들어서 푸리에 변환한 영역의 가운데를 지워 버림(하이패스 필터)
rows, cols = img.shape
crow,ccol = rows//2 , cols//2
fshift[crow-30:crow+30, ccol-30:ccol+30= 0
f_ishift = np.fft.ifftshift(fshift)
 
# 다시 이미지로 역변환
img_back = np.fft.ifft2(f_ishift)
img_back = np.abs(img_back)
 
# 그림으로 보여줌.
plt.subplot(131),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(132),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.subplot(133),plt.imshow(img_back, cmap = 'gray')
plt.title('Image after HPF'), plt.xticks([]), plt.yticks([])
 
plt.show()
 
print ("ok")
 
cs

 

 

  언제나 처럼 c:\python\code 폴더에 'python opencv_fft_sample.py' 란 이름으로, 파일형식은 '모든 파일'을 선택하고,  인코딩은 'utf-8' 로 저장하고 실행한다.

c:\Python\code>python opencv_fft_sample.py
ok

 


  위와 같이 그림이 세개 나오는데 왼쪽이 원본 이미지, 가운데의 빛이 가운데 모인듯한 이미지가 푸리에 변환을 통해 주파수 영역으로 변환된 이미지, 오른쪽이 주파수 영역의 가운데 데이터를 삭제하여(코드안에 로직이 있다). 다시 이미지로 복구하였을때, 주로 경계선 요소들만 남기고 평면 요소들이 사라진 경우이다.

 

  보시면 푸리에 변환이 왠지 머신러닝과 비슷한 느낌이 들지 않는가?(머신러닝은 사실 데이터를 적절히 해석해 내부 특징을 얻을 수 있는 모든 알고리즘 들을 가져다 쓴다고도 할수 있다) 차이라면 데이터에서 로직을 추출하진 않는다. 하지만 데이터를 넣으면(디지털 이미지 또한 좌표 기반의 데이터 이다) 임의의 처리를 해서 데이터에 특정한 특징을 노출해준다는 부분에서 왠지 비슷하다는 느낌이 들어 굳이 이 시점에 소개했다. 그럼 실제 머신러닝 로직으로 가보자.

 

 

 

 

[최소제곱법 이란]

  많은 머신러닝 및 데이터 분석 책에서, 제일 처음에 나오는게 회귀분석이고(과거의 데이터를 기반으로 미래의 데이터를 예측함), 그 중 가장 먼저 나오는게 최소 제곱법 같다(이후는 최우추정법, 베이즈 같은 낯선 이론들이 나온다). 디테일한 의미를 다 알진 못하지만 개념적으로만 근사한다면, 해당 컨셉은 제공되는 데이터들로부터 가장 가까운 어떤 선(직선 or 곡선 : 사실 직선은 곡선의 한 형태 일 것이지만)을 찾기위해서, 선과 데이터 들의 수직 거리를 제곱한 값들의 합이 제일 작아지는 선을 나타내는 방정식의 상수들을 찾는 것이다. 즉 직선이라고 가정한다면 y = ax + b 의 a, b 값을 계속 특정 방향으로 조금씩 변화해 가면서, 각 점과의 거리를 제곱한 합이 제일 작아지는 지점을 찾는다(엑셀의 추세선이라고 봐도 될것 같다). 뭐 자세한 내용은 관련 책이나 블로그 등에서 보시라고 하고 살짝 넘어간다 --; 

    

 

 


[numpy 에서의 최소제곱법 구현]

  원래 numpy 는 계획에 없었는데, 찾다보니 있어서 다른 라이브러리와 비교를 위해서 넣게 되었다.구글에서 'linear least square fit python' 라고 찾아 아래의 페이지를 찾는다.

https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.lstsq.html

 

  이후 적당히 편집하여, y = 3x + 1 의 공식에 맞도록 x, y 값을 바꿔 넣었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
import matplotlib.pyplot as plt
 
# x, y 좌표 지정(y = 3x + 1), 방정식 모델 지정
= np.array([1257])
= np.array([471622])
= np.vstack([x, np.ones(len(x))]).T
 
# 선형대수 라이브러리의 least squre 호출
slope, intercept = np.linalg.lstsq(A, y)[0]
print("기울기:", slope, ", 절편:", intercept)
 
# 기존 값을 점으로, 찾은 기울기를 선으로 그린다.
plt.plot(x, y, 'o', label='Original data', markersize=10)
plt.plot(x, slope*+ intercept, 'r', label='Fitted line')
plt.legend()
plt.show()
cs

 

  앞 시간의 예제들을 시연해 봤다면 이미 라이브러리들을 설치되어 있을테니(아니라면 설치 방법은 15, 16교시를 참고한다), c:\python\code 폴더에 'numpy_least_sample.py' 이름으로, 파일형식은 '모든 파일'을 선택하고,  인코딩은 'utf-8' 로 저장하고 실행한다(지금보니 utf-8 로 저장하면 한글 주석이 달리는 경우에도 상단에  '#-*- coding: utf-8 -*-' 를 안 넣어도괜찮다). 정상적으로 해를 찾아 기울기 3, 절편 1 을 찾게 된다.

 

c:\Python\code>python numpy_least_sample.py
기울기: 3.0 , 절편: 1.0

 

 

 

 

[scipy 에서의 최소제곱법 구현]

  이번에는 과학 라이브러리인 scipy 를 이용해 보자. 구글에 'linear least square fit python scipy' 라고 찾아 아래의 2개 페이지를 찾아 적당히 믹스 했다. 전체적인 흐름은 numpy 와 거의 같다고 보면 될 듯 하다.

http://www2.warwick.ac.uk/fac/sci/moac/people/students/peter_cock/python/lin_reg/
https://docs.scipy.org/doc/scipy-0.19.0/reference/generated/scipy.stats.linregress.html

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
 
# 데이터 지정
= np.array([1257])
= np.array([471622])
 
# 데이터에 맞는 값 찾기, Slope: 기울기, Intercept: 절편
slope, intercept, r_value, p_value, std_err = stats.linregress(x,y)
print ("Slope and intercept", slope, intercept)
print ("R-squared", r_value**2)
 
# 데이터를 점으로, 찾은 선과 같이 화면에 표시
plt.plot(x, y, 'o', label='original data')
plt.plot(x, intercept + slope*x, 'r', label='fitted line')
plt.legend()
plt.show()
cs

 

  c:\python\code 폴더 'scipy_least_sample.py' 이름으로, 파일형식은 '모든 파일'을 선택하고,  인코딩은 'utf-8' 로 저장하고 실행한다.

c:\Python\code>python scipy_least_sample.py
Slope and intercept 3.0 1.0
R-squared 1.0

 

  뭐 결과가 같으니 이미지도 동일하다.

 

 

 

 

[tensorflow 에서의 최소제곱법 구현]

  마지막으로 tensorflow 인데, 운이 좋게도 금년에 텐서플로우가 64비트 윈도우 버전의 python 3.5 까지 지원하게 되서 시연이 가능하게 됬다. 근데 뭐 아직 윈도우즈는 텐서플로우에게 관심의 바깥 같은 느낌이 좀 들어서, 텐서플로우를 사용하는 사람들은 보통 리눅스나 맥환경에서 많이 쓰는 것은 같다. 

 

  먼저 pip 를 이용해서 텐서플로우를 설치한다(꼭 윈도우 64비트에, 파이썬 3.5 대 버전이여야 가능하다!. 구글을 찾다 보니 새로 빌드를 해서 32비트에 설치해 쓰는 사람들도 있다고는 한다)

c:\Python\code>pip install tensorflow
Collecting tensorflow
  Downloading tensorflow-1.2.0-cp35-cp35m-win_amd64.whl (21.2MB)
...
Successfully installed backports.weakref-1.0rc1 bleach-1.5.0 html5lib-0.9999999 markdown-2.2.0 protobuf-3.3.0 tensorflow-1.2.0 werkzeug-0.12.2 wheel-0.29.0

 

 

  이후 구글에 'tensorflow least squares matplotlib' 라고 검색하여 아래의 두 페이지를 찾았다.

https://gist.github.com/tomonari-masada/ed2fbc94a9f6252036eea507b7119045
https://github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/2_BasicModels/linear_regression.py

 

  위의 페이지의 예제가 좀더 간단하게 보이지만 텐서플로우에서 생성된 변수(x, y_)를 matplotlib 의 넘겼을때 plot 에서 타입 에러가 난다. numpy 리스트 형태를 넘겨야 괜찮다. 그리고 두번째 예제가 좀 모범답안 같이 전체적인 사용 플로우를 잘 보여주는거 같아서 두번째 예제에서 훈련 후 검증 테스트를 하는 코드 부분만 제거하고, 적당히 편집하면 아래와 같다. (상세 로직은 어차피 텐서플로우에서 하라는데로 하는거니, 전체적인 코드의 흐름을 보자 )

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
import tensorflow as tf
import numpy
import matplotlib.pyplot as plt
rng = numpy.random
 
# 파라매터들 변수 조정 수치, 전체 실행 수, 몇 번마다 화면에 로그를 보여주는지
learning_rate = 0.01
training_epochs = 1000
display_step = 50
 
# 훈련용 데이터 지정
train_X = numpy.asarray([1257])
train_Y = numpy.asarray([471622])
n_samples = train_X.shape[0]
 
# 텐서플로우 변수들 만들기
= tf.placeholder("float")
= tf.placeholder("float")
 
= tf.Variable(rng.randn(), name="weight")
= tf.Variable(rng.randn(), name="bias")
 
# 모델 만들기 'y = Wx + b' 를 정의한다.
pred = tf.add(tf.multiply(X, W), b)
 
# least square 공식을 이용하여 최소값을 만들 요소를 지정한다. 
cost = tf.reduce_sum(tf.pow(pred-Y, 2))/(2*n_samples)
 
# 기울기를 보정하는 경사하강법이란것을 사용하고, 비용을 최소화 하는 방향으로 학습 한다.
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)
 
# 초기화?
init = tf.global_variables_initializer()
 
# 텐서플로우 기동
with tf.Session() as sess:
    sess.run(init)
 
    # 데이터 넣기
    for epoch in range(training_epochs):
        for (x, y) in zip(train_X, train_Y):
            sess.run(optimizer, feed_dict={X: x, Y: y})
 
        # 50번 마다 로그 뿌려서 찾는 값 변화를 보여 줌.
        if (epoch+1) % display_step == 0:
            c = sess.run(cost, feed_dict={X: train_X, Y:train_Y})
            print("Epoch:"'%04d' % (epoch+1), "cost=""{:.9f}".format(c), \
                "W=", sess.run(W), "b=", sess.run(b))
 
    # 완료 되면 결과 출력
    print("Optimization Finished!")
    training_cost = sess.run(cost, feed_dict={X: train_X, Y: train_Y})
    print("Training cost=", training_cost, "W=", sess.run(W), "b=", sess.run(b), '\n')
 
    # 찾은 결과 그래프로 보여주기
    plt.plot(train_X, train_Y, 'ro', label='Original data')
    plt.plot(train_X, sess.run(W) * train_X + sess.run(b), label='Fitted line')
    plt.legend()
    plt.show()
cs

 

    c:\python\code 폴더에 'tensorflow_least_sample.py' 이름으로, 파일형식은 '모든 파일'을 선택하고,  인코딩은 'utf-8' 로 저장하고 실행한다.

 

c:\Python\code>python tensorflow_least_sample.py
Epoch: 0050 cost= 0.031498514 W= 3.08722 b= 0.53246
Epoch: 0100 cost= 0.023787469 W= 3.07587 b= 0.593684
Epoch: 0150 cost= 0.017966598 W= 3.06593 b= 0.64688
Epoch: 0200 cost= 0.013570149 W= 3.0573 b= 0.693111
Epoch: 0250 cost= 0.010249456 W= 3.0498 b= 0.73329
Epoch: 0300 cost= 0.007741358 W= 3.04328 b= 0.768208
Epoch: 0350 cost= 0.005847037 W= 3.03761 b= 0.798554
Epoch: 0400 cost= 0.004416286 W= 3.03269 b= 0.824928
Epoch: 0450 cost= 0.003335610 W= 3.02841 b= 0.847849
Epoch: 0500 cost= 0.002519361 W= 3.02469 b= 0.867768
Epoch: 0550 cost= 0.001902884 W= 3.02146 b= 0.88508
Epoch: 0600 cost= 0.001437244 W= 3.01865 b= 0.900125
Epoch: 0650 cost= 0.001085538 W= 3.01621 b= 0.913202
Epoch: 0700 cost= 0.000819894 W= 3.01409 b= 0.924565
Epoch: 0750 cost= 0.000619259 W= 3.01224 b= 0.934442
Epoch: 0800 cost= 0.000467722 W= 3.01064 b= 0.943025
Epoch: 0850 cost= 0.000353270 W= 3.00925 b= 0.950485
Epoch: 0900 cost= 0.000266819 W= 3.00804 b= 0.956967
Epoch: 0950 cost= 0.000201530 W= 3.00698 b= 0.962601
Epoch: 1000 cost= 0.000152215 W= 3.00607 b= 0.967497
Optimization Finished!
Training cost= 0.000152215 W= 3.00607 b= 0.967497

 

  데이터를 보면 1000번을 찾으면서 50번마다 화면에 현재 상태를 출력하라고 했으니 (1000/50) 번 해서 20개의 로그가 화면에 표시된 것을 볼수 있다. Epoch(찾기 반복 횟수), cost 및 optimizer(해 찾기의 목표가 되는 기준 값과, 기준 값을 어떻게 사용할 것인지. 여기서는 최소제곱값 함수가 가장 작아지는 값을 기울기 하강법으로 찾는다). W(기울기) b(절편) 값의 변화를 보면 점점 횟수가 늘수록 기울기와 절편 값들이 의도했던 3과 1로 수렴해 가는 것을 볼 수 있다.

 

  이렇게 비교해 보면 뭔가 전문적인 머신러닝 라이브러리들은 위의 numpy, scipy 같은 일반 선형대수 라이브러리 처럼 결과만 그냥 보여주기 보다는, 모델을 지정하고(pred = tf.add(tf.multiply(X, W), b)), 잘된 학습의 판단기준도 정하고(cost = tf.reduce_sum(tf.pow(pred-Y, 2))/(2*n_samples)), 학습 과정을 살펴보며(로그) 조정하는 듯 체계적인 접근 방식을 유도하고 있다. 뭐 제가 모르는 다른 기능 들도 많겠지만, 꼭 개발쪽의 유닛테스트 프레임윅이 하는 역활과 비슷한 느낌이 든다(저런 프레임윅들은 관련 기능과 함께 조금 논란의 여지는 있지만 'best practice' 를 동시에 제시한다)

 

  matplotlib 으로 그린 그래프 화면은 원래 정답(3, 1)과 비교함 소수점 약간 차이라서, 앞의 2개 그래프와 구분이 안되서 생략했다. 설명의 일관성을 위해 정답을 제한해 놓은게 맘에 안드신다면, 아래와 같이 소스에서 train_X, train_Y 의 배열 값들을 임의의 값으로 바꾸거나 배열 데이터 갯수를 늘여보면 좀더 점들의 중심에 위치하고자 하는 선의 모양을 볼 수 있을 듯 하다. 머신러닝 및 텐서플로우에 이론과 기능에 대한 더 깊은 내용들은 관련 커뮤니티의 강좌나 책을 참고하시면 될듯 하다.

1
2
train_X = numpy.asarray([135912])
train_Y = numpy.asarray([45101813])
cs

 

 

 

[마치면서]

  자 그럼 마무리를 위해 먼 길을 돌아 처음 목적으로 돌아갈 시간이 됬다. 머신러닝에서의 파이썬의 역활은 무엇일까? 개인적인 생각으로는 numpy, panda, matplotlib 과 같이 입출력 데이터를 선, 후처리 할 수 있는 기능을 머신러닝 라이브러리들에게 무료로 제공해 주며, 더 나아가서 세계 공용어인 영어의 역활과 비슷하게 머신러닝과 관련된 여러 관련 라이브러리들이 서로 대화와 협력을 나눌 수 있도록 중재하는 역활을 한다고 하고 싶다. 아마 다른 언어에 의해서 뒤집어 지긴 힘들듯한 이런 중요성 때문에 머신러닝을 배우려는 분들은 꼭 파이썬을 조금씩, 하지만 꾸준히 관심을 가지는게 맞을 듯 싶다.

 

  그리고 다시 한번 얘기하지만 머신러닝 라이브러리는 최근 화려한 스포트라이트와 지원을 받고 있어 그렇지, 어쩌면 원래는 일반 공학 라이브러리와 같은 평범한 라이브러리 출신 일지도 모른다. 그래서 아마도 현재 유행되는 머신러닝 장르 이외에도 데이터의 특별한 측면을 노출해주는 기존의 평범한 알고리즘들이 재조명되어 새로운 머신러닝의 장르를 열수도 있을 것이다. 

 

  또한 앞의 푸리에 변환 라이브러리의 사용과 같이, 사용자가 가진 라이브러리의 동작 원리와 적용 대상에 대한 이해의 깊이에 따라서 그 유용성이 달라지게 된다. 그러므로 머신러닝 프레임윅들의 사용법이나 트레이닝, 오버피팅, 모델, 학습율, 초기값, 오차 등의 방법론들에 집중하는 것도 좋지만, 그것은 어쩌면 그림자만을 쫓는 행위일수도 있으므로, 파이썬을 공부할 때와 마찬가지로 그 기술의 배경이 되는 여러가지의 주변 요소들에 대해서도 교양이라고 생각하고 꾸준히 관심을 가지도록 해보자. 모든 진실은 데이터 안에 숨어있다는 것을 잊지말고...(데이터도 가끔 거짓말을 하긴 하지만 말이다.)

 

 

 

[보충]

1) 이런저런 블로그를 보다보니 푸리에 변환도 컴퓨터 비전 측면에서 머신러닝 쪽에 포함된다고 하는 얘기가 있다. 그럼 예제가 엉뚱한건 아니게 되서 더 다행이다~

 

2) 한 분이 numpy 예제를 보고 "파이썬에선 함수가 2개의 리턴이 가능해요?" 하고 물어보셨다. 그러려니 하고 별 생각 없이 넘어갔던 저였지만--;, 궁금해서 함 찾아봤다. google 에서 'python function multiple return' 으로 찾으면 여러 스타일이 나오는데, 가장 간단한 샘플을 보인다.

https://stackoverflow.com/questions/354883/how-do-you-return-multiple-values-in-python

1
2
3
4
def f():
    return 12
x, y = f()
print ("x = ",x, ",y = ", y)
cs

 

c:\Python\code>python return_test.py
x =  1 ,y =  2

 

3) 아래 글도 괜찮은 듯 해서 참고 자료로 링크를 건다.

머신러닝속 수학(번역)

https://mingrammer.com/translation-the-mathematics-of-machine-learning

 

 

2017.6.25 by 자유로운설탕
cs

 

  

 

 

 

 

 

 

posted by 자유로운설탕
2017. 5. 21. 15:38 프로그래밍

  이번 시간은 지난 시간에 이어 수학 라이브러리의 일환인 그래픽(정확하게는 plotting 이겠지만..) 라이브러리를 살펴보려고 한다. 지난 시간에 언급되었던, matplotlib 과 Plotly 로 예제를 구현해 보면서, 어떻게 사용에 접근하면 좋은지에 대해 얘기해 보려고 한다.

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  우선 예제를 구현해 보기전에 플로팅(plotting) 이라는게 뭔지 잠깐 생각해 보자. plot 이란 특정한 데이터 셋을 공간상에 표시해 주는 것을 이야기 한다. 굳이 2D 평면이 아닌 공간이라고 얘기해도 되는건, 미래에 홀로그램 형태의 3차원 형태의 디스플레이 같은 것이 일반화 된다면 아마 공간 상에 뿌려서 살펴 보는 것도 가능할 것 같아서이다(3D 프린터 같이 말이다). 뭐 여튼 현재 라이브러리는 2차원 화면상에 3차원 좌표까지 표시가 가능하긴 하지만, 아래와 같이 하나의 차원을 색으로 치환해, 평면상에 4차원의 데이터를 표시하는 것도 가능하다(뭐 공간적인 4차원이 아니라고 가짜라고 볼 수도 있겠지만, 어차피 plot 의 목적은 공간에 요소를 단순히 나타낸다기 보다는 각 구성 요소들의 독립, 복합적 관계를 파악하기 위한 목적이 크기 때문에(그 날의 매출 현황이라든지, 이상 이벤트 발생이라든지), 그래프 상에 사람이 차이를 식별할 수 있는 색이나, 점의 모양, 기울기 등도 하나의 인식가능한 차원의 그래프 요소라고 할 수 있을 것 같다.

http://engi-agora.tistory.com/37

 

  우리는 이렇게 좌표 형태로 그려진 그래프를 봄으로써 데이터의 추이, 이상현상, 군집, 분포, 차이 등의 특성에 대해 일련의 데이터 숫자 값들을 직접 보는 것보다 시각적으로 쉽게 파악할 수 있게 된다(물론 의미 있는 데이터를 잘 선택해 적절한 기법으로 뿌려줬을 때의 얘기긴 하지만 말이다). 어느정도 플로팅은 통계나 추이와 같은 감각들을 인간에게 시각적으로 전달해 주는 수학적 도구라고 볼 수 있을 것 같다.

 

 

  개인적으로 이 글을 쓰기 위해서 이런 저런 책과 웹페이지들을 참고해 보면서, 인상 깊었던 2개의 그래프는, 복소수의 곱을 좌표상에 표시해 준 것과, 산포도 이다. 첫번째 복소수의 곱을 좌표상에 표시한 부분은 아래의 페이지에 나온 것 같이 두 복소수를 곱했을 때의 의미가 공간 적으로는 아래와 같이 두 개의 좌표상의 각을 더한 회전으로 나타내게 된다는 것을 이야기 한다.  

[출처 : https://www2.clarku.edu/~djoyce/complex/mult.html ]

 

  두 번째 산포도는 특정한 두 요소가 서로 얼마나 상관이 있느냐 하는 것을 보여 주는 것이다. 예를 들어(통계는 잘 몰라서 예가 적절한진 모르겠지만 --;) 치킨을 100만큼 좋아하는 사람이 맥주를 얼마나 좋아하고, 치킨을 10만큼 좋아하는 사람이 맥주를 얼마나 좋아하는지를 각각 좌표안에 찍는다면, 아래의 그래프와 같이, 오른쪽으로 상승하는 좁은 타원 형태의 그래프 분포를 보인다면, 두 요소는 상관 관계를 가지고 있을 가능성이 존재한다고 합리적 의심을 가져볼 수 있다.

 

  그러면 한번 앞에 언급했던 2개의 많이 쓰이는 그래픽 라이브러리(matplotlib, plotly)를 이용해서, 복소수의 곱과, 산포도를 그려보는 예제를 만들어 보도록 해보자. 앞 시간에 살펴봤던 numpy 를 이용해 데이터를 생성하거나 담는 그릇으로 사용할 것이고, 해당 그릇을 그래프를 그리는 라이브러리에 전달하려고 한다.

 

 

 

 

 

[Matplotlib 을 이용한 복소수의 곱 과정을 그래프로 표시하기]

   뭐 저도 수학은 정말 잘 못해서, 복소수에 대해서 잘 모르기에, 이 예제를 만들기 위해서 책도 좀 읽고, 이것저것 급조를 좀 해봤다. 지나가는 수학과 분이 계시면 그냥 살포시 지나가시기를...

 

 

  위의 방정식의 해를 풀기 위해서는 x의 제곱이 -1 이 되어야 한다. 즉 아래와 같이 x가 루트 -1 이 되어야 하는데, 이 실수 관점에서 보면 혈통을 인정하기 힘든 가상의 수(루트 -1)를 i (복소수)라고 그런다. 그래서 i 의 제곱은 -1 이 된다.

 

그럼 그래프로 그려볼 간단한 곱을 하나 해보자. A 를 0.5+1i, B 를 1+0.5i 라고 하면,

(0.5+1i)*(1+0.5i) = 0.5*1 + 0.5*0.5i + 1i*1 + 1i*0.5i = 0.5 + 0.25i + 1i + 0.5i^2

= 0.5 + 0.25i + 1i + 0.5*(-1) = 1.25i 

 

  음 수식 상으로는 실수 부가 사라지고, 허수부만 '1.25i' 로 남게 된다.(고백하자면 일부러 각도가 합해지는게 잘 인지되는 그림을 그리기 위해서 복소수를 정할때, B는 x축 기준으로 30도, A는 60도 가 되게 만들어서, 두 개를 곱하면 직각인 90도가 되게 만들었다).

 

 

 

  이제 파이썬으로 그래프를 그려 보려 한다. 일단 첫번째로 막히는게, 파이썬에선 복소수를 어떻게 다루지? 하는 부분이다. 어떤 라이브러리를 써야 되는지도 모르겠고, 어떤 문법을 가졌는지도 모르겠다. 구글에다 우선 'python complex number' 라고 검색해 보자.

https://stackoverflow.com/questions/8370637/complex-numbers-usage-in-python

 

  위의 페이지를 보니, 생각보다 휠씬 간단해 보인다. 복소수를 쓰기 위해 complex 란 함수를 이용해도 되지만, 그냥 1 + 2j (파이썬은 i가 아니라 j임, 위키를 보면 전류를 나타내주는 기호 i와 구분하기 위해서 공학 쪽에서 j라고 쓴다고 하는 설이...) 라고 써주면 파이썬이 알아서 인식해 주나보다. 그럼 샘플을 참고하여 해당 방식으로 두 개의 복소수를 곱해서 결과를 화면에 나타내는 코드를 만들어 보자.

1
2
3
4
5
6
#-*- coding: utf-8 -*-
 
= 0.5+1j
= 1+0.5j
= a*b
print(c)
cs

 

  위의 파일을 c:\python\code 폴더에 utf-8 인코딩으로, complextnum.py 라고 저장한다(역시 이 부분을 잘 모르겠으면 2교시에서 복습을!). 이후 아래와 같이 실행해 본다.

c:\Python\code>python complextnum.py
1.25j

 

 

  아까 손으로 열심히 계산한, '1.25j' 결과가 나온다. 그럼 이제 그래프를 그려보기 위해서 일단 matplotlib 을 설치한다.

c:\Python\code>pip install matplotlib
Collecting matplotlib
  Downloading matplotlib-2.0.2-cp35-cp35m-win_amd64.whl (8.9MB)
..
Successfully installed cycler-0.10.0 matplotlib-2.0.2 pyparsing-2.2.0

 

 

  이후 구글에서 'complex number python plot' 라고 검색해서 아래의 스택오버플로우 페이지를 찾았다.

https://stackoverflow.com/questions/17445720/how-to-plot-complex-numbers-argand-diagram-using-matplotlib

 

  제일 밑에 보면, 복소수를 좀 더 잘 나타내는거 같은 극좌표 형식의 그래프도 있는 것 같지만, 좀 더 상황을 간단히 하기위해서(현재 그래프를 그려보는 시간이지, 복소수를 공부하는 시간은 아니므로...), 일반 평면 좌표계를 사용하려 한다.

 

  개인적으로 그리기 전에 원하던 그래프의 모양은 아래와 같았다.

  • 화면에 점만 찍는게 아니라 원점으로 부터 점까지 선이 연결되길 바랬다(그게 좀더 점들의 각도를 잘 보여줄 것 같아서)
  • 그래프의 비율이 실제 좌표와 동일하게 1:1 이길 바랬다. 비율이 다르다면 각도가 왜곡 되어 보일 수 있기 때문에 말이다
  • 각 선은 서로 다른 색으로 표시됬음 했고, 가로-세로 축 선을 표시하고, 그리드도 표시되었음 했다.

 

   여기 까지 계속 읽어 오신 분들은 이제 저랑 함께 초보 수준은 살짝 벗어났다고 생각해서, 읽는 지루함을 없애기 위해서 예전 시간들 처럼 모든 시행 착오를 다 보여주진 않고, 각각의 요소를 해결한 방법들을 간단히 명시후에, 최종 코드를 제시해 보이려고 한다^^

 

 

  먼저 위의 스택 오버플로우 코드를 참고해서 그래프를 그려 보니, 가로축과 세로축의 비율이 일정치 않아 각도가 정확하게 보이지 않았다. 그래서 구글에 '
matplotlib aspect ratio'  로 검색해 아래의 페이지의 코드를 찾았다.

https://matplotlib.org/examples/pylab_examples/equal_aspect_ratio.html

1
plt.axes().set_aspect('equal''datalim')
cs

 

 

  가로, 세로 축을 보이게 하는 코드는 'matplotlib show x y axis' 로 찾아 아래와 같다.

1
2
ax.axhline(y=0, color='k')
ax.axvline(x=0, color='k')
cs

 

 

  그리고 각각의 선의 색을 다르게 표시하기 위해 구글에 'matplotlib plot change color' 라고 검색해서 아래의 페이지에서 관련 코드를 찾았다. 여기서는 그래프에 화살표와 함께 주석을 다는 코드도 있어서 해당 코드를 이용해서 해당 되는 복소수의 선들에 이름표를 달기로 했다.

https://matplotlib.org/users/pyplot_tutorial.html

1
2
3
4
5
6
7
8
# c 가 색을 나타냄
Here are the available Line2D properties.
color or c : any matplotlib color 
 
# 그래프 안에 주석 
plt.annotate('local max', xy=(21), xytext=(31.5),
            arrowprops=dict(facecolor='black', shrink=0.05),
            )
cs

 

 

  참고로 그래프 내의 title 등에 한글 표시를 원하는 분들은 아래의 블로그 같은데에 해결 방법이 있지만('matplotlib 한글 깨짐' 같은 걸로 찾아서), 적용 해보니까 폰트에 따라서 숫자의 '-' 표시가 안 나온다든지 하는 여러가지 사소하게 귀찮은 일들이 생겨서 글의 논점을 흐릴 듯 해서 그냥 전 영문으로 진행했다.

http://pinkwink.kr/956

 

 

  그래서 우여곡절 끝에 완성된 최종 코드는 아래와 같다.

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
#-*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import numpy as np
 
# 수식 정의
= 0.5+1j
= 1+0.5j
= a*b
 
# numpy 에 복소수들 담기
arr = np.array([a, b, c])
 
# 여기서 i 는 색 루프를 돌리기 위해서..
= 0
for x in arr:
   # 칼라 지정용 배열
   arrColor = np.array(['yellow','brown','magenta'])
   # 지정된 색으로 해당 복소수의 실수부(real), 허수부(iamg)를 그림.
   plt.plot([0,x.real],[0,x.imag],'ro-',c=arrColor[i])
   i = i + 1
 
# 그래프 제목
plt.title('Complex Number Multiplication')
# 그리드 보이기
plt.grid(True)
# 가로세로 1:1 비율 만들기
plt.axes().set_aspect('equal''datalim')
# x,y 축 이름 짓기
plt.ylabel('Imaginary')
plt.xlabel('Real')
# 그래프가 보이는 최대 최소 범위 지정
plt.xlim((-2,2))
plt.ylim((-2,2))
# x, y 축 보이기
plt.axhline(y=0, color='k')
plt.axvline(x=0, color='k')
 
# 주석 달기       
plt.annotate('0.5+1i (a)', xy=(0.51), xytext=(0.70.8),
            arrowprops=dict(facecolor='black', shrink=0.01),
            )
 
plt.annotate('1+0.5i (b)', xy=(10.5), xytext=(1.20.3),
            arrowprops=dict(facecolor='black', shrink=0.01),
            )
            
plt.annotate('(1+0.5i)*(0.5+1i) (c=a*b)', xy=(01.25), xytext=(0.21.1),
            arrowprops=dict(facecolor='black', shrink=0.01),
            )           
 
# 그린 그래프 보여주기
plt.show()
cs

 

 

  저장 후 실행하면 아래와 같이 그래프가 보이게 된다. 두 개의 복소수가 곱해져서, 길이는 1.25 가 되고(1.25i), 각도는 x축 기준으로 30도(b), 60도(a)가 합해져 90도(c)가 된 모습을 기하학적으로 확인 할수 있다(개인적으로는 이렇게 대수학이 기하학으로 표시되어 직관적으로 이해되는 부분이 재밌는거 같다~).

c:\Python\code>python complextnum.py

 

 

 

 

 

 

[Plotly 을 이용해 산포도 그리기]

   먼저 진행 하기 앞서서 결론적인 하나의 부분만 얘기하면, plotly 는 matplotlib 처럼 완전 무료는 아니고(커뮤니티 라이센스는 있어 그래프 종류에 따라 무료로 하루에 50~250개의 그래프는 그려볼 수 있다-> 보충 부분에 링크를 걸어놓았지만, Offline 모드로 실행을 할 수 있다. 일단 온라인 모드에만 해당하는 이야기로 정정한다.), 파이썬에서 해당 사이트로 데이터를 보내서, 사이트 내의 계정에 데이터와 그래프를 저장하는 방식같다. 클라우드 그래프 라이브러리라고 보면 될듯 하다. 다만 그래프는 상용이라 그런지 조금 더 기본 디자인이 깔끔해 보이고 이런 저런 export 등 도 가능해 보인다(예제에는 ipython notebook 에 삽입 가능하다고 되어 있는데, 명령어로 실행하는 입장이라서 잘 모르겠다..). 가격별 차이의 상세한 부분은 아래 링크를 참조한다.

https://plot.ly/products/cloud/ 

 

  먼저 plotly 라이브러리를 설치한다.

c:\Python\code>pip install plotly
Collecting plotly
....
Successfully installed decorator-4.0.11 ipython-genutils-0.2.0 jsonschema-2.6.0 jupyter-core-4.3.0 nbformat-4.3.0 plotly-2.0.8 traitlets-4.3.2

 

 

  산포도를 그리는 방법을 찾기 위해 구글에서 'scattergram plotly' 라고 검색하여 아래의 샘플 페이지를 찾는다.

https://plot.ly/python/line-and-scatter/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-*- coding: utf-8 -*-
import plotly 
import plotly.plotly as py
import plotly.graph_objs as go
import numpy as np
 
# 랜덤 값 만들기
= 1000
random_x = np.random.randn(N)
random_y = np.random.randn(N)
 
# 트레이스 만들기
trace = go.Scatter(
    x = random_x,
    y = random_y,
    mode = 'markers'
)
 
data = [trace]
 
# 그리기
py.iplot(data, filename='basic-scatter')
cs

 

 위의 파일을 c:\python\code 폴더에 utf-8 인코딩으로, plotlytest.py 라고 저장한다. 이후 아래와 같이 실행해 본다(...)

c:\Python\code>python plotlytest.py

KeyError: 'plotly_domain'

 

  실행하면 위와 같은 에러가 발생한다. 구글에 'KeyError: 'plotly_domain''로 찾아서 확인해 보면 iplot 대신 plot 을 사용하라고 한다. 아래 해당 부분 코드만 수정해 다시 돌려본다. (편집 환경으로 ipython 을 사용하시는 분들은 에러가 안 날수도 있을 것 같다)

https://stackoverflow.com/questions/34929778/keyerror-plotly-domain-when-using-plotly-to-do-scatter-plot-in-python

1
py.plot(data, filename='basic-scatter')
cs

 

c:\Python\code>python plotlytest.py

Aw, snap! We don't have an account for ''. Want to try again? You can authenticate with your email address or username. Sign in is not case sensitive.
Don't have an account? plot.ly
Questions?
support@plot.ly

 

그런데 이번엔 계정이 없다고 얘기한다(첨에 나온 라이센스 관련 페이지가 이걸 만나서 찾아보다가 가게된 것이다. 그래서 사실 완전 무료가 아닌 이 라이브러리를 소개해야 되나마나 조금 고민하긴 했다). 다시 구글에 'Don't have an account? plot.ly' 라고 검색하여 아래의 안내 페이지를 찾는다.

https://plot.ly/python/getting-started/

 

 

결국 plotly 라이브러리를 사용하기 위해서는 아래 네 가지를 해야한다.

  • 회원 가입
  • API 키 발급(kisa api 를 사용할 때와 비슷하다)
  • 이메일 인증(API키만 발급 하고 해보니, 안되서 이메일 인증도 했다)
  • python 코드안에 계정 정보 넣기. (어떤 원리로 캐싱이 되는진 모르지만 한번 넣고 실행 후엔 해당 계정 정보는 코드에서 빼도 해당 계정으로 실행이 되긴 한다)

 

  그럼 회원 가입 페이지로 가서 회원 가입을 한다. (이메일, 계정 이름, 패스워드)

https://plot.ly/accounts/login/?next=%2Fsettings%2Fapi

 

 

  이후 로그인 한 상태에서 아래 페이지로 가서 API 키를 '재생성' 시킨다. 이후 해당 키를 적당히 메모장 등에 저장해 둔다.

https://plot.ly/settings/api

 

 

  자신의 메일로 가면 plotly 에서 보낸 인증 메일이 와 있을 것이다. 인증 링크를 클릭한다.

 

 

  이후 코드를 최종 수정하여, 아래와 같이 API 키 정보를 집어 넣는다. (whois API 때와 마찬가지로 코드내의 '본인계정', '본인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
#-*- coding: utf-8 -*-
import plotly 
import plotly.plotly as py
import plotly.graph_objs as go
import numpy as np
 
# API 키 정보 넣기
plotly.tools.set_credentials_file(username='본인계정', api_key='본인API키')
 
# 랜덤 값 만들기
= 1000
random_x = np.random.randn(N)
random_y = np.random.randn(N)
 
# 트레이스 만들기
trace = go.Scatter(
    x = random_x,
    y = random_y,
    mode = 'markers'
)
 
data = [trace]
 
# 그리기
py.plot(data, filename='basic-scatter')
cs

 

  다시 저장해 실행해 본다.

c:\Python\code>python plotlytest.py

High five! You successfuly sent some data to your account on plotly. View your plot in your browser at https://plot.ly/~계정이름/0 or inside your plot.ly account where it is named 'basic-scatter'

 

 

  실행이 잘 됬다고 나오면서, 브라우저 창에 https://plot.ly/~본인아이디/0/ url이 뜨면서, 로그인 창이 나온다. 로그인 탭을 눌러 아까 발급 받은 계정으로 로그인 한다.

 

 

  로그인이 되면 아래와 같이 그리려던 그래프가 웹 화면에 보이게 된다. 상단의 'export' 메뉴를 클릭하면 그래프를 이미지 파일로 저장하거나, 데이터를 문서로 만들거나, 다른 언어로(매트랩, R 등) 코드를 내보낼 수도 있다.

 

 

 

 

[마무리 하면서]

  설명은 엄청 장황했지만, 결론은 엄청 간단하다. 그래픽 라이브러리를 쓰는 방법은 아주 단순하다. 데이터들을 잘 모아서 numpy 배열 같은데에 담는다. 이후 plot 해 주는 라이브러리에 해당 배열을 전달해 주면 된다. 그리고 적당히 원하는데로 좌표 공간을 꾸미면 된다. 하지만 이 간단함에 숨어있는 어려움이 올바른 데이터를 수집하고, 그 수집한 데이터의 품질이 충분히 현실과 견주어 의미가 있어야 하고, 해당 특징을 잘 해석해 나타내 줄 적절한 그래프 함수를 선택하는 눈을 키우는 부분이다. 이것은 아마 다음 시간에 다룰 머신러닝에서의 파이썬 측면에서도 비슷한 문제가 될 것 같다.

 

 

 

[보충]

  검색을 하다보니 plotly 를 offline 모드로 하는 예제를 보았다. 이렇게 하면 위와 같은 인증 절차도 필요 없어 지는듯 싶다. (어쩐지 온라인 모드만 되는 툴이 저렇게 인기가 많나 했다;)

http://hamait.tistory.com/800

 

  plot 공식 사이트 쪽 설명은 아래에, 특별한 기능차이가 없다면 오프라인을 쓰는게 더 편할 듯 싶다. 온라인과 오프라인이 차이가 있는지는 명확하게 설명된 내용은 없는것 같다.

https://plot.ly/python/getting-started/

https://plot.ly/python/offline/

 

 

 

 

2017.5.28 by 자유로운설탕
cs

 

 

posted by 자유로운설탕
2017. 5. 6. 23:38 프로그래밍

  이번 시간의 주제는 향후 17교시에 '머신러닝에서의 파이썬의 역활'에 대한 얘기를 풀어보기 위한 사전 작업(밑밥...)이라고 볼수 있다. 파이썬에서 쓰는 여러가지 수학 라이브러리들을 어떤 관점에서 바라보는 것이 좋을지에 대해 설명 페이지와 간략한 샘플들을 통해 살펴보려고 한다.

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  머신러닝 관련 코드들을 보다 보면 numpy 나 pandas 같은 라이브러리(모듈)들이 단골로 등장 하게 된다. 대충 알기에는 해당 모듈들이 데이터들이 담긴 array 등을 처리해 주는 것으로 알고 있지만, 해당 라이브러리들이 도데체 어떤 기능들을 가지고 있길래 그렇게 자주 애용하게 되는걸까? 해당 코드들을 보면서 자주 쓰이는 기능들을 중심으로 이해하는 방법도 있겠지만, 실제 해당 라이브리러가 어떤 목적으로 만들어 졌고, 어떤 범위를 가지고 쓰이는지 전체적으로 살펴보는 것도, 그다진 나쁘진 않은 접근 방법이라고 생각하며 이야기를 시작한다.

 

  우선 파이썬에서 자주 쓰이는 수학 라이브러리들을 리스트업 하기 위해 구글에서 'python math libraries' 라고 검색한다. 아래의 페이지를 보면 2015년 8월 기준으로 파이썬 공식페이지에서 다운로드 수 기준으로 수학 라이브러리들의 순위를 매겼다.

http://www.palrad.com/top-python-math-statistics-libraries-w-12007/ 

 

  위로부터 7개 정도만 쭉 나열해 보면 NumPy, Pandas, Scipy, matplotlib, Patsy, Sympy, Plotly 이다. 이 중 matplotlib, Plotly 는 다음 시간에 살펴볼 그래픽 라이브러리라고 볼수 있고, Patsy 는 통계모델에 대한 라이브러리라는거 같아서 잘 모르니 패스하고, NumPy, Pandas, Scipy, Sympy 네 개의 라이브러리에 대해서 간단한 예제와 함께 살펴보려고 한다.

 

  실제로 'machine learning example' 또는 'tensorflow example' 등으로 구글에서 찾아서 아래의 이런 저런 페이지를 찾아 본 결과 그 이외의 특별한 라이브러리의 사용 예는 보이지 않았다.

http://machinelearningmastery.com/machine-learning-in-python-step-by-step/
https://www.toptal.com/machine-learning/machine-learning-theory-an-introductory-primer

https://github.com/aymericdamien/TensorFlow-Examples

 

 

 

 

[Numpy]

  어떻게 보면 파이썬을 이용하면서 가장 많이 보게 되는 수학 라이브러리 이다. 블로그의 예제들을 찾아보면서 쓰임을 알아 보는 것도 좋지만, 이렇게 유명한 라이브러리들은 설명 문서가 잘되어 있기 때문에, 해당 페이지를 전체적으로 훝어 보려고 한다. 그럼 구글에서 'numpy documentation' 라고 검색하면, 아래의 공식 문서 페이지가 나온다.

https://docs.scipy.org/doc/

 

  분위기를 보면 Numpy 와 Scipy 는 같은 목적에서 시작된 프로젝트 인거 같다. 'Numpy reference' 링크를 클릭한다.

https://docs.scipy.org/doc/numpy/reference/

 

  첫번째 'array objects' 설명을 보면(https://docs.scipy.org/doc/numpy/reference/arrays.html), 여러 차원의 array(수학적 의미로는 '행렬'이 적절할 듯 싶다)를 만들고, 임의의 부분만 짜르고, 특정 요소를 지정하고, 루프를 돌리고, array 차원을 재 배열 하고, 정렬하고 하는 등에 대한 설명들이 있다.

 

  두번째 'Universal functions' 설명을 보면(https://docs.scipy.org/doc/numpy/reference/ufuncs.html), 해당 array 를 수학적 연산에 의해 서로 사칙연산을 한다든지, 각각의 요소에 대해 로그 연산을 한다든지, 서로 비트 연산을 한다든지, 두개의 array 에서 최대 값들만을 뽑아 낸다든지 하는 여러가지 array 에 대한 연산들을 설명한다.

 

  세번째 'Routines' 에서는(https://docs.scipy.org/doc/numpy/reference/routines.html) 이제 상세한 부분으로 들어가 array 의 다양한 생성 방법이나, 합치거나, 자르는 등의 조작, 텍스트 및 스트링 등으로 읽거나 쓰기, 수학이나, 기본 통계함수 적용 등 상세한 사용시 필요한 세부 기능에 대해서 설명하는 듯 하다.

 

  그 이외는 다른 여러가지 기타 설명인걸 보니 결국 numpy 의 범위는 다양한 차원의 array 를 만들고, 해당 array 끼리 연산하거나, 각각의 array 요소들에 수학적 연산을 일괄 적용하거나, array 를 여러 다양한 차원의 형태로 자유롭게 변환하게 만들어 주는 라이브러리라고 봐도 무방할 듯 싶다. 그럼 간단한 샘플을 하나 만들어 보려고 한다.

 

 

  아래의 명령어로 numpy 를 설치 한다.

c:\Python\code>pip install numpy
Collecting numpy
  Downloading numpy-1.12.1-cp35-none-win_amd64.whl (7.7MB)
    100% |################################| 7.7MB 92kB/s
Installing collected packages: numpy
Successfully installed numpy-1.12.1

 

  해당 설명 페이지를 기반으로 샘플 기능을 만들어 보면 아래와 같다. 1) array 를 만들고, 2) 각 원소에 값을 더하거나, 3) array 를 재조정 하고 4) log 함수를 적용하는 등의 일을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#-*- coding: utf-8 -*-
import numpy as np
 
# 2*2 array 를 만든다.
= np.array([[12], [34]])
 
# array 원소들에 모두 1을 더해 b array 를 만든다.
= a + 1
print (b)
 
# b array 를 재 정렬해 1*4 로 c array 로 변환한다.
= np.reshape(b, (1,4))
print (c)
 
# c array 원소들에 log 함수를 적용해 d array 를 만든다. 
= np.log(c)
print (d)
cs

 

  언제나처럼 c:\python\code 폴더에 numpytest.py 라고 저장하고, 아래와 같이 실행을 한다. (잘 모르시는 분은 2교시를 참조한다)

c:\Python\code>python numpytest.py
[[2 3]
 [4 5]]  -> 원래의 array 에 1을 더한 결과
[[2 3 4 5]]  -> 1*4 array 로 재 조정함.

[[ 0.69314718  1.09861229  1.38629436  1.60943791]]  -> 각 요소에 log 함수를 적용함.

 

 

 

 

[Scipy]

  아까 처음의 numpy documentation 페이지에서, 'scipy reference' 링크를 클릭한다.

https://docs.scipy.org/doc/scipy/reference/

 

  목차를 살펴보면 해당 라이브러리는 numpy 구조를 기반으로, 전문적인 과학분야의 수학연산들을 도와주는 라이브러리들이다. 적분(Intergration)이나, 영상이나 신호처리 영역에서 자주 쓰이는 푸리에 변환(fourier Transforms), 선형 대수(Linear Algebra), 통계학(Statistics) 등에서 필요한 수학적 연산들을 모아두었다고 보면 될거 같다. 해당 부분의 특징은 '구슬이 서말이라도 꿰어야 보배'라는 속담처럼 해당 분야에 대한 이해를 바탕으로 특정 연산을 수행하고 싶을 때야 의미가 있을 것 같다. 지금으로선 적분 관련된 예제 하나만 살짝 보고 넘어가려고 한다.

 

 

  우선 scipy 를 설치하는데 오랜만에 에러가 발생했다.

c:\Python\code>pip install scipy
Collecting scipy

...

numpy.distutils.system_info.NotFoundError: no lapack/blas resources found

 

  구글에서 'numpy.distutils.system_info.NotFoundError: no lapack/blas resources found' 라고 찾아서 스택오버플로우 페이지로 간다.

http://stackoverflow.com/questions/28190534/windows-scipy-install-no-lapack-blas-resources-found

 

  빌드 툴을 다운받거나 통합 패키지인 아나콘다를 다운 받으라는 등 여러가지 가이드가 있지만, 그 중에 wheel 파일이 있다고 그걸 가져다 설치해 보라는 내용이 눈에 띈다(예전 시간에 설명했지만, wheel 파일은 빌드된 바이너리가 zip 으로 압축된 형태의 파일이다.) 

 

  명시된 uci 대학교 홈페이지로 가서(http://www.lfd.uci.edu/~gohlke/pythonlibs/#scipy), 64비트이고, python 3.5 를 위해 빌드된 wheel 파일(scipy‑0.19.0‑cp35‑cp35m‑win_amd64.whl)을 다운받으려 하다보니, 아래와 같은 문구가 눈에 띈다. (사실은 해당 문구를 못보고 scipy 설치만 먼저하고 샘플 코드를 실행하니 에러가 나서 구글을 찾아서 다시 돌아 왔다--;). 기존 numpy 만을 설치하면 동작이 안되고 mkl 이라는 패키지와 통합된 numpy 를 설치해야 하나 보다

Install numpy+mkl before installing scipy.

 

  해당 링크로 가면 python 3.5 버전의 64비트 wheel 파일(numpy‑1.12.1+mkl‑cp35‑cp35m‑win_amd64.whl)이 보인다. 위의 scipy wheel 파일과 함께 두개의 파일을 다운하여 c:\python\code 폴더에 저장한다. 그리고 아래와 같이 명령어로 두개를 순서대로 설치한다. 

 

c:\Python\code>pip install numpy-1.12.1+mkl-cp35-cp35m-win_amd64.whl
Processing c:\python\code\numpy-1.12.1+mkl-cp35-cp35m-win_amd64.whl
Installing collected packages: numpy
Successfully installed numpy-1.12.1+mkl

 

c:\Python\code>pip install scipy-0.19.0-cp35-cp35m-win_amd64.whl
Processing c:\python\code\scipy-0.19.0-cp35-cp35m-win_amd64.whl
Successfully installed scipy-0.19.0

 

 

  위와 같은 y = x^2 인 함수에 대해서 0~1 구간을 적분하는 소스는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
#-*- coding: utf-8 -*-
from scipy.integrate import quad
 
# y=x^2 함수를 정의
def myfn(x):
    return x**2
 
# 0~1 사이에 해당 y 함수를 적분 한다.
ans, err = quad(myfn, 01)
print (ans)
cs

 

c:\python\code 폴더에 scipytest.py 라고 저장하고, 아래와 같이 실행을 하면 적분 결과를 볼수 있다.

c:\Python\code>python scipytest.py
0.5

 

 

 

 

[Sympy]

구글에서 'sympy documentation' 이라고 검색한다.

http://docs.sympy.org/latest/index.html

 

  해당 메뉴얼을 보면 보통 대수학이나 기하학에 대한 기능들이 많은데, symbolic(기호) 연산이 가능하다는 특징이 있다고 한다. 이것은 혹시 'mathematica' 라는 툴을 써본 경험이 있는 분은 알 것 같은데(전 개인적으로 좋아하는 툴이다), 마치 수학 공식을 사람이 푸는 것처럼 기호 형태로서 풀이하는 것을 얘기한다. 온라인에서 경험해 보자면, https://www.wolframalpha.com/ 페이지로 가서, 검색 란에 'y=x^2-x-6' 를 넣고 검색을 하면, 해당 함수의 그래프나, 근, 인수분해 결과 및 과정 등을 보여준다.

 

  해당 부분은 수식 계산에도 쓸수 있지만, 수학 공부 할때 병행해 이리저리 사용해도, 뭐 나쁘진 않아 보인다. 먼저 아래와 같이 설치해 본다.

c:\Python\code>pip install sympy
Collecting sympy
....
Successfully installed mpmath-0.19 sympy-1.0

 

 

  위의 wolframalpha 페이지에 넣었던 'y=x^2-x-6' 를 인수분해 하는 코드는 아래와 같다. n승 기호가 '^' 가 아닌 '**' 로 좀 다르다(이런 부분은 만드는 사람 맘대로니...)

1
2
3
4
5
6
7
8
9
#-*- coding: utf-8 -*-
from sympy import *
 
# x 를 심볼로 지정
= symbols('x')
 
# x^2-x-6 의 인수분해를 한다.
ans = factor(x**2 - x - 6)
print(ans)
cs

 

c:\python\code 폴더에 sympytest.py 라고 저장하고, 아래와 같이 실행을 하면 인수분해한 결과가 표시 된다.

c:\Python\code>python sympytest.py
(x - 3)*(x + 2)

 

 

 

 

[pandas]

마지막으로 팬더스 차례다. 마찬가지로 구글에 'pandas documentation' 라고 검색한다.

http://pandas.pydata.org/pandas-docs/stable/

 

  뭐 여기서도 여러가지 설명들이 있지만, 기본적인 기능을 보면 csv, json 등의 데이터를 url 등으로 부터 가져와서 dataframe 이라는 numpy 의 arrary 와 비슷한 저장 공간에 넣거나, 데이터의 기준이 되는 시간 데이터 등을 자동으로 생성해 주거나, 마치 DB 안의 데이터와 같이 join 이나, 정렬, 그룹핑 등이 가능하다. 실제 panda는 numpy 를 베이스로 만들어 졌다고 한다.

http://stackoverflow.com/questions/11077023/what-are-the-differences-between-pandas-and-numpyscipy-in-python 

 

  근데 SQL 문법을 아는 분은 마지막에 있는 'Comparison with SQL' 항목을 살펴보게 되면 pandas 가 무슨일을 할수 있는지를 대략적이겠지만 좀 더 쉽게 이해할 수 있게 되는 것 같다. (R을 아는 분은 R 하고의 비교 링크도 있다) 결국 개인적인 생각으로는 pandas 는 데이터 분석 작업을 위해 메모리상에 구현되 있는 엑셀과 비슷하다고 개념을 잡으면 어떨까 싶다. 엑셀의 여러 내장 함수를 조합해서 다중 배열이나, SQL 의 여러 기능들과 비슷한 작업을 할 수 있듯이, pandas 도 메모리 상에 데이터들을 정렬해 뿌려 놓고 엑셀을 조작하듯이, 해당 데이터 개개에 여러가지 numpy 와 같은 방식으로 연산을 가하거나, DB 에 저장한 데이터 같이 여러가지 조건에 의해서 분류하거나, 조합하거나, 필터링 하는것을 쉽게 해주는 라이브러리라고 생각하면 될듯 싶다.

 

 

  그럼 샘플 실행을 위해 pandas를 설치한다.

c:\Python\code>pip install pandas
Collecting pandas
...
Successfully installed pandas-0.20.1 python-dateutil-2.6.0 pytz-2017.2

 

  pandas 용 dataframe 을 하나 만들고, 그 중에서 SQL 의 where 조건을 적용한 것과 비슷하게 'A' 열이 'fruit' 인 데이터만 추출하는 예제의 코드는 아래와 같다.  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-*- coding: utf-8 -*-
import pandas as pd
import numpy as np
 
# pandas 용 dataframe 을 만든다(메모리 버전 엑셀이라고 생각하자)
df = pd.DataFrame({'A''fruit drink cookie fruit'.split(),
                   'B''orange Coke chocopie mango'.split(),
                   'C': np.arange(4)})
 
# 만든 df 를 출력
print(df)
print('------------------------------')
 
# 뿌리면 아래와 같이 생김.
#      A         B      C
# 0  fruit    orange    0
# 1  drink    juice     1
# 2  cookie   chocopie  2
# 3  fruit     mango    3
 
# df 중에 A 열이 fruit 인 데이터만 추출
print(df.loc[df['A'== 'fruit'])
cs

 

c:\python\code 폴더에 pandatest.py 라고 저장하고, 아래와 같이 실행을 한다.

c:\Python\code>python pandatest.py
        A         B        C
0   fruit      orange    0
1   drink      Coke     1
2  cookie  chocopie   2
3   fruit     mango     3
------------------------------
       A       B      C
fruit  orange   0
fruit   mango  3

 

 

 

 

[마무리 하면서]

  위의 4개의 수학 모듈들을 살펴 보면서(사실 pandas 는 수학 모듈이라기 보단 범용 데이터 처리를 위한 공통 프레임 이라고 보는게 맞을 것 같지만...) 머신러닝이나 기타 분야에서 왜 numpy 나 pandas 같은 모듈들을 애용하는지에 대해서 공감을 가지게 됬음 하는 바램을 가진다. 참 이렇게 보면 파이썬은 여러 모듈들이 엃히고 설키면서 서로를 보조하며 시너지를 내어 더 인기가 많은 듯도 싶다. 이 시간을 진행함으로서 나중에 머신러닝에 대해서 얘기할때는 해당 코드에서 보이는 numpy 나 pandas 코드가 밥위의 콩처럼 분리되어 보여, 머신러닝 모듈 자체에만 집중 하게 되어, 좀 더 설명이 간략하게 될 거라고 기대를 하고 있다. 다음 시간에는 비슷한 목적으로 가공된 데이터를 좌표상에 표시해 주는 그래픽 라이브러리에 대해서 살펴보려고 한다.

 

 

 

 

2017.5.7 by 자유로운설탕
cs
posted by 자유로운설탕
2017. 4. 19. 20:34 프로그래밍

  이번 시간에는 작업 자동화라는 주제를 진행해 보려고 한다. 뭐 그다지 거창한건 아니라는걸 먼저 밝힌다. '작업' 이라는 것은 이전 시간에 얘기했던 웹이나, GUI 등 다른 자동화의 측면도 포함되는 주제지만, 좀더 단순하게 축소해서 윈도우 상에서 작업 하는 여러 자잘한 일들을 파이썬을 이용해서 쉽게 동작되게 만드는 과정이라고 정의해 보자.

 

  예제로는 하위 폴더를 가진 특정 폴더에서 특정한 확장자 들을 가진 파일 들만 zip 으로 압축하여, ftp 에 업로드 한후, 로컬에서 기간이 오래된 zip 파일을 삭제하는 작업을 순차적으로 진행하는 파이썬 프로그램을 만들려고 한다. 그리고 마지막으로 해당 작업을 주기적으로 실행 할때 사용할 수 있는 방법에 대해 살펴 보려 한다. 해당 기능을 구현할때 오직 파이썬 모듈들만을 이용해서 구현이 가능 할 수도 있겠지만, 여기서는 윈도우에서 지원하는 몇 가지 명령어와 무료 압축 프로그램인 7-zip 등을 이용해, 파이썬 코드에서 호출을 통한 구성을 해보려고 한다.

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  작업 자동화에 대해 어떻게 얘기를 풀어볼까 하다가 옛날 얘기로 시작해 보려 한다. 예전에 윈도우가 나오기 전에 도스란 cmd 타입의 OS 만 있을때의 컴퓨터는 지금 x-windows 를 사용안하는 리눅스 서버와 비슷한 모드로, 모든 작업이 까만 cmd 화면에서의 명령어 중심으로 이루어져 있었다(물론 그 안에서도 여러가지 트릭을 사용해서 그래픽으로 표시는 했었다). 도스 기반으로 컴퓨터를 처음 접했던 사람들은 아직도 윈도우, 맥 화면의 PC 나, 스마트 폰의 화려한 화면을 볼때도 왠지 뒷면에 놓여있는 cmd 창의 존재를 쉽게 떨쳐버리진 못할 것 이다.

 

  윈도우즈 또한 많은 부분이 GUI 로 감싸 이루어져 있어, 거의 cmd 창을 사용 안하더라도 가능한 부분들도 많지만, GUI 화면으로는 복잡하게 해야되는 일들을 좀더 간단하게 만들어주는 여러 유용한 명령어라든지, 서버 관리자용 확장쉘인 powershell 이라든지 하는 컴퓨터 관리에 도움을 주는 텍스트 기반의 툴들이 많이 내장되어 있다. 리눅스 또한 여러 쉘이나 유틸 기반의 유용한 툴들이 많으며, 어찌보면 GUI 로 구현된 화면들은 그러한 명령어 기능 중에 사람들이 자주 쓸 것 같은 기능들을 편하게 사용하도록 구현된거라고 봐도 무방할 듯 하다. 그래서 윈도우즈 10같이 점점 사용자들이 잘 안쓰는 디테일한 기능들을 디폴트로 숨기는 최신 OS 의 경우 이것저것 시스템을 건드리는 작업이 필요한 사람들 한테는 어떻게 보면 불편한 느낌까지도 주게되는 것 같다.

 

  뭐 여튼 그래서, 파일 관련 여러 작업들(복사, 압축, 백업..) 등의 기본적인 작업 들에 대해서는 대부분의 언어에서 지원하는 라이브러리보다 시스템에서 기본, 확장 지원 하는 기능들을 호출하여 사용하는 것이 좀더 효율 적인 경우도 있게 된다. 적당히 해당 명령어를 실행하고, 결과가 완료 되기만 기다릴 수 있다면, 어떤 언어를 사용하든 비슷한 효과를 가진 프로그램을 쉽게 만들 수 있게 될 것이다.

 

 

 

[무료 ftp 설치하기]

  그럼 실습을 위해서, 무료 ftp 를 하나 설치해서 운영해 보려 한다. ftp 는 'file transfer protocol' 의 약자로, 서버와 클라이언트 사이에서 서로 파일을 교환하기 위한 오래된 방식이다. 해당 방식을 이용해서, 클라이언트의 파일을 원격지에 있는 서버의 특정 폴더로 이동 등의 작업이 가능하다. 뭐 요즘으로 따지면 구글 드라이브 등의 클라우드, 웹 드라이브와 기능이  비슷하다고 봐도 무방할 듯 하다. 비슷한 용도로 쓰이는 개념으로는 공유 폴더 같은게 있다. 개인적으로 사용하고 있는 ftp 가 있다면 소스의 ftp 코드의 연결 부분만 바꾸어서 그 쪽에서 실습해도 된다.

 

 

[ftp 서버 설치]

  구글에서 'free ftp' 라고 검색하여, 파일질라를 사용하기 위해 https://filezilla-project.org/ 페이지로 이동한다. 'Download FileZiller Server' 버튼을 클릭하고, 다음 페이지에서 소스포지에서 'Download Now' 버튼을 클릭 한다.

 

  다운로드한 파일을 실행을 하여 디폴트로 설치를 진행 하다가, 테스트로 설치한 ftp 가 계속 실행 되고 있는게 좋을 건 없으니 시작 유형만 'Start Manually' 로 바꿔준다.

 

 

  설치 완료 후 '모든 프로그램' > 'FileZiller Server' > 'FileZiller Server Interface' 를 실행 한다. 아래의 다이얼로그가 나오면 'Connect' 버튼을 누른다. 그럼 서버가 실행 되게 된다.

 

  그럼 익명 사용자로 사용해도 되긴 하지만, 아무나 자기 서버에 들어와 사용하는건 그러므로, 사용자를 등록해 세팅해 보자. 그전에 우리가 매 시간 코드를 실습했던 c:\python\code 폴더에 아래와 같이 실습에 사용할 2개의 폴더를 만들어 보자

c:\python\code\ftproot

c:\python\code\ftproot\mybackup

 

  그 후 'Edit' > 'Users' 메뉴를 선택해 보자. 

 

  'Users' 창이 나오면 왼쪽에서 'General' 항목을 선택하고(기본이긴 하다), 오른쪽 'Users' 섹션에서 'Add' 버튼을 클릭한다. 이후 'Add user account' 다이얼로그가 뜨면, 'pyftpuser' 라고 사용자 이름을 넣고, 'OK' 버튼을 누른다.

 

   그리고 패스워드를 'test1234' 라고 적어 준다. 그럼 사용자의 id/pass 가 정해졌다.

 

  사용자를 생성하였으니 그 담에는 사용자에게 사용 가능한 특정 폴더를 할당해 주어야 한다. 사용자가 로그인 하였을때 해당 폴더가 디폴트로 보이며, 해당 폴더 안에서만 이런저런 작업이 가능하다(리눅스의 유저 기본 폴더와 같은 개념이다). 왼쪽에서 'Shared folders' 항목를 선택하고, 아래쪽 'add' 버튼을 눌러서 아까 만든 'c:\python\code\ftproot' 폴더를 선택한다. 이후 오른쪽의 체크 박스를 다 체크해서 해당 폴더에 대해서 풀 권한을 준다. 그리고 맨 하단의 'OK' 버튼을 눌러서, 'Users' 다이얼로그 창을 닫고 메인창으로 온다. 그럼 ftp 운영을 위한 설정이 모두 완료되게 된다.

 

 

[ftp 서버 동작 확인]

  그럼 파이썬 코드를 작성하기 전에, 세팅한 ftp 의 정상 동작을 확인해 보도록 하자(반드시 외부에서 쓰이는 프로그램들은 파이썬 코드를 만들기 전에 다른 외부 수단으로 기본 동작을 확인해 보는게 좋다. 그래야 파이썬 코드에서 연결 에러가 발생 시, 서버 세팅 문제가 아니라 코딩이나 모듈 잘못이라고 체크 하기가 쉬워 덜 헤메게 된다). 아까 파일질라 홈페이지에 있던 클라이언트를 설치해 확인해도 되지만, 그럼 또 클라이언트 사용법도 익혀야 되니, 간단히 하기 위해 브라우저를 하나 띄운 후 주소창에 ftp://localhost 라고 입력한다. 그럼 id/password 를 입력하라는 창이 뜨게 된다(시스템 환경에 따라 좀 느릴 수도 있다). 아까 설정한 pyftpuser/test1234 를 넣는다.

 

  그럼 ftp 에 연결되면서 아까 우리가 c:\python\code\ftproot 안에 만들어 놓은 'mybackup' 폴더가 보이게 된다. (/ 로 표시되는 사용자의 현재 폴더가 ftproot 폴더이다)

 

 

 

 

[7-zip 설치하기]

  그럼 마찬가지로 실습을 하기 필요한 7-zip 이라는 무료 zip 프로그램을 설치해 보자. 구글에서 '7zip' 이라고 검색하여, http://www.7-zip.org/ 로 이동한다.

 

  아래의 다운로드 링크에서 '64비트' 다운로드 링크(제 컴퓨터가 windows 10 home 64bit 라서 그렇다)를 클릭하여 설치한다. 설치 할때는 특별히 신경 쓰지 말고 디폴트 옵션으로 설치하면 된다.

 

 

 

[테스트 파일 준비하기]

  우리가 매일 실습하던 c:\python\code 폴더에 아래와 같이 실습에 사용할 4개의 폴더를 만들어 보자.

c:\python\code\source  (원본 폴더)

c:\python\code\source\subfolder   (원본폴더 서브 폴더)

c:\python\code\target   (복사할 폴더)

c:\python\code\zipfile    (압축 파일 떨굴 폴더)

 

 

  c:\python\code\source 폴더에 png 파일 한개와, txt 파일 한개, ini 파일 한개, exe 파일 한개를 넣어보자. 저 같은 경우 아래 같이 넣었다.

mypic.png
win.ini
winhlp32.exe
사고싶은 책.txt

 

  c:\python\code\source\subfolder 에는 txt 파일 한개를 넣어보자(원래 샘플 파일을 첨부 할까 하다가 왠지 사람들이 다운받기는 찜찜해 할 듯 싶어서 직접 만드는 것으로 했다). 아래와 같이 넣었다.

     사고싶은 책 2.txt

 

  png 파일은 캡쳐 도구 등으로 만들면 되고, txt 파일은 메모장 등으로 적당히 만들면 되고, ini, exe 파일은 c:\windows 폴더에서 복사(시스템 파일이니 옮기면 안된다 복사!!)해서 장만했다. 이렇게 여러 개의 확장자를 가진 파일을 만드는 이유는, 이 중 2개 정도의 확장자를 가진 파일만 선택적으로 압축하는 예제를 보여주고 싶어서이다. 실제 소스등을 백업하려다 보면 용량만 차지하고 백업은 필요없는 파일도 있기 때문에 제외하기 위해서 이다. 뭐 대충 아래와 같은 모양이다.

 

 

 

 

[파이썬 코드 만들기]

  이제 모든 밑작업이 완료 되었으니, 코드 작성을 시작해 보자. 우리가 만드려는 기능을 위해 현재 필요한 사항들을 아래와 같다.

  1) 특정 폴더에서 특정 확장자들의 파일만 서브 디렉토리 까지 포함해서 zip 으로 압축 하기

  2) 압축이 끝날때까지 기다리기

  3) 압축 된 내용을 ftp 에 올리기

  4) 7일 지난 오래된 zip 파일은 하드 용량을 줄이기 위해 삭제 하기

 

  모든 내용이 다 앞의 수업에서는 다루진 않았던 거니 하나하나 살펴보도록 하자.

 

 

[zip 만들기와 기다리기 구현]

  우선 zip 을 만드는데, 2가지 제약 사항이 있다. 첫째는 서브 폴더까지 포함되서 압축이 되야하며, 둘째는 특정 확장자 파일만을 선택 적으로 압축해야 한다. 구글에서 'python zip only extension' 라고 찾으면 아래의 python 에서 사용하는 zip 모듈이 나온다.

https://docs.python.org/2/library/zipfile.html 

 

  해당 모듈을 사용해서 특정 확장자를 포함하려 하면, 아래와 같은 스택 오버플로우에 나오는 방식처럼 특정 확장자를 하나 하나 이름에서 체크하면서, 압축 하거나 풀어야 할것 같다.

http://stackoverflow.com/questions/41965026/extracting-all-the-files-of-a-selected-extension-from-a-zipped-file

 

 

  좀더 간단하게 명령어 하나로만 완료해 보려고 7-zip을 이용하기 위해서, 다시 구글에서,  '7zip include extensions' 이라고 찾았다.

https://superuser.com/questions/409456/how-do-i-use-7-zip-to-add-a-folder-to-an-archive-including-only-files-with-cert

 

  그런데 위에 나타난 방법은 7-zip 명령어 하나만 사용해 구현할 수 있지만, 우리가 원하던 바와는 반대로 특정 확장자들을 포함 하는게 아니라, 특정 확장자들만 제외하는 것이다. 빼야하는 확장자를 잘 알고 압축해야 할 확장자를 잘 모를 경우엔 유용하지만, 반대로 압축해야 될 확장자를 잘알고, 빼야하는 확장자가 많거나 늘어날 수 있다면 조금은 귀찮은 일이다.(뭐 빼는 것도 나쁜건 아닌거 같지만 이렇게 가정해 보자^^) 

 

 

 

  일단은 원하는 확장자만를 압축하는 방식을 찾기 위해서 조금 더 검색을 해본다. 구글에서 '7zip only some extensions' 라고 검색해서 아래와 같이 2개의 페이지를 보다보면. cmd 에서 이용할 수 있는 for 명령어를 사용해서 특정 확장자를 가진 각 파일들을 하나씩 선택해서, zip 으로 만드는 예제가 있다. 그래서 왠지 구현이 잘 될거 같아서, 아래와 같이 만들어 실행해 보았다(몇 가지 테스트를 해보니 아래 페이지들에서 제시된 명령어 그대로 실행 하려면 bat 파일 안에서만 수행이 되며, cmd 창에서 바로 호출해 쓰려면 인자의 %% 를 %로 바꾸어야 한다)

https://superuser.com/questions/923775/7zip-batch-compression-for-a-specific-file-extension-in-different-folders

http://stackoverflow.com/questions/19848192/a-batch-script-to-zip-only-certain-types-of-files 

1
for /R C:/python/code/source %x in (*.txt *.png) do ("c:\Program Files\7-Zip\7z" a "myzip.zip" "%x" )
cs

 

  cmd 창에서 아래 명령을 입력한다.

c:\Python\code>for /R C:/python/code/source %x in (*.txt *.png) do ("c:\Program Files\7-Zip\7z" a "myzip.zip" "%x" )

c:\Python\code>("c:\Program Files\7-Zip\7z" a "myzip.zip" "C:\python\code\source\사고싶은 책.txt"  )

7-Zip [64] 16.04 : Copyright (c) 1999-2016 Igor Pavlov : 2016-10-04

Open archive: myzip.zip
......
Files read from disk: 1
Archive size: 9223 bytes (10 KiB)
Everything is Ok

 

  압축된 zip 파일을 보면 zip 명령어는 넘어온 파일들의 상대 경로를 따지지 않아서, 모든 파일이 하나의 폴더에 들어가 있다. 아래의 그림을 보면 alzip 으로 열었을때, 서브폴더의 파일도 정상적으로 가져오긴 하지만 원래는 source 폴더 안의 subfolder 에 들어가 있어야 할 '사고싶은 책 2.txt' 가 루트에 '사고싶은 책.txt'와 같이 저장되 있는게 보일 것이다. 그럼 해당 방식은 디렉토리 구조를 유지 못하고, 만약 이름이 같은 파일이 있을 경우 충돌이 날테므로 사용할 수 없다.

 

 

 

  그럼 명령어 하나로는 조금 힘들겠다고 생각하고, 2개로 나누어 실행하는 방식으로 다시 전략을 짜보자. 첫번째는 아까 만들어 놓은 target 폴더로 특정 확장자들을 가진 파일들만을 폴더 구조를 유지하면서 복사 후, 7-zip 을 이용하여 원하는 파일만 들어있는 target 폴더를 압축하면 된다. target 폴더의 파일과 디렉토리 들은 압축 완료 후 삭제하여 처음 상태로 리셋하면 된다. 만약 백업 대상 파일 용량이 크거나 숫자가 많아 복사가 부담스러운 경우엔 맨 처음에 찾아봤던 특정 확장자 들을 제외하는 방식으로 구현해도 좋겠지만, 그 경우는 zip 방식이 아닌 폴더 자체를 원격지 폴더와 싱크 시키는 다른 프로그램 등을 이용하는게 더 날지도 모르겠다.

 

  그럼 일단 특정 확장자들만 복사하는 명령어를 찾기위해 구글에서 이것저것 헤메다가 'xcopy copy only certain file extensions' 로 검색해, 앞에 보았던 for 명령을 이용해 파일을 xcopy 명령어로 넘겨서 복사하는 코드를 찾았다.

http://stackoverflow.com/questions/15753420/how-to-copy-specific-file-types-from-one-folder-into-another-folder 

1
for %x in (png txt) do xcopy "c:\python\code\source\*.%x" "c:\python\code\target\" /S /Y
cs

  위의 xcopy 옵션은 아래와 같다. 옵션이 궁금하면 cmd 창에서 'xcopy /?' 를 입력해 본다. 

 /S           비어 있지 않은 디렉터리와 하위 디렉터리를 복사합니다.

 /Y           기존 대상 파일을 덮어쓸지 여부를 묻지 않습니다.

 

 

  해당 명령어를 cmd 창에 입력하면 앞의 zip 과는 달리 다행히 정상적으로 서브 폴더까지 유지되면서 특정 확장자만 복사가 된다.

c:\Python\code>for %x in (png txt) do xcopy "c:\python\code\source\*.%x" "c:\python\code\target\" /S /Y

c:\Python\code>xcopy "c:\python\code\source\*.png" "c:\python\code\target\" /S /Y
C:\python\code\source\mypic.PNG
1개 파일이 복사되었습니다.

c:\Python\code>xcopy "c:\python\code\source\*.txt" "c:\python\code\target\" /S /Y
C:\python\code\source\사고싶은 책.txt
C:\python\code\source\subfolder\사고싶은 책 2.txt
2개 파일이 복사되었습니다.

 

 

  그럼 이제 target 폴더만 이제 전체 압축하면 된다. 7-zip 명령어를 사용하는데 이왕이면 'backup_현재날짜.zip' 으로 날짜를 포함하게 압축파일 이름을 지정하고 싶어서 구글에서 'cmd file name date' 로 검색해 아래 페이지와 참조해서 명령어를 만들어 낸다.

http://stackoverflow.com/questions/4984391/cmd-line-rename-file-with-date-and-time

1
"c:\Program Files\7-Zip\7z" a c:\python\code\zipfile\backup_"%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%".zip c:\python\code\target
cs

 

  위의 명령어를 cmd 창에 입력하면, 우리가 원하는 데로 정상적으로 압축되는 것을 볼 수 있다. 

c:\Python\code>"c:\Program Files\7-Zip\7z" a c:\python\code\zipfile\backup_"%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%".zip c:\python\code\target

7-Zip [64] 16.04 : Copyright (c) 1999-2016 Igor Pavlov : 2016-10-04

Open archive: c:\python\code\zipfile\backup_20170423.zip
...
Files read from disk: 3
Archive size: 9557 bytes (10 KiB)
Everything is Ok

 

 

  그럼 cmd 상으로 위의 명령어를 순차적으로 실행 시키면, 특정 확장자를 가진 파일들이 target 파일로 복사 된후, 'backup_현재날짜.zip' 로 압축되는 작업이 일어난다. 그럼 이젠 이용할 외부 로직은 다 해결 났으니, python 에서 해당 두 명령어를 순차적으로 실행해 주면서, 각각의 명령어가 끝날때까지 다음 코드의 실행을 안하고 기다리게 해야 한다. 구글에서 'python run windows cmd' 라고 검색하면 subprocess 모듈에 있는 'check_output' 명령어를 사용해 보라고 권한다. 근데 문제는 해당 subprocess 모듈로 cmd 명령을 실행 했을때, 실행한 명령이 끝날때까지 파이썬이 다음 코드를 실행 안하고 기다려 줄까 하는 부분이다.

http://stackoverflow.com/questions/14894993/running-windows-shell-commands-with-python

 

  해당 문제를 확인 하기 위해 'python subprocess check_output' 로 검색해 아래 파이썬 라이브러리 메뉴얼을 보면, 다행이 실행한 서브 프로세스가 끝날 때까지 기다린다고 한다(하나의 프로그램이 생성되면 윈도우 내부에 프로세스가 생성되고, 해당 프로세스가 다른 프로세스를 실행 했을때 해당 프로세스를 subprocess 또는 자식 프로세스라고 그런다). call 명령어는 성공 실패 코드만 반환해 가져오는 듯 하고, check_output 은 화면 출력등의 메시지도 받아 올수 있나 보다.

https://docs.python.org/2/library/subprocess.html

 

 

  그럼 첫번째 복사하는 cmd 명령어를 python 의 코드를 이용해서 호출해 동작하는지 검증해 보자.

1
2
3
4
import subprocess 
from subprocess import check_output
 
check_output('for %x in (png txt) do xcopy "c:\python\code\source\*.%x" "c:\python\code\target\" /S /Y', shell=True) 
cs

 

  위의 코드를 복사하여 'c:\python\code' 폴더에 'copytest.py' 라는 이름으로 'uft-8' 포맷으로 저장한다. 실행을 하면 계속 무한 루프가 나면서 이상하게 종료가 안된다. 'ctrl+z' 로 종료를 해본다.

c:\Python\code>python copytest.py

 

  흠 cmd 창으로 그대로 잘 실행됬던 명령어를 그대로 파이썬에서 실행했는데 에러가 나는 듯 하다. 왜 그런가 하고 상상의 나래를 펼치다가 보니 뭔가 하나 걸리는 게 있긴 하다. escape 문자 이다. escape 문자는 보통 어떤 언어에서든 문법적 요소로 쓰는 문자를 사용자의 문자열 안에 넣고 싶을때 해당 문자 앞에 적어주는 회피 문자 이다. 그 회피 문자를 보고 해당 언어는 이 문자가 문법 문자가 아닌 일반 표시 문자라는 것을 인식한다. 구글에서 'python string Double Quotation' 로 검색하면, 아래의 페이지에 escape 문자를 쓰는 방법이 나온다. 예를 들어 " 문자는 \" 로 넣어주어야 하는거다. (한국어 키보드 \(금액 표시) 문자가 밑의 예제 코드에 나오는 역 슬레쉬 이다). escape 문자는 '\n' 같이 키보드로 표시 못하는 기호를 표시할때도 쓴다.

https://docs.python.org/2.0/ref/strings.html

 

  이것 저것 조정해 보다보니 아래 코드로 정리됬다.

1
2
3
4
import subprocess 
from subprocess import check_output
 
check_output('for %x in (png txt) do xcopy \"c:\\python\\code\\source\\*.%x\" \"c:\\python\\code\\target\\\" /S /Y', shell=True) 
cs

 

  다시 실행해 보니 정상적으로 복사가 된다. 그럼 압축 하는 코드 까지 같이 적용해 만들어 보면 아래의 코드가 된다.

1
2
3
4
5
6
import subprocess 
from subprocess import check_output
 
check_output('for %x in (png txt) do xcopy \"c:\\python\\code\\source\\*.%x\" \"c:\\python\\code\\target\\\" /S /Y', shell=True) 
 
check_output('\"c:\\Program Files\\7-Zip\\7z\" a c:\\python\\code\\zipfile\\backup_\"%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%\".zip c:\\python\\code\\target', shell=True)
cs

 

  아래와 같이 실행 하면, cc:\Python\code\zipfile\ 폴더안에 backup_20170423.zip 파일이 생성되게 된다(날짜는 실행한 날짜별로 달라진다).

c:\Python\code>python copytest.py

 

 

 

[ftp 업로드 코드 만들기]

  그럼 만들어진 파일을 ftp 로 업로드하는 코드를 만들어 보려 한다. 구글에서 'python ftp upload' 로 검색을 하면, 아래의 스택오버플로우 페이지에서 바이너리와, 텍스트 형태의 파일을 각각 저장하는 ftp 샘플이 보인다.

http://stackoverflow.com/questions/17438096/ftp-upload-files-python

 

  추가로 만들어진 zip 파일 이름이 'backup_yyyymm' 형식이기 때문에 구글에서 'python filename date' 라고 검색하여 아래 페이지에서 datetime 모듈을 통해 날짜로 이름을 만드는 코드를 얻을 수 있다.

http://stackoverflow.com/questions/10607688/how-to-create-a-file-name-with-the-current-date-time-in-python

 

  두 개의 코드를 합쳐서 아까의 만들어진 zip 파일이 올라가는 코드를 만들어 보면 아래와 같다. 이미 zip 파일은 만들어져 있을테니 위의 압축하는 코드는 일단 제외하고 ftp 코드 부분만 만들어 검증해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-*- coding: utf-8 -*-
import ftplib
import os
from datetime import datetime
 
# datatime 모듈을 사용하여 오늘의 압축 파일 이름을 생성
filename = "backup_" + datetime.now().strftime("%Y%m%d"+ ".zip"
 
# ftp에 연결
ftp = ftplib.FTP("127.0.0.1")
ftp.login("pyftpuser""test1234")
 
# ftp 에서 myback 폴더로 이동함
ftp.cwd("/mybackup")
 
# zip 파일이 있는 폴더로 이동
os.chdir(r"c:\python\code\zipfile")
 
# 바이너리 형태로 파일을 업로드 함
ftp.storbinary("STOR " + filename, open(filename, 'rb'))
 
print ('upload completed')
cs

 

  위의 코드를 'c:\python\code' 폴더에 'utf-8' 인코딩으로 'ftpuplodtest.py' 라고 저장 후 실행해 본다.

c:\Python\code>python ftpuplodtest.py
upload completed

 

  위와 같이 업로드가 완료되었다고 출력되며, ftp 로 로그인 시 사용자에게 할당된 폴더인, 'c:\Python\code\ftproot\mybackup' 폴더로 가보면 backup_20170423.zip 파일이 업로드되어 있다(하나의 컴퓨터에서 ftp 서버를 같이 실행해서, 로컬 경로에서 확인을 하긴 했지만, 실제 프로그램 입장에서는 원격에 있는 ftp 폴더에 업로드 된 것이다)

 

 

[임시파일 삭제와 오래된 zip 파일 삭제]

  target 폴더안에 복사된 폴더와 파일들이 압축 후에는 필요 없기 때문에, 모두 삭제 하기 위해서 구글에서 'delete file and directory in directory' 를 찾았는데, powershell 명령어 이외에는 파일만 지우거나, 폴더 자체를 삭제 하는거나 둘 중에 하나만 가능한 것 같다. 밑에 제시된 바와같이 target 폴더를 지우고 같은 이름의 빈 폴더를 다시 생성하는 방법으로 구현하려 한다(파일 삭제를 테스트 할때는 조심히 테스트 폴더를 만들어 그 안에서만 명령어를 사용하는게 좋다. 저도 code 폴더에서 이것저것 시도해 보다가, 만들어 놨던 샘플 파일들을 싹 지워 버려 블로그에서 주섬주섬 복원해야 했다 --;). 리눅스 rm 명령 처럼 사용법이 모호한 상태에서 잘못 날리게 되면 시스템 파일과 같은 중요한 파일들을 다 지워 버릴 수도 있다.)

1
2
3
4
5
6
7
8
9
#-*- coding: utf-8 -*-
import subprocess 
from subprocess import check_output
 
# target 폴더를 통채로 삭제
check_output('rd /s /q c:\\python\\code\\target', shell=True) 
 
# target 폴더 다시 생성
check_output('md c:\\python\\code\\target', shell=True)
cs

 

  위와 비슷하게 c:\python\code 폴더에 utf-8 인코딩으로 deltest.py 라고 저장해 실행하면 target 폴더 전체를 삭제 된 후, 같은 이름의 빈 폴더를 생성한 것을 볼수 있다.

c:\Python\code>python deltest.py

 

 

  마지막으로 오래된 zip 파일들을 삭제하는 코드이다. 여기서는 7일이 지난 파일을 삭제한다고 해보자. 구글에서 'cmd delete file older than' 이라고 찾으면 아래 스택오버플로우 페이지가 나온다. forfiles 라는 명령어를 통해 7일 이내의 파일을 찾아 각각의 파일에 대해서 del 로 삭제 하는 코드는 아래와 같다. (비슷하게 escape 처리를 했는데, " 문자는 ' 안에 들어 있을때는 굳이 escape 처리를 안해도 되는듯 해서 가독성을 위해 제외했다).

http://stackoverflow.com/questions/51054/batch-file-to-delete-files-older-than-n-days

1
subprocess.call('forfiles /p "c:\\python\\code\\zip " /s /m backup*.zip /d -7 /c "cmd /c del @path"', shell=True)
cs

 

  'check_output' 대신 'call' 을 사용한 이유는 1주일 이상된 파일이 없어 forfiles 결과가 없을 경우엔 에러가 나버리는데, 해당 에러를 출력하는 부분이 파이썬 코드에 영향을 주어서 아래와 같은 오류가 나서, 이것저것 고민하다가 call 은 에러가 나도 실행에 관계가 없어서 대체 했다. 
오류: 검색 조건에 해당되는 파일을 찾을 수 없습니다.
Traceback (most recent call last):
  File "deltest.py", line 7, in <module>

 

 

[최종 코드]

그럼 긴 헤멤을 거쳐서 만들어진 최종 코드는 아래와 같다

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
#-*- coding: utf-8 -*-
import ftplib
import os
from datetime import datetime
import subprocess 
from subprocess import check_output
 
# 압축 코드
# png, txt 파일만 source 폴더에서 target 폴더로 복사한다. 
check_output('for %x in (png txt) do xcopy \"c:\\python\\code\\source\\*.%x\" \"c:\\python\\code\\target\\\" /S /Y', shell=True) 
 
# 7-zip 을 이용해 backcup_yyyymmdd 형식으로 압축을 한다.
check_output('\"c:\\Program Files\\7-Zip\\7z\" a c:\\python\\code\\zipfile\\backup_\"%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%\".zip c:\\python\\code\\target', shell=True)
 
print ('zip completed')
 
 
# ftp 코드 
# datatime 모듈을 사용하여 오늘의 압축 파일 이름을 생성
filename = "backup_" + datetime.now().strftime("%Y%m%d"+ ".zip"
 
# ftp에 연결
ftp = ftplib.FTP("127.0.0.1")
ftp.login("pyftpuser""test1234")
 
# ftp 에서 myback 폴더로 이동함
ftp.cwd("/mybackup")
 
# zip 파일이 있는 폴더로 이동
os.chdir(r"c:\python\code\zipfile")
 
# 바이너리 형태로 파일을 업로드 함
ftp.storbinary("STOR " + filename, open(filename, 'rb'))
 
print ('ftp upload completed')
 
 
# 삭제 코드
# target 폴더를 지움
check_output('rd /s /q c:\\python\\code\\target', shell=True) 
 
# taget 폴더를 재생성
check_output('md c:\\python\\code\\target', shell=True)
 
# 7일 이상 된 로컬의 backup 일을 삭제 함
subprocess.call('forfiles /p "c:\\python\\code\\zipfile " /s /m backup*.zip /d -7 /c "cmd /c del @path"', shell=True)
 
print ('del completed')
 
cs

 

  해당 내용을 'c:\python\code' 폴더에 'utf-8' 인코딩으로 'fileftpbackup.py' 파일로 저장 후 아래와 같이 실행해 본다.

c:\Python\code>python fileftpbackup.py
zip completed
ftp upload completed
오류: 검색 조건에 해당되는 파일을 찾을 수 없습니다.
del completed

 

 

[해당 기능을 주기적으로 돌리기]

  해당 기능을 주기적으로 돌리는 부분도 반복 주기가 짧다면 파이썬의 timer 모듈 등을 사용하여 구현할 수도 있겠지만, 하루에 한 번이라든지 몇 시간에 한번씩 돌릴 예정이라면, 파이썬을 계속 실행시켜 놓는 것보단, 윈도우즈 경우 작업 스케줄러에 걸어놓음 컴퓨터가 재시작 해도, 알아서 지정된 시간이나, 간격으로 돌아가기 때문에 딱히 신경 안 쓰고 반복적으로 실행할 수 있다. 내용이 넘 길어 졌으니 해당 내용은 구글에서 '윈도우즈 10 작업 스케줄러' 로 찾아서 아래와 같은 블로그를 보면 된다. 실행할 프로그램은 'python c:\python\code\fileftpbackup.py' 식으로 등록하면 될듯 하다.

http://blog.naver.com/PostView.nhn?blogId=celine2011&logNo=220673965249&beginTime=0&jumpingVid=&from=search&redirect=Log&widgetTypeCall=true

 

  참고로 만약 실행시 콘솔이 뜨는게 싫다면 아래 안내된 바와 같이,

소스 확장자를 pyw 라고 바꾸어 실행만 하면, pythonw.exe 가 실행이 되면서 콘솔 창이 없는 사일런스 모드로 실행 되는 듯하다.

http://stackoverflow.com/questions/764631/how-to-hide-console-window-in-python

 

 

 

[마무리 하면서]

  어찌 보면 그다지 복잡하지 않은 기능을 꼬아서 복잡하게 설명한 것 같기도 하다 --; 요지는 작업 자동화를 할때 꼭 해당 언어의 스콥 안에서 작업할 필요는 없다는 것이다. 그러다 보면 오히려 외부 cmd 방식의 명령어를 이용하면 좀더 쉽게 구현할 수 있는 코드를 복잡하게 구현하게 되면서 시간이 낭비될 수도 있다. 특히 파일이나 디렉토리 등을 기반으로 하는 작업들은 for 나 forfiles 같이 일괄로 작업하는 명령어 들이나 powershell 기반의 명령어들이 있으니, 가능한 필요한 기능을 구글에 검색해서 가져다 쓰는 것도 좋은 것 같다. 특별한 사정이 없는 이상 내부 모듈이 있다고 꼭 그 모듈을 쓸 필요는 없다고 생각한다. 여튼 이렇게 해서 자동화 시리즈를 마무리 하고, 다음 시간에는 파이썬으로 접근해 보는 머신러닝에 발을 살짝 담궈보기 위한 사전 작업으로 파이썬의 몇몇 수학 라이브러리들이 하는 일들을 살펴보려고 한다.

 

 

[보충]

  최근에 내용을 다시 리뷰하다가, 7zip 으로 여러확장자의 파일이 디렉토리 구조까지 유지하면서 압축 가능하다는 부분을 알게 되었습니다(어쩐지 그런 명령어가 없는게 이상하긴 했습니다--;)

 

 왜 글 쓰는 시점에서 놓쳤는지 모르겠지만, 아래 링크를 보시면 있습니다.

https://stackoverflow.com/questions/28636349/7zip-cli-whitelist-files-to-add-by-extension

 

이 글의 샘플을 예로들면 아래와 같습니다. 삽질을 했네요;

c:\"Program Files"\7-Zip\7z a -r -tzip test.zip c:\python\code\source\*.txt c:\python\code\source\*.jpg

 

 

2017.4.23 by 자유로운설탕
cs
posted by 자유로운설탕
2017. 4. 7. 23:30 프로그래밍

  이번 시간은 Windows GUI 자동화를 살펴보는 시간이다. 만들어 보려는 프로그램은 1) 메모장을 열어서, 2) 작은 python 소스를 입력 후, 3) 콤보 박스에서 인코딩과, 파일 형식을 선택하고 특정 폴더에 저장을 하는 프로그램이다. 이후 이전 시간에 잠시 언급했던 상용 GUI 자동화 툴인 unified functional testing 으로 구현된 코드와 비교해 보며, 오픈소스 모듈과 상용 모듈의 차이점에 대해서 얘기하려 한다.

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  굳이 파이썬 자동화 공부를 하면서 상용 자동화 툴을 언급하는 이유는, 첫째는 파이썬 언어 또는 연관된 selenium,  beaulifulsoup 같은 모듈들을 공부하는 이유가 효율적인 프로그래밍적 사고 및 구현 방식을 찾기 위한 것이라고 생각하기 때문이다. 그런 측면에서 향후 더 효율적인 언어나 개선된 모듈들이 나왔을 때, 현재 언어의 선입견에 갇혀서 고정된 관점에서 새로운 것들을 바라보는 것보다, 현재 사용하고 있는 언어의 장단점에 대해 객관적인 생각을 가지고 있는게 맞을 것 같기 때문이다. 둘째로는 뒤에서 보면 알겠지만, 상용툴은 사람들이 제품을 구입 하도록 어필할 수 있는 부분들이 있어야 하기 때문에, 오픈소스의 경우 수동으로 구축을 해야되는 설계, 구조적 측면 이나, 초창기 버전 들에서 흔히 간과되는 사용성 및 유지보수에 관련된 기능들이 제품 안에 기본으로 포함되어 있는 경우가 많다. 하지만 역으로 그러한 사용자를 돕는 기능들이 툴을 사용하는 프로세스를 고정시켜 버려서, 사용 범위의 제한을 가져오는 독이 될 수도 있다.

 

 

  앞의 selenuim 이나 beautifulsoup 을 보면 해당 모듈이 웹 페이지를 인식 할때, 태그(element)나 속성(attribute), css selector, xpath(이전 시간의 예제에는 사용을 안했지만, selenium은 xpath 를 지원한다. beautifulsoup 은 지원 안 하는 거 같지만, 최근 크롤링에서 많이 사용이 된다고 하는 scrapy 란 모듈도 xpath 를 지원한다). xpath 에 대해선 잘은 모르지만, xml 도큐먼트의 요소들을 정의할 때 사용된다고 하며, 왠지 정규 표현식 같은 스타일이여서, 문법에 익숙해지면 꽤 효율적일 것 같다. xpath 에 대한 상세 내용은 아래 링크를 참고한다.

  https://www.w3schools.com/xml/xpath_syntax.asp

 

 

  비슷한 맥락에서 gui 자동화 모듈은 원하는 개체(윈도우, 메뉴, 버튼, 리스트 박스 등)를 선택하고 조작하기 위해서, 위의 web 자동화의 element, attiribute 와 비슷한 기준 요소가 필요하게 되며, 그런 부분이 class 라든지 text 라든지, 좌표(position) 라든지 하는 속성들이다. 밑의 visual studio 로 gui 프로그램을 작성하는 화면을 보면, 폼 안에 위치한 버튼을 선택 했을때, 오른쪽 properies 창에서 해당되는 버튼 내의 text 등 버튼을 정의하는 많은 속성(property)들을 볼 수 있다. 모든 윈도우즈 gui 프로그램은 이 속성들을 기준으로 개체를 식별하고 메시지들을 교환한다. 이 부분이 바로 오늘 진행하는 내용의 핵심이다.

 

  그럼 앞에서 web 자동화를 구현하는데는 관련된 웹 기술들을 잘 알아야 유리하다고 얘기했듯이, gui 자동화의 구현에는 windows(또는 x-windows 든지 osx 든지) 및 그 환경에서 돌아가는 gui 프로그램 들의 구조에 대해서 잘 아는 것이 유리하다(하지만 잘 알게 되는게 쉬운일은 아니다 --; 이 부분은 저도 초보이다). 

 

 

 

 

 

[GUI 코드 구현]

  그럼 본격적으로 코드 구현으로 들어가 원하는 기능을 만들기 위해 필요한 부분을 생각해 보자.

1) 어떤 자동화 모듈을 사용해야 되는지 결정해야 한다.

2) 메모장을 띄울 수 있어야 한다.

3) 메뉴장의 메뉴를 선택하거나, 글을 입력할 수 있어야 한다.

4) 저장 다이얼 로그에서, 경로를 지정하고, 인코딩 콤보 박스와, 확장자 콤보 박스에서 원하는 항목을 선택하고, 파일 이름을 넣은 후, 저장 버튼을 누르는 작업을 할 수 있어야 한다.

 

 

 

[메모장 실행과 메뉴 선택]

  우선 메모장을 조작할 수 있는 적절한 모듈을 찾기 위해서 구글에 'windows gui automation python notepad' 라고 검색한다.

  https://pywinauto.github.io/

 

  위의 'pywinauto' 라는 모듈의 홈페이지를 보면 원하는 코드가 다 있는 건 아니지만, 기본적으로 메모장을 실행하고, '도움말 > 메모장 정보' 메뉴를 선택해서 창을 띄운 후 닫고, 키를 입력하는 코드가 들어있다. 근데 소스 내용을 보니 영문 윈도우 기준 코드인거 같아서, 한글 윈도우에선 잘 돌아가려는지 확신이 안 든다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
from pywinauto.application import Application
 
# Run a target application
app = Application().start("notepad.exe")
 
# Select a menu item
app.UntitledNotepad.menu_select("Help->About Notepad")
 
# Click on a button
app.AboutNotepad.OK.click()
 
# Type a text string
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

 

  뭐 일단 좀더 정보를 수집하기 위해서 홈페이지 오른쪽에 있는 'Documentation' 링크를 눌러 보자.

  https://pywinauto.readthedocs.io/en/latest/index.html

 

  비교적 도움말이 잘되어 있다, 천천히 살펴보고 싶을 경우는, 맨 위의 'What is pywinauto' 부터 'Methods available to each different control type' 까지의 5개 정도의 설명들을 살펴 보면 될것 같다. 마지막 'Methods available to each different control type' 부분은 menu 나 textbox 등의 GUI 컨트롤 들을 어떻게 다룰수 있는지 설명하는 전반적인 레퍼런스 이다.

 

 

  도움말 첫 페이지의 내용을 보다보면 눈에 띄는 내용이 하나 있는데, 여러가지 오픈소스와 무료 및 상용 자동화 툴을 소개한 리스트가 있다. 몇 가지를 살펴 봤는데, 먼저 python 에서 쓸수 있는 winguiauto,  pyautogui 라는 툴은 메뉴얼이 상세하지 않은거 같아서 제외했고, 파이썬 모듈은 아니지만 독립적으로 돌아가는 무료 어플리케이션인 Autoit 은 윈도우즈 gui 자동화에서 유명하지만, 2015년 9월이후 더 이상 업데이트가 안되고 있어, win 7과 win 10의 지원이 명시되어 있지 않다(혹시 돌리면 돌아가는 지는 잘 모르겠지만 말이다...). 또 활발하게 버전업 되고 있는 다른 무료툴인 autohotkey 는 gui 컨트롤의 자동화 보다는 키보드와 마우스 동작 중심의 macro 프로그램(상용 프로그램으로 따지면 macro express 정도의 포지션)에 좀 더 가까운것 같다. 또 상용 gui 자동화 툴인 Winrunner 는 나중에 언급할 unified functional testing 의 과거 이름이다. silktest 는 예전엔 무척 독특하게 좋다고 생각 했었지만, 그 독특함 때문에 범용적인 winrunner 에게 시장을 많이 뺐긴 후, 오픈소스 와의 통합으로 방향을 틀었었는데 지금은 어찌 되고 있는지 잘 모르겠다. 웹이나 GUI 자동화 자체에 관심있는 분들은 해당 링크의 툴들을 찬찬히 살펴보면 괜찮을 듯 싶다.

 

 

  그럼 우선 도움말에 명시된 pip 명령어를 이용해 pywinauto 모듈을 설치해보자. 아래와 같이 명령어를 입력하면 잘 설치가 된다.

c:\Python\code>pip install pywinauto
Collecting pywinauto
  Running setup.py install for pywinauto ... done
Successfully installed pywinauto-0.6.2

 

 

  그럼 위의 샘플 소스 내용(좀 많이 위가 됬다)을 긁어서, c:\python\code 폴더에 utf-8 인코딩으로 notepad1.py 라고 저장 후, 실행해 보자(실행 부분을 잘 모르겠으면 2교시에서 복습을...)

c:\Python\code>python notepad1.py
Traceback (most recent call last):
  File "notepad1.py", line 7, in <module>
    app.UntitledNotepad.menu_select("Help->About Notepad")

.......
pywinauto.findbestmatch.MatchError: Could not find 'Help' in 'dict_keys(['서식(&O)', '보기(&V)', '도움말(&H)', '편집(&E)', '파일(&F)'])'

 

  그런데 메모장이 실행되긴 하지만, 위의 에러가 난다. 에러가 난 부분을 살펴보면 'Help->About Notepad' 메뉴를 선택하면서 에러가 난 것 이다. 위에서 보면 'menu_select' 를 실행하다 에러가 났고, 메뉴를 찾으려 하는데 'Help' 란 메뉴는 없고 자신이 알고 있는 상단 메뉴들은 (['서식(&O)', '보기(&V)', '도움말(&H)', '편집(&E)', '파일(&F)'] 밖에 없다고 한다. 대충 유추해 보면 'Help' 라는 부분을 pywinauto 가 인지하고 있는 '도움말(&H)' 로 바꿈 될거 같다. 그럼 소스를 수정해 보자(장황하겠지만 우선은 step by step 으로 진행한다--;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# '도움말' > '메모장 정보' 메뉴를 선택한다.
app.UntitledNotepad.menu_select("도움말(&H)->About Notepad")
 
# '확인' 버튼을 눌러서 다이얼로그를 닫는다.
app.AboutNotepad.OK.click()
 
# 메모장에 내용을 적는다.
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

 

  저장 후 다시 실행해 본다.

c:\Python\code>python notepad1.py
Traceback (most recent call last):
  File "notepad1.py", line 8, in <module>
    app.UntitledNotepad.menu_select("도움말(&H)->About Notepad")
...

pywinauto.findbestmatch.MatchError: Could not find 'About Notepad' in 'dict_keys(['', '도움말 보기(&H)', '메모장 정보(&A)'])'

 

 

  위를 보면 앞에 난 에러와 비슷하게 'About Notepad' 부분에서 에러가 발생했다. 그럼 한번 해본거니 비슷하게 대응되는 한글 부분으로 수정한다. 이후 저장 후 다시 실행 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# '도움말' > '메모장 정보' 메뉴를 선택한다.
app.UntitledNotepad.menu_select("도움말(&H)->메모장 정보(&A)")
 
# '확인' 버튼을 눌러서 다이얼로그를 닫는다.
app.AboutNotepad.OK.click()
 
# 메모장에 내용을 적는다.
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

c:\Python\code>python notepad1.py
Traceback (most recent call last):
...
pywinauto.findbestmatch.MatchError: Could not find 'OK' in 'dict_keys(['', 'Edit'])'

 

  그런데 또 에러가 난다. 이번엔 버튼을 클릭하는 부분이다(근데 슬슬 익숙해 지지 않는가?). 이번엔 힌트도 잘 표시되지 않지만, 이 경우는 실제 실행된 메모장 화면을 참고하면 된다.

 

  위의 화면을 보면 위쪽 타이틀엔 '메모장 정보' 가 아래쪽 버튼엔 '확인' 이라는 텍스트 들어가 있다. 여기 까지 오게 되면 메뉴얼을 찬찬하게 보진 않았지만, 조금은 pywinauto 가 gui 개체를 인식하는 방식을 알수 있을 것도 같다. 메뉴는 메뉴의 이름으로(단축키 기호 포함) 접근하고, 창은 타이틀로, 버튼은 버튼에 쓰여 있는 텍스트로 접근하나 보다. 최종으로 샘플 파일을 수정해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# '도움말' > '메모장 정보' 메뉴를 선택한다.
app.UntitledNotepad.menu_select("도움말(&H)->메모장 정보(&A)")
 
# '확인' 버튼을 눌러서 다이얼로그를 닫는다.
app.메모장_정보.확인.click()
 
# 메모장에 내용을 적는다.
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

c:\Python\code>python notepad1.py

 

  실행하면 정상적으로 메모장이 실행되서, '메모장 정보' 창이 열렸다가 닫히고, 메모장에는 pywinauto Work! 라는 글자가 입력이 된다.

 

 

 

[파일 저장하기]

  그럼 이제 utf8 방식으로 인코딩을 선택해, c:\python\code 폴더에 samplecode.py 란 이름으로  저장하는 코드를 구현해 보자. 먼저 메모장에서 저장 메뉴를 선택했을 때 뜨는 '다른 이름으로 저장' 다이얼로그를 한번 살펴 보면서 고민을 해보자.

 

  위의 그림 상에서 원하는 대로 파일을 저장하려면 4가지 부분을 구현해야 하는데, 1) c:\python\code 로 폴더를 선택해야 하며, 2) 파일이름 텍스트 박스에 'samplecode.py' 라고 텍스트를 입력해야 하며, 3) 파일형식 콤보박스에서 '모든 파일' 로 선택해 바꿔 주어야 하고, 4) 인코딩을 ANSI 에서 'UTF-8' 로 변경해 준 후 저장 버튼을 클릭하면 된다. 일단 이렇게 되면 애매한 부분이 각각의 개체들을 어떤 '특성' 으로 접근해야 하는지 알수가 없다. 버튼처럼 타이틀이 있는 것도 아니고, 가리키는 텍스트가 있는 것도 아니다(텍스트 박스 옆에 있는 '파일이름(N):' 이라는 문장은 사실 텍스트 박스와 직접 관계는 없는 독립된 개체이다).

 

 

  일단 아직까진 힌트가 별로 없으니 구글에서 'pywinauto save as dialog' 라고 찾아보자. 그런데 파일을 save 하는 코드는 잘 보이진 않고, 아래의 2가지 스택오버플로우 글이 눈에 띈다. 

 

  http://stackoverflow.com/questions/9482019/how-do-i-select-a-folder-in-the-saveas-dialog-using-pywinauto 

  첫번째 글은 save 하며 폴더를 지정하고 싶은데, 어떻게 지정이 가능하냐는 문의다. 근데 답변 글은 대안을 제시하며, 파일 이름 텍스트 박스에 c:\python\code\samplecode.py 라고 풀 경로로 적으라는 것이다. 폴더를 선택 할수 있는 기능을 실제로 구현할수 있을지도 모르지만(이건 전적으로 pywinauto 를 만든 사람이 해당 컨트롤을 다룰 수 있게 기능을 구현해 넣었냐에 달려있다), 동일한 결과를 가지므로 폴더 선택 코드 부분은 이런 식으로 해결하자. 

 

 

  http://stackoverflow.com/questions/37027644/open-file-from-windows-file-dialog-with-python-automatically/37214623

  2번째 글은 파일을 save 하는 예제는 아니고 open 을 하는 코드이다. 근데 어차피 save 나 open 창이 비슷하기도 하고, 창을 열어 값을 입력하고, (콤보 박스 값을 선택하는 코드)만 넣음 비슷할 듯 하니 이 코드를 참고하자.

1
2
3
4
5
6
7
8
9
from pywinauto import application
 
app = application.Application().start_('notepad.exe')
 
app.Notepad.MenuSelect('File->Open')
 
# app.[window title].[control name]...
app.Open.Edit.SetText('filename.txt')
app.Open.Open.Click()
cs

 

 

  그럼 그 다음은 콤보 박스를 어떻게 선택할 것이냐는 문제가 된다. 구글에서 다시 'pywinauto combobox select' 라고 검색하여 아래의 글을 참고한다.

  https://pywinauto.github.io/docs/code/pywinauto.controls.win32_controls.html

 

  해당 내용들을 pywinauto 에서 다룰수 있는 모든 컨트롤 들을 설명한 페이지인데, 중간 쯤에 보면 콤보 박스 관련 설명이 있다.

class pywinauto.controls.win32_controls.ComboBoxWrapper(hwnd)

   Bases: pywinauto.controls.HwndWrapper.HwndWrapper

....

Select(item)
Select the ComboBox item

item can be either a 0 based index of the item to select or it can be the string that you want to select

 

  콤보박스 컨트롤 뒤에 .select 로 호출해 숫자(순서)나, 이름을 넣음 된단다.

 

 

  근데 여기까지 오니 마지막 문제에 다다르게 된다. 위의 '다른 이름으로 저장' 다이얼로그 화면을 보면, 콤보 박스가 여러 개 있다. 각각의 콤보 박스의 이름을 어떻게 알아 낼 수 있을까? 상용 자동화 툴같이 레코딩 기능(레코딩 버튼을 누르고 사용자가 원하는 행동을 하면 해당 동작을 (완벽하진 않지만) 자동화 코드로 만들어 주는 기능)이라도 지원해 주면, 결과로 저장되는 코드에서 인식되는 개체 이름들을 파악하면 되지만, pywinauto 는 그러한 레코딩 기능도 없는것 같다.

 

  다시 pywinauto 메뉴얼 페이지를 하나씩 훝어보고, 구글을 검색하고 하다가 아래 페이지 들을 찾게 되었다.  
  https://pywinauto.readthedocs.io/en/latest/getting_started.html#attribute-resolution-magic

  http://stackoverflow.com/questions/5039642/how-to-access-the-control-identifiers-in-pywinauto

 

  해결책은 print_control_identifiers() 함수를 사용하게 되면, 각 창 안에 있는 모든 컨트롤 들의 속성들이 쭈루룩 나타난다는 거다. 그걸 보고 원하는 개체를 찾아서 이용해 코딩하면 된다고 한다(이건 윈도우 개발툴인 spy++의 텍스트 버전같다. --; spy++는 아래 글을 참고로...).

  http://happyguy81.tistory.com/51

 

  왠지 좀 노가다 일 것 같은 같은 불길한 예감이 들긴 했지만 뭐 시키는 데로 했다. --;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# 메모장에 code를 적는다.
app.UntitledNotepad.Edit.type_keys("print ('test')", with_spaces = True)
 
# '파일' > '저장' 메뉴 실행
app.UntitledNotepad.menu_select("파일(&F)->저장(&S)")
 
# '다른 이름으로 저장' 창의 속성을 리스트업 한다.
app.다른_이름으로_저장.print_control_identifiers()
cs

 

  위의 내용을 c:\python\code 폴더에 utf-8 인코딩으로 notepad2.py 라고 저장해서 실행해 보자. 헐 그런데 위의 마지막 코드에 의해 '다른 이름으로 저장' 창에 있는 컨트롤 특성들이 쭈르륵 나오는데 거의 100페이지쯤은 된다.(정말 저 함수를 무슨 생각으로 저렇게 나이브하게 만들었는지 싶다. 찾는 사람 입장도 생각해야지...)

 

c:\Python\code>python notepad2.py
Control Identifiers:

Dialog - '다른 이름으로 저장'    (L770, T191, R1709, B800)
['다른 이름으로 저장', 'Dialog', '다른 이름으로 저장Dialog']
child_window(title="다른 이름으로 저장", class_name="#32770")
   |
   | DUIViewWndClassName - ''    (L781, T287, R1698, B697)
   | ['DUIViewWndClassName', '다른 이름으로 저장DUIViewWndClassName']
   | child_window(class_name="DUIViewWndClassName")
   |    |
   |    | DirectUIHWND - ''    (L781, T287, R1698, B697)
   |    | ['DirectUIHWND1', '다른 이름으로 저장DirectUIHWND1', 'DirectUIHWND0', '다른 이름으로 저장DirectUIHWND0', '다른 이름으로 저장DirectUIHWND', 'DirectUIHWND']
   |    | child_window(class_name="DirectUIHWND")

.... 이런 식으로 100페이지쯤 됨

 

 

  내용을 위의 다이얼로그 화면과 비교해 몇번의 시행 착오를 거쳐서, 아래의 항목들을 찾아내서 한땀한땀(정말로 이런 기분으로) 코드를 작성했다.

 

   |    |    |    | Edit - '*.txt'    (L972, T562, R1661, B589)
   |    |    |    | ['다른 이름으로 저장Edit1', 'Edit', '다른 이름으로 저장Edit', 'Edit1', 'Edit0', '다른 이름으로 저장Edit0']

 

ComboBox - '텍스트 문서(*.txt)'    (L969, T595, R1690, B628)
   |    |    |    | ['ComboBox2', '다른 이름으로 저장ComboBox2']
   |    |    |    | child_window(title="텍스트 문서(*.txt)", class_name="ComboBox")

 

   | ComboBox - 'ANSI'    (L1147, T738, R1372, B771)
   | ['ComboBox3', '인코딩(&E):ComboBox']

 

   |    |    |    | Button - ''    (L781, T287, R781, B287)
   |    |    |    | ['Button0', 'Button', '다른 이름으로 저장Button', 'Button1']
   |    |    |    | child_window(class_name="Button")

 

 

-> 불편한 걸 직접 느껴봤음 이제부턴 맨 밑의 '보충 내용'에 있는 AutoHotKey 의 spy++를 써서 이름을 알아내보자!

 

 

[최종 코드]

  결과적으로 아래와 같이 최종 코드를 만들어 내게 됬다.(하지만 솔직히 이런 방식으로 개발하게 되면, 메모장 말고 다른 프로그램을 대상으로 하게 되면 잘 될련지 자신은 없다.)

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
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# 메모장에 code를 적는다.
app.UntitledNotepad.Edit.type_keys("print ('test')", with_spaces = True)
 
# '파일' > '저장' 메뉴 실행
app.UntitledNotepad.menu_select("파일(&F)->저장(&S)")
 
# '다른 이름으로 저장' 창의 속성을 리스트업 한다.
# app.다른_이름으로_저장.print_control_identifiers()
 
# 파일 full 경로 입력
app.다른_이름으로_저장.Edit1.SetEditText("c:\python\code\samplecode.py")
 
# '파일이름' 콤보박스에서 파일 종류 선택
app.다른_이름으로_저장.ComboBox2.Select("모든 파일")
 
# '파일형식' 콤보박스에서 인코딩 선택
app.다른_이름으로_저장.ComboBox3.Select("UTF-8")
 
# 바로 저장 버튼을 누르면 미처 콤보 박스가 안 바뀌어 에러가 나서 1초 시간 줌
import time
time.sleep(1.0)
 
# 저장 버튼 누름
app.다른_이름으로_저장.Button1.click()
 
 
cs

 

  최종 완성된 소스를 저장하고, 실행해 보자. 아래와 같이 해당 폴더에 정상적으로 문서가 저장이 된다.

c:\Python\code>python notepad2.py

 

 

 

 

[최종 코드의 유지보수 문제점]

  위의 코드의 유지보수 문제를 하나 생각해 보자(약간 이건 테스팅 관점이다). 저 코드는 메모장 프로그램이 대상이라 사실 변경될 일이 없지만, 만약 메모장이 아니라 자주 변경되는 프로그램에 대한 코드라 가정하면 '다른 이름으로 저장' 창의 타이틀이 바뀌면 어떻게 될까? 저 코드 내의 모든 '다른_이름으로_저장' 문자열을 모두 바뀐 이름으로 치환해 주어야 한다. 그런데 만약 해당 다이얼로그를 언급하는 파이썬 파일이 수십, 수백개라면 바꾸는 작업은 카오스가 될 것이다(파일이 많아지다보면, 이름들이 일부 겹치기도 하기(저장, 다른이름으로저장, 저장하기 등등) 때문에 일괄로 바꾸게 되면 경험상 분명 예상치 못한 오류가 날수 있다). 그래서 원래 저런 변화될수 있는 값들은 따로 빼놓아 관리하면 좋은데(마치 프로그램에서 하드코딩 하지 않고, 상수 등으로 빼는 것과 비슷하다고 보면 될 듯 하다), pywinauto 의 경우 아쉽게도 아래와 같이 유지보수에 조금이라도 도움을 주는 코딩 방식를 지원하지 않아 에러가 난다.

1
2
3
4
5
6
# 관리를 위해 변수로 뺌
saveAs = '다른_이름으로_저장'
 
# 파일 full 경로 입력
#app.다른_이름으로_저장.Edit1.SetEditText("c:\python\code\samplecode.py")
app.saveAs.Edit1.SetEditText("c:\python\code\samplecode.py")
cs

 

pywinauto.findbestmatch.MatchError: Could not find 'saveAS' in 'dict_keys(['Dialog', 'Notepad', '제목 없음 - 메모장', ' 다른 이름으로 저장', '제목 없음 - 메모장Notepad', '다른 이름으로 저장Dialog'])' <- saveAS 에 대응되는 컨트롤을 못 찾아 이런 에러가 난다.

 

  ※ 아마 내부적으로 이미 '다른_이름으로_저장' 이 컨트롤 이름으로 강제로 취급되어 관리되는 듯 하다(뭐 이 부분은 제가 pywinauto 를 제대로 이해 못해 그럴 수도 있으니 방법이 있을 지도 모른다고 꼬리를 남기는게 날 것 같다).

 

 

 

 

[한 걸음 더 - 상용툴(unified function testing)과의 비교]

  처음에 얘기했듯이, 위의 pywinauto 의 부족한 부분들을 이해하고, 혹시나 더 좋은 모듈이 나온다면 선택 할 수 있게 하기 위해서, 상용툴의 기능과 함 비교해 보겠다. 다만 현재 글의 목적에 맞게 테스팅 관점은 배제하고, 유지보수와 코드 구현의 관점에서만 살펴보려 한다.

 

  해당툴은 회사가 2번인가 바뀌면서 이름까지 바뀌었지만 지금도 왠지 자리를 못잡은 느낌도 나긴 한다(왜냐하면 현재 최신 버전이 윈도우7 밖에 지원 안한다). 그래도 예전의 무거운 느낌의 사용감은 요즘의 SSD 의 힘 때문인지 부드럽게 동작한다. 

 

  지금은 UI 자동화 업무는 명시적으로 안하기 때문에 해당 툴이 없으므로(예전 기준으로 몇 백만원 이상 하기 때문에 이런저런 잡무에 쓰면 좋겠다고 생각은 하지만...) trial 버전을 찾아봤다. 다행히 다운로드 방식의 트라이얼을 제공하고, 트라이얼 기간도 60일로 늘어난듯 해서, 옛 향수를 떠올리며 다운하여 설치를 했다. 뭐 파이썬 강좌이기 때문에 구현 과정을 생략하고 결과만 얘기한다.

 

 

  해당 툴의 레코딩 기능을 이용해서 pywinauto 코드를 비슷하게 구현한 코드가 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 윈도우 시작메뉴에서 notepad 실행함
Window("시작 메뉴").WinObject("항목 보기").WinList("항목 보기").Select "notepad"
 
# 메모장에 test 라고 입력 함
Window("메모장").WinEditor("Edit").Type "print ('test')"
 
# 메뉴에서 파일 > 저장 선택
Window("메모장").WinMenu("Menu").Select "파일(F);저장(S)    Ctrl+S"
 
# 다이얼 로그의 디렉토리 창에서 c:\python\code 로 이동 
Window("메모장").Dialog("다른 이름으로 저장").WinObject("항목 보기").WinList("항목 보기").Activate "python"
Window("메모장").Dialog("다른 이름으로 저장").WinObject("항목 보기").WinList("항목 보기").Activate "code"
 
# 파일 형식과 인코딩 선택
Window("메모장").Dialog("다른 이름으로 저장").WinComboBox("파일 형식:").Select "모든 파일"
Window("메모장").Dialog("다른 이름으로 저장").WinComboBox("인코딩(E):").Select "UTF-8"
 
# 이름 저장
Window("메모장").Dialog("다른 이름으로 저장").WinEdit("파일 이름:").Set "testscript.py"
 
# 저장 버튼 
Window("메모장").Dialog("다른 이름으로 저장").WinButton("저장(S)").Click
cs

 

  위의 코드를 찬찬히 보게되면, notepad 실행도 실제 시작메뉴의 메뉴 리스트를 '선택해' 실행하고, 디렉토리 선택도 실제 디렉토리 리스트를 '선택'해서 이동 했다. 나머지 부분은 거의 비슷해 보인다. 하지만 'object repository' 라는 인식된 컨트롤을 관리해 주는 메뉴로 가면 하나의 중요한 차이가 있다. 아래의 그림을 보면 코드에서 쓰는 이름과 속성들이 트리 형태로 구조적으로 정의되어 있다. 그럼 1) 코드상에 쓰이는 이름을 수정할 수도 있고(아마 관련된 코드에 자동 반영되는 걸로 안다). 2), 4)  속성이 여러개 조합될 수 있고, 추가 삭제도 할수 있다. 그리고 전체적인 윈도우와 그 안의 컨트롤 들에 대한 트리구조를 왼편에 보여준다. 아래 기능은 pywinauto의 print_control_identifiers() 함수의 결과를 시각화해서 관리하는 모드라고 볼 수있다. 이 기능은 실제 써보면 꽤 직관적이고, 메이저 GUI 상용툴은 거의 이런 식으로 컨트롤들의 실제 속성들을 개념적으로 코드와 분리해 관리한다.

 

 

  또 하나는 print_control_identifiers() 의 GUI 버전으로 아까 소개한 spy++ 와 비슷한 object spy 라는 기능이다. print_control_identifiers() 를 매번 코드에 넣어 디버깅 하듯 특성을 확인하는 것보다 스파이형태로 직접 윈도우 창을 클릭해 확인하는게 확실히 효율적이 아닐까 싶다.

 

 

  마지막으로 특정 이미지 영역을 커스텀 컨트롤로 지정해서 인식가능한 기능이 최근 추가됬다고 한다(이 기능은 정말 있었음 하던 기능이다). 이 기능으로 인해서 비표준적인 컨트롤 영역에 대한 인식을 좌표 방식과 이미지 방식 사이에서 취사선택 할 수 있는 선택점이 생긴 것 같다.

https://www.itcentralstation.com/product_reviews/hpe-uft-qtp-review-33718-by-don-ingerson

 

  뭐 업체에서 광고하는 여러가지 다른 요소들도 있겠지만, 제가 느끼기엔 위의 3가지 정도가 현재의 오픈소스 gui 자동화 툴과 상용툴의 가장 큰 차이인 것 같다. 반대로 다양한 언어를 지원하는 것은 보통 상용툴은 한가지 언어(이 툴의 경우 vbscript)만 주로 지원하고, 보통 자바나 .net 정도만 플러그인 같은 형식으로 추가적으로 지원 하기 때문에, 오픈소스 쪽이 휠씬 유리한 것 같다. 혹시 트라이얼 버전을 사용해보고 싶으면 아래의 링크에서 'free trial' 버튼을 클릭해 정보를 넣고, 메일 인증을 받은 후, 로그인해 다운 받으면 된다.

https://saas.hpe.com/en-us/software/uft 

 

 

 

 

[마치면서]

  개인적으로 pywinauto 같은 오픈 소스 gui 자동화 툴은 selenium 의 완성도에 비해서는 아직은 부족한 듯한 느낌이 있다. 하지만 위의 한계들을 인지하고, 구글에 있는 여러 레퍼런스 글들을 참조하여, 다른 좋은 파이썬 모듈들과 결합하여 사용한다면 해당 장점이 단점을 상쇄 할듯 싶다. 개인적으로 상용툴 만큼의 편리성을 지니도록 발전되었음 하는 바램을 가지면서, 다음 시간에는 자동화의 마지막 시간으로 작업 자동화 부분을 진행하려고 한다.

 

 

 

 

[보충내용]

<지나가는나그네님 문의 답변>

1) 브라우저에서 뜬 업로드 팝업을 어떻게 다루느냐에 대해서 autoit, pywinauto, win32 함수를 이용해서 3가지 해결책을 보여줌

https://sqa.stackexchange.com/questions/12851/how-can-i-work-with-file-uploads-during-a-webdriver-test

 

 

2) 위의 글에 있는 pywinauto 예제가 connect 를 안해 에러나서 찾음

https://github.com/pywinauto/SWAPY/issues/45

 

 

3) visual studio 설치시 제공하는 spy++ 를 이용해서 pywinauto 코드를 만드는 설명 - 이 글은 pywinauto 의 아쉬운 부분을 spy++로 보강할 수 있는 꼭 한번 읽어볼만한 글 인거 같음. 요즘은 visual studio 도 개인 개발자에겐 무료인듯 하니 설치해 같이 사용해 보시길... 설치시 옵션에서 공통 툴을 선택해야 깔리다고 함) --> 그리고 그것보다 본문에서 잠시 언급했던 autohotkey 설치할때 같이 깔리는 Active Window Info 툴이 제일 좋다고 쓰여 있다. (밑에 '다른 이름으로 저장' 창에 대한 정보를 보여주는 해당 툴의 그림을 추가했다-> print_control_identifiers 쓰지 말고, autohotkey 에서 깔리는 이 프로그램을 쓰자!!

http://stackoverflow.com/questions/42213490/pywinauto-how-to-select-this-dialog-which-spying-tool-to-use-what-information

 

 

  위의 세 개를 조합하여 만든 코드. 네이버 메일에서 팝업 창이 띄워져 있다는 가정하에서, 아래 코드를 실행 시키면 팝업 창의 파일 이름에 test.txt 라고 쓰여지고 열기 버튼 클릭.(물론 해당 폴더에 test.txt 파일이 없다고 에러 메시지는 날거임. IE 에서 확인)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#-*- coding: utf-8 -*-
from pywinauto.application import Application
import pywinauto
 
# 열려진 다이얼로그 창에 컨넥트 함
app = pywinauto.application.Application()
app.Connect(title="업로드할 파일 선택"
 
# 다이얼로그 창 정의
mainWindow = app['업로드할 파일 선택'# main windows' title
 
# 파일 이름 입력하는 창에가서 'test.txt' 라고 입력
ctrl=mainWindow['Edit'
mainWindow.SetFocus()
ctrl.ClickInput()
ctrl.TypeKeys("test.txt")
 
# 열기 버튼 클릭
mainWindow.Button1.click()
cs

 

 

<비주얼스튜디오_자동화님 문의 답변>

4) 일단 메뉴 아이콘을 인식하여 클릭하는 방법을 찾긴 어려울거 같고, 비주얼 스튜디오 프로그램 특성상 화면의 변경이 없이 매번 일정할 것이기 때문에, 화면 좌표를 이용한 매크로 방식으로 접근하기로 결정.

 

  a) 전체 윈도우 창을 기준으로 클릭 하긴 뭔가 예상못한 일이 생길 수도 있을 듯 해서, 일단 비주얼 스튜디오 윈도우를 인지하고, 그 창 내부의 상대 좌표에서 클릭하기 위해서 'pywinauto click within window' 로 찾아서 아래 페이지를 찾음.

https://stackoverflow.com/questions/28665941/python-click-by-coordinate-inside-a-window

 

  b) Open 아이콘을 클릭하기 위해서, 위에 언급했던, Active Window Info 로 아이콘 위치를 파악.

 

  c)  위의 코드를 적당히 맞춰 변경해서, Open 다이얼로그가 뜨는 것까지 확인 했음. 창이 넘 작으면 위의 메뉴가 겹쳐져서 좌표가 틀려질 수 있으니 주의 할것. 이번에 알았는데 타이틀로 창 이름 지정할시, '공백'이나 '-' 등은 생략해도 인지한다('Start Page - Microsoft Visual Studio' ->StartPageMicrosoftVisualStudio,  '-' 같은 경우는 생략을 안하면 에러가 난다. 아마 공백이나 특수문자는 모두 빼버려도 무방한듯 싶다). --;

1
2
3
4
5
6
7
8
9
10
from pywinauto.application import Application
import pywinauto
 
# 창 타이틀로 프로그램을 연결
app = pywinauto.Application().connect(title='Start Page - Microsoft Visual Studio')
# 메인 윈도우 창을 찾고, (200, 100) 상대 좌표를 클릭함
app.StartPageMicrosoftVisualStudio.ClickInput(coords=(200100))
 
# 최대 창으로 하고 그냥 윈도우 좌표로 클릭하고 싶을 경우는 아래의 코드로
# pywinauto.mouse.click(button='left', coords=(200, 100))
cs

 

 

 

[추가]

파이썬 3.7에서는 pywinauto 설치시 에러가 날텐데, 링크의 설명대로 wheels 를 업데이트 해주면 됩니다^^

https://stackoverflow.com/questions/14296531/what-does-error-option-single-version-externally-managed-not-recognized-ind

 

 

2017.4.8 by 자유로운설탕
cs

 

posted by 자유로운설탕
prev 1 2 next