Pwnable - setvbuf

Intro

얼마 전 문제를 풀다가 처음 보는 개념이 나와서 정리해보려고 한다. scanf("%d", &n)으로 int형 값을 입력받는데 이상하게 오버플로우(?)가 발생하지도 않는데 데이터가 heap에 저장되는 것이다. write up을 살펴봐도 익스 코드만 있고 heap에 paylaod를 pivot한다는 식만 주석으로 간단하게 적혀있어서 왜 heap에 payload가 저장되는지 이해하기가 힘들었다. 이에 대해 간단하게 알아보자.

결론을 말하면 setvbuf 함수 때문이었다.

setvbuf

1
2
#include <stdio.h>  
int setvbuf(FILE* stream, char* buffer, int mode, size_t size);

스트림 버퍼 방식을 변경한다. 버퍼링은 시스템 자원을 효율적으로 사용하기 위해 데이터를 버퍼라는 공간에 담아두고 사용하는 것을 의미한다. 버퍼링을 하지 않으면 버퍼라는 공간을 쓰지 않는 걸로 해석된다.

인자를 살펴보자.

  • File* stream
    • 작업을 수행할 열린 스트림의 FILE 객체를 가리키는 포인터
    • 포너블 문제를 풀어본 경험이 있다면 main 초반에 stdin, stdout, stderr 등을 본 경험이 있을 것이다.
  • char* buffer
    • 유저가 할당한 버퍼인데 포너블 문제를 풀어본 경험이 있다면 보통 NULL로 설정되어 있다.
    • NULL이라면 함수는 자동으로 지정한 크기의 버퍼를 할당하게 된다.
  • int mode
    • 파일 버퍼링의 형식을 지정하는데 3가지가 있다.
    • _IOFBF: 버퍼가 다 차지 않는 이상 쓰기 작업을 하지 않는다. 즉 버퍼에 size 바이트 이상 쓰이지 않는 이상 stream에 쓰이지 않는다.
    • _IOLBF: 출력할 때 버퍼가 다 채워지거나 개행 문자가 입력 되었으면 데이터가 버퍼에서 출력되고, 입력 시에는 개행문자까지 입력받는다.
    • _IOLBF: 포너블 문제를 풀어본 경험이 있다면 보통은 이 경우다. 버퍼를 사용하지 않겠다는 뜻이다. stdoutstderr는 보통 이 모드로 설정한다.
  • size_t size
    • 지정할 버퍼의 크기의 단위
    • NULL이라면 컴퓨터가 알아서 최소의 크기를 지정한다.

example

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
setvbuf(stdin, 0, 2, 0); // 있는 것과 없는 것의 차이?

int n = 0;
scanf("%d", &n);
}

예시 코드이다. 입력으로는 아래를 보냈다.

1
1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

내 예상으로는 1만 n에 들어가고 (setvbuf가 있던 말던 ㅎ;) 나머지는 입력이 다 씹힐 것이라고 생각했다.

  • setvbuf(stdin, 0, 2, 0)가 없는 경우

일단 stack을 살펴보면 지역변수 n 자리에 1만 정상적으로 들어간 모습을 확인할 수 있다. 하지만 heap 상황은 어떨까?


scanf 로직에 의해 malloc을 하고 버퍼링을 한다. heap에다가 1을 포함한 값들을 씹지 않고 저장한 모습을 볼 수 있다. 후에 이 값은 나중에 scanf로 값을 다시 입력받을 때 사용자에게 입력받지 않고 이 공간에서 꺼내서 씀으로써 속도 등의 측면에서 성능을 항상시킨다고 한다.

포너블 문제에서 해당 구문이 있는 것은 이 때문인데, 이것이 없으면 입력과 출력 간의 순서가 모호해지고 leak한 값을 출력을 해야하는데 출력이 안될 수도 있다. 그래서 setvbuf 함수로 버퍼를 사용하지 않는, 즉 버퍼링을 하지 않아야 한다.

  • setvbuf(stdin, 0, 2, 0)가 있는 경우

디버깅으로 확인했는데 단호하게 1 뒤에 값은 다 씹는다.

Conclusion

포너블 문제에서 일반적으로 setvbuf(stdin, 0, 2, 0)와 같은 구문이 있어야한다. 하지만 없는 경우도 있다. 저 구문이 없고 scanf와 같은 함수(malloc을 시도하는 함수)로 사용자에 입력을 받는다면? heap에 payload를 저장해서 ROP를 시도하는 방향으로 진행해봐도 좋을 듯하다.