__int64 choose_option() { int choice; // [rsp+Ch] [rbp-4h] BYREF
printf("\nEnter your choice: "); __isoc99_scanf("%d", &choice); getchar(); putchar(10); if ( choice == 1 ) { roll_the_dice(); } elseif ( choice == 2 ) { if ( coupon ) // checking the global variable for ROP { puts("Sorry, but you only get one chance to enter the coupon code!"); } else { coupon = 1; enter_coupon(); // only 1 time } } else { puts("It's too sad to see you go :("); quit = 1; } if ( quit ) exit(0); return choose_option(); }
이 함수에서 메뉴를 고를 수 있다.
1 2 3 4 5 6
Welcome To The Festive Lottery!
1. Roll the dice! 2. Enter a coupon code 3. Exit Enter your choice:
1번 메뉴는 roll_the_dice() 함수를 실행시켜준다. 2번 메뉴에서 여러가지 검사를 한다. coupon 전역 변수를 검사하는데, player가 한 번만 enter_coupon() 함수만 실행시킬 수 있다는 것을 의미한다.
switch ( rand() % 6 ) { case0: result = puts("Hurray! You got a 1"); break; case1: case2: case3: puts("Congrats! You won a flag!"); result = win(); break; case4: result = puts("Ah! You just missed it"); break; case5: if ( got_6 ) { result = puts("Sorry, you can avail this offer only once!"); } else { got_6 = 1; another_chance = 1; result = puts("You get another chance to enter the coupon!"); } break; default: result = puts("Did you tamper with the dice? This number should not be there!"); break; } return result; }
case 3일때 출력해주는 저 flag는 가짜 플래그이다.
case 5의 another_chance에 주목해보자. 미리 이야기하자면 이 전역 변수는 2번 메뉴인 enter_coupon() 함수에서 사용된다.
got_6 전역 변수는 이런 another_chance를 단 한번만 1로 세팅할 수 있다는 것을 의미하는 것 같다.
v6 = __readfsqword(0x28u); strcpy(coupon_code, "YCHHZZHHMSHWI"); v3 = 0; memset(v4, 0, sizeof(v4)); v5 = 0; while ( 1 ) { printf("Enter the coupon code: "); gets(input); // bof if ( !strcmp(input, coupon_code) ) // same as cooupon code { successfull = 1; puts("Coupon applied successfully!"); goto LABEL_7; } printf("Invalid coupon code! You entered: "); printf(input); // fsb putchar(10); if ( !another_chance ) // if another_chance = 1 ? break; // pass puts("You get one more chance to enter the coupon!"); another_chance = 0; // if another_chance == 1, // It can make another_chance = 0. } puts("No more chances left!"); LABEL_7: if ( successfull != 1 ) // if (input != coupon_code), I can't do ROP. // => should (input == coupon_code) choose_option(); return __readfsqword(0x28u) ^ v6; }
해당 함수를 열어보면 bof와 fsb 취약점이 하나씩 눈에 보인다.
1번 메뉴 roll_the_dice() 함수에서 봤던 another_chance 전역 변수에 주목해보자.
bof가 먼저 터지고 fsb가 그 다음 터진다. 그리고 another_chance 전역 변수를 사용한다. 얘가 1이면 break가 되지 않고 while을 한 번 더 돌 수 있게 된다. 그리고 해당 전역 변수를 0으로 set한다.
이 말은 뭐다? 루프를 두 번 돌 수 있다. 즉, bof와 fsb를 한 번 더 활용할 수 있다는 뜻.
그리고 프로그램 흐름을 바꾸기 위해서 ret를 바꿔줘야하는데, 이를 위해선 함수 return을 해야한다. 코드를 보면 coupon_code를 틀리면 successfull 전역 변수가 0으로 유지되어 함수가 종료되기 전에 choose_option() 함수를 실행하게 된다. 그러면 안되기 때문에 coupon_code를 맞춰주어서 successfull을 1로 set해줘야한다.
마침 스택에 우리의 input 다음에 coupon_code가 위치하고 있기 때문에 굳이 input에 “YCHHZZHHMSHWI”을 적지 않아도 우회할 수 있다.
Vulnerability
취약점은 위에서 봤듯 enter_coupon() 함수에서 bof와 fsb가 발생한다. canary와 pie가 걸려있기 때문에 fsb로 이것을 릭해야하는 필요성을 느낀다.
Exploit
우리는 bof와 fsb가 순차적으로 두 번씩 터진다. 생각해보면, pie를 굳이 릭할 필요가 없다. 왜냐하면 NX bit disabled이기 때문에 우리는 스택에서 쉘코드를 실행시킬 수 있다.
stack 주소를 릭해서 우리의 input buf와 얼마나 떨어져있는지 offset을 구할 수 있기 때문에 return 주소를 거기로 돌려줄 수 있다.
이때 익스 확률을 높이기 위해 NOP Sled를 해줬다. 이 때 페이로드 구성은 아래와 같다.
$rsp : 0x007ffd7b94f028 → 0x0000000000000008 $rip : 0x007ffd7b94f029 → 0x0000000000000000 ────────────────────────────────────────────────────────── stack ──── 0x7ffd7b94f024 xor esi, esi 0x7ffd7b94f026 push rsi 0x7ffd7b94f027 push 0x8 → 0x7ffd7b94f029 add BYTE PTR [rax], al 0x7ffd7b94f02b add BYTE PTR [rax], al 0x7ffd7b94f02d add BYTE PTR [rax], al 0x7ffd7b94f02f add BYTE PTR [rax], al 0x7ffd7b94f031 add BYTE PTR [rax], al 0x7ffd7b94f033 add BYTE PTR [rax], al
.. 쉘코드를 실행하다가 멈췄다. 쉘코드가 저장된 곳에 값을 쓰고 있어서 한마디로 실행할 코드가 저장된 곳이 스택인데 그 스택에 이것저것 쓰면서 사용하고 있기 때문에 실행할 코드가 더럽혀졌다.
그래서 sub rsp, 0x1000을 추가해서 사용하는 스택이랑 쉘코드가 저장되는 스택이랑 다르게 위치시켜주었다.
Flag
1
jadeCTF{d1d_y0u_l1k3_th3_du4l_pwn?}
tmi
대회 끝나고 풀었다. 이 바이너리에서 취약점은 비교적 찾기 쉽지만.. flag를 얻기 위한 과정이 순탄치 않았다……….. 새롭게 알게된 것이 있다. base64를 이용해서 리모트 파일을 복사해온다는 정도?
그리고 원래 알고 있었지만 문제 풀 때 리마인드 된 것들. 예를 들면 return address를 건들기 위해서는 함수 return을 해줘야 한다거나, NX bit가 해제되어 있으면 스택에서 Shellcode를 실행시킬 수 있고, 이때 익스 확률을 높이기 위해서 NOP Sled를 할 수 있다는 것 정도?