WriteFileHook.cpp



 - 위 소스는 Kernel32.dll의 WriteFile 함수를 후킹하는 DLL의 소스코드입니다. 32비트의 경우 밑에서 설명할 Redirect 루틴을 탈 경우 0x25FF(Absolute Jump)를 사용하고, 실제 함수 구현체 루틴으로 갈 경우 E9(Relative Jump)를 사용합니다. 64비트의 경우 Redirect 루틴을 탈 경우 KernelBase의 함수를 후킹하며, 실제 함수 구현체 루틴으로 갈 경우엔 해당 함수를 바로 후킹하는데 둘 모두 기존 코드를 점프에 필요한 크기만큼 백업해두고 인라인 후킹을 진행합니다.


 - Windows 7에서부터 Kernel32.dll 외에 KernelBase.dll 이라는 DLL 파일이 생겨 일부 함수들에 대해 구현은 KernelBase.dll에 존재하고 Kernel32.dll 을 통해호출시 KernelBase.dll 의 함수로 리다이렉트시킵니다. 자세한 내용은 여기 참고. Redirect Call 명령은 0x25FF 로 Absolute Jump를 하고 있기 때문에 x86 환경에서는 이를 주소 오퍼랜드만 변경하여 쉽게 후킹이 가능하지만 x64환경에서는 6바이트만으로는 원하는 메모리 주소로의 점프가 불가능합니다. 이를 위해 위 코드에서는 64비트 환경에서 KernelBase.dll 로의 리다이렉트 코드를 발견[각주:1]할 경우 KernelBase.dll 에서 해당 함수의 주소를 가져와 인라인 후킹을 진행하도록 작성하였습니다.


 - 함수를 후킹한 후 내부에서 기존 함수를 실행하여 결과를 그대로 반환하고 싶은 경우에는 인라인 후킹을 진행할 때 NOP 다섯 개와 MOV EDI, EDI; PUSH EBP; MOV EBP, ESP로 총 10바이트의 여유 공간이 있기 때문에 이를 이용하여 후킹 함수로의 점프 5바이트, 후킹 함수 내에서 기존 함수 주소 -5로의 점프[각주:2], 기존 함수 주소 -5에 PUSH EBP; MOV EBP, ESP와 기존 함수 주소 +5로의 점프(EB 05)[각주:3]를 넣어 언훅을 하지 않고도 호출이 가능하게 할 수 있습니다. 하지만 x64에서는 함수 도입부가 정형화되어있지 않은데다 위에서 설명한 KernelBase.dll로의 Redirect Call이 있을 경우 6바이트의 여유밖에 없기 때문에 이것이 불가능하여 위 코드에서는 어떤 방식으로 후킹을 하던 간에 내부에서 언훅 후 기존 함수를 실행하고, 다시 후킹을 건 다음 리턴하도록 작성해 두었습니다.


 - 145번째 줄에서 타겟 함수 주소 - 5를 하는 이유는 여기 참고




  1. 260번째 줄 [본문으로]
  2. 145번째 줄 [본문으로]
  3. 148번째 줄 [본문으로]
블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag Hooking

댓글을 달아 주세요

 LD_PRELOAD를 이용하여 고정 인자 함수를 후킹하는 방법의 경우에는 여기저기 설명이 잘 된곳이 많으므로 생략하겠습니다.

이런 블로그 글들을 참고하시면 되겠습니다.


 LD_PRELOAD를 이용하여 함수들을 후킹하는 도중, 인자의 개수가 정해진 것이 아니라 가변적인 함수라면 어떻게 후킹을 해야 하는지 고민을 한 적이 있습니다. 대표적인 예가 printf, sprintf류의 포맷팅 함수들입니다. 이런 함수들은 포맷 문자의 개수에 따라 뒤따라 들어가는 인자의 개수가 계속 늘어날 수 있습니다. 이 포스팅에서는 이런 함수들을 LD_PRELOAD를 이용해 후킹하는 방법에 대해서 설명합니다.


 우선 가변 인자 함수를 만드는 방법에 대하여 알아보아야 합니다. 관련된 내용은 여기에 자세히 잘 정리되어 있습니다.

위 내용을 참고하여 fprintf 함수를 후킹하는 코드를 작성하면 다음과 같이 나오게 됩니다.


1
2
3
4
5
6
7
8
9
int fprintf(FILE *fp, const char *format, ...)
{
    int ret;
    va_list ap;
    va_start(ap, format);
    // 원하는 후킹 행위
    va_end(ap);
    return ret;
}
cs


 위 코드와 LD_PRELOAD를 이용하면 바로 fprintf 함수를 후킹하는 것이 가능합니다. 하지만 아직 완전하지는 않습니다. 문제는 바로 원본 함수인 fprintf 함수를 실행시킬 수가 없기 때문입니다. 이미 스스로 fprintf 함수를 정의해버렸기 때문에 그냥은 libc 함수를 호출할 수 없고, 만약 dlsym(RTLD_NEXT, "fprintf")을 이용해 libc의 fprintf 함수 주소를 가져오면 실행은 시킬 수 있겠지만 fprintf는 말 그대로 가변 인자 함수이기 때문에 인자가 몇 개 들어갈지 알 수 없는 상황에서 사용할 수가 없습니다.


 이럴 때 사용할 수 있는 대체 함수들이 libc에 정의되어 있습니다.

fprintf의 경우에는 vfprintf라고 하며, 이 두 함수의 선언부는 다음과 같습니다.


1
2
int fprintf(FILE *fp, const char * format, ...);
int vfprintf(FILE *fp, const char * format, va_list arg );
cs


 둘 모두 비슷하지만 차이가 있다면 fprintf에서 가변 인자 방식으로 전달받는 인자를 vfprintf는 va_list 타입으로 받는다는 점입니다.

그 점을 제외하고는 두 함수는 하는 일이 동일합니다. 그렇다면 이제 이 vfprintf 함수를 사용하여 후킹하더라도 기존과 똑같이 동작하도록 위의 코드를 수정해보도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
int fprintf(FILE *fp, const char *format, ...)
{
    int ret
    va_list ap;
    va_start(ap, format);
    // 함수 호출 전 원하는 후킹 행위
    ret = vfprintf(fp, format, ap);
    // 함수 호출 후 원하는 후킹 행위
    va_end(ap);
    return ret;
}
cs


 위와 같이 코드를 작성하고 위아래로 함수 실행 전과 후에 원하는 후킹 행위를 추가로 작성하면 원본 함수 실행에 영향을 미치지 않고 후킹을 수행할 수 있습니다. fprintf는 vfprintf라는 함수가 있었고, 이외에도 libc에는 vfscanf, vprintf, vsprintf등 가변 인자 함수에 대응하는 va_list 인자 함수들이 정의되어 있습니다. 이런 함수들을 후킹할 일이 있다면 위 함수들을 찾아서 가져다 쓰면 되겠습니다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag C/C++

댓글을 달아 주세요


 최근 개인적으로 리눅스 환경에서 C/C++을 혼합해서 쓸 일이 생겼는데, 그때 extern "C"를 사용하면서 알게 된 점들을 작성해 보려고 합니다. 해당 프로젝트는 'https://github.com/skyclad0x7b7/MiniHook' 요놈인데, LD_PRELOAD라는 환경변수를 이용하여 간단하게 함수들을 후킹하여 프로세스가 어떤 행위를 했는지 로깅하는 툴입니다. 윈도우 악성코드 자동 분석 시스템은 많이 만져 봤지만 리눅스는 한번도 해본적이 없었기 때문에 윈도우에서와 비슷한 방식으로 자동 분석이 가능하지 않을까 하여 만들어 보게 되었습니다.



 우선 extern "C"란 무엇인가에 대해 알아보아야 합니다. extern "C"라는 키워드는 C++ 소스에서 선언한 전역 변수나 함수를 C에서 사용해야 할 경우에 쓰입니다. 그 이유는 함수명이나 특정 전역변수명을 '심볼'로 저장하는 방식이 다르기 때문입니다.


1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
void test()
{
    printf("Hello World!\n");
}
 
int main()
{
    test();
    return 0;
}
cs


 위의 간단한 소스코드를 예로 들겠습니다. test라는 함수 안에서 Hello World를 출력하고 종료하는 프로그램입니다. 위 소스코드를 각각 gcc와 g++로 컴파일하고, readelf 명령을 이용해 만들어진 바이너리 안의 심볼을 찾아보았습니다.




 gcc로 컴파일한 경우 test라는 함수명이 그대로 출력되지만, g++로 컴파일한 경우 _Z4testv 라는 복잡한 이름으로 변경된 것을 볼 수 있습니다. 이는 C와 C++의 함수 특성에서 차이가 발생하기 때문입니다. C++에는 '함수 오버로딩'이라고 하는 기능이 있는데, 이는 같은 함수명이라고 하더라도 전달받는 타입이 달라진다면 새 함수로 선언 및 사용이 가능한 특징입니다. int func(int a)라는 함수와 int func(double a) 라는 함수를 둘 다 선언하고 정의하더라도 문제가 없게 된다는 것입니다. 하지만 이렇게 했을 시, func라는 함수명만으로 해당 함수들을 구별하는 것이 불가능해집니다. 그래서 C++ 컴파일러들은 각 컴파일러마다 자신들만의 규칙으로 함수 이름을 변경합니다. 이것을 '네임 맹글링(Name Mangling)'이라고 합니다.



각 컴파일러별 네임 맹글링 규칙

Compilervoid h(int)void h(int, char)void h(void)
Intel C++ 8.0 for Linux_Z1hi_Z1hic_Z1hv
HP aC++ A.05.55 IA-64
IAR EWARM C++ 5.4 ARM
GCC 3.x and higher
Clang 1.x and higher[1]
IAR EWARM C++ 7.4 ARM_Z<number>hi_Z<number>hic_Z<number>hv
GCC 2.9xh__Fih__Fich__Fv
HP aC++ A.03.45 PA-RISC
Microsoft Visual C++ v6-v10 (mangling details)?h@@YAXH@Z?h@@YAXHD@Z?h@@YAXXZ
Digital Mars C++
Borland C++ v3.1@h$qi@h$qizc@h$qv
OpenVMS C++ V6.5 (ARM mode)H__XIH__XICH__XV
OpenVMS C++ V6.5 (ANSI mode)CXX$__7H__FIC26CDH77CXX$__7H__FV2CB06E8
OpenVMS C++ X7.1 IA-64CXX$_Z1HI2DSQ26ACXX$_Z1HIC2NP3LI4CXX$_Z1HV0BCA19V
SunPro CC__1cBh6Fi_v___1cBh6Fic_v___1cBh6F_v_
Tru64 C++ V6.5 (ARM mode)h__Xih__Xich__Xv
Tru64 C++ V6.5 (ANSI mode)__7h__Fi__7h__Fic__7h__Fv
Watcom C++ 10.6W?h$n(i)vW?h$n(ia)vW?h$n()v

(출처 : https://en.wikipedia.org/wiki/Name_mangling)


 이렇게 네임 맹글링이 되는 대상은 함수만 있는 것이 아니고, 심볼을 통해 접근해야 하는 전역변수도 똑같이 적용됩니다. 여기서 몇번 테스트를 거치며 깨달은 점이 있는데, C++ 소스에서 선언한 전역변수라고 하더라도 모든 전역변수가 다 네임 맹글링이 되지는 않는다는 점입니다. 


test.h

1
2
3
4
5
6
#include <string>
#include <vector>
 
extern int TestInt;
extern std::string TestString;
extern std::vector<std::string> TestVector;
cs


test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string>
#include <vector>
#include "test.h"
 
int TestInt;
std::string TestString;
std::vector<std::string> TestVector;
 
void test()
{
    printf("Hello World!\n");
}
 
int main()
{
    TestInt = 1;
    TestString = "Test";
    TestVector.push_back("Test");
    test();
    return 0;
}
cs


 test.h에서 선언한 전역변수를 test.cpp에서 가져다가 정의하는 소스코드입니다. 위 소스코드를 컴파일한 후 마찬가지로 readelf로 심볼을 확인해보겠습니다.



 실제로 네임 맹글링이 적용된 것은 std::string 타입인 TestString과 std::vector<std::string> 타입인 TestVector뿐이라는 것을 알 수 있습니다. 단순 int형이었던 TestInt의 경우에는 네임 맹글링이 적용되지 않았습니다. 여기서 예측 가능한 것은 C에서도 사용하는 일반적인 타입들(함수 포인터, 기본 타입 변수들)의 전역변수의 경우에는 네임 맹글링이 적용되지 않는다는 것입니다.



(https://stackoverflow.com/questions/17064471/g-name-mangling-of-global-const-variables)

 

 또 이걸 조사하다가 스택오버플로우에서 재밌는 글을 하나 발견했는데, 바로 static 전역변수의 경우는 일반적인 C 타입의 변수라고 할지라도 네임 맹글링을 진행한다고 합니다. 사실 잘 생각해 보면 당연한 것인데, static 전역변수라고 함은 다른 소스코드에서는 접근할 수 없는 변수이므로 다른 소스코드 안에 같은 이름을 가진 다른 변수가 있을 수 있기 때문에 충돌을 피하기 위해 맹글링을 하는 것이 맞습니다. 또한 저 답변의 implicitly static 이라는 말이 뭔가 해서 다시 조사를 해 봤습니다.



(https://stackoverflow.com/questions/3709207/c-semantics-of-static-const-vs-const)


 결론만 말하면, C++에서 전역변수에 static const를 붙이는 것과 const만 붙이는 것은 동일한 동작을 합니다. 즉 const로 선언한 전역변수의 경우에는 extern 키워드를 붙여주지 않는 이상 static 변수로 취급되기 때문에 네임 맹글링이 진행됩니다. 여기까지 정리하면 C++에서 전역변수로 선언한 일반 타입의 변수들의 경우, const 혹은 static 변수에 대해서만 네임 맹글링이 진행된다고 볼 수 있겠습니다.




 어쨌든 네임 맹글링이 되는 조건은 이렇게 된다고 치고, 다시 돌아와서 이런 이유로 네임 맹글링이 일어나기 때문에 C++ 헤더 안에서 test라고 선언한 함수를 C에서 그대로 가져다 쓰려고 하면 네임 맹글링에 의해 변환된 심볼을 알 방도가 없기 때문에 에러가 발생할 수밖에 없습니다. 이를 가능하도록 해주는 것이 extern "C" 키워드입니다. extern "C"를 이용하여 변수나 함수를 선언할 경우 네임 맹글링이 진행되지 않습니다.


test.h

1
2
3
4
5
6
7
8
#include <string>
#include <vector>
 
extern "C" {
    extern int TestInt;
    extern std::string TestString;
    extern std::vector<std::string> TestVector;
}
cs


 위에서 사용한 test.h 헤더 파일에서 변수 선언부를 extern "C"로 감싸준 후 컴파일해보면



 위와 같이 변수명들이 맹글링되지 않고 원본 그대로 출력되는 것을 볼 수 있습니다. 빌드 혹은 실행 시 undefined symbol 에러와 함깨 네임 맹글링이 진행된 심볼이 나타난다면 대부분은 이 부분 문제일 것이므로 앞으로 문제 해결할때 참고하여 고쳐나가야겠습니다.





블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag C/C++

댓글을 달아 주세요



 Python 2.7과 selenium + webdriver(chromedriver), BeautifulSoup4를 이용하여 만든 크롤러입니다. 네이버에 로그인하고, 메일 페이지에서 특정 사용자로 검색 및 안 읽은 메일만 가져오도록 필터링하여 메일 내부의 첨부파일을 특정 위치로 다운로드받도록 하여 영상을 찍어 봤습니다. 네이버의 경우 로그인할 때 의심스러운 행위를 자동으로 감지하여 캡차로 막기 때문에 이를 우회하기 위해 네이버 메인을 경유하여 로그인 페이지로 이동하고, 중간중간 랜덤한 시간동안 Sleep하는 코드를 넣어두었습니다. 소스코드는 다음과 같으며, 라이센스는 MIT 이므로 누구나 원하는 곳에 사용이 가능하지만 이 프로그램을 사용함에 있어 발생하는 모든 책임은 사용자에게 있습니다.




소스코드


NaverMailCrawler.py



Utils.py

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

  • True Worship 2019.01.26 00:08 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요.
    글 잘 읽고 네이버 로그인을 selenium으로 시도하고 있습니다.
    이 글을 작성하실 때는 동영상 찍으신 것 처럼 되셨을것 같은데
    지금은 안되네요..ㅠㅠ
    계속 여러가지 방법으로 시도해보고 있지만 결국은 캡차 때문에 막히고 있습니다.
    혹시라도 캡차를 우회할 수 있는 방법이 있으시다면 공유를 부탁드려도 될까요?

    • __미니__ 2019.01.26 00:21 신고  댓글주소  수정/삭제

      안녕하세요.
      본문의 Sleep을 이용한 우회법의 경우 절대로 완벽한 우회 방법이 아니고 무척 단순한 방식이기 때문에 우회가 불가능한 경우가 상당히 많을거라고 생각합니다. 정말 완전한 우회를 원한다면 실제 이용자가 로그인을 시도하는 것처럼 User Agent, Referer, Host 등 HTTP Request Header를 완벽히 꾸미고 조작하여 넣으면 되겠지만 이는 불가능에 가깝습니다.

      이외에 가장 간단한 방법이라면 미리 직접 로그인한 후 세션을 끊지 않은 상태로 쿠키 값을 저장해 뒀다가 Selenium의 add_cookie 함수 등으로 쿠키를 넘겨 자동으로 로그인된 상태로 만드는게 있습니다. 하지만 이 방식은 본인이 원하시는 방식이 아닐 것 같네요.

      제가 생각하는 가장 좋은 방법은 네이버에서 제공하는 API를 이용하는 것입니다. OAuth라고들 하는데요, https://developers.naver.com/docs/login/overview/
      여기에서 오픈API 이용 신청을 통해 정식으로 사용 허가를 받고 클라이언트 ID와 Secret 값을 발급받아서 API로 사용하면 원하시는대로 로그인이 가능할 것이라고 생각합니다.

      문제는 이용 신청 및 허가가 떨어져야 한다는 것이고 그게 귀찮거나 어려울 수 있는 환경이라는 것이네요...

      하지만 이 부분은 별다른 수가 떠오르지 않네요 ㅠㅠ
      답변이 되었다면 좋겠습니다.

  • True Worship 2019.01.26 00:50 신고  댓글주소  수정/삭제  댓글쓰기

    답변 정말 감사합니다!!

    캡차화면에서 뒤로가기 한 다음에 제가 직접 입력하면 로봇으로 보지 않고 로그인이 되는걸보니 희망이 생겨서 pyautogui를 같이 이용해서 우회해보려고 했지만 안되더군요..ㅠㅠ

    한번 알려주신대로 API이용해서 OAuth로 시도해봐야겠네요!

    감사합니다!


[GS (Stack Canary/Cookie) 해제법]


 프로젝트 속성 -> C/C++ -> 코드 생성 -> 보안 검사 -> 보안 검사 사용 안 함(/GS-) 으로 변경.

함수 프롤로그 이후 스택에 버퍼 할당 전에 랜덤한 값을 Push해 두고, 함수 에필로그 직전에 검사하여 변조되었는지 체크하는 보호 기법. 보통 윈도우에서는 GS 또는 Stack Cookie라고 불리고 리눅스에서는 Stack Canary라고 불리지만 그거나 그거나이므로 넘어가자.



[DEP(NX) 해제법]


 프로젝트 속성 -> 링커 -> 고급 -> DEP(데이터 실행 방지) -> 아니요(/NXCOMPAT:NO) 로 변경.

각 페이지마다 Read/Write/Execute 권한 플래그를 설정하여 Write/Execute 권한을 동시에 주지 않는 보호 기법. 간단하면서도 강력한 기법.


[ASLR 해제법]


 프로젝트 속성 -> 링커 -> 고급 -> 임의 기준 주소 -> 아니요(/DYNAMICBASE:NO) 로 변경.

매핑된 바이너리 및 DLL 주소, 버전에 따라서는 힙, 스택, PEB, TEB까지 프로그램 실행 시마다 랜덤하게 변환해버리는 보호 기법. 높은 버전에서는 메모리 릭을 통해서 주소를 찾거나 고정된 DLL 주소를 찾지 않으면 주소값을 사용할 수 없기 때문에 골치아픈 보호 기법이다.



[SafeSEH 해제법]


 프로젝트 속성 -> 링커 -> 명령줄 -> 추가 옵션(D) -> /SAFESEH:NO 추가 작성.

SEH가 가리키는 주소가 스택이거나, MS Library이지만 핸들러로 등록된 주소가 아닐 경우, 혹은 SafeSEH가 적용된 모듈의 주소일 경우 탐지하여 실행되지 않게 하는 보호 기법. 이 보호 기법을 우회하고 SEH Overwrite를 하기 위해서는 SafeSEH가 적용되지 않은 고정된 바이너리의 주소를 찾아서 점프해야 한다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

[Python] PyV8을 이용한 Javascript 분석


여기에 작성된 글을 보고 이것을 이용하면 난독화된 악성 스크립트를 안전하고 효율적으로 분석이 가능하겠다는 생각이 들어서 비슷한 방식으로 만들어보기로 마음먹었다.


[PyV8은 여기에서 다운받을 수 있다. 마지막 업로드가 2012년인 것이 조금 걸리지만...]

 PyV8은 구글에서 만든 자바스크립트 엔진인 V8의 파이썬 래퍼로, 자바스크립트 코드를 해석하고 실행하여 그 결과를 파이썬에서 받아볼 수 있다. 하지만 PyV8은 자바스크립트 문법을 해석해 줄 뿐 운영체제의 API에 연결되어 있지 않기 때문에, 악성 스크립트에서 자주 사용하는 WScript.Shell 등을 사용하면 정의되지 않았다면서 에러가 발생한다. 그래서 직접 더미 API를 만들어서 넘겨주어야 하는 필요성이 있고, 위에서 언급했던 블로그에서는 각 모듈과 함수들을 직접 정의하여 넘겨주는 방식으로 제작했었다. 그런데 그렇게 만들게 되면 모든 모듈과 함수에 대응하는 클래스 및 메서드를 직접 다 만들어서 전달해야 한다는 불편함이 있었고, 결국 내가 원하는 것은 '어떤 함수가 어떤 인자를 받아서 실행되었는가' 이기 때문에 전부 간단하게 추상화시킬 수 있다고 판단하여 결국 간단하게 추상화 하는 것에 성공하였다. 자세한 내용은 소스코드를 보면서 작성하겠다.


http://5kyc1ad.kr/files/ScriptAnalyzer.zip

(악성코드 분석하려고 만든건데 티스토리에서 악성코드라며 첨부파일을 막아버렸네요; 첨부파일은 제 서버 주소로 대체하겠습니다.

꽤 시간이 흐른 뒤에 발견해버린 터라 실제 본문의 스크립트와는 상당히 달라졌을 수 있습니다.)

ScriptAnalyzer.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os
 
import PyV8
 
from DummyAPI.Global import Global
 
class ScriptAnalyzer:
    def __init__(self):
        pass
 
    def __del__(self):
        pass
 
    def Analyze(self, targetJSFile):
        if not os.path.exists(targetJSFile):
            print "[-] Target File not found : %s" % (targetJSFile)
            return False
 
        with open(targetJSFile, "rb") as f:
            javaScript = f.read()
 
        ctx = PyV8.JSContext(Global())
        ctx.enter()
        ctx.eval(javaScript)
 
        return True
 
def main():
    scriptAnalyzer = ScriptAnalyzer()
    scriptAnalyzer.Analyze("..\\script4.js-")
 
if __name__ == '__main__':
    main()
cs

 PyV8을 이용하여 Javascript를 실행하는 분석기와 그를 이용하는 main 함수가 존재하는 소스코드 파일이다. 별다른건 없고 이후 나올 DummyAPI.Global 모듈에서 Global 클래스를 가져와 넘기는 것만 확인하면 된다.


Global.py

1
2
3
4
5
6
7
import PyV8
 
from DummyAPI.AbstractType import AbstractType
 
class Global(PyV8.JSClass):
    def __getattr__(self, name):
        return AbstractType(name)
cs

 추상화에 성공하여 무척 간결해져 버린 Global 클래스의 소스코드이다. 원래라면 클래스 멤버 변수로 각 모듈들에 대응하는 클래스의 인스턴스를 가지고 있어야 하지만, 어떤 모듈을 호출할 지 모르므로 __getattr__ 메서드를 정의하는 것으로 한번에 해결해 버렸다. __getattr__ 메서드는 classA.not_exist_value 처럼 클래스 내부에 존재하지 않는 변수, 혹은 메서드에 접근했을 경우 호출되는 함수이다. 현재 Global 클래스 내부에 저 함수 외에는 아무것도 정의되지 않았으므로 Javascript 내에서 어떤 모듈 혹은 Global 함수를 호출하던 간에 AbstractType(name) 을 리턴받게 될 것이다. AbstractType은 직접 작성한 타입으로, 다음 소스코드에서 확인이 가능하다.


AbstractType.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import PyV8
import Utils
 
class AbstractType(PyV8.JSClass):
    def __init__(self, thisName):
        self.thisName = thisName
 
    # Type is Function
    def __call__(self, *args, **kwargs):
        if len(args) > 0 and len(kwargs.keys()) > 0:
            print ("[*] %s(%s, %s)" % (self.thisName, Utils.argsToStr(args), Utils.kwargsToStr(kwargs)))
        elif len(kwargs.keys()) > 0:
            print ("[*] %s(%s)" % (self.thisName, Utils.kwargsToStr(kwargs)))
        else:
            print ("[*] %s(%s)" % (self.thisName, Utils.argsToStr(args)))
 
        if len(args) > 0:
            return AbstractType(args[0])
        else:
            return AbstractType("By_%s" % (self.thisName))
 
    # Type is Module
    def __getattr__(self, name):
        return AbstractType("%s.%s" % (self.thisName, name))
cs

 이번에 추상화에서 얻어낸 가장 큰 산물인 AbstractType 클래스이다. 생성자에서 호출한 이름을 인자로 받으며(Global.py 참고), __call__ 메서드를 정의하여 인스턴스가 함수로써 사용될 경우 *args, **kwargs 등 가변 길이 변수를 받아서 print로 이름과 함께 로깅하도록 하였다. 또한 Global.py에서처럼 __getattr__ 메서드를 정의하여 모듈 내의 함수에 접근하는 것과 같은 행위를 할 경우 현재 이름에 새 이름을 붙여 AbstractType 클래스를 새로 생성해 리턴하는 것으로 함수의 경우 위에서 정의한 __call__ 에 의해 자동으로 로깅이 되도록 하였다.


 이를 이용하면 따로 모듈과 함수를 직접 구현해주지 않고도 호출하는 문자열을 이용해 그때그때 상황에 맞는 AbstractType을 생성하여 로깅하는 것이 가능해지며, 악성 스크립트 분석에 큰 도움이 될 수 있다.


[실제 악성 스크립트 분석 결과]


 실제로 스크립트 분석 결과 구체적인 모듈이나 함수 정의는 전혀 하지 않았음에도 모듈 및 함수, 그 인자까지 전부 로깅되는 것을 볼 수 있다. 한 언어에 대한 이해가 깊어지면 이렇게 효율적이고 추상적인 코드 작성이 가능해진다는 것을 제대로 느낄 수 있는 프로젝트였다. 현재는 print로밖에 로깅을 진행하지 않지만 이후 파일이나 리턴 값으로 받을 수 있게 하면 자동 분석에도 응용이 가능할 것으로 보인다. 좀 더 고도화를 진행해 볼 예정이다.


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

  • 2019.03.26 17:28  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

    • __미니__ 2019.03.28 14:33 신고  댓글주소  수정/삭제

      알려주셔서 감사합니다.
      본문에 작성된 소스코드에서 좀 더 개발을 진행한 소스코드만 가지고 있어서 해당 버전으로 제 개인 서버에 업로드 후 다시 링크를 걸어두었으니 참고해주시면 되겠습니다.

      혹시나 잘 작동하지 않거나 본문의 소스코드를 원하시면 드래그 및 복사 허용되어있으니 본문의 소스코드를 복사해 사용해주세요~

Python TimedRotatingFileHandler.doRollOver() Error 32 문제


 Python의 logging 모듈에서 기본적으로 제공하는 TimedRotatingFileHandler를 사용하는 도중, 아래와 같은 에러가 발생했다.


https://stackoverflow.com/questions/29220435/windowserror-using-rotatingfilehandler-and-subprocess-popen


Traceback (most recent call last):

  File "c:\Python27\lib\logging\handlers.py", line 77, in emit

self.doRollover()

  File "c:\Python27\lib\logging\handlers.py", line 142, in doRollover

    os.rename(self.baseFilename, dfn)

WindowsError: [Error 32] The process cannot access the file because it is being used by another process


 doRollOverTimedRotatingFileHandler에 지정한 기간이 지났을 경우 사용하던 파일을 백업하고 새로운 로그 파일을 생성하도록 하는 함수이다. Error 32는 다른 프로세스나 스레드에서 같은 파일의 핸들러를 가지고 있기 때문에 변경이 불가능할 때 발생하는 에러이다. 당시 Multiprocessing 환경에서 사용중이었기 때문에 이것이 문제인가 싶어서 프로세스별로 다른 Logger를 사용하도록 변경해 보았으나 그래도 마찬가지로 발생하였고, 이를 해결하려고 수없이 돌아다녔다.

=== [2019-04-03] 업데이트

한동안 발생하지 않아서 방심하고 있었는데 여전히 버그는 발생했다. Error 32가 뜻하는 바도 그렇고 분명 뭔가 멀티프로세스 환경에서 다른 프로세스가 핸들을 물고 있어서 Rotating이 되지 않는 것이 분명했으므로 이를 확인하기 위해 테스트 코드를 짜 봤다.


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
import os
import time
import logging
import logging.handlers
import multiprocessing
 
def GetParentDirectory(filePath):
    return os.path.sep.join(filePath.split(os.path.sep)[:-1])
 
def Mkdirs(dirPath):
    if not os.path.exists(dirPath):
        os.makedirs(dirPath)
 
def CreateLogger(loggerName):
    # Create Logger
    logger = logging.getLogger(loggerName)
    if len(logger.handlers) > 0:
        return logger
 
    logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('\n[%(levelname)s|%(name)s|%(filename)s:%(lineno)s] %(asctime)s > %(message)s')
 
    # Create Handlers
    logPath = os.path.join(os.path.realpath(""), "logs", loggerName + ".log")
    Mkdirs(GetParentDirectory(logPath))
    streamHandler = logging.StreamHandler()
    streamHandler.setLevel(logging.DEBUG)
    streamHandler.setFormatter(formatter)
    rotatingHandler = logging.handlers.TimedRotatingFileHandler(logPath, when='M', backupCount=10)
    rotatingHandler.setLevel(logging.DEBUG)
    rotatingHandler.setFormatter(formatter)
 
    # Add handlers to logger
    logger.addHandler(streamHandler)
    logger.addHandler(rotatingHandler)
    return logger
 
def LoggingTest(pid):
    subLogger = CreateLogger("Process_%d" % (pid))
    count = 0
    while True:
        subLogger.info("Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
def main():
    PROC_COUNT = 10
    mainLogger = CreateLogger("Main-Process")
 
    mainLogger.info("Start Creating Process")
    procList = []
    for i in range(PROC_COUNT):
        procList.append(multiprocessing.Process(target=LoggingTest, args=(i,)))
        procList[i].start()
        time.sleep(0.5)
    mainLogger.info("End Creating Process")
 
    count = 0
    while True:
        mainLogger.info("Main-Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
if __name__ == '__main__':
    main()
cs


 1분마다 Rotating하도록 설정하여 돌려봤는데, 아니나다를까 Main-Process 로거에서 doRollOver 에러가 발생한다. multiprocessing.Process로 생성한 자식 프로세스에서 main의 mainLogger 핸들러를 물고 있는 것이 아닌가 하는데... 


 돌려놓고 Rotating이 발생하기 전에 Process Hacker를 이용해 파일 핸들러를 찍어본 모습이다. Main과 Sub 모두 자신의 핸들러를 가지고 있는데, Sub에서 Main의 핸들러를 물고 있는 것을 볼 수 있다.



 doRollOver Error32가 발생한 이후 찍은 모습이다. Main에서는 Rotating을 위해 핸들러를 닫았지만 Sub에서는 계속 물고 있는 것을 볼 수 있다. 이래서 Error32가 발생한 것으로 파악할 수 있었다. 문제는 이걸 어떻게 해결하냐는 것인데... 파이썬 logging 라이브러리 자체가 멀티 프로세싱 환경을 고려하지 않고 만들어졌기 때문에 발생한 일이라 간단하게 해결하긴 힘들어 보인다. 여기(https://docs.python.org/ko/3/howto/logging-cookbook.html)에서는 소켓에 로그를 작성하고 이를 읽어서 기록하는 스레드를 따로 만들어 두고 돌리는 방식으로 처리가 가능함을 설명하고 있으며 실제로 multilog(https://pypi.org/project/multilog/)라고 하는 모듈이 이미 존재한다. 어느 쪽이든 로깅 모듈이 쓰이던 곳이 한두군데가 아니었기 때문에 고치기는 힘들겠지만 최대한 노력해 봐야겠다 (._.


 Windows 환경 해결법

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
import os
import time
import platform
import logging
import logging.handlers
import multiprocessing
 
def GetParentDirectory(filePath):
    return os.path.sep.join(filePath.split(os.path.sep)[:-1])
 
def Mkdirs(dirPath):
    if not os.path.exists(dirPath):
        os.makedirs(dirPath)
 
def CreateLogger(loggerName):
    # Create Logger
    logger = logging.getLogger(loggerName)
    if len(logger.handlers) > 0:
        print "Already Exists : %s" % (loggerName)
        return logger
    print "Non-Exists : %s" % (loggerName)
    logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter('\n[%(levelname)s|%(name)s|%(filename)s:%(lineno)s] %(asctime)s > %(message)s')
 
    # Create Handlers
    logPath = os.path.join(os.path.realpath(""), "logs", loggerName + ".log")
    Mkdirs(GetParentDirectory(logPath))
    streamHandler = logging.StreamHandler() 
    streamHandler.setLevel(logging.DEBUG)
    streamHandler.setFormatter(formatter)
    rotatingHandler = logging.handlers.TimedRotatingFileHandler(logPath, when='M', backupCount=10)
    rotatingHandler.setLevel(logging.DEBUG)
    rotatingHandler.setFormatter(formatter)
    if platform.system() == 'Windows':
        import msvcrt
        import win32api
        import win32con
        win32api.SetHandleInformation(msvcrt.get_osfhandle(rotatingHandler.stream.fileno()), win32con.HANDLE_FLAG_INHERIT, 0)
 
    # Add handlers to logger
    logger.addHandler(streamHandler)
    logger.addHandler(rotatingHandler)
    
    return logger
 
def LoggingTest(pid):
    subLogger = CreateLogger("Process_%d" % (pid))
    count = 0
    while True:
        subLogger.info("Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
def main():
    PROC_COUNT = 10
    mainLogger = CreateLogger("Main-Process")
 
    mainLogger.info("Start Creating Process")
    procList = []
    for i in range(PROC_COUNT):
        procList.append(multiprocessing.Process(target=LoggingTest, args=(i,)))
        procList[i].start()
        time.sleep(0.5)
    mainLogger.info("End Creating Process")
 
    count = 0
    while True:
        mainLogger.info("Main-Log : 0x%08x" % count)
        time.sleep(5)
        count += 1
 
if __name__ == '__main__':
    main()
cs


CreateLogger 함수 내부, TimedRotatingFileHandler를 생성하는 부분 밑에 다음의 코드가 추가되었다.

1
2
3
4
5
    if platform.system() == 'Windows':
        import msvcrt
        import win32api
        import win32con
        win32api.SetHandleInformation(msvcrt.get_osfhandle(rotatingHandler.stream.fileno()), win32con.HANDLE_FLAG_INHERIT, 0)
cs


파이썬 2.7~3.4에서는 multiprocessing으로 프로세스를 생성했을 때, 자식 프로세스가 부모 프로세스의 핸들러들을 상속받아서 같이 물고 있기 때문에 위와 같은 버그가 발생했었는데, 핸들러의 File Handle을 직접 받아와서 SetHandleInformation API를 사용해 상속하지 않도록 설정해주면 자식 프로세스가 생성될때 받아가지 않는다. (참고 : https://stackoverflow.com/questions/948119/preventing-file-handle-inheritance-in-multiprocessing-lib)


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag Python

댓글을 달아 주세요

파이썬 공부를 계속하다가 데코레이터(Decorator)를 공부하게 되었고, 그 내용을 정리하고자 포스팅한다.


여기 아주 엄청난 일을 하는 함수가 있다.

1
2
def HelloName(name):
    print "Hello, %s!" % (name)
cs

 무려 인자로 이름을 입력받아 인사를 해주는 무척 대단한 함수이다. 아무 문제 없이 함수를 사용하다 보니, 이제 만났을때 하는 인사 외에도 헤어질 때 하는 인사를 만들고 싶어졌다. 정말 열심히 공을 들여서 다음과 같은 함수를 만들어냈다.


1
2
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
cs

 만났을 때 인사하고, 헤어질 때 인사하고 나니 안부를 묻는 말도 만들고 싶어졌다. 또 열심히 공을 들여서 다음과 같은 함수를 만들어냈다.


1
2
def HowAreYouName(name):
    print "How are you, %s?" % (name)
cs

 뿌듯하게 바라보는 와중, 문득 머릿속에서 이 함수들의 기능만으로는 너무 밋밋하다는 생각이 들어서 출력하기 전에 위아래에 경계선을 출력하도록 만들고 싶어졌다. 그래서 만들어진 결과가 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
def HelloName(name):
    print "============================"
    print "Hello, %s!" % (name)
    print "============================"
 
def GoodbyeName(name):
    print "============================"
    print "Goodbye, %s!" % (name)
    print "============================"
 
def HowAreYouName(name):
    print "============================"
    print "How are you, %s?" % (name)
    print "============================"
cs

 딱 봐도 무엇인가 많이 반복되고 있고, 불편함이 느껴진다. 이 함수들은 모두 다른 함수들이지만, 공통적으로 위아래에 경계선을 출력한다는 행위는 동일하다.

 파이썬에서 함수는 다른 함수의 인자로 넘길 수도 있고, 리턴할 수도 있는 특징을 가지고 있다. 이를 이용해서 일단 데코레이터를 사용하지 않고 원시적인 방법을 사용하여 이 문제를 해결해 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def AddLine(targetFunction, args):
    print "============================"
    targetFunction(args)
    print "============================"
 
def HelloName(name):
    print "Hello, %s!" % (name)
 
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    AddLine(HelloName, "5kyc1ad")
    AddLine(GoodbyeName, "5kyc1ad")
    AddLine(HowAreYouName, "5kyc1ad")
cs

 함수와 그 인자를 넘겨 함수 시작 전, 함수가 끝난 후에 경계선을 출력하도록 하는 새로운 함수를 만들고, 그것을 이용해 각자의 함수를 실행하도록 변경해 보았다. 원하던 대로 결과가 나오기는 하지만, 무엇인가 불편한 기분이 드는 것은 여전하다. 호출하고 싶은 함수는 HelloName, GoodbyeName, HowAreYouName 인데 실제 호출하는 함수는 AddLine이고, 인자의 전달도 직관적으로 눈에 들어오지 않는다.

직관적으로 HelloName 함수를 실행하는 것이 보이면서 인자의 전달도 깔끔하게 정리할 수 있는 방법이 없을까?

답은 '함수를 새로 만들어서 사용하는 것' 이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def AddLine(targetFunction):
    def wrapper(args):
        print "============================"
        targetFunction(args)
        print "============================"
    return wrapper
 
def HelloName(name):
    print "Hello, %s!" % (name)
 
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    newHelloName     = AddLine(HelloName)
    newGoodbyeName   = AddLine(GoodbyeName)
    newHowAreYouName = AddLine(HowAreYouName)
 
    newHelloName("5kyc1ad")
    newGoodbyeName("5kyc1ad")
    newHowAreYouName("5kyc1ad")
cs

 AddLine 함수 내에 wrapper라는 이름으로 다른 함수 하나를 선언하고, 내부에서 인자로 전달받은 함수를 실행하기 전, 실행이 끝난 후에 줄을 추가로 출력하도록 한 후, AddLine 함수가 끝날 때 wrapper 함수 자체를 반환하도록 만들었다. 이제 main 에서 AddLine 함수에 원하는 출력 함수를 넣은 후 리턴된 함수를 받아와서 사용해주면 된다.

 정답에 아주 가까워졌지만 여전히 불편한 점이 몇가지 있다. 내가 선언한 함수는 HelloName인데 새로 newHelloName을 만들어서 사용해주어야 한다는 점, 함수마다 전부 저 작업을 추가로 해주어야 한다는 점 등이다. 선언한 함수를 그냥 사용할 수 없다는 점에서 또 직관성이 떨어지는 코드이기도 하다.

 이 모든 문제점을 해결하기 위해 사용하는 것이 데코레이터(Decorator)이다. 다른 함수들의 시작 전이나 후에 특정한 장식(Decoration)을 붙여 주는 것이다. 여기의 예제에서는 위아래에 붙은 경계선이 '장식' 되시겠다.

사용법은 생각보다 무척이나 간단하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def AddLine(targetFunction):
    def wrapper(*args, **kwargs):
        print "============================"
        targetFunction(*args, **kwargs)
        print "============================"
    return wrapper
 
@AddLine
def HelloName(name):
    print "Hello, %s!" % (name)
 
@AddLine
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
@AddLine
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    HelloName("5kyc1ad")
    GoodbyeName("5kyc1ad")
    HowAreYouName("5kyc1ad")
cs

wrapper 에서 받는 인자가 *args, **kwargs 가 되어서 당황할 수 있지만, 사실 이는 가변 인자를 넘겨주기 위한 것으로 생각보다 간단한 개념이다. 여기에 잘 설명되어 있으므로 참고.

 AddLine 함수는 달라진 점이 전혀 없고, 각 함수 위에 @AddLine 한 줄씩이 추가된 것을 볼 수 있다. @AddLine은 데코레이터 AddLine을 이 함수에 적용시키겠다는 의미이다. 각 함수들은 선언 순간 데코레이터 AddLine이 적용되어 리턴된 wrapper 함수를 자기 자신으로서 갖게 되고, 따라서 사용할 때는 별도의 작업 없이 선언한 함수를 그대로 사용하면 된다.

이것이 가장 기초적인 데코레이터의 사용법이다.


이제 여기서 한 계단만 더 올라가보자.

 데코레이터를 사용하다 보면 데코레이터 자체가 인자를 받는 모습을 볼 수 있다. 예를 들면 Flask를 이용한 REST API 개발을 해본 사람이라면, 다음과 같은 코드가 함수 앞에 붙는 것을 봤을 것이다.


1
2
3
@app.route("/file", methods=["GET"])
def blah_blah():
    pass
cs

 딱 봐서는 blah_blah 라는 함수에 app.route라는 데코레이터를 적용하는 코드로 보이는데, 지금까지 보지 못한 부분이 추가되었다.

바로 app.route에 ("/file", methods=["GET"]) 로 인자를 받는 부분이 추가되었다는 것이다.


 생각해 보면 지금까지 작성한 예시에서 데코레이터가 하는 일을 나타내어 봤을 때 '함수 시작 전과 끝난 후에 특정 줄을 하나씩 출력한다'라고 표현할 수 있다. 그런데, 모든 함수가 다 같은 줄만을 출력하는 것이 아닌 특정 문자열을 출력하고 싶은 것일 수도 있다. 즉, 같은 데코레이터를 이용하여 다른 내용을 출력할 수 있도록 만들고 싶을 수 있는 것이다.

 이렇게 데코레이터에 인자를 넣기 위해서는 데코레이터 함수를 한번 더 감싸주면 된다.


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
def AddLine(targetStr):
    def RealDecorator(targetFunction):
        def wrapper(*args, **kwargs):
            print targetStr
            targetFunction(*args, **kwargs)
            print targetStr
        return wrapper
    return RealDecorator
 
@AddLine("===HelloName===")
def HelloName(name):
    print "Hello, %s!" % (name)
 
@AddLine("===GoodbyeName===")
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
@AddLine("===HowAreYouName===")
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    HelloName("5kyc1ad")
    GoodbyeName("5kyc1ad")
    HowAreYouName("5kyc1ad")
cs

이런 식으로 말이다.

함수 위에 @AddLine으로 명시된 함수들은 선언 순간 AddLine 데코레이터를 적용시키게 되며, AddLine 데코레이터에는 출력할 문자열을 인자로 넘기도록 되어 있다. 위 예시의 실행 결과는 다음과 같다.


각각의 함수에서 AddLine 데코레이터에 인자로 넘긴 문자열이 함수의 시작과 끝에서 정상적으로 출력되는 모습을 확인할 수 있다.


 데코레이터를 잘만 이용하면 Repeat 등의 이름을 가진 데코레이터를 만들어서 한 함수를 여러 번 반복시키는 것도 가능하다.

대표적인 문제가 codefights.com 의 Arcade - Python의 77번째 문제인 'Merging Vines' 이다.

상당히 재미있고 데코레이터를 익히는 데에 적절한 문제이므로 한번씩 풀어보는 것도 좋을 것 같다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요


Codefights.com에서 Arcade - Python 을 풀다가 상당히 재미있는 버그를 하나 알게 되어 작성한다.


1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
 
int division(int x, int y)
{
    return x / y;
}
int main()
{
    std::clog << division(15-4<< std::endl;
    return 0;
}
cs

 C에서 이렇게 int끼리의 나눗셈의 결과가 나머지가 존재할 경우, '/' 연산자 특성상 나머지를 버리고 몫만을 리턴하기 때문에 결과 값은 -3이 된다. 소숫점으로 나타내면 -3.xx~ 로 나타나겠지만 int 형으로 형변환하면서 -3이 되는 것이다. 

( C에서는 두 오퍼랜드 모두 Integer형일 경우 Integer를 반환하고, 하나라도 Float일 경우 Float형으로 소숫점을 붙여 반환한다. 이를 Classic Division이라고 한다. )


1
2
3
4
def division(x, y):
    return x / y
 
print division(15-4)
cs

 하지만 같은 역할을 할 법한 코드를 Python 2에서 작성하여 쓸 경우, 예상과는 달리 -4가 리턴된다. 

 이는 흔히 Floor Division 이라고 일컫는 정수 나눗셈 방법인데, 두 오퍼랜드가 모두 정수이고 나누어져서 나온 결과가 정수가 아니라면 항상 나온 값보다 작은 정수를 리턴하는 방식이다. 예를 들어, 5/3 의 경우 1.xx 로 1보다는 크고 2보다는 작은 값이 나오므로 1이 리턴된다. 따라서 위의 예시인 15/-4 를 할 경우 -3과 -4 사이의 값이므로 더 작은 값인 -4가 리턴되는 것이다. 이 방식이 문제가 되는 이유는, 흔히 참이라고 생각하는 연산이 거짓이 된다는 점에 있다.

 예를 들면, 정수 x와 y에 대해서, (x/-y) == -(x/y) 는 당연히 참이 되어야 하지만, 실제 위의 예시를 들어 보면 (15/-4) 는 -4가 되고 -(15/4) 는 -3이 되므로 거짓이 되어버린다. 

 이 문제는 Python 3 버전에서는 고쳐졌다고 한다. Python 3에서는 정수를 정수로 나누어도 Float을 반환해버리는 True Division을 사용하기 때문에 애초부터 저런 문제가 발생하지 않으며, Floor Division을 사용하고 싶을 때에는 15//-4 처럼 '//' 연산자를 사용하면 된다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요

Split.cpp

 아쉽게도 C++에서는 std::string에 대해서 특정 문자나 문자열에 대해 split해서 반환해주는 편리한 함수가 존재하지 않는다.

자주 쓸 일이 있는 함수임에도 기본제공하지 않는다는 점이 아쉬워 개인적으로 사용하기 위해서 간단하게 함수를 만들어 보았다. 

※ 개인적으로 사용하기 위해 만들어 본 함수이므로 예상치 못한 버그가 존재할 수 있습니다.


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
#include <string>
#include <iostream>
#include <vector>
 
std::vector<std::string> Split(std::string targetStr, std::string token)
{
    // Check parameters
    if(token.length() == 0 || targetStr.find(token) == std::string::npos)
        return std::vector<std::string>({targetStr});
 
    // return var
    std::vector<std::string> ret;
 
    int findOffset  = 0;
    int splitOffset = 0;
    while ((splitOffset = targetStr.find(token, findOffset)) != std::string::npos)
    {
         ret.push_back(targetStr.substr(findOffset, splitOffset - findOffset));
         findOffset = splitOffset + token.length();
    }
    ret.push_back(targetStr.substr(findOffset, targetStr.length() - findOffset));
    
    return ret;
}
 
int main()
{
    // Test
    std::vector<std::string> splitted = Split("Hello World a b c"" ");
    for(auto s : splitted)
        std::clog << s << std::endl;
 
    std::clog << "=================================" << std::endl;
 
    std::vector<std::string> splitted2 = Split("abcd//ef//gh//ijk""//");
    for (auto s : splitted2)
        std::clog << s << std::endl;
    return 0;
}
cs



블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag C++

댓글을 달아 주세요