最近在 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;
}
因为闲得慌,又萌生出了一个念头:其他语言是否也都可以实现这个表达式呢?
其实仔细分分析一下题干,要使得表达式成真,可以从两个思路切入:
- 要么
==
的判定逻辑被篡改 - 要么
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
是对象,右侧是数字,则会隐式调用对象 a
的 valueOf
方法将其转换成数字;若转换失败则调用 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
/3
是 int
基础类型,因此 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 一样属于强类型语言,但是因为没有限制运算符重写而被钻了空子
在渗透测试中,或者可以利用类似的手段,绕过一些条件语句达到目的。