[Kernel] printf 함수

Linux Kernel 2013. 11. 23. 09:39

※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


Linux Kernel이라는 폴더를 만들어 놓은지 한참 된 것 같은데 첫 포스팅이 지금에서야 올라간다. 그것도 약간 억지스럽게 올라가게 된다. 사실 이 printf 함수는 format string vulnerability를 공부하기 위해서 분석했던 것인데 이것을 vulnerability 폴더에 집어넣자니 다루는 내용이 너무 kernel의 기본에 충실한 것 같기도 하여(해킹은 기본에 충실해야 한다고 생각하는 한 사람이긴 하지만...) Linux Kernel 폴더에 넣기로 했다.


아무튼 그리하여 printf 함수에 대해서 포스팅 하기로 한 것이다.


우선 printf 함수가 어떤 함수인지 설명하도록 하겠다. 할 것도 없나? 그냥 출력해주는 함수이다. 출력해주는 함수인데 옵션들을 활용하여 여러 가지 기능을 실행할 수 있다. 옵션들은 아래와 같다.(아래의 옵션이 전부가 아니다.)

우선 printf 함수의 위치부터 공유하고자 한다. Kernel source에서 printf 함수는 다음의 위치에 C파일로 선언되어 있다.

 

/arch/x86/boot/printf.c

 

printf.c 파일 내에 존재하는 함수는 아래와 같다.


사용자가 직접적으로 사용하는 함수는 printf 함수이다. 우선 printf 함수부터 자세히 보자.

 

위의 함수에서 볼 수 있듯이, va_start vsprintf→ va_end 순서대로 호출하여 프린트할 결과물을 만든 후, 마지막에 puts함수를 이용하여 실제로 print를 진행한다.

 

va_start va_end 함수는 va_arg함수와(vsprintf 함수 안에서 사용된다.) 함께 자세하게 이해해두어야 하는 일종의 보조함수이다. va_start, va_end, va_arg 함수에 대해서 알아보자.

 

해당 함수는 #define을 통해 /include/acpi/platform/acenv.h에 선언되어있다.

va_start(ap, A)는 사용자가 출력하기 위해 입력한 문자열의 주소값(A) ap에 넣어주는 과정이다. va_arg(ap, T) va_start 함수가 사용된 후 사용되는데, va_start 함수에 의해 설정된 ap 값에 변수 T의 크기를 더한다. 예를 들어, va_arg(ap, unsigned int)를 수행할 경우 기본적으로 ap가 가지고 있는 값에 unsigned int의 크기인 4 byte를 더하게 된다. va_end는 종료하기 위한 마무리 과정 정도로 알고 넘어가도 될 듯 하다.

 

va_start, va_arg, va_end가 왜 중요한지에 대해서는 아직 설명하지 않았다. 그 이유에 대해서는 조만간 설명할 예정이다. 조그만 참고 vsprintf 함수에 대하여 알아보자.

 

vsprintf함수는 for문 안에서 사용자가 입력한 문자열을 하나씩 체크하면서 동작한다.


가장 밑에 있는 if문을 보면, %가 아닌 문자를 처리하는 것을 볼 수 있다. 중요한 부분은 검사할 문자열에 %가 포함되어있을 경우이다. 이러할 경우 switch문을 통해 처리를 하게 되는데 이 부분이 중요한 부분이다switch문은 아래와 같다.


해당 switch문에는 여러 다른 옵션들에 대한 코드도 구현되어있지만 Format String Vulnerability를 설명하면서 사용할 옵션들만 보자. %s %n%x와 달리 case문 안에서 va_arg를 수행하고 continue문을 통해 for문의 가장 처음으로 돌아가는 것을 볼 수 있다. %s %n 이외에는 모두 break문을 통해 switch문을 빠져 나온 후 A영역을 수행하게 되어있다.

 

여기서 잠깐, 계속 va_arg함수를 수행한다고는 하는데 해당 함수가 무엇을 하는지는 아직 정확히 이야기하지 않았다. 아직까지는 이것만 알자. 각 옵션은 va_arg 함수를 수행해야 number 함수를 통해 각 옵션에 대한 올바른 값을 얻을 수 있다는 것만 알자. 왜 그런지는 “4. Format String Vulnerability에 대한 자세한 분석에서 자세하게 설명할 생각이다.

 

다시 본론으로 돌아와서, Format String Vulnerability에서 사용될 %x, %s, %n, 옵션에 대해서 자세하게 봐보자.

 

우선 %x부터 보자.


flag |= SMALL; 연산을 한 후, base 16으로 놓는 것을 보니 후에 do_div(n, base)함수를 통해 16진수로 바꿀 예정으로 보인다. %x는 간단한 구조로 되어있으니 더 이상의 긴 설명은 생략하도록 하겠다.

 

다음은 %s를 보자.

위에서 말했듯이 %s 옵션은 va_arg 함수를 case문에서 사용한다. 그리고 strlen함수를 통해 프린트할 문자열의 길이 s를 구하고 for 문을 통해 원하는 문자열(길이가 s) str에 저장한다.

 

 

다음은 %n을 보자.

이 옵션의 경우 qualifier의 값에 따라 두 가지 경우로 나누어지는데 우선은 else 부분인 int* ip부분만 먼저 보자. str buf를 뱨고 있다. 그러면 현재까지 프린트한 문자의 개수가 나온다. buf의 값은 최초의 str값과 같기 때문이다. 연산을 하면서 str값은 1씩 늘어나게 되고 buf는 고정되어있기 때문에 그 차이는 결국 프린트한 문자열의 개수가 된다.

 

%s, %n, %x에 대한 설명을 모두 마쳤다. 그런데 아직까지 뭔가 많이 부족한 이유는 무엇일까?  C 코드만 가지고는 아직 부족할 것 같다. 그래서 아래에 printf 함수를 사용하는 방법부터 스택에 파라미터가 쌓이는 방법을 자세하게 설명해놨다.


보통 printf함수는 다음과 같이 사용한다.

 

printf(“abc”);                            // abc를 출력한다.

printf(“abc%dabc%d”, a, b);          // abc3abc5 (, a = 3, b = 5)

printf(“%s%s%d”, str1, str2, num);  // abcdefgh123 (, str1 = “abcd”, str2 = “efgh”, num = 123)

printf(“abcd%n”);                      // abcd4

 

위의 예시 중에서 두 번째 세 번째 예를 보자. 두 번째의 경우, “” 안에 %d가 두 개 있으니까 “”뒤에 두 개의 변수가 왔다. 세 번째의 경우 “” 안에 %s가 세 개 있으니까 “” 뒤에 세 개의 변수가 왔다. , 옵션의 개수만큼 “” 뒤에 변수의 개수가 있어야 한다. 사용자는 printf 함수를 사용할 때 이와 같이 파라미터(매개변수)를 전달할 수 있다.

 

 이 문서에서 처음에 이런 말을 했다. 모든 프로그램은 메모리 상에서 동작한다. 그 다음 배경지식으로 설명한 내용이 스택에서 값이 쌓이는 구조이다. 다시 한번 보자. main 함수가 sum 함수를 호출할 때 아래와 같이 sum함수의 스택이 쌓인다.

※ 아래로 쌓이는 스택이라는 점 다시 되세기자.

 

위의 그림에서 return address 바로 위에 보이는 argument가 바로 파라미터이다. 예를 들어 우리가 main함수에서 printf 함수를 printf(“%s%s%d”, str1, str2, num); 이와 같이 함수를 호출할 경우 스택에는 아래와 같이 쌓인다.


%s 옵션의 경우 실제로 스택의 argument자리에 쌓이는 것은 abcd와 같은 실제 문자열이 아니라 abcd라는 문자열을 지니고 있는 주소값이다. 따라서 fmt, str1, str2의 주소가 return address 위에 쌓인다. 그리고 그 값들이 가리키는 곳을 가보면 x0730x250x730x25, hgfe, dcba와 같이 실제 값을 찾을 수 있다.

 

스택 구조를 이해한 지금 시점에서 printf 함수에서 사용하는 va_arg함수를 더욱 잘 이해할 수 있다. va_arg(ap, T) 함수는 ap라는 주소에 T라는 변수의 size를 더하여 주소값을 늘리는 효과를 보인다. , va_arg함수를 사용하면 제일 처음에는 스택의 첫 번째 파라미터인 fmt를 가리키는 포인터가 그 다음 두 번째 파라미터를 가리키게 되고 한번 더 사용하면 세 번째 파라미터를 가리키게 된다..

 

자, 이제 어느 정도 printf 함수가 정말 어떻게 동작하는지 설명할 수 있을 것 같다.

가령 printf("abcd %s hello world %d", str, val); (단, str = "I said", val = 100) 이와 같은 함수를 호출했을 경우 우선 아래의 함수로 "abcd "를 프린트한다.


그리고 ("abcd %s hello world %d" 이 문자열의 주소가 있는) return address 바로 위의 스택 포인터의 값이 +4되는데 그 곳에는 "I said"문자열의 주소 값이 있다. 이 부분에 대한 코드는 아래와 같고


" hello world "를 normal 하게 출력한 다음 %d를 만나면 아까 전에 %s를 가리키고 있었던 스택 값을 한 칸 더 올린다. 그곳에는 아마 100이라는 값이 있을 것이며 그 값을 출력한다. 따라서

 

"abcd I said hello world 100" 이라는 문자열을 출력하게 된다.


printf.c 파일은 인터넷에서 아주 쉽게 찾을 수 있다. 내가 설명한 부분은 Format String Vulnerability를 설명하기 위해 필요한 내용들만 분석해놓은 것인데, 그 외에 필요한 것들이 많을 수 있으므로 그 부분에 대해서는 각자의 역량에 맡기겠다.


다음 포스팅은 Format String Vulnerability이다. 그 부분에서는 이 포스팅에 대한 내용은 미리 알고 있다는 전제하에 할 것이므로 그것을 공부하고 싶은 사람은 위의 포스팅을 잘 보시길 바란다.

'Linux Kernel' 카테고리의 다른 글

[Kernel] System Call Calling Convention  (0) 2014.01.05
시작하며  (0) 2013.08.30
Posted by 빛나유
,