作者:xd_xd
作者博客:http://xdxd.love/

solveme.peng.kr winter sleep

solveme是一個CTF的練習平臺,其中winter sleep題目是這樣的。

<?php
   error_reporting(0);
   require __DIR__.'/lib.php';
   if(isset($_GET['time'])){
       if(!is_numeric($_GET['time'])){
           echo 'The time must be number.';
       }else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
           echo 'This time is too short.';
       }else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
           echo 'This time is too long.';
       }else{
           sleep((int)$_GET['time']);
           echo $flag;
       }
       echo '<hr>';
   }
   highlight_file(__FILE__);

輸入一個字符串,通過is_numric的判斷,要大于5184000小于777600,最后通過sleep函數,就可以輸出flag。顯然,如果輸入一個較大的數,會sleep很長時間。需要一個數大于5184000,然后int之后又要是一個很小的數。

解決的方案是這樣的:

<?php
echo 60 * 60 * 24 * 30 * 2;
echo "\n";
echo 6e6;
echo "\n";
echo (int)'6e6';
echo "\n";
echo 60 * 60 * 24 * 30 * 3;

可以看以上腳本輸出內容:

5184000
6000000
6
7776000

使用科學計數法。

看了一些writeup,只是給出了解決的辦法,但是并沒有詳細的說明,為什么會這樣。有的地方提到說是弱類型,雖然這幾次比較存在類型的自動轉換,但是跟我理解的弱類型的自動轉換存在差異。所以想要探究一番。

黑盒測試

可以看到當接收到科學計數法表示的字符串跟一個整型變量運算(‘6e6’-0),6e6自動并不是自動轉換成了int型,而是轉換成了float,所以最終的數字是float型的6000000。最后兩行代碼可以直接的說明了問題。使用int強制轉換一個科學計數法表示的字符串,轉換過程中并不能識別科學計數法,只是把e當做普通字符了。效果跟6a6是一樣的。而用float轉成浮點數,則可以成功識別科學計數法。

feature or bug

我的感覺是這應該是php的一個bug。同一個字符串,轉換成int型和float型有著兩種解釋。正常的邏輯應該是(int)’6e6’ = (int)(float)’6e6’。這樣才比較符合正常的一個理解邏輯。

找了幾個php的版本,分別做了下測試:

測試腳本如下:

import docker
client = docker.from_env()


php_versions = ['5.3','5.4','5.5','5.6', '7.0','7.1','7.2']
for version in(php_versions):
php = "php:"+version + "-cli"

print(php)
print("echo((int)'6e6')")
print(client.containers.run("php:"+version+"-cli", '''php -r "echo((int)'6e6');"'''))
print("echo((float)'6e6')")
print(client.containers.run("php:"+version+"-cli", '''php -r "echo((float)'6e6');"''’))

結果如下:

?  dockerpy python phptest.py
php:5.3-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:5.4-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:5.5-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:5.6-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:7.0-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:7.1-cli
echo((int)'6e6')
6000000
echo((float)'6e6')
6000000
php:7.2-cli
echo((int)'6e6')
6000000
echo((float)'6e6')
6000000

在php7.0以前的版本中(int)’6e6’結果是6,但是在7.1以后的版本中,(int)’6e6’已經是6000000,符合(int)’6e6’ = (int)(float)’6e6’這個邏輯了。

php內核分析

以下內容引用自《php7內核剖析》:

PHP是弱類型語言,不需要明確的定義變量的類型,變量的類型根據使用時的上下文所決定,也就是變量會根據不同表達式所需要的類型自動轉換,比如求和,PHP會將兩個相加的值轉為long、double再進行加和。每種類型轉為另外一種類型都有固定的規則,當某個操作發現類型不符時就會按照這個規則進行轉換,這個規則正是弱類型實現的基礎。 除了自動類型轉換,PHP還提供了一種強制的轉換方式:

  • (int)/(integer):轉換為整形 integer
  • (bool)/(boolean):轉換為布爾類型 boolean
  • (float)/(double)/(real):轉換為浮點型 float
  • (string):轉換為字符串 string
  • (array):轉換為數組 array
  • (object):轉換為對象 object
  • (unset):轉換為 NULL

無論是自動類型轉換還是強制類型轉換,不是每種類型都可以轉為任意其他類型。

4.1.3 轉換為整型

其它類型轉為整形的轉換規則:

  • NULL:轉為0
  • 布爾型:false轉為0,true轉為1
  • 浮點型:向下取整,比如:(int)2.8 => 2
  • 字符串:就是C語言strtoll()的規則,如果字符串以合法的數值開始,則使用該數值,否則其值為 0(零),合法數值由可選的正負號,后面跟著一個或多個數字(可能有小數點),再跟著可選的指數部分
  • 數組:很多操作不支持將一個數組自動整形處理,比如:array() + 2,將報error錯誤,但可以強制把數組轉為整形,非空數組轉為1,空數組轉為0,沒有其他值
  • 對象:與數組類似,很多操作也不支持將對象自動轉為整形,但有些操作只會拋一個warning警告,還是會把對象轉為1操作的,這個需要看不同操作的處理情況
  • 資源:轉為分配給這個資源的唯一編號

具體處理:

ZEND_API zend_long ZEND_FASTCALL _zval_get_long_func(zval *op) { try_again:

switch (Z_TYPE_P(op)) {
    case IS_NULL:
    case IS_FALSE:
        return 0;
    case IS_TRUE:
        return 1;
    case IS_RESOURCE:
        //資源將轉為zend_resource->handler
        return Z_RES_HANDLE_P(op);
    case IS_LONG:
        return Z_LVAL_P(op);
    case IS_DOUBLE:
        return zend_dval_to_lval(Z_DVAL_P(op));
    case IS_STRING:
        //字符串的轉換調用C語言的strtoll()處理
        return ZEND_STRTOL(Z_STRVAL_P(op), NULL, 10);
    case IS_ARRAY:
        //根據數組是否為空轉為0,1
        return zend_hash_num_elements(Z_ARRVAL_P(op)) ? 1 : 0;
    case IS_OBJECT:
        {  
            zval dst;
            convert_object_to_type(op, &dst, IS_LONG, convert_to_long);
            if (Z_TYPE(dst) == IS_LONG) {
                return Z_LVAL(dst);
            } else {
                //默認情況就是1
                return 1;
            }
        }
    case IS_REFERENCE:
        op = Z_REFVAL_P(op);
        goto try_again;
        EMPTY_SWITCH_DEFAULT_CASE()
}
return 0;
}

4.1.4 轉換為浮點型

除字符串類型外,其它類型轉換規則與整形基本一致,就是整形轉換結果加了一位小數,字符串轉為浮點數由zend_strtod()完成,這個函數非常長,定義在zend_strtod.c中,這里不作說明。

書中提到,字符串轉換為整型,是C語言strtol()的規則,由ZEND_STRTOL函數完成的,字符串轉換成浮點數,是用zend_strtod函數完成的。

對比一下C語言的strtol和strtod

strtol不能識別科學計數法,字符串6e6轉成整型是6,而strtod可以識別科學計數法,6e6轉成浮點數是6000000。

動態調試php內核

編譯debug版php。

git clone http://git.php.net/repository/php-src.git
cd php-src
git checkout PHP-7.0
./buildconf
./configure --disable-all --enable-debug --prefix=$HOME/myphp
make
make install

gdb調試

gdb --args php -r "echo((int)'6e6');”

在類型轉換函數上下斷點:

b _zval_get_long_func

可以看到使用zend_strtol函數進行轉換。

zent_strtol 直接是使用strtoll。

調試一下7.1版本php

可以看到7.1版中使用了新的函數is_numeric_string替代strtoll。注釋中說明使用新函數是為了避免strtoll的溢出問題,自己實現了is_number_string函數來替代strtoll。然而并沒有提到科學計數法表示的字符串的問題。但是實際實現上跟strtoll有不同。妥善的處理科學計數法表示的數字。

最終的字符串轉整型的邏輯如下:

最終的處理邏輯是如果發現了小數點或者數字e,就采用zend_strtod來處理,這樣就跟字符串轉浮點數是一模一樣的處理邏輯了。所以最終的結果也就符合了(int)’6e6’ = (int)(float)’6e6’這個邏輯。

思考

那么這到底是個bug還是feature呢。最終的結果來看,php7.0及以前的版本使用strtoll轉字符串到整型,7.1以后的版本使用了strtod來轉換。所以strtoll不能識別科學計數法表示的數字是不是一個bug。

參考資料


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/566/