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 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,


 최근 개인적으로 리눅스 환경에서 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 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,


 해당 프로그램은 악성코드 분석 연구 도중 흥미 위주로 만들어본 프로그램이며, 꼭 사용 전 파일을 백업해주세요. 필자는 이 프로그램을 사용함에 있어 생긴 피해에 대해 어떠한 책임도 지지 않습니다.


PyCLDecryptor.exe


 


 테스트를 위해 실제로 가상머신 내에서 PyCL 랜섬웨어를 돌려 일부러 감염시킨 테스트 파일들입니다.


 

 프로그램 실행 후 복호화 할 파일/폴더의 경로와 키 값을 입력하면 자동으로 해당 경로 하위를 전부 돌면서 감염된 확장자를 확인하고 입력받은 키로 복호화를 시도합니다. 복호화에 실패하더라도 원본 파일을 수정하거나 지우지는 않지만 혹시 모르니 항상 백업은 하고 사용해주시기 바랍니다. 랜섬웨어의 키를 찾는 방법은 이전 포스트의 마지막에 작성해두었습니다.


 소스코드는 gist로 첨부하겠습니다.

PyCLDecryptor.cpp 

블로그 이미지

__미니__

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

,

CodeFights - alternatingSums

2018. 1. 15. 14:21

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

CodeFights - commonCharacterCount

2017. 12. 27. 23:12

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.


공용체라는 것은 쓸 일이 별로 없지만, 실제 업무에서 보게 되었으므로 공부해 봤다.


사실 공부라고 할 것도 없을만큼 간단한 내용이라...


우선 공용체는 구조체와 비슷하게 생겼다.


하지만 구조체는 내부에 변수나 함수(C++)가 추가됨에 따라 그만큼 메모리를 차지하기 때문에 사용하는 메모리가 증가하지만, 공용체는 가장 큰 변수의 크기만큼 메모리를 할당하고 해당 메모리를 말 그대로 '공유'하기 때문에 더 커지지 않는다.


기본적인 개념은 다른 곳에서도 쉽게 설명하고 있기 때문에 예제를 적도록 하겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>
 
union MyUnion {
    unsigned char c;
    unsigned short s;
    int i;
    unsigned char myCharArray[6];
};
 
int main()
{
    MyUnion myUnion;
    memcpy(myUnion.myCharArray, "\xAA\xBB\xCC\xDD\xEE\xFF"6);
    printf("char  : %012X\n", myUnion.c);
    printf("short : %012X\n", myUnion.s);
    printf("int   : %012X\n", myUnion.i);
    printf("array : %02X%02X%02X%02X%02X%02X\n", myUnion.myCharArray[0], myUnion.myCharArray[1], myUnion.myCharArray[2], 
        myUnion.myCharArray[3], myUnion.myCharArray[4], myUnion.myCharArray[5]);
    return 0;
}
cs



간단한 예제로, union 내부에 char, short, int, char array의 4가지 변수가 선언되어 있다.


여기서 가장 큰 변수는 myCharArray로 총 6바이트를 차지한다.

따라서 공용체 MyUnion은 선언 시 6바이트의 메모리 공간을 차지하게 되며(패딩은 고려하지 않는다) 해당 공간을 나머지 변수들이 공유한다.

공유한다고 해서 따로 가지는 것이 아니라 변수마다 가장 앞에서부터, 즉 offset 0에서부터 변수의 크기만큼 메모리를 가진다.


그림으로 나타내면 다음과 같다.



[그림 1] MyUnion 메모리 구조도


 그리고 위 프로그램을 실행시켜서 나온 output은 다음과 같다.



[그림 2] 프로그램 실행 결과


왜 short와 int의 출력 결과가 저렇게 나오는지는 바이트 오더(Byte order)에 대해서 공부하면 알 수 있을 것이다.

'Programming' 카테고리의 다른 글

[Go] Defer, Panic, Recovery  (0) 2016.10.09
[DLL Injection] DLL Injector  (0) 2016.10.05
[Go] struct{} 와 &struct{} 선언의 차이점?  (3) 2016.07.23
[C++] Reference In Low-level  (0) 2016.07.02
[MITM] Create Repository 'WLAN-Crack'  (0) 2016.06.17
블로그 이미지

__미니__

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

,