OpenJDK类加载实现浅析#2:安全检查

来源:互联网 发布:机械动画软件 编辑:程序博客网 时间:2024/05/18 01:36

今天来看下类加载过程中的一些安全检查。在那之前得先来了解下Java的Access Control。

Access Control

The access control architecture in the Java platform protects access to sensitive resources (for example, local files) or sensitive application code (for example, methods in a class). All access control decisions are mediated by a security manager, represented by the java.lang.SecurityManager class. A SecurityManager must be installed into the Java runtime in order to activate the access control checks.

访问控制属于Java Security Architecture的一部分。一张图看懂:)

alright,接下来我们还是直接看代码吧。FileInputStream

    public FileInputStream(File file) throws FileNotFoundException {        String name = (file != null ? file.getPath() : null);        /////// 进行安全检查        SecurityManager security = System.getSecurityManager();        if (security != null) {            security.checkRead(name);        }        if (name == null) {            throw new NullPointerException();        }        if (file.isInvalid()) {            throw new FileNotFoundException("Invalid file path");        }        fd = new FileDescriptor();        fd.incrementAndGetUseCount();        this.path = name;        open(name);    }

SecurityManager#checkRead(java.lang.String)

    public void checkRead(String file) {        checkPermission(new FilePermission(file,            SecurityConstants.FILE_READ_ACTION));    }
    public void checkPermission(Permission perm) {      /////// 直接交由AccessController处理      java.security.AccessController.checkPermission(perm);    }

AccessController#checkPermission

    public static void checkPermission(Permission perm)                 throws AccessControlException    {        //System.err.println("checkPermission "+perm);        //Thread.currentThread().dumpStack();        if (perm == null) {            throw new NullPointerException("permission can't be null");        }        AccessControlContext stack = getStackAccessControlContext();        // context为null不需要进行检查        // if context is null, we had privileged system code on the stack.        if (stack == null) {            Debug debug = AccessControlContext.getDebug();            boolean dumpDebug = false;            if (debug != null) {                dumpDebug = !Debug.isOn("codebase=");                dumpDebug &= !Debug.isOn("permission=") ||                    Debug.isOn("permission=" + perm.getClass().getCanonicalName());            }            if (dumpDebug && Debug.isOn("stack")) {                Thread.currentThread().dumpStack();            }            if (dumpDebug && Debug.isOn("domain")) {                debug.println("domain (context is null)");            }            if (dumpDebug) {                debug.println("access allowed "+perm);            }            return;        }        /////// 交由AccessControlContext处理        AccessControlContext acc = stack.optimize();        acc.checkPermission(perm);    }

AccessControlContext#checkPermission

        /* if ctxt is null, all we had on the stack were system domains,           or the first domain was a Privileged system domain. This           is to make the common case for system code very fast */        /////// 与classloader为null同一招        if (context == null)            return;        for (int i=0; i< context.length; i++) {            if (context[i] != null &&  !context[i].implies(perm)) {                if (dumpDebug) {                    debug.println("access denied " + perm);                }                if (Debug.isOn("failure") && debug != null) {                    // Want to make sure this is always displayed for failure,                    // but do not want to display again if already displayed                    // above.                    if (!dumpDebug) {                        debug.println("access denied " + perm);                    }                    Thread.currentThread().dumpStack();                    final ProtectionDomain pd = context[i];                    final Debug db = debug;                    AccessController.doPrivileged (new PrivilegedAction<Void>() {                        public Void run() {                            db.println("domain that failed "+pd);                            return null;                        }                    });                }                throw new AccessControlException("access denied "+perm, perm);            }        }

上面的context是一个ProtectionDomain数组,

private ProtectionDomain context[];

所以接下来是交由ProtectionDomain#implies进行检查了,

    public boolean implies(Permission permission) {        if (hasAllPerm) {            // internal permission collection already has AllPermission -            // no need to go to policy            return true;        }        if (!staticPermissions &&            Policy.getPolicyNoCheck().implies(this, permission))            return true;        if (permissions != null)            return permissions.implies(permission);        return false;    }

到这里终于出现Policy了,Policy#getPolicyNoCheck

    static Policy getPolicyNoCheck()    {        PolicyInfo pi = policy.get();        // Use double-check idiom to avoid locking if system-wide policy is        // already initialized        if (pi.initialized == false || pi.policy == null) {            synchronized (Policy.class) {                PolicyInfo pinfo = policy.get();                if (pinfo.policy == null) {                    String policy_class = AccessController.doPrivileged(                        new PrivilegedAction<String>() {                        public String run() {                            /////// 读取{java.home}/lib/security/java.security配置文件                            /////// policy.provider=sun.security.provider.PolicyFile                            return Security.getProperty("policy.provider");                        }                    });                    if (policy_class == null) {                        policy_class = "sun.security.provider.PolicyFile";                    }                    try {                        pinfo = new PolicyInfo(                            (Policy) Class.forName(policy_class).newInstance(),                            true);                    } catch (Exception e) {                        /*                         * The policy_class seems to be an extension                         * so we have to bootstrap loading it via a policy                         * provider that is on the bootclasspath.                         * If it loads then shift gears to using the configured                         * provider.                         */                        // install the bootstrap provider to avoid recursion                        Policy polFile = new sun.security.provider.PolicyFile();                        pinfo = new PolicyInfo(polFile, false);                        policy.set(pinfo);                        final String pc = policy_class;                        Policy pol = AccessController.doPrivileged(                            new PrivilegedAction<Policy>() {                            public Policy run() {                                try {                                    ClassLoader cl =                                            ClassLoader.getSystemClassLoader();                                    // we want the extension loader                                    ClassLoader extcl = null;                                    while (cl != null) {                                        extcl = cl;                                        cl = cl.getParent();                                    }                                    return (extcl != null ? (Policy)Class.forName(                                            pc, true, extcl).newInstance() : null);                                } catch (Exception e) {                                    if (debug != null) {                                        debug.println("policy provider " +                                                    pc +                                                    " not available");                                        e.printStackTrace();                                    }                                    return null;                                }                            }                        });                        /*                         * if it loaded install it as the policy provider. Otherwise                         * continue to use the system default implementation                         */                        if (pol != null) {                            pinfo = new PolicyInfo(pol, true);                        } else {                            if (debug != null) {                                debug.println("using sun.security.provider.PolicyFile");                            }                            pinfo = new PolicyInfo(polFile, true);                        }                    }                    policy.set(pinfo);                }                return pinfo.policy;            }        }        return pi.policy;    }

Policy#implies

    public boolean implies(ProtectionDomain domain, Permission permission) {        PermissionCollection pc;        if (pdMapping == null) {            initPolicy(this);        }        synchronized (pdMapping) {            pc = pdMapping.get(domain.key);        }        if (pc != null) {            return pc.implies(permission);        }        pc = getPermissions(domain);        if (pc == null) {            return false;        }        synchronized (pdMapping) {            // cache it            pdMapping.put(domain.key, pc);        }        return pc.implies(permission);    }

所以最终其实都是交由PermissionCollection#implies处理,

    /**     * Checks to see if the specified permission is implied by     * the collection of Permission objects held in this PermissionCollection.     *     * @param permission the Permission object to compare.     *     * @return true if "permission" is implied by the  permissions in     * the collection, false if not.     */    public abstract boolean implies(Permission permission);

总结一下就是,

  1. 通过System.getSecurityManager()拿到SecurityManager
  2. SecurityManager直接交给AccessController处理;
  3. AccessController通过调用getStackAccessControlContext取得AccessControlContext,并交给AccessControlContext处理;
  4. AccessControlContext交给它所持有的一个ProtectionDomain数组处理;
  5. ProtectionDomain交给PermissionCollection处理,有两种方式拿到PermissionCollection,一种是使用自身持有的PermissionCollection(构造函数传入),另一种是使用Policy来获得。使用哪种方式由staticPermissions决定。

那么现在有两个问题,System.getSecurityManager()getStackAccessControlContext分别做了啥?我们一个一个来看下。

SecurityManager

System.getSecurityManager()其实比较简单,直接返回了一个SecurityManager

    public static SecurityManager getSecurityManager() {        return security;    }

那么这个security又是什么时候设置的呢?是在虚拟机启动的时候,由launcher来设置的,

    public Launcher() {        ...        // Finally, install a security manager if requested        String s = System.getProperty("java.security.manager");        if (s != null) {            SecurityManager sm = null;            if ("".equals(s) || "default".equals(s)) {                sm = new java.lang.SecurityManager();            } else {                try {                    sm = (SecurityManager)loader.loadClass(s).newInstance();                } catch (IllegalAccessException e) {                } catch (InstantiationException e) {                } catch (ClassNotFoundException e) {                } catch (ClassCastException e) {                }            }            if (sm != null) {                System.setSecurityManager(sm);            } else {                throw new InternalError(                    "Could not create SecurityManager: " + s);            }        }    }

所以我们是可以通过设置java.security.manager这个系统属性来使用我们自己的SecurityManager的。

getStackAccessControlContext

接下来看看比较关键的getStackAccessControlContext方法,

    /**     * Returns the AccessControl context. i.e., it gets     * the protection domains of all the callers on the stack,     * starting at the first class with a non-null     * ProtectionDomain.     *     * @return the access control context based on the current stack or     *         null if there was only privileged system code.     */    private static native AccessControlContext getStackAccessControlContext();

是个本地方法,具体实现我们暂不深究,下面直接贴出这个地方所使用的权限校验算法,具体参考官方文档,

Suppose the current thread traversed m callers, in the order of caller 1 to caller 2 to caller m. Then caller m invoked the checkPermission method. The basic algorithm checkPermission uses to determine whether access is granted or denied is the following

  i = m;  while (i > 0) {     if (caller i's domain does not have the permission)        throw AccessControlException     else if (caller i is marked as privileged) {        if (a context was specified in the call to doPrivileged)           context.checkPermission(permission);        return;     }     i = i - 1;  };  // Next, check the context inherited when  // the thread was created. Whenever a new thread is created, the  // AccessControlContext at that time is  // stored and associated with the new thread, as the "inherited"  // context.  inheritedContext.checkPermission(permission);

有一点值得说明下,就是我们平时经常看到的AccessController#doPrivileged方法,

That is, a caller can be marked as being “privileged” when it calls the doPrivileged method. When making access control decisions, the checkPermission method stops checking if it reaches a caller that was marked as “privileged” via a doPrivileged call without a context argument. If that caller’s domain has the specified permission, no further checking is done and checkPermission returns quietly, indicating that the requested access is allowed. If that domain does not have the specified permission, an exception is thrown, as usual.

也就是说遇到privileged的caller,安全检查一定会停止,不管是成功还是失败。应该是出于性能考虑。

Protection Domain

AccessControlContext我们没有深究到底是怎么生成的,那它所持有的ProtectionDomain这货又是个啥?

A domain conceptually encloses a set of classes whose instances are granted the same set of permissions. Protection domains are determined by the policy currently in effect.

还是一张图看懂:)

每个domain所拥有的权限就是我们上面所看到的PermissionCollection

AccessControlContext所持有的ProtectionDomain其实就是在调用栈上面,每个Class所属的ProtectionDomain。那么每个Class所属的ProtectionDomain又是怎么来的呢?妥妥的,就是在类加载的时候塞进去的,这个我们下面会再分析。

再放一个栗子,来看看ProtectionDomain具体长啥样,

    public static void main(String[] args) throws Throwable {        System.out.println(System.class.getProtectionDomain());        System.out.println(GroovyClassLoader.class.getProtectionDomain());        System.out.println(Main.class.getProtectionDomain());    }
ProtectionDomain  null null <no principals> java.security.Permissions@73c94b51 ( ("java.security.AllPermission" "<all permissions>" "<all actions>"))ProtectionDomain  (file:/C:/Java/jdk1.7.0_51/jre/lib/ext/groovy-2.4.0.jar <no signer certificates>) sun.misc.Launcher$ExtClassLoader@d325aef <no principals> java.security.Permissions@3aeb3f66 ( ("java.io.FilePermission" "\C:\Java\jdk1.7.0_51\jre\lib\ext\groovy-2.4.0.jar" "read"))ProtectionDomain  (file:/E:/Projects/just4fun/target/classes/ <no signer certificates>) sun.misc.Launcher$AppClassLoader@35f784d7 <no principals> java.security.Permissions@2a8f5fc2 ( ("java.lang.RuntimePermission" "exitVM") ("java.io.FilePermission" "\E:\Projects\just4fun\target\classes\-" "read"))

OK,接下来就来看下类加载过程中的一些安全检查。

findClass

首先是findClass过程中,寻找Resource时会有安全检查,代码在URLClassPath#check

    /*     * Check whether the resource URL should be returned.     * Throw exception on failure.     * Called internally within this file.     */    static void check(URL url) throws IOException {        SecurityManager security = System.getSecurityManager();        if (security != null) {            URLConnection urlConnection = url.openConnection();            Permission perm = urlConnection.getPermission();            if (perm != null) {                try {                    security.checkPermission(perm);                } catch (SecurityException se) {                    // fallback to checkRead/checkConnect for pre 1.2                    // security managers                    if ((perm instanceof java.io.FilePermission) &&                        perm.getActions().indexOf("read") != -1) {                        security.checkRead(perm.getName());                    } else if ((perm instanceof                        java.net.SocketPermission) &&                        perm.getActions().indexOf("connect") != -1) {                        URL locUrl = url;                        if (urlConnection instanceof JarURLConnection) {                            locUrl = ((JarURLConnection)urlConnection).getJarFileURL();                        }                        security.checkConnect(locUrl.getHost(),                                              locUrl.getPort());                    } else {                        throw se;                    }                }            }        }    }

如果是本地资源需要校验文件权限,如果是网络资源需要校验网络权限。

getAndVerifyPackage

接下来在defineClass的时候,首先需要一些关于package的检查,URLClassLoader#getAndVerifyPackage

    private Package getAndVerifyPackage(String pkgname,                                        Manifest man, URL url) {        Package pkg = getPackage(pkgname);        if (pkg != null) {            // Package found, so check package sealing.            if (pkg.isSealed()) {                // Verify that code source URL is the same.                if (!pkg.isSealed(url)) {                    throw new SecurityException(                        "sealing violation: package " + pkgname + " is sealed");                }            } else {                // Make sure we are not attempting to seal the package                // at this code source URL.                if ((man != null) && isSealed(pkgname, man)) {                    throw new SecurityException(                        "sealing violation: can't seal package " + pkgname +                        ": already loaded");                }            }        }        return pkg;    }

类加载的时候会将类的package定义保存下来,其中包括package的一些相关属性,这里需要校验的主要是看看package的Sealed属性。Sealed属性通过Manifest来定义,这其实是一个关于Java中package访问权限的补充。看下面的栗子,

package me.kisimple.just4fun;public class Bar {    static String secret = "you know too much";}
package me.kisimple.just4fun;public class Foo {    public static String gotIt() {        return Bar.secret;    }}

Bar#secret是package的访问权限,Foo可以访问到它,这是没有问题的。但假如我不希望Bar#secret其他jar包访问到,不怀好意的人却可以通过将Foo打进自己jar包的方法来访问secret,就像下面这样,

> jar cvf bar.jar me/kisimple/just4fun/Bar.class> jar cvf foo.jar me/kisimple/just4fun/Foo.class

然后我同时依赖bar.jarfoo.jar,这样我就可以在自己代码里面通过Foo间接访问Bar#secret了,

    public static void main(String[] args) throws Throwable {        System.out.println(Foo.gotIt());    }

这不得不说是Java的package访问权限的一个漏洞,怎么办呢?这时候就可以使用上面我们说到的ManifestSealed属性了,在打Bar的jar包的时候,我们可以指定Manifest并添加Sealed属性,就像下面这样,

Name: me/kisimple/just4fun/Sealed: true
> jar cvfm bar.jar imanifest me/kisimple/just4fun/Bar.class

OK,这时候再通过Foo来访问Bar#secret的时候将会报错,Foo是无法被类加载器加载的,

Exception in thread "main" java.lang.SecurityException: sealing violation: can't seal package me.kisimple.just4fun: already loaded    at java.net.URLClassLoader.getAndVerifyPackage(URLClassLoader.java:395)    at java.net.URLClassLoader.defineClass(URLClassLoader.java:417)    at java.net.URLClassLoader.access$100(URLClassLoader.java:71)    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)    at java.security.AccessController.doPrivileged(Native Method)    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)    at java.lang.ClassLoader.loadClass(ClassLoader.java:425)    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)    at java.lang.ClassLoader.loadClass(ClassLoader.java:358)    at me.kisimple.just4fun.Foo.gotIt(Foo.java:10)    at me.kisimple.just4fun.Main.main(Main.java:10)

报错的地方其实就在getAndVerifyPackage方法,具体可以再回过头去看看代码。

getPermissions

接下来在构建Class所属的ProtectionDomain时,也有安全检查,URLClassLoader#getPermissions

        // make sure the person that created this class loader        // would have this permission        if (p != null) {            final SecurityManager sm = System.getSecurityManager();            if (sm != null) {                final Permission fp = p;                AccessController.doPrivileged(new PrivilegedAction<Void>() {                    public Void run() throws SecurityException {                        sm.checkPermission(fp);                        return null;                    }                }, acc);            }            perms.add(p);        }

需要确保这个即将grant给该ProtectionDomain的权限,类加载器自己是有的。

preDefineClass

接下来是真正defineClass之前最后的检查了,ClassLoader#preDefineClass

    /* Determine protection domain, and check that:        - not define java.* class,        - signer of this class matches signers for the rest of the classes in          package.    */    private ProtectionDomain preDefineClass(String name,                                            ProtectionDomain pd)    {        if (!checkName(name))            throw new NoClassDefFoundError("IllegalName: " + name);        if ((name != null) && name.startsWith("java.")) {            throw new SecurityException                ("Prohibited package name: " +                 name.substring(0, name.lastIndexOf('.')));        }        if (pd == null) {            pd = defaultDomain;        }        if (name != null) checkCerts(name, pd.getCodeSource());        return pd;    }

注释写得很清楚,有两个校验,确保不是在加载java.*包下的类,确保相同package的签名信息是一致的(类似上面的package访问权限的校验)。

getProtectionDomain

alright,到这里安全检查的话题就结束了。但是上面我们还留了一个坑,就是ProtectionDomain在类加载的时候是怎么整出来的。

我们来看看几个类加载器defineClass方法签名,

/////// java.net.URLClassLoaderdefineClass(String name, Resource res)/////// java.security.SecureClassLoaderdefineClass(String name, ByteBuffer b, CodeSource cs)/////// java.lang.ClassLoaderdefineClass(String name, ByteBuffer b, ProtectionDomain pd)

其实就是将Resource转化成了ByteBufferCodeSource,然后再将CodeSource转化成了ProtectionDomain。来看下具体的SecureClassLoader#getProtectionDomain

    private ProtectionDomain getProtectionDomain(CodeSource cs) {        if (cs == null)            return null;        ProtectionDomain pd = null;        synchronized (pdcache) {            pd = pdcache.get(cs);            if (pd == null) {                PermissionCollection perms = getPermissions(cs);                pd = new ProtectionDomain(cs, perms, this, null);                pdcache.put(cs, pd);                if (debug != null) {                    debug.println(" getPermissions "+ pd);                    debug.println("");                }            }        }        return pd;    }

getPermissions方法由子类自己来定义,也就是要定义这个Class所属的ProtectionDomain所拥有的权限了。具体可以再去看看Launcher.AppClassLoaderURLClassLoader各自的实现,这样也能更好的理解上面我们栗子中所输出的ProtectionDomain的值。

参考资料

  • http://docs.oracle.com/javase/8/docs/technotes/guides/security/spec/security-specTOC.fm.html
  • https://docs.oracle.com/javase/tutorial/deployment/jar/sealman.html
0 0
原创粉丝点击