본문 바로가기

Reversing/▷ Study

ELF Simple Anti Reverser

흔히들 사용하는 Reversing 이라는 말은 Reverse Engineering, 역공학의 준말이다. 위키 백과에서는 역공학을 " 장치 또는 시스템의 기술적인 원리를 그 구조 분석을 통해 발견해 내는 과정이다 " 라고 정의한다. 또한 " 목적은 원본 생산의 절차에 관한 지식이 거의 없는 상태에서, 최종 제품을 가지고 디자인 결정 과정을 추론하는 것이다. " 라고 말한다. 


어떤 행위를 하게 되면, 자연히 그 행위에 대한 결과가 발생한다. [ 행위 --> 결과 ] 라는 순서가 존재하는 것이다. 역공학이란, 행위가 결과를 만드는 일반적인 순서에 대조하여, 어떤 결과를 가지고 대상이 한 행동, 행위를 분석하는 과정이다.


전통적인 순학인 Forward Engineering 이 어떠한 개념을 실체화 시키는 것을 목적으로 한다면, 역공학인 Reverse Engineering 은 실체화된 객체로 부터 개념을 얻어내는 것을 목적으로 한다. 이 중 컴퓨터 분야에서는 소프트웨어를 대상으로 역공학적 분석을 진행하고, 소프트웨어 역공학을 코드 역공학이라고 부르기도 한다. 


개발자는 프로그래밍 언어를 이용해서 시초 프로그램을 작성한다. 그 후, 컴파일과 링크의 과정을 거쳐서 실행 가능한 프로그램을 만들어 낸다. 완성된 프로그램은 외관상으로 보았을 때, 내부적으로 어떤 일을 수행하는지 알 수 없다. 역공학에서는 개발의 결과인 프로그램을 가지고 개발자가 설계한 내부 루틴에 대해서 분석한다. 


이러한 역공학적 분석을 통해서 프로그램 내부의 결함을 찾아 내고, 결함을 패치 하는 등, 보안을 강화하는 작업을 할 수 있고, 설계 과정을 알 수 없는 악성코드를 분석함으로써, 악성코드의 행동 방향을 알아내 사전에 대비 하는 등의 일을 할 수 있다. 


하지만 이런 역공학 기술들은 악의적으로 사용되었을 경우 여러가지 문제를 야기한다. 인증이 필요한 프로그램의 경우, 인증을 우회하도록 프로그램을 변경할 수 있고, 프로그램 설계상의 취약점을 찾아 내어 차후에 악의적인 목적으로 사용하는 등의 일도 충분히 일어날 수 있다.


하지만 이런 역공학 기술들은 악의적으로 사용되었을 경우 여러가지 문제를 야기한다. 인증이 필요한 프로그램의 경우, 인증을 우회하도록 프로그램을 변경할 수 있고, 프로그램 설계상의 취약점을 찾아 내어 차후에 악의적인 목적으로 사용하는 등의 일도 충분히 일어날 수 있다.


개발자의 입장에서는 자신의 프로그램을 역으로 분석하는 것이 껄끄러울 수밖에 없다. 보안 전문가들에 의해서 찾아진 취약점이라면, 다음 패치 때 보완할 수 있지만, 악의적인 사용자가 취약점을 찾는다면, 그 규모에 따라 개인, 단체, 사회적 차원에서 문제가 발생할 수 있다. 개발자는 이러한 상황을 대비하기 위해서 실제로 분석을 하기 힘든 형태로 프로그램을 작성하거나, 역공학에 더 많은 시간과 노력이 필요로 하게 프로그램의 구조를 바꾸기도 한다. 


다음과 같이 역공학 기술을 방해하는 행동들을 Anti Reverse Engineering 이라고 한다. ( 이하 Anti Reversing ). 일차적으로 모든 프로그램이 고정된 VM 과 CPU 에서 실행되기 때문에, 완전히 역공학적 분석을 방어하는 것은 불가능해 보인다. 그러므로, 분석가들이 분석하는데 더 많은 자원을 소비하도록 하거나 분석 시간을 늘리고, 분석이 끝나기 전 새로운 패치를 적용하여 처음부터 다시 분석하게 하는 등 역공학 기술들을 효과적으로 방해하고 일부는 차단할 수 있도록 노력해야 한다. 


[ ELF Simple Anti Reverse Engineering ] 


앞서 말했듯이 Anti Reversing 기술들은 바이너리 내에서 악의적인 역공학적 분석을 막기 위한 기술들이다. 이러한 안티 리버싱 기술들은 대체적으로 역공학적 분석 과정을 방해하는 역할을 한다. 어떤 데이터를 볼 수 있는 양을 제한한다거나, 무의미하게 변경할 수도 있다. 디버깅을 탐지하여 디버거로부터 분리할 수도 있고, 디버깅 툴에 혼란을 주거나 강제 종료시킬 수도 있다. 아니면 애초에 코드를 가상화 시켜 분석 시간을 길게 끌 수도 있다. 이러한 기술들 중 간단하게 구현할 수 있는 것들을 적어보려 한다.


http://www.hackintherandom2600nldatabox.nl/archive/slides/2012/aczid.pdf

거의 모든 예시들은 위의 슬라이드에서 가져왔으며, 부가적인 설명을 붙이고, 실습을 통해 알아나갈 것이다.


< Remove Symbol 심볼 제거 >


리버스 엔지니어링 과정에서 문자열, 함수의 이름과 같은 심볼들은 많은 힌트가 된다. 실제로 어느 부분에서 오류가 발생하는지, 어떤 부분에서 인증이 성공하는지에 대한 루틴들은 문자열을 통해서 찾는 경우가 많다. 그렇기 때문에 문자열이나 심볼을 지우거나 무의미한 값들로 치환시키는 것으로써 역공학적 분석을 방해할 수 있다.


실행파일에서 심볼을 지워도 실행파일은 정상적으로 실행된다. 실행 파일은 기계 수준에서 실행 파일 내 주소와 각 부분의 크기만을 가지고 실행되어지기 때문에, 사용자가 이름붙인 함수명, 클래스명과 같은 문자열, 심볼들은 지워져도 실행에 지장을 주지 않는다. 


ELF 파일내에서는 .symtab 섹션에 심볼을 저장하고 .strtab 에 함수명과 같은 문자열들을 저장한다. 이러한 심볼들은 역공학적 분석을 진행할 때 많은 도움이 된다. 이러한 심볼들이 없어도 실행파일은 정상적으로 작동하기 때문에 심볼을 지움으로써 분석에 도움이 될만한 요소를 줄일 수 있다.



실제 ELF 파일들의 여러 섹션들이다. 해당 파일의 섹션들 중 .strtab 의 오프셋이 001a64 임을 확인할 수 있다.



strtab 에는 실제로 함수명 과 같은 문자열들이 저장되어 있었다. 이러한 값들은 IDA와 같은 디스어셈블러에서 그대로 노출된다. 이러한 함수명들을 의미 없는 이름들로 바꾸어 보았다.



함수명만으로는 이게 어떤 행동을 하는 함수인지 추측할 수 없는 상황이 되었다. 분석가들은 모든 함수를 봐야할 것이고, 그로 인해서 많은 시간이 추가적으로 필요 해질 것이다


Linux 환경에서 strip 은 오브젝트에서 심볼을 삭제해주는 툴이다. 일반적으로는 빌드를 완료한 실행 파일이나 라이브러리에서 불필요한 심볼을 제거해주는 역할을 한다. 다양한 옵션이 있지만, [ strip ./test ] 와 같이 사용함으로써 문자열과 같이 불필요하다 여겨지는 심볼들을 삭제할 수 있다. 



실제 ELF 파일의 섹션들이다. 이중에 심볼을 저장하고 있는 .symtab 이 있고, 문자열을 저장한 .strtab 도 있다.



strip 을 이용하여 ELF 파일에서 불필요한 파일을 삭제하였다. 그 결과 섹션의 갯수가 29개에서 27개로 줄었다. 이 때 삭제된 두개의 섹션이 .symtab 과 .strtab 이다. 하지만 이 또한 어느정도 선까지는 복구가 가능하기 때문에, 완전한 방법이라고 이야기할 수는 없다.


< Packing & Encrypt 패킹과 암호화 >


Packing 을 통해서 프로그램을 보호할 수도 있다. Packing 은 압축하여 크기를 줄이기 위한 패킹과, 보안을 목적으로 하는 패킹이 존재한다. 패킹을 하면, 패커가 본 파일의 명령어 부분을 변경하기 때문에 정적으로 분석할 수 없게 된다. 이렇게 변경된 명령어들은 메모리에 올라가고, 프로그램 명령어가 unpacking 과정을 거치면서 본래 상태의 명령어들로 돌아오기 때문에, 프로그램의 본래 실행 코드들을 확인하기 위해서는 프로그램을 일정 부분까지 실행을 시키거나 unpacker 을 이용해서 packing 패킹을 풀어야 한다.


만약에 패킹된 프로그램의 패킹 방식이 시중에서 잘 사용되지 않거나, 개인 사용자에 의해 직접 만들어졌다면, unpacker 을 구하지 못할 뿐만 아니라, 이게 어떤 packing 기법인지도 알 수 없다. 이러한 상황에서는 직접 프로그램을 일정 부분까지 실행하는 방법밖에 없는데, 이 때 디버거를 감지하여, 디버거를 종료 시키거나 프로그램을 강제로 종료시킬 수 있다면, unpacking 을 해야 볼 수 있는 본래 명령어들을 확인하기 힘들어진다.


Packing 과 비슷하게 명령어를 Encrypt 암호화를 시킬 수도 있다. 암호화된 파일은 메모리에 적재된 후 복호화된다. Packing 과 Encryption 은 둘다 프로그램을 실행시키지 않은 상태에서 분석하는 것을 방지하기 위해서 할 수 있는 보호 기법들이다.


< Undumpable File 메모리 상황을 알 수 없는 파일 >


리눅스 환경에서 프로그램에 Segment fault 와 같은 오류가 발생하면, 당시 메모리 상황을 덤프뜬 코어 파일을 만든다. 이 때 코어 파일의 크기는 시스템 설정값에 따라 결정되는데, 코어 파일의 크기가 클 수록 메모리의 많은 부분이 기록된다. 코어 파일의 크기가 0 이라면, 메모리 오류가 발생했을 때 코어 파일이 생성되지 않는다. 이는 [ ulimit -c 0 ] 을 통해서 코어 파일의 크기 설정을 0 으로 조정함으로써 메모리에 관한 힌트를 제공하지 않을 수 있다. C 언어로 프로그램을 작성할 경우에는 < sys / prctl.h > 에서 prctl 함수의 인자를 PR_SET_DUMPABLE, 0 을 줌으로써 메모리 덤프를 막을 수 있다. [ prctl ( PR_SET_DUMPABLE, 0 ); ]


1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
    char arr[10];
    
    scanf("%s", arr);
    printf("input : %s\n", arr);
}
cs

위와 같은 소스가 있다. scanf 에서 버퍼의 크기를 지정하지 않았기 때문에, 버퍼를 넘어설 수 있다. 버퍼의 크기 보다 큰 데이터를 입력하면 stack smashing detected 혹은 Segment fault 를 통해 버퍼가 넘쳤다는 것을 알려주고, core dumped 라는 문구와 함께 당시 메모리를 본떠 core 파일을 만든다. 이 때 core 파일의 크기는 시스템에서 설정한 코어파일의 크기를 따른다. 코어 파일의 크기는 [ ulimit -c n ] 을 통해 n 바이트로 설정할 수 있고 ( n 은 상수 ) , unlimited 를 통해 코어 파일의 크기에 제한을 두지 않음으로써, 메모리 전체 상황을 본뜰 수 있다.



실제 버퍼의 크기보다 큰 데이터를 입력 하였고, core 파일이 생성 되었다. 이러한 코어 파일에는 버퍼가 넘쳤을 당시의 메모리가 그대로 저장되어 있기 때문에, 메모리를 분석하는데 도움을 줄 수 있다. 자체적으로 코어 파일 생성을 막는 방법을 도입함으로써 분석을 방해할 수 있다. 


1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <sys/prctl.h>
 
int main() 
{
    char arr[10];
    
    pretl(PR_SET_DUMPABLE, 0);
 
    scanf("%s", arr);
    printf("input : %s\n", arr);
}
cs


처음 소스에서 prctl 헤더와 함수만 추가한 것이다. 실제로 prctl 함수는 < sys / prctl.h > 에 정의되어 있고, 프로토타입은 다음과 같다. prctl 은 프로세스에 대해 설정값을 바꾼다거나, 값을 가져오는 등의 작업을 도와주는 함수이다. option 값에 따라 인자가 달라지며, 데이터 정렬 방식 ( big endian, little endian ) , 메모리 덤프 가능 여부, 메모리 오염에 대한 프로세스 강제 종료 여부 등을 설정, 혹은 설정되어져 있는 값을 가져올 수 있다. 이러한 option 들은 < sys / prctl.h > 에 정의되어 있다.


다양한 옵션들 중에서 PR_SET_DUMPABLE 옵션은 dumpable 플래그의 값을 변경한다. 0 일경우 SUID_DUMP_DISABLE 로 덤프가 불가능하다. 1일 경우 SUID_DUMP_USER 로 덤프가 가능하다. 또한 커널 버전에 따라 2 를 인자로 받을 수도 있는데, 2일 경우 관리자 권한일 때만 읽을 수 있는 core 파일을 생성하게 된다. prctl 함수를 통해 dumpable 플래그를 0 으로 바꿈으로써 메모리 dump 가 불가능해지고, core 파일이 생성되지 않게 된다. 


실행결과 core dumped 라는 구문도 사라지고, core 파일도 생성되지 않았다.


< Avoid LD Overrides >


리눅스 환경에서 LD_* 환경변수들은 동적 라이브러리의 로드에 관여하는 환경변수이다. LD_PRELOAD 와 LD_LIBRARY_PATH 환경 변수를 통해서 사용자가 직접 정의한 라이브러리를 프로그램에 적재할 수 있다. 이 때 문제가 되는 것은, LD_PRELOAD 환경변수의 값으로 설정된 라이브러리가 일반 라이브러리 보다 메모리에 먼저 적재되고, 함수를 호출할 때도 같은 함수명이 존재한다면, 일반적인 라이브러리의 함수보다 환경 변수에 설정된 라이브러리의 함수가 우선권을 갖는다는 것이다. 


1
2
3
int printf (const char *restrict_format, ...) {
    system("/bin/sh");
}
cs


예를 들어 사용자가 printf 를 위와 같이 재정의 하는 라이브러리를 만들었다. 그리고 라이브러리를 LD_PRELOAD 환경변수의 값으로 설정하였다면, 이후에 printf 를 호출 하는 프로그램은 stdio.h 의 문자열을 출력하는 printf 함수가 아닌, 사용자가 정의한 라이브러리 내의 /bin/sh 를 실행시키는 printf 함수를 호출할 것이다. 즉 문자열이 출력되지 않고 [ system ( "/bin/sh " ); ] 가 실행이 된다. 


이런식으로 LD_PRELOAD 와 LD_LIBRARY_PATH 를 이용하여 함수를 내가 원하는 모습으로 재정의가 가능하다. 프로그램의 생성자나 main 함수에 도달하기 전에 이미 LD_PRELOAD 나 LD_LIBRARY_PATH 환경변수에 설정된 라이브러리를 메모리에 적재하기 때문에 할당된 라이브러리를 할당 해제 하지 않는 이상, 프로그램이 실행되고, 끝날 때까지 내가 의도한 함수들이 실행될거란 보장을 할 수 없다.

 

그래서 필자가 내린 결론 중 하나는 환경변수명 중에서 LD_ 로 시작하는 문자열이 있다면, 프로그램을 강제로 종료하는 것이다. C 언어의 경우 main 의 인자로 argc, argv, envp 를 받는다. 이 때 envp 에는 환경 변수들이 이중 포인터의 형태로 넘어 온다. envp 의 문자열을 순회하면서, 시작이 LD_ 로 시작하는 문자열이 있으면, 프로그램을 종료하는 것이다


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char **argv, char **envp) {
    char arr[10];
    char **p;
 
    for (p = envp; *!= NULL; p++) {
        if ((*p)[0== 'L' && (*p)[1== 'D' && (*p)[2== '_' ) {
            exit(0);
        }
    }
 
    scanf("%s", arr);
    printf("input : %s\n", arr);
 
    return 0;
}
cs


위와 같은 코드를 통해서 환경변수에서 LD_ 로 시작하는 문자열이 있을 경우, LD_PRELOAD 와 LD_LIBRARY_PATH 가 있는 것으로 판단하고 프로그램을 강제로 종료 시키는 것이다. 그렇다면 프로그램이 종료되는 한이 있어도, 의도하지 않은 흐름대로 프로그램이 실행되지 않을 것이다.


하지만 이런 방법도 사용자가 정의한 라이브러리의 생성자에서 LD_PRELOAD 환경 변수를 unset 함으로써 LD_ 문자열을 검색하는 위의 루틴을 우회할 수 있게된다.


1
2
3
4
5
6
7
8
void __attribute__((constructor)) clean() {
    unsetenv("LD_PRELOAD");
    unsetenv("LD_LIBRARY_PATH");
}
 
int printf (const char *restrict_format, ...) {
    system("/bin/sh");
}
cs

LD_PRELOAD 에 의해서 라이브러리가 메모리에 적재되면, 라이브러리의 생성자가 실행 ( void __attribute__((constructor)) clean() ) 된다. 이 때 생성자에서 unsetenv("LD_PRELOAD"); 를 호출하게되고 LD_PRELOAD 환경 변수를 할당 해제하게된다. 라이브러리는 이미 적재되어 있고, 함수도 사용자가 정의한 라이브러리의 함수가 우선권을 가지기 때문에 LD_PRELOAD 의 효과를 보면서 환경변수에서 LD_ 문자열을 찾는 루틴을 우회할 수 있게 된다. 그렇기에 앞서 말한 방법은 LD_PRELOAD 를 정확히 걸러낼 수 없고, 다른 방법이 필요할지도 모른다.


< PID, Name Randomize >


위해서는 프로세스의 pid 나 프로세스 명 둘 중 하나는 알아야 한다. 프로그램 내부적으로 pid 와 프로세스 명을 임의로 바꾸게 된다면, 해당 프로세스의 pid 와 프로세스명을 알기 어려워지고, debugger 을 붙이지 못해 분석에 문제가 생길 것이다. 이전에 앞서본 prctl 함수를 통해서 프로세스명을 바꿀 수 있다. 


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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/prct1.h>
 
char *randString() {
    int i;
    char arr[10];
    char lib[65= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    
    srand(time(NULL));
    
    for(i=0; i < 10++i) {
        arr[i] = lib[rand() % 62];
    }
 
    return arr;
}
 
void main() {
    pid_t pid = fork();
    
     prctl(PR_SET_NAME, randString());     
 
    if( pid > 0
        exit(rand());
 
    else
        setsid();
 
    printf("hi ~~\n");
}
cs

위 소스에서 fork 는 자신과 같은 프로세스를 만든다. 새로만들어진 프로세스는 0 을 반환하고, 기존에 있던 프로세스는 자신의 pid 를 반환하게 된다. 그 후 prctl 을 통해서 프로세스 명을 바꾼다. 기존에 있던 프로세스는 exit 함수에 의해서 종료되고, 새로 만들어진 함수는 setsid 를 통해서 독립된 새로운 프로세스 그룹을 만들게 된다. 결론적으로 fork 에 의해서 생성된 프로그램은 pid 도 프로세스명도 알 수 없게 된다.


prctl 에서 PR_SET_NAME 을 통해서 현재 프로세스의 이름을 바꿀 수 있다. 해당 이름을 임의로 컴퓨터가 지정해버림으로써 현재 프로세스명을 알기 어려워 졌고, 프로세스명을 가지고 pid 를 찾아 디버거를 붙이는게 더욱 어려워졌다.


이외에도 elf 코드 난독화, 코드 가상화와 같은 기술들이 있으며, 그에 대한 많은 우회 기법들도 개발이 되고 있다. 앞서 이야기한 내용은 정말 간단한 이야기였으며, 자세한 내용은 더 찾아보길 바랍니다.


http://revsic.tistory.com/3 << 출처입니다.

'Reversing > ▷ Study' 카테고리의 다른 글

angr 설치  (0) 2018.01.03
Retargetable Decompiler  (0) 2017.12.23
프로그램 실행구조  (0) 2017.03.15
어셈블리와 C언어의 포인터 구문 형식  (0) 2017.03.08
Stack Frame(스택 프레임)  (0) 2017.03.08