2018년 5월 18일 금요일

[Fortify 웹취약성분석] 로그인 기능의 SQL Injection

SQL Injection 을 설명하기 위해서 샘플 코드를 만들어보았습니다.
아래는 로그인 승인을 하는 서블릿 샘플 코드입니다.

package kr.co.kcsp.fortify.sample;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author kcjang
 *
 */
public class LoginServlet extends HttpServlet {

 @Override
 protected void doGet(HttpServletRequest request, HttpServletResponse response)
   throws ServletException, IOException {
  String userId = request.getParameter("userId"); 
  String userPass = request.getParameter("userPass");

  String content=null;
  ResultSet rs=null;
  Connection con=null;
  Statement stmt=null;
  
  String url = "jdbc:mysql://localhost/test";
  
  try {
   Class.forName("com.mysql.jdbc.Driver");
   System.out.println("MySQL JDBC Driver loading Success!");
  } catch(ClassNotFoundException e) {
   System.err.println("MySQL JDBC Driver loading error!");
   System.err.println(e.toString());  
   return;
  }

  //쿼리문 실행
  try{
   con = DriverManager.getConnection(url,"cyber","cyber");
   stmt= con.createStatement();
   String  sql="select id,pass from member where id = '"+userId+"' ";
   sql += " and pass = '"+userPass+"'";
   // 쿼리 를 실행 
   System.out.println("SQL : " + sql);
   rs = stmt.executeQuery(sql);
   
   request.getSession().setAttribute("query", sql);

   if (rs.next()) {
    request.getSession().setAttribute("userId", userId);
    response.sendRedirect(request.getContextPath()+"/login_ok.jsp");
   } else {
    response.sendRedirect(request.getContextPath()+"/login_fail.jsp");
   }

  }catch(Exception e){
   System.err.println("rs.next() Error ");
   System.err.println(e.toString());
   return;
  } finally {
   try {
    if (rs != null)
     rs.close();
    
   } catch (SQLException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } finally {
    try {
     if (stmt != null)
      stmt.close();
    } catch (SQLException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    } finally {
     try {
      if (con != null)
       con.close();
     } catch (SQLException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
    }
   }
   
  }

  
 }

 @Override
 protected void doPost(HttpServletRequest req, HttpServletResponse resp)
   throws ServletException, IOException {
  doGet(req,resp);
 }

 
}
기능은 간단하게 아이디와 비밀번호를 입력받아 쿼리를 만들고 매칭되는 결과 셋이 있으면
로그인을 승인해주고 없으면 승인을 하지 않는 루틴입니다.

물론 실제로 로그인 승인 기능을 만든다면 ID로 쿼리를 한 결과에서 비밀번호를 읽어와
서블릿에서 String 비교를 수행하는 형태로 코딩을 하겠지만 위와 같은 코딩도 가능하다고 생각합니다.
실제로 옛날에 그렇게 코딩했던 생각도 납니다.

위 소스코드는 많은 위험성을 내포하고 있습니다.

아래 포스팅된 게시물의 SQL Injection 과 같은 < ‘ OR ‘a’=’a > 문자를 비밀번호로 입력한다면

  select id,pass from member where id = 'kcjang'
   and pass = '' OR 'a'='a'
라는 쿼리가 되어서 항상 로그인은 승인되게 됩니다.

이것보다 더한 위험성은 만약 <  ‘; delete from member; —  > 을 비밀번호로 입력한다면
모든 회원 정보가 날아가 버리는 사태가 발생합니다. 위 소스는 mysql을 사용하므로 그와 같은 쿼리를 허용하지 않지만
(mysql을 잘 사용하지 않는데 실제로 허용하지 않나요? 이건 테스트를 해보고 게시물을 수정하겠습니다.)
Microsoft(R) SQL Server 2000을 포함한 많은 데이터베이스 서버에서 여러 SQL 문을 세미콜론으로 구분하여 한꺼번에 실행하는 것을 허용합니다. 물론 마지막의 “–” 는 이후 쿼리를 전부 주석처리해버리는 역할을 할테고요…

그렇다면 이런 SQL Injection의 위험성을 어떻게 방지할까요?

첫번째는 사용자 입력값의 검증입니다.
Single Quotate나 Dash 같은 문자의 블랙리스트를 만들어 응답을 거부하는 방법이 블랙리스트이고
특정한 길이의 알파벳 문자열만을 받아들이는 등의 화이트리스트 방법도 있겠습니다.
보다 안전한것은 화이트리스트를 이용하는 방법이 되겠죠.

두번째는  매개변수가 있는 SQL문을 사용하는 방법입니다.
매개 변수가 있는 SQL 문은 일반 SQL 문자열을 사용하여 생성되지만, 사용자가 제공하는 데이터를 포함해야 하는 경우에 이후에 삽입되는 데이터의 자리 표시자인 바인딩 매개 변수를 포함합니다. 다시 말해, 바인딩 매개 변수를 사용하여 프로그래머가 명령으로 처리해야 할 것과 데이터로 처리해야 할 것을 데이터베이스에 명시적으로 지정할 수 있습니다. 프로그램이 SQL 문을 실행할 준비가 되면 각 바인딩 매개 변수에 사용할 런타임 값을 데이터베이스에 지정하여 데이터가 명령 수정 코드로 해석될 위험을 피할 수 있습니다.

위의 코드를 수정하면 다음과 같이 되겠네요.


      String  sql="select id,pass from member where id = ? and pass = ?";

      PreparedStatement stmt= con.prepareStatement();
      stmt.setString(1,userId);

      stmt.setString(2,userPass);
      rs = stmt.execute();

위와 같이 한다면 좀더 안전한 코드를 만들수 있겠죠?

그리고 덧붙여서 위의 서블릿 샘플코드를 Fortify로 돌리면
Hot 3개, Warning 10개, Info 7개 , 초 20개의 위험성이 우루루~~ 쏟아집니다. 

2018년 5월 11일 금요일

[펌] 전역 생성자, 전역 소멸자에 대해

=======================================================================================
Title: gcc-4.1.1/gcc/crtstuff.c 코드 분석을 통한 전역 생성자, 소멸자 분석
Author : 유동훈 (Xpl017Elz) in INetCop
E-mail : szoahc@hotmail.com
Home: http://x82.inetcop.org
=======================================================================================


- 전역 생성자, 전역 소멸자란 무엇인가?

C의 전역 생성자, 전역 소멸자는 C++에서 제공하는 클래스 생성자, 소멸자와 유사한 개념이다.
main 함수 이전과 이후로 전역 개체를 핸들링하기 위해 gcc에서는 두 가지 함수 속성 타입을
제공한다. 이는 main 함수 이전에 개체를 초기화하는 전역 생성자와, main 함수 종료 후
개체를 소멸하는 전역 소멸자로 나뉜다.

--
전역 생성자 함수 속성 타입: __attribute__ ((constructor));
전역 소멸자 함수 속성 타입: __attribute__ ((destructor));
--

전역 생성자와 전역 소멸자가 언제 어떻게 호출되는지 알아보도록 하겠다.



- 함수 호출 순서 분석

함수 호출 순서를 분석을 통해 우리는 전역 생성자와 전역 소멸자가 언제 호출되는지 정확히
알 수 있다. 또한, 어떤 함수가 이들을 핸들링하는지도 정확히 분석할 수 있다. gcc-4.1.1,
glibc-2.5 버전을 사용하는 Fedora core 6 시스템에서 분석한 결과이다.

--
_start 함수 (사용자 프로그램)
|
+- __libc_start_main() 호출 (libc.so)
   |
   |<__libc_start_main+127>:       call   *%edi
   +- __libc_csu_init() 호출 (사용자 프로그램)
   |  |
   |  +- _init() 호출 (사용자 프로그램)
   |     |
   |     +- __do_global_ctors_aux() 호출 (사용자 프로그램) - 전역 생성자
   |
   |
   |<__libc_start_main+217>:       call   *0x8(%ebp)
   +- main() 함수 호출 (사용자 프로그램)
   |  |
   |  +- libc.so 라이브러리 함수 재배치 수행 (※ 재배치 문서 내용 참고할 것)
   |
   |
   |<__libc_start_main+223>:     call   0x81a850 <exit>
   +- exit() 함수 호출 (libc.so)
      |
      |
      |<exit+231>:    call   *%eax
      +- _dl_fini() 함수 호출 (ld-linux.so)
         |
         |
         |<_dl_fini+480>:        call   *%eax
         +- _fini() 함수 호출 (사용자 프로그램)
            |
            +- __do_global_dtors_aux() 호출 (사용자 프로그램) - 전역 소멸자
--

참고로, 수행에 중요한 부분만 간추린 것이며, 전역 생성자와 소멸자를 핸들링하는 함수를
찾을 수 있게 되었다. 전역 생성자는 __do_global_ctors_aux() 함수에 의해서 다뤄지고,
전역 소멸자는 __do_global_dtors_aux() 함수에 의해서 다루어진다. init() 호출과
exit()->fini() 호출에 의해 수행된다는 점을 기억하자.



- 전역 생성자, 소멸자를 사용하는 프로그램 예제와 디버깅 결과

그럼, 실제로 전역 생성자와 소멸자를 다루는 간단한 프로그램을 소개하도록 하겠다.

--
[root@localhost tmp]# cat test.c
#include <stdio.h>

void __attribute__((constructor)) ctors_1()
{
        printf("ctors_1();\n");
}

void __attribute__((constructor)) ctors_2()
{
        printf("ctors_2();\n");
}

int main()
{
        printf("main();\n");
}

void __attribute__((destructor)) dtors_1()
{
        printf("dtors_1();\n");
}

void __attribute__((destructor)) dtors_2()
{
        printf("dtors_2();\n");
}

[root@localhost tmp]# gcc -o test test.c
[root@localhost tmp]# ./test
ctors_2();
ctors_1();
main();
dtors_1();
dtors_2();
[root@localhost tmp]#
--

결과를 살펴보면, constructor 속성 함수는 main() 함수 이전에 호출되었고, destructor 속성
함수는 main() 함수 이후에 호출된 것을 볼 수 있다. 여기서 재미있는 점은 전역 생성자
함수의 경우, 맨 나중에 선언된 전역 생성자 함수가 제일 처음 호출된다는 점이다. 어떤
내용을 참고하여 각 호출이 이루어지는지 다음 디버깅 결과를 살펴보면 한 눈에 확인할 수
있다.

--
[root@localhost tmp]# objdump -h test | grep -e ctors -e dtors
 15 .ctors        00000010  080494e8  080494e8  000004e8  2**2
 16 .dtors        00000010  080494f8  080494f8  000004f8  2**2
[root@localhost tmp]# gdb -q test
(no debugging symbols found)
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) x &__CTOR_LIST__
0x80494e8 <__CTOR_LIST__>:      0xffffffff
(gdb)
0x80494ec <__CTOR_LIST__+4>:    0x0804837c ; ctors_1() 함수
(gdb)
0x80494f0 <__CTOR_LIST__+8>:    0x08048394 ; ctors_2() 함수
(gdb)
0x80494f4 <__CTOR_END__>:       0x00000000
(gdb) x/i 0x0804837c
0x804837c <ctors_1>:    push   %ebp
(gdb) x/i 0x08048394
0x8048394 <ctors_2>:    push   %ebp
(gdb) x/x &__DTOR_LIST__
0x80494f8 <__DTOR_LIST__>:      0xffffffff
(gdb)
0x80494fc <__DTOR_LIST__+4>:    0x080483da ; dtors_1() 함수
(gdb)
0x8049500 <__DTOR_LIST__+8>:    0x080483f2 ; dtors_2() 함수
(gdb)
0x8049504 <__DTOR_END__>:       0x00000000
(gdb) x/i 0x080483da
0x80483da <dtors_1>:    push   %ebp
(gdb) x/i 0x080483f2
0x80483f2 <dtors_2>:    push   %ebp
(gdb)
--

디버깅 결과를 통해 우리가 선언한 함수들이 __CTOR_LIST__ 섹션과 __DTOR_LIST__ 섹션에
포인터로 적재되어 있는 것을 볼 수 있다. 그럼, 왜 이런 구조를 갖는지
gcc-4.1.1/gcc/crtstuff.c 코드 분석을 통해 답을 찾아보도록 하겠다.



- gcc-4.1.1/gcc/crtstuff.c 코드 분석

우리는 crtstuff.c 코드 전체를 분석할 필요가 없다. 이들 중 __CTOR_LIST__ 섹션과
__DTOR_LIST__ 섹션을 참조하여 수행되는 실속있는 코드들만 분석해보면 쉽게 답을 얻을 수
있다. 먼저, 전역 생성자를 핸들링하는 __do_global_ctors_aux() 함수이다.

-- __do_global_ctors_aux() 함수 --
...
   145  /*  Declare a pointer to void function type.  */
   146  typedef void (*func_ptr) (void);
...
   513  static void __attribute__((used))
   514  __do_global_ctors_aux (void)
   515  {
   516    func_ptr *p;
   517    for (p = __CTOR_END__ - 1; *p != (func_ptr) -1; p--)
   518      (*p) ();
   519  }
--

참고로, func_ptr은 void 형을 뜻한다. 잘 살펴보면, __CTOR_END__-1(워드 단위) 위치를
기준으로 p의 값이 -1(0xffffffff)이 나올 때까지 검사하며, p(__CTOR_LIST__ 섹션)에 위치한
함수들을 하나씩 호출하는 것을 볼 수 있다. 결국, __CTOR_LIST__ 섹션 시작 부의 0xffffffff 
값을 만나면 전역 생성자 함수 호출을 멈추게 된다. 이러한 이유로 constructor에 예약된
전역 생성자 함수 중 맨 나중에 선언된 함수가 제일 처음으로 수행된 것이다.

다음은 해당 코드를 disassemble 한 결과이다.

-- __do_global_ctors_aux() 함수 --
<__do_global_ctors_aux+0>:   push   %ebp
<__do_global_ctors_aux+1>:   mov    %esp,%ebp
<__do_global_ctors_aux+3>:   push   %ebx
<__do_global_ctors_aux+4>:   push   %edx
<__do_global_ctors_aux+5>:   mov    0x80494f0,%eax // (1) __CTOR_LIST__ 내용(값) 복사
<__do_global_ctors_aux+10>:  cmp    $0xffffffff,%eax // (2) -1인지 검사
<__do_global_ctors_aux+13>:  je     0x8048485 <__do_global_ctors_aux+33>
<__do_global_ctors_aux+15>:  mov    $0x80494f0,%ebx // (3) 주소 위치 복사
<__do_global_ctors_aux+20>:  call   *%eax // (4) 호출
<__do_global_ctors_aux+22>:  mov    0xfffffffc(%ebx),%eax // (5) -4 위치 내용(값) 복사
<__do_global_ctors_aux+25>:  sub    $0x4,%ebx // (6) -4 만큼 감소
<__do_global_ctors_aux+28>:  cmp    $0xffffffff,%eax // (7) -1인지 검사
<__do_global_ctors_aux+31>:  jne    0x8048478 <__do_global_ctors_aux+20>
<__do_global_ctors_aux+33>:  pop    %eax
<__do_global_ctors_aux+34>:  pop    %ebx
<__do_global_ctors_aux+35>:  leave
<__do_global_ctors_aux+36>:  ret
<__do_global_ctors_aux+37>:  nop
<__do_global_ctors_aux+38>:  nop
<__do_global_ctors_aux+39>:  nop
--

0x80494f0 주소는 __CTOR_END__ -1(워드 단위) 위치이며, 이 주소가 가진 값을 %eax
레지스터에 넣고 call *%eax 코드로 해당 위치에 있는 함수를 호출한다. %ebx 레지스터는
__CTOR_END__-1 위치부터 4byte씩 자신을 감소시키며, 자신이 가지고 있는 주소 값을 %eax
레지스터에 복사하는 역할을 한다. 결과적으로, %eax 레지스터에 -1(0xffffffff) 값이 나올
때까지 함수를 반복 호출하게 된다.

그럼, 이번에는 전역 소멸자를 핸들링하는 __do_global_dtors_aux() 함수를 살펴보자.

-- __do_global_dtors_aux() 함수 --
...
   145  /*  Declare a pointer to void function type.  */
   146  typedef void (*func_ptr) (void);
...
   260  static void __attribute__((used))
   261  __do_global_dtors_aux (void)
   262  {
   263  #ifndef FINI_ARRAY_SECTION_ASM_OP
   264    static func_ptr *p = __DTOR_LIST__ + 1;
   265    func_ptr f;
   266  #endif /* !defined(FINI_ARRAY_SECTION_ASM_OP)  */
   267    static _Bool completed;
   268
   269    if (__builtin_expect (completed, 0)) // 0인지 검사
   270      return;
...
   281    while ((f = *p))
   282      {
   283        p++;
   284        f (); // 해당 위치 값이 null이 아닐 경우 함수 호출
   285      }
...
   300    completed = 1; // 함수 내용이 두 번 이상 호출되지 않도록
   301  }
--

__DTOR_LIST__ +1(워드 단위) 위치부터 함수 호출을 시작하며, completed 섹션의 값이 0인지
검사 후, 함수 호출을 시작한다. 1일 경우, 이미 __DTOR_LIST__ 섹션에 있는 함수들이 수행된
것으로 간주하고 함수 수행을 중단한다. 함수 호출은, p 섹션 위치를 4byte씩 증가시켜
반복적으로 호출하며, null일 경우 함수 호출을 중단하게 된다. 앞서, constructor와는 달리,
destructor는 예약된 함수를 순서대로 호출하는 것을 볼 수 있다.

다음은 해당 코드를 disassemble 한 결과이다.

-- __do_global_dtors_aux() 함수 --
<__do_global_dtors_aux+0>:   push   %ebp
<__do_global_dtors_aux+1>:   mov    %esp,%ebp
<__do_global_dtors_aux+3>:   sub    $0x8,%esp
<__do_global_dtors_aux+6>:   cmpb   $0x0,0x80495fc // (1) completed 섹션 값이 0인지 검사
<__do_global_dtors_aux+13>:  je     0x804833e <__do_global_dtors_aux+30>
<__do_global_dtors_aux+15>:  jmp    0x8048350 <__do_global_dtors_aux+48>
<__do_global_dtors_aux+17>:  lea    0x0(%esi),%esi
<__do_global_dtors_aux+20>:  add    $0x4,%eax // (5) %eax 4byte 증가
<__do_global_dtors_aux+23>:  mov    %eax,0x80495f8 // (6) p 섹션에 증가한 주소를 복사
<__do_global_dtors_aux+28>:  call   *%edx // (7) 함수 호출
<__do_global_dtors_aux+30>:  mov    0x80495f8,%eax // (2) p 섹션 값을 %eax에 복사
<__do_global_dtors_aux+35>:  mov    (%eax),%edx // (3) %eax가 가진 값을 %edx에 복사
<__do_global_dtors_aux+37>:  test   %edx,%edx // (4) 해당 위치에 값이 있는지 검사
<__do_global_dtors_aux+39>:  jne    0x8048334 <__do_global_dtors_aux+20>
<__do_global_dtors_aux+41>:  movb   $0x1,0x80495fc
<__do_global_dtors_aux+48>:  leave
<__do_global_dtors_aux+49>:  ret
--

0x080495fc는 completed 섹션이며, 이 값이 0인지 검사하여 수행을 여부를 결정 짓는다.
0x080495f8는 p 섹션이며, __DTOR_END__+1(워드 단위) 위치 주소 값을 가지고 있다. 이 주소를
%eax에 넣고, 해당 위치 값을 %edx에 넣어 null인지 검사한다. 호출할 예약 함수가 있는 경우,
4byte만큼 증가시킨 %eax 레지스터를 p 섹션에 복사하므로써, 다음 위치에 있는 함수가
반복적으로 호출되도록 구성한다.



- 정리

지금까지 전역 생성자 constructor와 전역 소멸자 destructor에 대해 알아보았다. 전역 생성자와
소멸자, 재배치 부분은 사용자 프로그램 내에서 매우 중요한 역할을 하고 있다. 부디 이 글을
통해 한층 더 재미있고 즐거운 exploit 작성에 도움이 되었길 빈다.


- 레퍼런스

최호진 - main 함수 이전에 일어나는 일에 대한 간단한 추적
Guido Bakker - Overwriting ELF .dtors section to modify program execution