Welcome to Number Store(TM)! A new FREE password manager. However, due to budget constraints we were only able to add support for storing numbers. Store your favorite secret numbers or generate new random ones! Also comes with a super secret flag! nc cha.hackpack.club 41705
[48 solves / 116 points]
아마 이 블로그 최초 uaf 롸업이지 않나 싶다. 힙 문제를 기피했었는데 태그가 easy여서 풀어봤다.. ctf 시작하고 바이너리를 다운받고 끝나고 풀어봤는데 중간에 바이너리가 바뀌었다. printFlag 함수 주소만 바꿔주니 remote로 플래그를 얻을 수 있어서 그냥 예전 바이너리 코드를 보겠다 ㅎ.. (실제로 분석은 예전 바이너리로 했기 때문..)
setvbuf(_bss_start, 0LL, 2, 0LL); Number_obj = (constchar **)calloc(10uLL, 8uLL);// 8짜리 10개 Number_obj_copy = (char *)calloc(10uLL, 16uLL);// 16짜리 10개 for ( i = 0; i <= 159; i += 16 ) strncpy(&Number_obj_copy[i], "none", 0xFuLL); _func_result = 0LL; v5 = 1; printStr("WELCOME TO NUMBER STORE\n"); printStr("Store your favorite or secret numbers here! You can even generate new random numbers!\n"); printStr("Now includes a super secret flag!\n\n"); while ( v5 ) { printStr("1.) Store New Number\n"); printStr("2.) Delete Number\n"); printStr("3.) Edit Number\n"); printStr("4.) Show Number\n"); printStr("5.) Show Number List\n"); printStr("6.) Generate Random Number\n"); printStr("7.) Show Random Number\n"); printStr("8.) Show Super Secret Flag\n"); printStr("9.) Quit\n\n"); printStr("Choose option: "); switch ( (unsignedint)getUserNum() ) { case1u: printStr("Enter index of new number (0-9): "); UserNum = getUserNum(); if ( UserNum >= 10 ) goto Index_out_of_bound; createStaticNum(UserNum, (__int64)Number_obj); strncpy(&Number_obj_copy[16 * UserNum], Number_obj[UserNum], 0xFuLL); break; case2u: printStr("Enter index of number to delete (0-9): "); IndexNum = getUserNum(); if ( IndexNum < 10 ) { deleteObject(IndexNum, (__int64)Number_obj); strncpy(&Number_obj_copy[16 * IndexNum], "none", 0xFuLL); } else { printStr("Invalid Index!\n"); // 음수 안됨 oob x } break; case3u: printStr("Enter index of number to edit (0-9): "); edit_idx = getUserNum(); if ( edit_idx < 0xA ) editObject(edit_idx, (__int64)Number_obj); else printStr("Invalid Index\n"); break; case4u: printStr("Select index of number to print (0-9): "); print_idx = getUserNum(); if ( print_idx >= 0xA ) Index_out_of_bound: printStr("Index out of bounds!\n"); else readObject(print_idx, (__int64)Number_obj); break; case5u: showList((__int64)Number_obj_copy); break; case6u: if ( !_func_result ) _func_result = createRandomNum(); func_result = ((__int64 (*)(void))_func_result[2])();// printFlag 실행해야함 _func_result[1] = *_func_result; *_func_result = func_result; goto print_random_num; case7u: print_random_num: if ( _func_result ) printf("%ld\n", *_func_result); else printStr("No Random Number has been generated\n"); break; case8u: printStr("Access Denied\n"); break; case9u: v5 = 0; break; default: printStr("Option does not exist\n"); break; } printStr("\n"); } if ( _func_result ) free(_func_result); for ( j = 0; j <= 9; ++j ) { if ( strcmp(&Number_obj_copy[16 * j], "none") ) free((void *)Number_obj[j]); } free(Number_obj_copy); free(Number_obj); return0; }
일단 1번 메뉴를 살펴보자. createStaticNum 함수이다. 참고로 여러 기능을 실행하기 전에 getUserNum 함수를 통해 인덱스를 받아오는데 음수를 잘 거르고 있으니 oob와 같은 취약점이 발생할 수 없다.
name 부분은 이상해졌는데,.. num은 그대로 보존되어 있다. 해제 후에도 이 값을 이용할 수 있기 때문에 이 값을 이용할 것이다. (int)라서 주소 넣기 딱 좋아보인다.
이제 또 다른 malloc을 찾을 것이다. 왜냐하면 heap chunk를 해제하고 똑같은 크기의 heap chunk를 할당하면 동일한 위치한다는 것을 이용할 것이기 때문이다. malloc을 사용하는 곳이 총 두 곳 있는데, 하나는 위의 createStaticNum 함수이고 다른 하나는 createRandomNum이다.
이 구조는 뭔가 아까 위에서 본 name_ptr과 유사하다. 서로 다른 구조체가 있는데 용도는 다르지만 크기가 동일하다. A 용도에서 해제할 때 데이터가 초기화가 되지 않아 B 용도일 때 사용될 수 있다. uaf 취약점은 보통 이렇게 이용된다.
createRandomNum 함수는 아래와 같이 main 함수에서 호출되고 이용된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// main case6u: if ( !_func_result ) _func_result = createRandomNum(); func_result = ((__int64 (*)(void))_func_result[2])();// printFlag 실행 _func_result[1] = *_func_result; *_func_result = func_result; goto print_random_num; case7u: print_random_num: if ( _func_result ) printf("%ld\n", *_func_result); else printStr("No Random Number has been generated\n"); break;
_func_result가 없는 경우 createRandomNum 함수를 통해 랜덤한 수를 받아온다. 랜덤한 수를 어떻게 받아오냐? func_result = ((__int64 (*)(void))_func_result[2])(); 이 라인을 통해 createRandomNum 함수에서 메모리에 저장한 generateRandNum 함수 포인터를 실행한다. 그 결과를 7번 메뉴에서 출력할 수 있다.
우리는 generateRandNum 함수 대신 printFlag 함수를 실행할 것이다. 그러면 바로 func_result = ((__int64 (*)(void))_func_result[2])(); 이 라인에서 함수가 실행되어 flag.txt가 보일 것이다.
How to leak
문제는 이 문제에 pie가 걸려있다. leak은 어떻게 하면 좋을까? 아래 함수를 이용하자.
s = *(char **)(8LL * idx + a2); puts(s); returnprintf("%ld\n", *((_QWORD *)s + 2)); // 주소 leak }
*((_QWORD *)s + 2)을 출력하는 것으로 보아 name_ptr의 num을 출력해준다. 이 함수를 통해 leak을 할 수 있는데, 먼저 obj 구조체를 만들고 해제한다. 그 다음 createRandomNum 함수를 통해 obj[x]가 가리키는 name_ptr+2 즉 num 위치에 generateRandNum 함수 포인터가 위치하게 한다. 마지막으로 readObject 함수를 실행시켜 generateRandNum 함수의 주소를 구하고 pie address까지 구할 수 있다.
Solve
createStaticNum 함수를 이용하여 malloc
deleteObject 함수를 이용하여 free
createRandomNum 함수를 이용하여 num 위치에 func_ptr이 위치하도록 malloc
readObject 함수를 이용하여 pie 주소 leak
deleteObject 함수를 이용하여 free
createStaticNum 함수를 이용하여 malloc 이 때 num 위치에 printFlag 함수 주소 입력
deleteObject 함수를 이용하여 free
6번 메뉴를 이용하여 printFlag 함수 실행
이것이 가능한 이유가 아래와 같이 태초에 한 번만 random한 수를 받기 때문이다.
1 2 3 4
case6u: if ( !_func_result ) _func_result = createRandomNum(); func_result = ((__int64 (*)(void))_func_result[2])();// printFlag 실행
3번 과정에서 createRandomNum 함수를 한 번 실행했으므로 _func_result은 이미 값이 채워져있다.