포맷 스트링이 부릅니다. 헬게이트~

@codemaru · October 11, 2012 · 10 min read

게임 보안을 하면 참 다양한 이슈를 겪는다.

그 이슈들도 크게 몇 가지 종류로 나뉘는데 종류별로 심각성을 살펴보면 다음과 같다.

서버 크래시 >>>>> 78.2차원문 >>>> 넘사벽 >>> 블랙홀 >> BSOD > 미로 > 클라이언트 크래시 >= 충돌 > (퇴근 가능 영역) 오진

그렇다. 다른 모든 것을 떠나서 서버 크래시는 재앙이다. 대재앙이다. 클라이언트에서 발생한 몇 년 동안의 이슈를 모두 합쳐도 서버 이슈 한 건 보다 못하다고 할만큼 서버 이슈는 심각하다. 서버 이슈는 크리티컬 하고 엄청나며 어렵고, 애매하고, 난감한 그야말로 안드로메다급 문제라고 할 수 있다. 이런 어려움에도 불구하고 고품격 보안 기능을 제공하기 위해서는 복잡하고 난해한 서버 코드를 추가할 수 밖에 읍다. 흙~

썰이 너무 장황했다. 맞다. 서버가 다운됐다. 큭~ 그 오랜기간 문제없이 동작하던 서버가 다운됐다. 다운됐다. 다운됐다. (에코 메아리)~~

서버 이슈가 어려운건 한 가지 밖에 없다. 재현이 쉽지 않고 동작하는 서버를 우리가 라이브 단계에서 어떻게 할 수 없다는 점이다. 즉, 서버 문제는 모두가 추측의 영역에서 이루어질 수 밖에 없다. 그나마 이런 어려운 문제를 돕기 위해서 대부분의 게임 서버 프로그램은 고품격 덤프 기능을 가지고 있다. 그런데 그런 고품격 덤프 기능들이 실상은 도움이 별반 되질 않는다. 왜냐하면 진짜 문제가 생겼을 때 제대로된 정보를 우리에게 주는 경우가 드물기 때문이다. 그래도 오늘은 운이 좋았다. 덤프는 없었으나 크래시 로그가 남았고 콜스택이 있었다. 부왘~ 천만다행. 거기다 심볼까지 아름답게 존재해 주시고, 콜스택 로그 또한 아름답게 착착 맞아떨어졌다.

이쯤되면 뭔가 사소한 버그겠구나 라고 생각할 법 한데, 여기에 반전은 그 콜스택이 가르키는 지점은 다름아닌 StringCchVPrintfW 라는 점, 그리고 그 안의 _woutput_l이라는 함수에서 발생 했다는 점. 그 넓디 넓은 서버 소스 코드 중에 StringCchVPrintfW라는 함수를 얼마나 많이 쓰겠냐는 말. 이쯤되면 해운대 바닷가에서 낚시바늘 찾기만큼 어려워져 버렸다고 생각하는 것이 당연한 진리~ 그런데 왠걸 ㅋㅋㅋ~ 희한하게도 소스 코드에 전부 StringCbVPrintfW를 썼는데 단 한 곳. 단 한 곳만 StringCchVPrintfW를 쓴 것이었다. 럭키가이라고 생각을 하는데. 그 함수가 엄청 많이 호출되는 로그 출력 함수라는 건 또 반전 ㅠㅜ~

중간 썰도 길었다. IDA로 파일을 열어서 번지를 보니 이렇다. _woutput_l이라는 이 망망대해 함수 속에 우리의 크래시 지점은 저 빨갛게 표시된 좁쌀만한 영역 ㅠㅜ~

인간이 이렇게 긴 함수를 작성하는 것이 가능하단 말인가?

외계인이 작성한 것이라고 밖에는 생각되지 않는 _woutput_l

여기서 난 과연 무엇을 깨달아야 하는가?

_woutput_l이라는 거대한 함수 속에서 난 어떤 힌트도 찾을 수 없었다. 그저 함수가 굉장히 거대하다는 것 밖에는 느끼지 못했다. 이제는 그저 촉에 맞기는 수 밖에. 딱 떠오르는 첫번째 생각은 그랬다. 그래 %s 따위를 썼는데 뒤에 문자열 포이턴가 먼가 이상했던거야. 아마 종료 NULL이 없었겠지. 그래서 메모리를 계속 읽었겠지. 재수없게 그 종료 널 다음이 딱 커밋되지 않은 메모리 영역이었겠지. 하필 또 난 접근 위반 예외처리를 해놓지 않았지. 그러니까 당연히 크래시가 발생했겠지~ 라고 소설을 써봤다.

가설을 검증하기 위해서 소설대로 샘플을 만들어서 해봤는데. 너.무.도. 당연하게 깔끔하게 종료됐다. 그 어떤 크래시도 없이 말이지. 그런데 그 샘플이 나를 구원했다. ㅋ~ 크래시가 안나서 _woutput_l 함수 내부로 들어가 볼 수 있었는데, 거기서 디스어셈블 창으로 전환하자 앞으로 내가 10년 더 Visual Studio를 써야 할 것 같은 화면이 출력됐다. 그렇다. 소스코드와 함께 디스어셈블 코드가 출력된 것이다. 장장 2370줄에 이르는 _woutput_l 코드를 에디터에 복사했다. 그리곤 IDA에서 보았던 8b 48 04를 검색했다. 이런 마법이!!! 딱 한 군데 밖에 나오질 않는 기적이 발생하는 것이 아닌가? 거기다 코드가 똑같다. 크래시난 지점과. 빙고~

                if (pstr == NULL || pstr->Buffer == NULL) {
00B61D1E 85 C0            test        eax,eax 
00B61D20 74 3A            je          $LN97+224h (0B61D5Ch) 
00B61D22 8B 48 04         mov         ecx,dword ptr [eax+4] 
00B61D25 85 C9            test        ecx,ecx 
00B61D27 74 33            je          $LN97+224h (0B61D5Ch) 

역시 먼가 NULL과 관계된 문제였어. 코드는 나에게 말해주고 있었다. 그런데 진정한 힌트는 바로 그 위에 있었다. 아래와 같은 케이스 문이 있었던 것이다. Z. 그렇다 이 코드는 %Z 따위를 탔을 때 UNICODE_STRING의 Buffer가 NULL인지를 체크하는 코드였던 것이다.

            case _T('Z'): {

이제 제대로 소설을 갈겨 보자면 이렇다. 포맷 스트링으로 %Z가 넘어왔는데 그놈이 가리키는 UNICODE_STRING 포인터는 있는데 그 안의 Buffer를 접근하려고 하니까 예외가 발생한 것이다. 이쯤되면 이제 내가 %Z를 쓴 곳만 찾으면 되지 않겠는가? 근데 사실 %Z는 드라이버 코드에서나 쓰지 유저 코드에서는 거짐 쓰지도 않는데 라고 하면서 로그 출력 함수를 호출하는 부분을 grep했다. 그리곤 %Z로 검색했는데 역시나 없다. 그래서 로그 출력 함수로 넘어가는 포맷 스트링을 모두 점검하는 와중에 난 놀라운 한 줄을 발견하고야 말았다. 내가 작성한 것이라 믿고 싶지 않은 한 줄이었다.

  1. lb_->Append(LLV_INFO, buffer);
lb_->Append(LLV_INFO, buffer);

이거슨 무엇인가? 대재앙을 부른다는 바로 그 코드. 악마를 워프시킨다는 바로 그 코드. 핡~ 포맷 스트링으로는 절대로 변수를 넘겨서는 안 된다는 불변의 진리~ 그 망망대해 속에 존재하던 딱 한 줄의 코드가 나에게 말.하.고. 있었다. 이 멍청아~ 꺼져~ ㅠㅜ~

확인 사살을 위해서 샘플을 만들었다. 당.연.하.게 이 샘플은 한치의 오차도 없이 나의 소설대로 동일한 번지에서 크래시를 발생시켰다. 휴~

int main()
{
    wchar_t buffer[80] = L"%Z";
    PUNICODE_STRING us = (PUNICODE_STRING) 0x100;
    wprintf(buffer, us);
    return 0;
}

사실 위 샘플보다는 아래 샘플이 원 코드의 심각성을 더 잘 보여준다. 마구 %Z%z%Z%Z%Z%Z 따위를 입력해보자. printf의 포맷스트링으로 변수를 전달하는 것이 얼마나 위험 천만한 짓인지 깨닫게 된다.

int main()
{
    char buffer[80];
    fgets(buffer, ARRAYSIZE(buffer), stdin);
    printf(buffer);
    return 0;
}

아직도 헤매는 독자 여러분을 위해서 어떻게 고치는지 살펴보면 아래와 같이 고쳐야 한다.

int main()
{
    char buffer[80];
    fgets(buffer, ARRAYSIZE(buffer), stdin);
    printf("%s", buffer);
    return 0;
}

이래놓곤 난 다음 주엔 VS2012로 어떻게 우아하게 버그를 잡는지 발표하겠지~ 기가찬다 진짜~

신이시여, 발표 자료가 뚝 떨어지는 기적을 보여 주옵소서~ 아멘!!!

가을가을한 요즘. 깜냥도 안 되면서 너무 많은 걸 약속한 건 아닐까 반성하는 중 ㅠㅜ~

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중