Earlier this week, I read an interesting post on the state of JRuby, the Java version of the Ruby programming language.

The post had briefly mentioned use of JRuby to obfuscate Java code. Since non-obfuscated Java class files can be decompiled, many obfuscate the class files containing their proprietary code. Using a higher-level compiler on top of the Java code seemed more simple than trying to use one of the class-obfuscator tools.

It had been quite a while since I’d toyed with Ruby, so I thought I would see how one might use JRuby to protect their Java code. I don’t really have any Java code to protect, so I’ll just try to see what a decompiled JRuby-compiled program looks like.

In April of 2009, I wrote up a post on a small Ruby utility that I wrote to help keep one of my POP3 inboxes free of spam.

I have been using a slightly modified version of poppy.rb that simply deletes POP3 mails larger than 100,000 bytes in size. I copied this Ruby file to a new file called JPoppy.rb. I’ll use this script as our sample code.

JPoppy.rb

# JPoppy
# A POP3 mail-cleaner
#
# License: MIT / X11
# Copyright (c) 2010 by James K. Lawless
# jimbo@radiks.net http://www.radiks.net/~jimbo
# http://www.mailsend-online.com
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

require 'socket'

def removeSpam
    sock=TCPSocket.new("your.pop.server",110)

    ["","USER your.username", "PASS your.password", "STAT", "QUIT" ].each do |msg|
        if msg.length != 0
            sock.send(msg+"\r\n",0)
        end
        s=sock.recv(4000)
        if s.downcase.split(" ")[0] != "+ok"
            return false
        end
        if msg=="STAT"
            lookThroughEmails(sock,s)
        end
    end
    sock.close
end

def lookThroughEmails(sock,status)
    numOfEmails = status.split(" ")[1]
    for i in 1..numOfEmails.to_i
        sock.send("LIST " + i.to_s + "\r\n",0)
        s=sock.recv(4000)
        sz=s.split(" ")[2].to_i
        if checkForSpam(sock,i,sz)
            sock.send("dele " + i.to_s + " \r\n",0)
            s=sock.recv(4000)
            puts s
            puts "(deleted)"
        else
            puts "(kept)"
        end
    end
end

def checkForSpam(sock,i,sz)
    sock.send("TOP " + i.to_s + " 0 \r\n",0)
    flag = 0
    subject=from=to=replyTo=""
    while flag == 0 do
        s=sock.recv(8000)
        s.split("\r\n").each do |lin|
            if lin == ""
                flag = 1
            else
                word = lin.downcase.split(" ")[0]
                if word == "subject:"
                    subject=lin.downcase
                elsif word == "from:"
                    from=lin.downcase
                elsif word == "to:"
                    to=lin.downcase
                elsif word == "reply-to:"
                    replyTo=lin.downcase
                end
            end
        end
    end

    puts "------------"
    puts "# " + i.to_s
    puts to
    puts from
    puts replyTo
    puts subject
    puts "(" + sz.to_s + ")"
    return (sz>100000)
end

removeSpam

I first ran this utility to see if it would work unchanged under JRuby.

jruby JPoppy.rb

I didn’t encounter any problems. The script ran just as it had under the native Ruby interpreter.

I then compiled the program leaving a Java class file as the output:

jruby -S jrubyc JPoppy.rb

The output JPoppy.class file executes just as the script had done in interpretive mode.

I then decompiled JPoppy.class with Pavel Kouznetsov’s JAD Java decompiler utility.

jad JPoppy

JAD displayed the following errors during decompilation:

Parsing JPoppy...
Couldn't fully decompile method block_0$RUBY$__block__
Couldn't resolve all exception handlers in method block_0$RUBY$__block__
Couldn't fully decompile method block_1$RUBY$__for__
Couldn't resolve all exception handlers in method block_1$RUBY$__for__
Couldn't fully decompile method rescue_1$RUBY$__rescue___3
Couldn't resolve all exception handlers in method rescue_1$RUBY$__rescue___3
Couldn't fully decompile method block_2$RUBY$__block__
Couldn't resolve all exception handlers in method block_2$RUBY$__block__
Couldn't fully decompile method load
Couldn't resolve all exception handlers in method load
Couldn't fully decompile method main

Here is the decompiled output:

JPoppy.jad

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: JPoppy.rb

import org.jruby.*;
import org.jruby.ast.executable.AbstractScript;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.methods.CallConfiguration;
import org.jruby.javasupport.util.RuntimeHelpers;
import org.jruby.runtime.*;
import org.jruby.runtime.builtin.IRubyObject;

public class JPoppy extends AbstractScript
{

    private static void setPosition(ThreadContext threadcontext, int i)
    {
        threadcontext.setFileAndLine("JPoppy.rb", i);
    }

    public JPoppy()
    {
        filename = "JPoppy.rb";
        super.runtimeCache = new org.jruby.ast.executable.AbstractScript.RuntimeCache();
        super.runtimeCache.initCallSites(70);
        setFunctionalCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setFunctionalCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setFunctionalCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setFunctionalCallSite(setFunctionalCallSite(setFunctionalCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setFunctionalCallSite(setFunctionalCallSite(setCallSite(setCallSite(setFunctionalCallSite(setFunctionalCallSite(setFunctionalCallSite(setFunctionalCallSite(setFunctionalCallSite(setCallSite(setCallSite(setCallSite(setCallSite(setVariableCallSite(super.runtimeCache.callSites, 69, "removeSpam"), 68, ">"), 67, "to_s"), 66, "+"), 65, "+"), 64, "puts"), 63, "puts"), 62, "puts"), 61, "puts"), 60, "puts"), 59, "to_s"), 58, "+"), 57, "puts"), 56, "puts"), 55, "=="), 54, "downcase"), 53, "=="), 52, "downcase"), 51, "=="), 50, "downcase"), 49, "=="), 48, "downcase"), 47, "=="), 46, "downcase"), 45, "split"), 44, "[]"), 43, "=="), 42, "split"), 41, "each"), 40, "recv"), 39, "to_s"), 38, "+"), 37, "+"), 36, "send"), 35, "puts"), 34, "puts"), 33, "puts"), 32, "recv"), 31, "to_s"), 30, "+"), 29, "+"), 28, "send"), 27, "checkForSpam"), 26, "split"), 25, "[]"), 24, "to_i"), 23, "recv"), 22, "to_s"), 21, "+"), 20, "+"), 19, "send"), 18, "to_i"), 17, "each"), 16, "split"), 15, "[]"), 14, "close"), 13, "lookThroughEmails"), 12, "=="), 11, "downcase"), 10, "split"), 9, "[]"), 8, "=="), 7, "recv"), 6, "+"), 5, "send"), 4, "length"), 3, "=="), 2, "each"), 1, "new"), 0, "require");
        super.runtimeCache.initFixnums(3);
        super.runtimeCache.initConstants(1);
        super.runtimeCache.initBlockBodies(2);
        super.runtimeCache.initBlockCallbacks(1);
        AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(AbstractScript.createByteList(super.runtimeCache.initStrings(25), 2, ""), 20, "reply-to:"), 5, "STAT"), 7, "\r\n"), 15, "TOP "), 17, "subject:"), 16, " 0 \r\n"), 21, "------------"), 22, "# "), 19, "to:"), 12, " \r\n"), 0, "socket"), 3, "USER your.username"), 11, "dele "), 6, "QUIT"), 8, " "), 4, "PASS your.password"), 9, "+ok"), 23, "("), 18, "from:"), 24, ")"), 10, "LIST "), 1, "your.pop.server"), 13, "(deleted)"), 14, "(kept)");
    }

    public static IRubyObject __file__(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject airubyobject[], Block block)
    {
        Ruby ruby = threadcontext.getRuntime();
        IRubyObject irubyobject1 = ruby.getNil();
        setPosition(threadcontext, 29);
        jpoppy.getCallSite0().call(threadcontext, irubyobject, irubyobject, jpoppy.getString0(ruby));
        setPosition(threadcontext, 31);
        RuntimeHelpers.def(threadcontext, irubyobject, jpoppy, "removeSpam", "method__0$RUBY$removeSpam", RuntimeHelpers.constructStringArray("sock"), 0, 0, 0, -1, CallConfiguration.FrameFullScopeFull);
        setPosition(threadcontext, 49);
        RuntimeHelpers.def(threadcontext, irubyobject, jpoppy, "lookThroughEmails", "method__1$RUBY$lookThroughEmails", RuntimeHelpers.constructStringArray("sock", "status", "numOfEmails", "i", "s", "sz"), 2, 2, 0, -1, CallConfiguration.FrameFullScopeFull);
        setPosition(threadcontext, 66);
        RuntimeHelpers.def(threadcontext, irubyobject, jpoppy, "checkForSpam", "method__2$RUBY$checkForSpam", RuntimeHelpers.constructStringArray("sock", "i", "sz", "flag", "subject", "from", "to", "replyTo", "s"), 3, 3, 0, -1, CallConfiguration.FrameFullScopeFull);
        setPosition(threadcontext, 101);
        return jpoppy.getCallSite(69).call(threadcontext, irubyobject, irubyobject);
    }

    public IRubyObject __file__(ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject airubyobject[], Block block)
    {
        return __file__(this, threadcontext, irubyobject, airubyobject, block);
    }

    public static IRubyObject method__0$RUBY$removeSpam(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, Block block)
    {
        Ruby ruby = threadcontext.getRuntime();
        IRubyObject irubyobject1 = ruby.getNil();
        DynamicScope locals = threadcontext.getCurrentScope();
        setPosition(threadcontext, 32);
        locals.setValueZeroDepthZero(jpoppy.getCallSite1().call(threadcontext, irubyobject, jpoppy.getConstant0(threadcontext, "TCPSocket"), jpoppy.getString1(ruby), jpoppy.getFixnum0(ruby, 110)));
        setPosition(threadcontext, 34);
        jpoppy.getCallSite2().callIter(threadcontext, irubyobject, RuntimeHelpers.constructRubyArray(ruby, jpoppy.getString2(ruby), jpoppy.getString3(ruby), jpoppy.getString4(ruby), jpoppy.getString5(ruby), jpoppy.getString6(ruby)), RuntimeHelpers.createBlock(threadcontext, irubyobject, jpoppy.getBlockBody0(threadcontext, "block_0$RUBY$__block__,1,msg;s,false,2,false")));
        setPosition(threadcontext, 46);
        return jpoppy.getCallSite(14).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject1));
    }

    public static IRubyObject block_0$RUBY$__block__(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject irubyobject1)
    {
        DynamicScope locals;
        Ruby ruby;
        IRubyObject irubyobject2;
        ruby = threadcontext.getRuntime();
        irubyobject2 = ruby.getNil();
        locals = threadcontext.getCurrentScope();
        locals.setValueZeroDepthZero(irubyobject2);
        locals;
        JVM INSTR swap ;
        setValueOneDepthZero();
        JVM INSTR pop ;
        locals.setValueZeroDepthZero(irubyobject1);
_L5:
        setPosition(threadcontext, 35);
        if(RuntimeHelpers.negate(jpoppy.getCallSite3().call(threadcontext, irubyobject, jpoppy.getCallSite4().call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2)), 0L), ruby).isTrue())
        {
            setPosition(threadcontext, 36);
            jpoppy.getCallSite5().call(threadcontext, irubyobject, locals.getNextCapturedScope().getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getCallSite6().call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getString7(ruby)), RubyFixnum.zero(ruby));
        }
        setPosition(threadcontext, 38);
        locals.setValueOneDepthZero(jpoppy.getCallSite7().call(threadcontext, irubyobject, locals.getNextCapturedScope().getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getFixnum1(ruby, 4000)));
        setPosition(threadcontext, 39);
        if(RuntimeHelpers.negate(jpoppy.getCallSite8().call(threadcontext, irubyobject, jpoppy.getCallSite9().call(threadcontext, irubyobject, jpoppy.getCallSite(10).call(threadcontext, irubyobject, jpoppy.getCallSite(11).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject2)), jpoppy.getString8(ruby)), 0L), jpoppy.getString9(ruby)), ruby).isTrue())
        {
            setPosition(threadcontext, 40);
            threadcontext.pollThreadEvents();
            throw RuntimeHelpers.returnJump(ruby.getFalse(), threadcontext);
        }
        setPosition(threadcontext, 42);
        if(!jpoppy.getCallSite(12).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getString5(ruby)).isTrue()) goto _L2; else goto _L1
_L1:
        setPosition(threadcontext, 43);
        jpoppy.getCallSite(13).call(threadcontext, irubyobject, irubyobject, locals.getNextCapturedScope().getValueZeroDepthZeroOrNil(irubyobject2), locals.getValueOneDepthZeroOrNil(irubyobject2));
          goto _L3
_L2:
        irubyobject2;
_L3:
        return;
        JVM INSTR pop ;
        if(true) goto _L5; else goto _L4
_L4:
    }

    public static IRubyObject method__0$RUBY$removeSpam(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject airubyobject[], Block block)
    {
        Arity.checkArgumentCount(threadcontext.getRuntime(), airubyobject, 0, 0);
        return method__0$RUBY$removeSpam(jpoppy, threadcontext, irubyobject, block);
    }

    public static IRubyObject method__1$RUBY$lookThroughEmails(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject irubyobject1, IRubyObject irubyobject2, Block block)
    {
        Ruby ruby = threadcontext.getRuntime();
        IRubyObject irubyobject3 = ruby.getNil();
        DynamicScope locals = threadcontext.getCurrentScope();
        IRubyObject airubyobject[] = locals.getValues();
        RuntimeHelpers.fillNil(airubyobject, ruby);
        locals.setValueZeroDepthZero(irubyobject1);
        locals.setValueOneDepthZero(irubyobject2);
        setPosition(threadcontext, 50);
        locals.setValueTwoDepthZero(jpoppy.getCallSite(15).call(threadcontext, irubyobject, jpoppy.getCallSite(16).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject3), jpoppy.getString8(ruby)), 1L));
        setPosition(threadcontext, 51);
        return jpoppy.getCallSite(17).callIter(threadcontext, irubyobject, RubyRange.newInclusiveRange(ruby, threadcontext, RubyFixnum.one(ruby), jpoppy.getCallSite(18).call(threadcontext, irubyobject, locals.getValueTwoDepthZeroOrNil(irubyobject3))), RuntimeHelpers.createSharedScopeBlock(threadcontext, irubyobject, 1, jpoppy.getBlockCallback0(ruby, "block_1$RUBY$__for__"), false, 2));
    }

    public static IRubyObject block_1$RUBY$__for__(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject irubyobject1)
    {
        DynamicScope locals;
        Ruby ruby;
        IRubyObject airubyobject[];
        IRubyObject irubyobject2;
        ruby = threadcontext.getRuntime();
        irubyobject2 = ruby.getNil();
        locals = threadcontext.getCurrentScope();
        airubyobject = locals.getValues();
        locals.setValueThreeDepthZero(irubyobject1);
_L5:
        setPosition(threadcontext, 52);
        jpoppy.getCallSite(19).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getCallSite(20).call(threadcontext, irubyobject, jpoppy.getCallSite(21).call(threadcontext, irubyobject, jpoppy.getString(ruby, 10), jpoppy.getCallSite(22).call(threadcontext, irubyobject, locals.getValueThreeDepthZeroOrNil(irubyobject2))), jpoppy.getString7(ruby)), RubyFixnum.zero(ruby));
        setPosition(threadcontext, 53);
        airubyobject[4] = jpoppy.getCallSite(23).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getFixnum1(ruby, 4000));
        setPosition(threadcontext, 54);
        airubyobject[5] = jpoppy.getCallSite(24).call(threadcontext, irubyobject, jpoppy.getCallSite(25).call(threadcontext, irubyobject, jpoppy.getCallSite(26).call(threadcontext, irubyobject, airubyobject[4], jpoppy.getString8(ruby)), 2L));
        setPosition(threadcontext, 55);
        if(!jpoppy.getCallSite(27).call(threadcontext, irubyobject, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), locals.getValueThreeDepthZeroOrNil(irubyobject2), airubyobject[5]).isTrue()) goto _L2; else goto _L1
_L1:
        setPosition(threadcontext, 56);
        jpoppy.getCallSite(28).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getCallSite(29).call(threadcontext, irubyobject, jpoppy.getCallSite(30).call(threadcontext, irubyobject, jpoppy.getString(ruby, 11), jpoppy.getCallSite(31).call(threadcontext, irubyobject, locals.getValueThreeDepthZeroOrNil(irubyobject2))), jpoppy.getString(ruby, 12)), RubyFixnum.zero(ruby));
        setPosition(threadcontext, 57);
        airubyobject[4] = jpoppy.getCallSite(32).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getFixnum1(ruby, 4000));
        setPosition(threadcontext, 58);
        jpoppy.getCallSite(33).call(threadcontext, irubyobject, irubyobject, airubyobject[4]);
        setPosition(threadcontext, 59);
        jpoppy.getCallSite(34).call(threadcontext, irubyobject, irubyobject, jpoppy.getString(ruby, 13));
          goto _L3
_L2:
        setPosition(threadcontext, 61);
        jpoppy.getCallSite(35).call(threadcontext, irubyobject, irubyobject, jpoppy.getString(ruby, 14));
_L3:
        return;
        JVM INSTR pop ;
        if(true) goto _L5; else goto _L4
_L4:
    }

    public static IRubyObject method__1$RUBY$lookThroughEmails(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject airubyobject[], Block block)
    {
        Arity.checkArgumentCount(threadcontext.getRuntime(), airubyobject, 2, 2);
        return method__1$RUBY$lookThroughEmails(jpoppy, threadcontext, irubyobject, airubyobject[0], airubyobject[1], block);
    }

    public static IRubyObject method__2$RUBY$checkForSpam(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject irubyobject1, IRubyObject irubyobject2, IRubyObject irubyobject3, Block block)
    {
        Ruby ruby = threadcontext.getRuntime();
        IRubyObject irubyobject4 = ruby.getNil();
        DynamicScope locals = threadcontext.getCurrentScope();
        IRubyObject airubyobject[] = locals.getValues();
        RuntimeHelpers.fillNil(airubyobject, ruby);
        locals.setValueZeroDepthZero(irubyobject1);
        locals.setValueOneDepthZero(irubyobject2);
        locals.setValueTwoDepthZero(irubyobject3);
        setPosition(threadcontext, 67);
        jpoppy.getCallSite(36).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject4), jpoppy.getCallSite(37).call(threadcontext, irubyobject, jpoppy.getCallSite(38).call(threadcontext, irubyobject, jpoppy.getString(ruby, 15), jpoppy.getCallSite(39).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject4))), jpoppy.getString(ruby, 16)), RubyFixnum.zero(ruby));
        setPosition(threadcontext, 68);
        locals.setValueThreeDepthZero(RubyFixnum.zero(ruby));
        setPosition(threadcontext, 69);
        airubyobject[4] = airubyobject[5] = airubyobject[6] = airubyobject[7] = jpoppy.getString2(ruby);
        setPosition(threadcontext, 70);
        rescue_1$RUBY$__rescue___3(jpoppy, threadcontext, irubyobject, irubyobject1, irubyobject2, irubyobject3, block);
        threadcontext.pollThreadEvents();
        setPosition(threadcontext, 89);
        jpoppy.getCallSite(56).call(threadcontext, irubyobject, irubyobject, jpoppy.getString(ruby, 21));
        setPosition(threadcontext, 90);
        jpoppy.getCallSite(57).call(threadcontext, irubyobject, irubyobject, jpoppy.getCallSite(58).call(threadcontext, irubyobject, jpoppy.getString(ruby, 22), jpoppy.getCallSite(59).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject4))));
        setPosition(threadcontext, 91);
        jpoppy.getCallSite(60).call(threadcontext, irubyobject, irubyobject, airubyobject[6]);
        setPosition(threadcontext, 92);
        jpoppy.getCallSite(61).call(threadcontext, irubyobject, irubyobject, airubyobject[5]);
        setPosition(threadcontext, 93);
        jpoppy.getCallSite(62).call(threadcontext, irubyobject, irubyobject, airubyobject[7]);
        setPosition(threadcontext, 94);
        jpoppy.getCallSite(63).call(threadcontext, irubyobject, irubyobject, airubyobject[4]);
        setPosition(threadcontext, 95);
        jpoppy.getCallSite(64).call(threadcontext, irubyobject, irubyobject, jpoppy.getCallSite(65).call(threadcontext, irubyobject, jpoppy.getCallSite(66).call(threadcontext, irubyobject, jpoppy.getString(ruby, 23), jpoppy.getCallSite(67).call(threadcontext, irubyobject, locals.getValueTwoDepthZeroOrNil(irubyobject4))), jpoppy.getString(ruby, 24)));
        setPosition(threadcontext, 97);
        return jpoppy.getCallSite(68).call(threadcontext, irubyobject, locals.getValueTwoDepthZeroOrNil(irubyobject4), 0x186a0L);
    }

    public static IRubyObject rescue_1$RUBY$__rescue___3(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject irubyobject1, IRubyObject irubyobject2, IRubyObject irubyobject3, Block block)
    {
        DynamicScope dynamicscope;
        Ruby ruby;
        IRubyObject airubyobject[];
        IRubyObject irubyobject4;
        ruby = threadcontext.getRuntime();
        irubyobject4 = ruby.getNil();
        dynamicscope = threadcontext.getCurrentScope();
        airubyobject = dynamicscope.getValues();
          goto _L1
_L2:
        setPosition(threadcontext, 71);
        airubyobject[8] = jpoppy.getCallSite(40).call(threadcontext, irubyobject, dynamicscope.getValueZeroDepthZeroOrNil(irubyobject4), jpoppy.getFixnum2(ruby, 8000));
        setPosition(threadcontext, 72);
        RaiseException raiseexception;
        jpoppy.getCallSite(41).callIter(threadcontext, irubyobject, jpoppy.getCallSite(42).call(threadcontext, irubyobject, airubyobject[8], jpoppy.getString7(ruby)), RuntimeHelpers.createBlock(threadcontext, irubyobject, jpoppy.getBlockBody1(threadcontext, "block_2$RUBY$__block__,1,lin;word,false,2,false")));
        break; /* Loop/switch isn't completed */
        raiseexception;
        throw RuntimeHelpers.unwrapRedoNextBreakOrJustLocalJump(raiseexception, threadcontext);
        JVM INSTR pop ;
        if(true) goto _L2; else goto _L1
_L1:
        if(jpoppy.getCallSite(55).call(threadcontext, irubyobject, dynamicscope.getValueThreeDepthZeroOrNil(irubyobject4), 0L).isTrue()) goto _L2; else goto _L3
        JVM INSTR pop ;
          goto _L1
        org.jruby.exceptions.JumpException.BreakJump breakjump;
        breakjump;
        RuntimeHelpers.breakJumpInWhile(breakjump, threadcontext);
          goto _L4
_L3:
        irubyobject4;
_L4:
        return;
    }

    public static IRubyObject block_2$RUBY$__block__(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject irubyobject1)
    {
        DynamicScope locals;
        Ruby ruby;
        IRubyObject irubyobject2;
        ruby = threadcontext.getRuntime();
        irubyobject2 = ruby.getNil();
        locals = threadcontext.getCurrentScope();
        locals.setValueZeroDepthZero(irubyobject2);
        locals;
        JVM INSTR swap ;
        setValueOneDepthZero();
        JVM INSTR pop ;
        locals.setValueZeroDepthZero(irubyobject1);
_L13:
        setPosition(threadcontext, 73);
        if(!jpoppy.getCallSite(43).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2), jpoppy.getString2(ruby)).isTrue()) goto _L2; else goto _L1
_L1:
        setPosition(threadcontext, 74);
        locals.getNextCapturedScope().setValueThreeDepthZero(RubyFixnum.one(ruby));
          goto _L3
_L2:
        setPosition(threadcontext, 76);
        locals.setValueOneDepthZero(jpoppy.getCallSite(44).call(threadcontext, irubyobject, jpoppy.getCallSite(45).call(threadcontext, irubyobject, jpoppy.getCallSite(46).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2)), jpoppy.getString8(ruby)), 0L));
        setPosition(threadcontext, 77);
        if(!jpoppy.getCallSite(47).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject2), jpoppy.getString(ruby, 17)).isTrue()) goto _L5; else goto _L4
_L4:
        setPosition(threadcontext, 78);
        locals.setValue(4, jpoppy.getCallSite(48).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2)), 1);
          goto _L3
_L5:
        if(!jpoppy.getCallSite(49).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject2), jpoppy.getString(ruby, 18)).isTrue()) goto _L7; else goto _L6
_L6:
        setPosition(threadcontext, 80);
        locals.setValue(5, jpoppy.getCallSite(50).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2)), 1);
          goto _L3
_L7:
        if(!jpoppy.getCallSite(51).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject2), jpoppy.getString(ruby, 19)).isTrue()) goto _L9; else goto _L8
_L8:
        setPosition(threadcontext, 82);
        locals.setValue(6, jpoppy.getCallSite(52).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2)), 1);
          goto _L3
_L9:
        if(!jpoppy.getCallSite(53).call(threadcontext, irubyobject, locals.getValueOneDepthZeroOrNil(irubyobject2), jpoppy.getString(ruby, 20)).isTrue()) goto _L11; else goto _L10
_L10:
        setPosition(threadcontext, 84);
        locals.setValue(7, jpoppy.getCallSite(54).call(threadcontext, irubyobject, locals.getValueZeroDepthZeroOrNil(irubyobject2)), 1);
          goto _L3
_L11:
        irubyobject2;
_L3:
        return;
        JVM INSTR pop ;
        if(true) goto _L13; else goto _L12
_L12:
    }

    public static IRubyObject method__2$RUBY$checkForSpam(JPoppy jpoppy, ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject airubyobject[], Block block)
    {
        Arity.checkArgumentCount(threadcontext.getRuntime(), airubyobject, 3, 3);
        return method__2$RUBY$checkForSpam(jpoppy, threadcontext, irubyobject, airubyobject[0], airubyobject[1], airubyobject[2], block);
    }

    public IRubyObject load(ThreadContext threadcontext, IRubyObject irubyobject, IRubyObject airubyobject[], Block block)
    {
        RuntimeHelpers.preLoad(threadcontext, new String[0]);
        RuntimeHelpers.postLoad(threadcontext);
        return __file__(this, threadcontext, irubyobject, airubyobject, block);
        RuntimeHelpers.postLoad(threadcontext);
        throw ;
    }

    public static void main(String args[])
    {
        RubyInstanceConfig rubyinstanceconfig;
        rubyinstanceconfig = new RubyInstanceConfig();
        rubyinstanceconfig.setArgv(args);
        Ruby ruby;
        (new JPoppy()).load((ruby = Ruby.newInstance(rubyinstanceconfig)).getCurrentContext(), ruby.getTopSelf(), IRubyObject.NULL_ARRAY, Block.NULL_BLOCK);
    }

    private final Class $class = Class.forName("JPoppy");
}

I tried to recompile the JAD output but was unable to do so. A lot of raw JVM instructions show up in the decompiled output that the stock Java compiler itself doesn’t handle.

The literal strings are not obfuscated ( as obfuscation really isn’t JRuby’s job ) so the strings that contain the POP3 user ID and password appear in cleartext as most of us would have expected.

I also tried to manually trace through this code and really couldn’t. I suppose that it’s possible for someone to make sense of it, but I would doubt that many developers could. I think that I now understand more about how JRuby could be leveraged to obfuscate Java code.

I believe that in order to really protect one’s code in this instance, the bulk of the logic ( if not the entire program ) should be written in JRuby; it’s really the JRuby code that’s being obfuscated. The more of your intellectual property that you’re able to capture in pure JRuby, the more likely your code will be protected. Your mileage may vary.

Please note that someone will likely come along and build a decompiler is aware of the way JRuby generates code.

Toying with JRuby has renewed my interest in both Ruby and Java. I’ll be writing more posts in the near future that pertain to these technologies.

Unless otherwise noted, all code and text entries are Copyright ©2010 by James K. Lawless