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

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

Notice

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 자유로운설탕
prev 1 next