포맷 스트링 버그 분석

 1603, 1/81 회원가입  로그인  
   asdzxc301
   포맷 스트링 버그 분석

http://www.hackerschool.org/HS_Boards/zboard.php?id=Free_Lectures&no=1152 [복사]


원문 : http://downloads.securityfocus.com/library/format-bug-analysis.pdf

Andreas Thuemmel, a.thuemmel@web.de
-Version 1.0, 15-02-2001-


1. 안내 및 요약
포멧스트링버그는 로컬 이나 원격공격에서 사용할 수 있는 새로운 기술이다.
1999년 9월 위험에 대한 내용이 발표되고 2000년 6월 wu-ftpd 2.6.0에 대한
공격코드가 발표되었다. 공격코드는 1년정도 언더그라운드에서 배포되었으며
2000년 여름이후 많은 수의 포맷스트링버그를 기초로한 공격코드가
발표되었으며 리눅스와 유닉스 배포자들과 밴더들의 관심사가 되었다.

. 원격 공격:
  wu-ftpd, BSD ftpd, proftpd, rpc.statd, PHP 3와 4, TIS-Firewall Toolkit, ...

. 로컬 공격:
  lpr, LPRng, ypbind, BSD chpass 와 fstat, libc의 localistaion, ...

. 라면 웜은 포멧스트링버그를 이용하여 wu-ftpd와 rpc.statd 그리고
  LPRng등을 공격한다.

이 기사는 포멧스트링 공격코드의 속임수와 제한사항등에 대하여 생각하고
분석하기위해서 쓰여졌다. 워싱턴대학의 ftp서버(wu-ftpd 2.6.0)와 Red Hat
Linux 6.2를 이용해서 실질적인 예를 들것이다.  이 기사의 구조는 다음과 같다.

. 문제의 정의
. 스텍읽기
. 문자열 변수의 내용 읽기
. 정수값의 쓰기
. 포맷스트링 버그에 대한 방어

첨부의 샘플코드는 이 기사의 예를 위하여 쓰여졌다. 이 코드는 몇몇의
다른시스템에서 유용한 포멧트스링을 생성할 수 있다.

2. 문제와 생각

C언어(C++포함)에서는 인수의 수를 가변적으로 사용할 수 있게
정의되어있다. 불려질때에 몇개의 인수가 함수에 주어졌는지을 알려준다.
표준 C의 경우 fprintf(), printf(), sprintf(), snprintf(), vprintf(),
vsprintf(), vsnprintf(), setproctitle()와 syslog()등이 이들중의
하나이다. 이 모든 함수들은 공통된 점이 두가지 부분이 있다.

. 처음인수는 불려진 포멧문자열이다.
. 다음에 오는 인수는 포멧문자열의 형태에 따라 여러갖로 변환된다.

다음에 이야기하는 부분들은 쉽게 이해할 수 있도록 printf()함수를
기준으로 설명한다. 다음에 설명하는 문장들은 포멧문자열을 처리하는
모든 함수에서 잘 동작한다. 포멧문자열은 다음의 두가지경우에 사용한다.

. 따라오는 인수를 문자열로 변경하는 방법을 정의
. 얼마나 많은 인수가 필요로 하는지 정의

포멧문자열자신은 출력스트림으로 복사되고 따라오는 인수들을 어떻게
변환할것인지에 대한 정의를 담은 변환지정자들인 평범한 문자들의 혼합으로
되어있다.

int i = 20;
int j = 10;
char *format_string = "The numbers are %d and %d";
printf(format_string, i, j);

위의 코드는 "The numbers are 20 and 10"이라고 표준출력으로 인쇄한다.
"%d"가 변환지정자 이다. 변환지정자는 %로 시작한다. %뒤에 따라오는
문자들은 출력(정렬, 폭, padding등)의 형태를 지정하고, 주어진 인수의
형태를 결정한다. 출력스트림에서 모든 %의 형태지시자는 적절한 인수의
값으로 대치된다. (%자체를 출력하는 %%는 제외)
중요한 변환지시자는 다음과 같다.

. %d - 정수(int)를 십진법수(decimal)
. %x - 정수(int)를 16진법수(hex)
. %s - 문자열

버퍼오버플로우와 같이 포멧스트링버그의 문제는 프로그램개발새의 무지와
게으름에의하여 발생한다. 다음의 코드를 생각해 보자

char *user_supplied_input;
[...]
printf(user_supplied_input);

또는

char *user_supplied_input;
char *some_string;
[...]
sprintf(some_string, "%s", user_supplied_input);
[...]
printf(some_string);

양쪽경우다 사용자가 제공한 입력이 printf()함수의 포멧문자열로 제공된다.

printf("%s", user_supplied_input);
보다는
printf("%s", some_string);
이 더욱 정확한 사용법이다(?). 위의 예에서 사용자가 %x가 포함되어 있는
문자열을 입력한다면 어떤 현상이 발생할까? printf()는 정수 인수가
포멧문자열의 뒤에 있을것이라고 가정할 것이다. 그러나 거기에는 인수가
없다. 이와같은 잘못된점은 컴파일시에 인식되지 않는다.


예: wu-ftpd 2.6.0

wu-ftpd 2.6.0의 문제는 vreply()(src/ftpd.c)에 있다.
간단하게 vreply()는 다음과 같이 보인다.

void vreply([...], char *fmt, [...])
{
        char buf[BUFSIZ];
        [...]

        snprintf(buf, sizeof(buf), fmt);
        [...]
}


이와같은 경우에 site exec명령의 *fmt는 SITE EXEC명령과 함께 사용자가
제공한 문자열을 포함하고 있다. 이 방법으로 ftp사용자는 snprintf()를
제어할 수 있다. 다음장에서는 어떻게 이러한 버그가 공격되는지 보여줄
것이다.

3. 스택읽기
함수에 인자를 넘겨주면서 caller는 활성화 자료(activation record(or
frame))를 스택에 넣는다. 예를 들면 함수 f(int i, int *j)가 불려질때에
다음과 같은 스택의 구조를 가지게 된다.


    |  +----------------------------+   <--------- Bottom of stack
    |  | Local variables,           |
    |  | saved registers,           |
    |  | activation record of       |
    |  | other functions            |
    |  |                            |
    |  |                            |
    |  +----------------------------+   // -------------------------
    |  |                            |   // --                     --
    |  |  Pointer to J              |   // --  이 부분이 f()에    --
    |  |                            |   // --  대한 활성화 자료   --
    |  +----------------------------+   // --  가 된다.           --
    |  |                            |   // --                     --
    |  |  value of I                |   // --                     --
    |  |                            |   // --                     --
    |  +----------------------------+   // --                     --
    |  |                            |   // --                     --
    |  |  Return address to caller  |   // --                     --
    |  |          f()               |   // --                     --
    |  |                            |   // --                     --
    |  +----------------------------+   // -------------------------
    |  |  saved registers           |
    |  +----------------------------+
    |  |  local variables of        |
    |  |          f()               |
    |  |                            |
    |  +----------------------------+   <--------- Top of stack
    V

만약 적당한 인수가 없는상태의 포멧스트링을 printf()에 준다면 어떤
상황이 나타날까?  printf()를 위한 모든 인수는 스택에 놓여진다.
printf()는 activation record가 포멧스트링의 모든 변환지시자가 가리키는
수만큼의 인수가 스택에 있다고 가정하게 된다. 모든 %는 스텍에서 적당한
위치의 값을 읽게 된다. 이것은 스택을 아래쪽으로 읽게되고 실질적인
활성화 자료(activation record)의 범위를 벗어났는지와 상관없이
출력스트림쪽으로 인쇄를 하게된다. activation record에는 범위검사가
없다.

일반적인 상황에서 모멧스트링은 caller에 의해 적절한 크기의 자료가
실제 activation record에 넣어지게 된다. 포멧스트링 조작 공격자는
printf()가 실제 activation record보다 크다라고 생각하게 만든다.

이와같은 방법으로 printf()함수가 출력스트림으로 출력을 하게되면
공격자는 스택의 값들을 읽을수 있게 된다.

예: wu-ftpd 2.6.0
Red Hat 6.2 리눅스시스템에서의 ftp세션예를 들어본다. ftp클라이언트
대신에 netca을 사용하였다. 사용자의 입력은 굵은(?)글씨로 표현한다.
사용자 "andreas"가 "SITE EXEC %x %x %x %x"명령을 실행한다.  %x는
포멧스트링으로 인식되여 결과는 "31 bffff53c 1ee 6d"로 나타나 실제 ftpd
process의 스택값을 출력한다.


% nc jeddy3 21
220 jeddy3 FTP server (Version wu-2.6.0(2) Thu Aug 3 18:24:27 CEST
2000)
ready.
USER andreas
331 Password required for andreas.
PASS 2138
230 User andreas logged in.
SITE EXEC %x %x %x %x
200-31 bfffff53c 1ee 6d
200 (end of '%x %x %x %x')
QUIT
221-You have transferred 0 bytes in 0 files.
221-Total traffic for this session was 291 bytes in 0 transfers.
221-Thank you for using the FTP service on jeddy3.
221 Goodbye.


4. 프로세서 메모리의 임의 위치의 문자열 읽기

만약 printf의 출력이 사용자에게 보여진다면, 공격자는 스택의 내용만을
보는것이 아니고, text나 data또는 heap등의 임의위치의 문자열을 읽을 수
있다. 어떻게 이렇게 동작하는지는 문자열 인수가 어떻게 함수에게
전달되는지에 관한 내용을 알아야 한다. 문자열인수에 대한 activation
record는 참조(포인터같은..)값만을 저장하고 있다. 따라서 %s를 이용해서
원하는 내용을 표시하기 위해서는 포인터를 activation record에
기록하여야 한다. 그러나 공격자는 프로그램코드를 수정할수 없고 추가적인
포인터를 printf()의 스택에 넣을수 있을 뿐이다. 그들은 %s를
입력문자열에 넣을수 있다. 하지만 어떻게 적절한 포인터를 activation
record에 넣을수 있을까? 그것에 대한 정답은 포멧스트링에 있다.

포멧스트링자신이 스택에 저장된다고 가정해 보자. (지금부터 트릭) %d나
%x를 %s앞에 넣어주면 printf()는 포멧스트링의 시작부분앞까지 스택읽기를
실행한다. 포멧스트링 자신은 공격자가 관심있는 문자열을 지시하는 메모리의
포인터를 구성하는 몇바이트(32비트 architectures인경우 4바이트)로
시작한다. printf()가 %s를 해석하기 시작하면, printf()는 정확하게
포인터에서 문자열을 추출하게 된다.(?)

위와 같은 경우의 printf()가 출력하는 예를 보면
+----------------------+----------------------------------+-------------------+
| Adress of string     |  Lots of trash: local vars,      |  the string       |
| copied as characters |  registers, return addresses,    |  that attacker is |
| to the output stream |  that are interpreted as integers|  interested in    |
+----------------------+----------------------------------+-------------------+

위와같은 경우의 트릭은 activation record를 포멧스트링이 시작하는
위치까지 확장하는 것이다. 이 방법으로 공격자는 activation record의
몇비트를 제어할 수 있게 된다.




앞에서 보여진 기술인 포멧스트링이 함수의 로컬변수처럼 스텍에 저장될때에만
가능한 기술이다. 다른말로 출력스트림이 다른버퍼로 인쇄되면(sprintf),
이버퍼를 이용할 수 있다.

두번째 제약사항이 있다. C에서 문자열은 ASCIIZ형태로 저장되어야 한다.
그러므로 포인터가 지시하는 문자열에는 0x00을 포함할 수 없다.
32-bit아키텍처에서 이것은 2%의 주소공간이 점검되지 않는다는 뜻이다.

Remark:
스택에 있는 포멧스트링을 찾기 위하여 충분한 %x들로 %s을 채우면 결과는
거대한 포멧스트링에 있다.(?)  C표준라이브러리에서 지원된다면, $플래그의
사용은 주어진 인수로 직접 JUMP하는 아주 효과적인 방법이다.
이것은 입력버퍼의 공간을 잘약할 뿐만아니라, 로컬변수들이나 레지스터값과
정수로 변환된 리턴주소가 잘못 출력되는것을 막아준다.


예: wu-ftpd 2.6.0

이번에는 좀더 조심해서 만들어진 포멧스트링이 "SITE EXEC"의 인수로 사용된다.

"AA"는 padding을 위해서 사용된다. 다음의 이상한 문자열 "@e가가"와 "pj가가"는
passwd구조체 *pw의 pw_name과 pw_passwd필드를 지시하는 포인터인 0x8086a70과
0x8086540을 표현하는 문자들이다. "277$"는 277 %X를 대신해서 사용되었다.        
아래에서 보여지는 "andreas"는 pw->pw_name의 값이고
"$1$P3aRAfUA$ATCfz9G/KGUiKn9NZSV6M1"는 pw->pw_passwd(/etc/passwd처럼 암호화)값이다.

% nc jeddy3 21
220 jeddy3 FTP server (Version wu-2.6.0(2) The Aug 3 18:24:27 CEST 2000)
ready.
4.1.1 USER andreas
331 Password required for andreas.
4.1.2 PASS 2138
230 User andreas logged in.
4.1.2.1 SITE EXEC AA@e가가%227$s
200-aa@2가가andreas
200   (end of 'aa@e가%277$s')
SITE EXEC AApj가%227$s
200-aapj가$1$P3aRAfUA$ATCfz9G/KGUiKn9NZSV6M1
200   (end of 'aapj가%277$s')


5. 프로세서의 임의의 위치에 정수값 쓰기

일반적인 문자열로 변환하는 변환지정자 외에 특별한 목적으로 사용하는 지정자로 %n이 있다.

BSD의 멘페이지에는 다음과 같이 정의되어있다.

. %n: The number of characters written so far is into the integer indicated
      by the [corresponding] int * (or variant) pointer agrument.
      (정수포인터가 지시하고 있는곳에 출력된 문자의 갯수를 정수값으로 기록한다.)

예를들면 다음의 일부 코드의 결과로 i=5이다.

int i;
printf("12345%n", &i);

위에서 본것처럼 printf()가 메모리의 아무위치에나 정수값을 쓸 수 있다. 주어진 포멧스트링이
스택에 위치하고, 공격자가 앞장에서 설명한 기술을 사용할 수 있다면, 충분한 %x뒤에 %n을
사용하여 스택을 따라서 내려간 다음 포멧스트링이 위치한 곳에 다다를수 있다. 포멧스트링은
다음을 할 수 있는 메모리로 구성된 바이트들(포인터로 번역된)로 시작한다.
공격자가 할수 있는것
. 중요한 프로그램의 접근제어를 덥어쓰기
. 내부연결 테이블들, 함수포인터, setjmp/longjmp버퍼들, 스택의 리턴주소등의 덥어쓰기

그러나 쓰여진 값은 %n이 나타나기 전까지의 출력된 문자들의 갯수에 의하여 결정된다.
정말로 완하는 정수값을 기록할 수 있을까? 그것은 가능하다, 하지만 다른 두개의 트릭이 필요하다.

첫번째 트릭은 더미출력문자를 사용하는 것이다. 1000을 기록하기 위해서는 1000개의 더미문자를
출력하면 된다. 물론 스택을 따라 내려가며 포멧스트링에 다다르기위한 %x의 사용길이를 고려해야 한다.
(%.8x가 32-bit구조에서 좋은선택이다. 이것의 출력길이는 실제 출력되는것과 상관없이 8자리이다.
또한 $플래그도 사용 가능하다.) 1000개의 더미문자를 출력하는 긴포멧스트링의 사용을 피하기 위하여,
폭지정자가 사용될 수 있다. ....

이와같은 이론에 의하여 원하는 값을 기록할 수는 있지만 표준 C라이브러리의 구현에 따라
임의의 긴 폭 지정자를 사용할 수 없는 특별한 제한이 걸리는 경우가 있어서 두번째 트릭이 필요하다.

두번째 트릭은 %n을 한번이상 사용하는 것이다. %n을 한번이상 사용하면, 하나의 주소에 대하여 이동이
되면서 몇번의 쓰기가 진행된다. 예를들면 리틀엔디안 32비트(IA32같은) 구조에서 "misaligned"기록이
허락되는데, 이것은 하나의 포인터에서 1씩 증가하여  4번의 성공적인 정수의 기록이 가능하다.
이와같은 방법은 항상 3바이트의 자료를 덥어쓰므로 남아있는 하나의 바이트는 다음 쓰기에서 변경되지않고
남아있게 된다. 서로다른 %n사이에 첫번째트릭이 사용되고 다음 기록에서 바이트의 값을 조정함으로서
LSB가 변경되지 않도록 한다. 이같은 방법은 하나의 바이트 기록시에 255개의 더미문자를 사용할 수 있다.





빅엔디안 구조에서의 위의 내용이 뒤집혀서 나타난다. 각각의 성공적인 기록은 1씩 감소하면서 기록된다.
~~~아 귀찬아~~~~~~

예: wu-ftpd 2.6.0

마지막으로 "SITE EXEC"는 스택의 리턴주소를 덥어쓰기 위해서 사용된다. 실질적인 포멧스트링은 첨부에
주어진 코드로 생성되었다. (인수는 -n 1098 -m 0xbfffe4c8 -k 0xbfffd55a -d 이지만 시스템마다
다를수 있다.) %hn지정자(h플래그는 int대신에 short int를 의미한다.)다음에는 x86의 리눅스 쉘 코드가
따라온다. ftp제어문자회피가 필요하다.(0xff를 두번사용하면 0xff하나의 의미가 된다.) 다음의 예를 보면
공격자는 쉘을 얻었고, "uname -a"와 "id"명령어를 사용하여 root가 되었음을 확인하고 있다.

% nc jeddy3 21
220 jeddy3 FTP server (Version wu-2.6.0(2) Thu Aug 3 18:24:27 CEST 2000)
ready.
5.1.1 USER andreas
331 Password required for andreas.
5.1.2 PASS 2138
230 User andreas logged in.
SITE EXEC AA.............format string ............중간에 %hn이 네개있다.....................
200-aa...........................^^.............................................
200 (end of 'aa ........................................)

uname -a

Linux jeddy3 2.2.14-5.0 #1 Tue Mar .....

id

uid=0(root) gid=0(root)


버퍼오버플로우 처럼, 성공적인 공격을 위해서 공격자는 몇개의 프로세스내부 주소에 대해서 알고 있거나
추측해야 한다.

. 덥어쓰기 원하는 주소의 위치(예: 리턴주소)
. 쓰고자 하는 값(예: 공격자의 쉘코드 주소)
. 포멧스트링까지 내려가기위해 필요한 바이트(이 정보는 버퍼오버플로우에서는 필요없는 것이였다.)

만약 출력이 공격자에게 보내지면, 공격자는 스택을 읽을수 있다. 이것은 공격자가 위의 값을 추측하기
쉽게한다. 원래 알려진 wu-ftpd를 위한 expolit코드는 이것을 찾는것을 자동화 했다.
진정한 스크립트기티 expolit이다.

[출처] 포맷 스트링 버그 분석 -Andreas|작성자 미스티

  Hit : 12676     Date : 2008/12/24 07:50



    
     [공지] 강좌를 올리실 때는 말머리를 달아주세요^ㅡ^ [29] 멍멍 02/27 19951
1602   해커스쿨 만화의 자동으로 스캔하는 프로그램     해킹잘하고싶다
02/18 423
1601   시스템 콜 추적 확장판[2]     해킹잘하고싶다
01/19 587
1600   간단한 시스템 콜 추적 프로그램 만들기     해킹잘하고싶다
01/18 579
1599   [overthewire.org] - leviathan1     해킹잘하고싶다
01/14 894
1598   [overthewire.org] - leviathan0     해킹잘하고싶다
01/14 622
1597   [Write Up] Crypto Cat's CTF 2024 - BabyFlow     해킹잘하고싶다
12/29 618
1596   [pwnable.kr] bof     해킹잘하고싶다
12/25 627
1595   [pwnable.kr] Shellshock[1]     해킹잘하고싶다
11/23 746
1594   Shellshock의 기본 요약     해킹잘하고싶다
11/23 716
1593   [pwnable.kr] fd     해킹잘하고싶다
11/23 718
1592   VPN이 연결되었다가 도중에 꺼도 웹 브라우저상에서 유지되는 이유     해킹잘하고싶다
11/22 660
1591   해커들이 해킹시 사용하는 디렉토리 공간[1]     해킹잘하고싶다
11/22 741
1590   Keyboard Hooking -part2 - (Python3 ver)     해킹잘하고싶다
11/20 682
1589   [Windows API] Keyboard Hooking     해킹잘하고싶다
11/20 525
1588   [pwnable.kr] cmd1 공략     해킹잘하고싶다
10/23 700
1587   netdiscover 파이썬으로 구현하기     해킹잘하고싶다
08/13 889
1586   파이썬을 이용한 심플 웹 크롤러     해킹잘하고싶다
08/13 760
1585   파이썬 random모듈을 이용한 숫자맞추기 게임 구현     해킹잘하고싶다
05/30 1354
1584   파이썬 채팅 프로그램 구현     해킹잘하고싶다
05/28 1285
1 [2][3][4][5][6][7][8][9][10]..[81]

Copyright 1999-2025 Zeroboard / skin by Hackerschool.org / Secure Patch by Hackerschool.org