加载中...

如何令永假式成真?


最近在 Stack Overflow 无意中发现一个挺有意思的问题:

是否可以令永假式 a == 1 && a == 2 && a == 3 的值为 true

C++

当时看到题干,条件反射就想到了可以通过 C++ 实现,因为 C++ 是可以重写运算符的,其实现代码如下:

#include <iostream>
using namespace std;

class NumOP {

    private:
        int num;

    public:
        NumOP(int num) {
            this->num = num;
        }

        bool operator==(const int& num) {
            return this->num <= num;
        }
};


int main() {
    NumOP a(1);

    if (a == 1 && a == 2 && a == 3) {
        cout << "impossable!" << endl;

    } else {
        cout << "It's right." << endl;
    }
    return 0;
}

因为闲得慌,又萌生出了一个念头:其他语言是否也都可以实现这个表达式呢?

其实仔细分分析一下题干,要使得表达式成真,可以从两个思路切入:

  1. 要么 == 的判定逻辑被篡改
  2. 要么 a 的值要在判断过程中自动变化,此时 a 不可能是基础数据类型(可能是对象、是函数、或是引用)

Python

因为 Python 和 C++ 同样支持运算符重写,于是类似地可以得到 Python 的实现代码:

class NumOP :

    def __init__(self, num) :
        self.num = num

    def __eq__(self, num) :
        return self.num <= num


def main() :
    a = NumOP(1);

    if (a == 1 and a == 2 and a == 3) :
        print("impossable!")

    else :
        print("It's right.")



if __name__ == '__main__' :
    main()

Ruby

而对于 ruby 则可以利用它的一个语法糖简单实现:调用函数函数时,其参数列表可以不写括号。

那么只需要定义一个无入参的函数 a ,根据条件动态控制函数 a 的返回值即可,其实现代码如下:

def a
    $i ||= 0    # $i 是全局变量
    $i += 1
end

if (a == 1 && a == 2 && a == 3)
    puts "impossable!"
else
    puts "It's right."
end

JavaScript

对于 JavaScript ,可以利用运算符 == 的松散相等特性:当 == 两边操作数的类型不相同时, JS 引擎会尝试把其中一个操作数类型转换成另一个操作数类型。

在这题里面,若左侧操作数 a 是对象,右侧是数字,则会隐式调用对象 avalueOf 方法将其转换成数字;若转换失败则调用 toString 方法后再将其转换成数字。

显然,只需要控制 valueOf 逻辑使其满足每次 == 的判定即可,其实现代码如下:

注:此方法对于严格相等运算符 === 不起作用。

var a = {
    i: 1, 
    valueOf: function() {
        return this.i++;
    }
}

if (a == 1 && a == 2 && a == 3) {
    console.log("impossable!")

} else {
    console.log("It's right.")
}

Java

这么多语言中,最麻烦的就是 Java 了。主要是 Java 不允许重写运算符,只能利用 a 做文章。

但 Java 要求 == 两边类型一致,而右侧的 1/2/3int 基础类型,因此 a 会受到 Java 的 编译语法 约束,只可能是 int 基础类型或其包装类 Integer。而结合本题来看,a 只可能是 Integer 对象。

根据 Java 的语言特性,Integer == int 在比对之前,会自动拆包使得两边的类型一致,事实上会变成 Integer.intValue() == int

理论上本应只需要重写 Integer.intValue() 即可。

而事实上 Integer 声明了 final,不允许被继承,直接导致无法重写 Integer.intValue()

public final class Integer extends Number implements Comparable<Integer> {

    ......

    public int intValue() {
        return value;
    }

    ......

}

换言之无法直接实现。

但是若条件变更如下,则有可能实现:

a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3

该条件比对的是 Integer == Integer,由于两侧操作数均是对象,实际比对的是对象地址的引用,只需要想办法篡改两个引用的对象(使其相同)即可达到目的。


此时可以利用 Java【静态缓存】的特性 —— Integer 为了优化空间和效率,对于特定范围的常量值会放入常量池:

  • Integer第一次 被载入内存时,会通过内部类 IntegerCache[-128, 127] 范围内的整数包装成 Integer 对象并缓存到 Integer cache[] 数组。
  • 以后再用 Integer 初始化变量时,若其赋值范围在 [-128, 127] 之间,则直接返回 cache 数组中对应的引用,不再重新开辟内存。

详细可见 Integer 的源码:

public final class Integer extends Number implements Comparable<Integer> {

    ......

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    @HotSpotIntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    ......

}

回到这题判断条件中的 1/2/3,因为是通过计算在 IntegerCache 数组索引,从而获取其包装类对象:

  • (Integer) 1 => Integer.valueOf(1) => IntegerCache.cache[129]
  • (Integer) 2 => Integer.valueOf(2) => IntegerCache.cache[130]
  • (Integer) 3 => Integer.valueOf(3) => IntegerCache.cache[131]

那么只需要篡改 IntegerCache 数组,使得:

  • IntegerCache.cache[130] = IntegerCache.cache[129]
  • IntegerCache.cache[131] = IntegerCache.cache[129]

就可以令 1/2/3 取得的包装类是同一个对象(此时的 1/2/3 纯粹就是索引值)。

篡改方法可以用例 Java 的反射机制:

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;

public class Java {

    public static void main(String[] args) throws Exception {

        // 利用反射机制获取 Integer cache[] 数组
        Class clazz = Integer.class.getDeclaredClasses()[0];
        Field field = clazz.getDeclaredField("cache");

        field.setAccessible(true);
        Integer[] cache = (Integer[]) field.get(clazz);
        cache[130] = cache[129];    // 令 (Integer) 2 = (Integer) 1
        cache[131] = cache[129];    // 令 (Integer) 3 = (Integer) 1
        field.setAccessible(false);

        Integer a = Integer.valueOf(1);
        if(a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3) {
            System.out.println("impossable!");
        } else {
            System.out.println("It's right.");
        }
    }

}

但是这种做法不够优雅,毕竟改了题目。

那有没有不改题目的实现方式呢?

是有的。

虽然 Integer 声明了 final,不允许被继承,导致无法重写 Integer.intValue()

但是可以利用 AOP 切到 Integer.intValue() 方法进行篡改。

在 Stack Overflow 就有人给出了类似的解题思路(理论上是可行的,但我并没有去验证,有兴趣的同学可以试试):

import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@PrepareForTest(Integer.class)
@RunWith(PowerMockRunner.class)
public class TestJava {

    /**
     * 利用 AOP 把 Integer.intValue() 替换为 AtomicInteger.getAndIncrement()
     */
    @Before
    public void before() {
        AtomicInteger ai = new AtomicInteger(1);                // 自增整数
        replace(method(Integer.class, "intValue")).with(
                (proxy, method, args) -> ai.getAndIncrement()   // lambda
        );
    }

    @Test
    public void test() {
        Integer a = 1;
        if(a == 1 && a == 2 && a == 3) {
            System.out.println("impossable!");
        } else {
            System.out.println("It's right.");
        }
    }

}

结语

通过前面的解题过程可以发现,弱类型语言 相较于 强类型语言 会更容易实现底层逻辑篡改,主要是因为对语法特性的校验会更宽松。

C++ 虽然和 Java 一样属于强类型语言,但是因为没有限制运算符重写而被钻了空子

在渗透测试中,或者可以利用类似的手段,绕过一些条件语句达到目的。


文章作者: EXP
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 EXP !
  目录