같은 Demon 팀의 수민님이 내신 문제다. 대회 전에 문제 검수하면서 스스로 풀어봤는데 이때까지 한 검수 중에서 세 손가락 안에 들 정도로 재밌는 문제여서 블로그에 꼭 기록하고 싶었다. 이 문제를 풀면서 디버깅을 굉장히 많이 하였다.. 미리 스포하자면 이 문제는 Chrome은 아니지만 Chrome 1-day와 관련있는데 나중에 시간이 난다면.. 관련 내용을 블로그에 적어 공유할까 싶기도 한다. (안 할 수도 있다.)
Hello my name is Calculator
[ 1 solves / 500 points]
아래는 solve가 0일 때 나온 힌트이다.
1 2 3
Ref. 1day Vulnerbility : https://bugs.chromium.org/p/chromium/issues/detail?id=1315192 (type confusion lead to oob)
Hint : Static_cast, C++, NaN, Infinity, Buffer Overflow
Analysis
main 부터 살펴봤다. (분석하면서 call_num의 타입을 struct를 하나 새로 만들어서 지정해주었다.)
Interger_cal()함수에서는 취약점일 것처럼 생긴게 안보였다. 하나 특징이 있다면 한 수식을 사용자로부터 입력받고 그 수식에 대한 연산을 진행하는데 연산 결과가 *cal_num->_int34에 담긴다는 것이다. int 연산 결과가 int 타입의 변수에 담기고 음수 연산도 잘 예외처리가 되어있어서 일단 넘어갔다.
[*] 를 보자. 처음에 이 함수를 보고 저기서 무조건 타입 컨퓨젼이 날 것이라 생각했다. 왜냐면 float 연산 결과를 int에 담으면 매우매우 큰 수가 담기기 떄문이다. 하지만 내가 scanf("%1f %c %1f", cal_num->_float35, &s1, cal_num->_float36); 이 구문을 잘못 이해하고 있었다. %1f의 이미는 정수형으로 한 자리만 입력받겠다는 것을 의미한다. 나는 소수점 한자리인 줄 알고 디버깅해보면서 의문이 너무나 많았다.
LODWORD(cal_num->_float38) = *cal_num->_int34; *cal_num->_int34 = *cal_num->_float37; // [*] *cal_num->_float37 = SLODWORD(cal_num->_float38); result = *cal_num->_int34; if ( result < 0 ) { result = cal_num->_int34; --*result; } return result; }
대놓고 float 타입의 값을 int 타입의 변수로 옮겨준다. 여기서 type confusion이 발생할 가능성이 있다. 그러나.. float 연산을 하던 Real_cal 함수에서는 정수 한자리로 계산하기 때문에 무의미하다고 생각했다. 여기서 오랜 시간 고민을 하다가 예전에 Chrome 1-day 분석했던 것이 떠올랐다. 정확히 뭔지는 기억 안나는데 연산 결과가 NaN이 나와서 OOB가 발생하는 취약점이 있었다. NaN은 0/0같은 것을 하면 나온다. Not a Number라는 뜻이다.
아무튼 바로 아래 로직으로 테스트를 진행했고 overflow를 트리거 할 수 있었다.
Real_Cal에서 0/0 입력
Number_Change 함수 -> 여기서 float의 NaN이 int로 넘어가는데 0보다 작기 때문에 문제 로직에 따라 -1을 함
overflow 함수를 통해 overflow 발생 -> -1은 read 세 번째 인자로 들어가면서 unsigned int로 형변환되기 때문에 매우 큰 수가 되어 overflow가 발생한다.
해당 과정은 디버깅을 하면서 이해하였다.
Solve
취약점 트리거도 꽤 고생을 했는데 익스할 때도 고생을 조금 했다. OOB가 발생하는데 leak도 해야하고 어떠한 함수의 got를 system 함수로 덮어야하고 어딘가에 ‘/bin/sh\x00’ 써야하는데 어디에 쓰지.. 란 생각이 동시에 들어서 혼란스러웠다.
1 2 3 4 5 6 7 8 9 10
intPrint_number(void) { puts("Print Number"); printf("Num 1. %d\n", *cal_num->_int32); printf("Num 2. %d\n", *cal_num->_int33); printf("Result %d\n", *cal_num->_int34); printf("Float Num 1. %f\n", *cal_num->_float35); printf("Float Num 2. %f\n", *cal_num->_float36); returnprintf("Float Result %f\n", *cal_num->_float37); }
하지만 main의 6가지 기능 중에 이런 것이 있었고, 나는 현재 cal_num에 overflow를 발생시켜 입력을 할 수 있으니 위에 보이는 저 값 즉, *cal_num->_int32나 *cal_num->_int33와 같은 값들을 덮을 수 있다. 이 기능을 이용하여 libc leak을 할 수 있다. 주의해야할 점은 이게 int라서 4바이트씩 끊어져서 leak이 된다. 하위 4바이트 출력값이 때로 음수가 될 때도 있어서 0보다 이상일 때만 걸러주었다. 아니면 그냥 예외를 강제로 발생시켜 다시 연결하였다.
이런식으로 libc base를 정상적으로 구할 수 있고 이제 어떠한 함수를 system 함수로 덮어야 한다.
1 2 3 4
intPrint_Note(void) { returnputs(cal_num); }
기능 중에 이런 기능이 있었고, puts 함수를 system 함수로 덮고 cal_num이 우리 입력값이니까 ‘/bin/sh\x00’을 쉽게 넣어 쉘을 따자고 생각했다. 재밌게도 이 함수는 Interger_cal 함수가 종료될 떄 쯤 호출이 된다.
Interger_cal 함수 상단에선 사용자 입력에 따라 이러한 연산을 수행한다. 이걸 이용하여 puts@got를 system 함수로 덮을 수 있겠다고 생각했다. 먼저 overflow 함수에서 피 연산자 두 개에 puts@got를 4바이트씩 잘라서 넣어놓고 Interger_cal 함수에서 puts@got에다가 값을 쓸 수 있다. 이 때 중요한 것은 4바이트씩 잘 잘라서 넣어줘야한다는 것이다. 그럼 이제 쉘을 획득할 수 있다.