Skip to content

Commit e09f887

Browse files
committed
Revert "Fixes large-string expansion in JSObfu."
This reverts commit 14fed8c.
1 parent 4cb04b6 commit e09f887

File tree

3 files changed

+74
-181
lines changed

3 files changed

+74
-181
lines changed

lib/rex/exploitation/jsobfu.rb

Lines changed: 50 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -48,68 +48,14 @@ module Exploitation
4848
#
4949
class JSObfu
5050

51-
# A single Javascript scope, used as a key-value store
52-
# to maintain uniqueness of members in generated closures.
53-
# For speed this class is implemented as a subclass of Hash.
54-
class Scope < Hash
55-
56-
# these keywords should never be used as a random var name
57-
# source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words
58-
RESERVED_KEYWORDS = %w(
59-
break case catch continue debugger default delete do else finally
60-
for function if in instanceof new return switch this throw try
61-
typeof var void while with class enum export extends import super
62-
implements interface let package private protected public static yield
63-
)
64-
65-
# these vars should not be shadowed as they may be used in other obfuscated code
66-
BUILTIN_VARS = %w(
67-
String window unescape
68-
)
69-
70-
# @param [Rex::Exploitation::JSObfu::Scope] parent an optional parent scope,
71-
# sometimes necessary to prevent needless var shadowing
72-
def initialize(parent=nil)
73-
@parent = parent
74-
@rand_gen = Rex::RandomIdentifierGenerator.new(
75-
:max_length => 15,
76-
:first_char_set => Rex::Text::Alpha+"_$",
77-
:char_set => Rex::Text::AlphaNumeric+"_$"
78-
)
79-
end
80-
81-
# @return [String] a unique random var name that is not a reserved keyword
82-
def random_var_name
83-
loop do
84-
text = random_string
85-
unless has_key?(text) or
86-
RESERVED_KEYWORDS.include?(text) or
87-
BUILTIN_VARS.include?(text)
88-
89-
self[text] = nil
90-
return text
91-
end
92-
end
93-
end
94-
95-
# @return [Boolean] var is in scope
96-
def has_key?(key)
97-
super or (@parent and @parent.has_key?(key))
98-
end
99-
100-
# @return [String] a random string
101-
def random_string
102-
@rand_gen.generate
103-
end
104-
105-
end
106-
107-
#
108-
# The maximum length of a string that will be passed through
109-
# #transform_string without being chopped up into separate
110-
# expressions and concatenated
111-
#
112-
MAX_STRING_CHUNK = 10000
51+
# these keywords should never be used as a random var name
52+
# source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words
53+
RESERVED_KEYWORDS = %w(
54+
break case catch continue debugger default delete do else finally
55+
for function if in instanceof new return switch this throw try
56+
typeof var void while with class enum export extends import super
57+
implements interface let package private protected public static yield
58+
)
11359

11460
#
11561
# Abstract Syntax Tree generated by RKelly::Parser#parse
@@ -123,8 +69,12 @@ def initialize(code)
12369
@code = code
12470
@funcs = {}
12571
@vars = {}
126-
@scope = Scope.new
12772
@debug = false
73+
@rand_gen = Rex::RandomIdentifierGenerator.new(
74+
:max_length => 15,
75+
:first_char_set => Rex::Text::Alpha+"_$",
76+
:char_set => Rex::Text::AlphaNumeric+"_$"
77+
)
12878
end
12979

13080
#
@@ -172,8 +122,23 @@ def obfuscate
172122
obfuscate_r(@ast)
173123
end
174124

125+
# @return [String] a unique random var name that is not a reserved keyword
126+
def random_var_name
127+
loop do
128+
text = random_string
129+
unless @vars.has_value?(text) or RESERVED_KEYWORDS.include?(text)
130+
return text
131+
end
132+
end
133+
end
134+
175135
protected
176136

137+
# @return [String] a random string
138+
def random_string
139+
@rand_gen.generate
140+
end
141+
177142
#
178143
# Recursive method to obfuscate the given +ast+.
179144
#
@@ -217,12 +182,12 @@ def obfuscate_r(ast)
217182
# Variables
218183
when ::RKelly::Nodes::VarDeclNode
219184
if @vars[node.name].nil?
220-
@vars[node.name] = @scope.random_var_name
185+
@vars[node.name] = random_var_name
221186
end
222187
node.name = @vars[node.name]
223188
when ::RKelly::Nodes::ParameterNode
224189
if @vars[node.value].nil?
225-
@vars[node.value] = @scope.random_var_name
190+
@vars[node.value] = random_var_name
226191
end
227192
node.value = @vars[node.value]
228193
when ::RKelly::Nodes::ResolveNode
@@ -244,7 +209,7 @@ def obfuscate_r(ast)
244209
# Functions can also act as objects, so store them in the vars
245210
# and the functions list so we can replace them in both places
246211
if @funcs[node.value].nil? and not @funcs.values.include?(node.value)
247-
@funcs[node.value] = @scope.random_var_name
212+
@funcs[node.value] = random_var_name
248213
if @vars[node.value].nil?
249214
@vars[node.value] = @funcs[node.value]
250215
end
@@ -346,16 +311,14 @@ def transform_string(str)
346311
str = str[1,str.length - 2]
347312
return quote*2 if str.length == 0
348313

349-
if str.length > MAX_STRING_CHUNK
350-
return safe_split(str, quote).map { |args| transform_string(args[1]) }.join('+')
351-
end
352-
353-
transformed = nil
354314
case rand(2)
355315
when 0
356316
transformed = transform_string_split_concat(str, quote)
357317
when 1
358318
transformed = transform_string_fromCharCode(str)
319+
#when 2
320+
# # Currently no-op
321+
# transformed = transform_string_unescape(str)
359322
end
360323

361324
#$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}"
@@ -390,12 +353,12 @@ def safe_split(str, quote)
390353
break unless str[len]
391354
break if len > max_len
392355
# randomize the length of each part
393-
break if (rand(max_len) == 0)
356+
break if (rand(4) == 0)
394357
end
395358

396359
part = str.slice!(0, len)
397360

398-
var = @scope.random_var_name
361+
var = Rex::Text.rand_text_alpha(4)
399362
parts.push( [ var, "#{quote}#{part}#{quote}" ] )
400363
end
401364

@@ -446,6 +409,12 @@ def transform_string_split_concat(str, quote)
446409
final
447410
end
448411

412+
413+
# TODO
414+
#def transform_string_unescape(str)
415+
# str
416+
#end
417+
449418
#
450419
# Return a call to String.fromCharCode() with each char of the input as arguments
451420
#
@@ -454,17 +423,9 @@ def transform_string_split_concat(str, quote)
454423
# output: String.fromCharCode(0x41, 10)
455424
#
456425
def transform_string_fromCharCode(str)
457-
"String.fromCharCode(#{string_to_bytes(str)})"
458-
end
459-
460-
#
461-
# Return a comma-separated list of byte values with random encodings (decimal/hex/octal)
462-
#
463-
def string_to_bytes(str)
464-
len = 0
426+
buf = "String.fromCharCode("
465427
bytes = str.unpack("C*")
466-
encoded_bytes = []
467-
428+
len = 0
468429
while str.length > 0
469430
if str[0,1] == "\\"
470431
str.slice!(0,1)
@@ -511,12 +472,16 @@ def string_to_bytes(str)
511472
else
512473
char = str.slice!(0,1).unpack("C").first
513474
end
514-
encoded_bytes << rand_base(char)
475+
buf << "#{rand_base(char)},"
515476
end
477+
# Strip off the last comma
478+
buf = buf[0,buf.length-1] + ")"
479+
transformed = buf
516480

517-
encoded_bytes.join(',')
481+
transformed
518482
end
519483

484+
520485
end
521486
end
522487
end

spec/lib/rex/exploitation/jsobfu_scope_spec.rb

Lines changed: 0 additions & 50 deletions
This file was deleted.

spec/lib/rex/exploitation/jsobfu_spec.rb

Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,66 +7,44 @@
77
described_class.new("")
88
end
99

10-
# surround the string in quotes
11-
def quote(str, q='"'); "#{q}#{str}#{q}" end
10+
describe '#random_var_name' do
11+
subject(:random_var_name) { jsobfu.random_var_name }
1212

13-
describe '#transform_string' do
14-
context 'when given a string of length > MAX_STRING_CHUNK' do
15-
let(:js_string) { quote "ABC"*Rex::Exploitation::JSObfu::MAX_STRING_CHUNK }
13+
it { should be_a String }
14+
it { should_not be_empty }
1615

17-
it 'calls itself recursively' do
18-
expect(jsobfu).to receive(:transform_string).at_least(2).times.and_call_original
19-
jsobfu.send(:transform_string, js_string.dup)
20-
end
16+
it 'is composed of _, $, alphanumeric chars' do
17+
20.times { expect(jsobfu.random_var_name).to match(/\A[a-zA-Z0-9$_]+\Z/) }
2118
end
2219

23-
context 'when given a string of length < MAX_STRING_CHUNK' do
24-
let(:js_string) { quote "A"*(Rex::Exploitation::JSObfu::MAX_STRING_CHUNK/2).to_i }
25-
26-
it 'does not call itself recursively' do
27-
expect(jsobfu).to receive(:transform_string).once.and_call_original
28-
jsobfu.send(:transform_string, js_string.dup)
29-
end
20+
it 'does not start with a number' do
21+
20.times { expect(jsobfu.random_var_name).not_to match(/\A[0-9]/) }
3022
end
31-
end
3223

33-
describe '#safe_split' do
34-
let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") }
35-
let(:quote) { '"' }
36-
let(:parts) { 50.times.map { jsobfu.send(:safe_split, js_string.dup, quote).map{ |a| a[1] } } }
24+
context 'when a reserved word is generated' do
25+
let(:reserved) { described_class::RESERVED_KEYWORDS.first }
26+
let(:random) { 'abcdef' }
27+
let(:generated) { [reserved, reserved, reserved, random] }
3728

38-
describe 'quoting' do
39-
context 'when given a double-quote' do
40-
let(:quote) { '"' }
41-
it 'surrounds all the split strings with the same quote' do
42-
expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be_true
43-
end
29+
before do
30+
jsobfu.stub(:random_string) { generated.shift }
4431
end
4532

46-
context 'when given a single-quote' do
47-
let(:quote) { "'" }
48-
it 'surrounds all the split strings with the same quote' do
49-
expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be_true
50-
end
51-
end
33+
it { should be random }
5234
end
5335

54-
describe 'splitting' do
55-
context 'when given a hex-escaped series of bytes' do
56-
let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") }
36+
context 'when a non-unique random var is generated' do
37+
let(:preexisting) { 'preexist' }
38+
let(:random) { 'abcdef' }
39+
let(:vars) { { 'jQuery' => preexisting } }
40+
let(:generated) { [preexisting, preexisting, preexisting, random] }
5741

58-
it 'never splits in the middle of a hex escape' do
59-
expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be_true
60-
end
42+
before do
43+
jsobfu.stub(:random_string) { generated.shift }
44+
jsobfu.instance_variable_set("@vars", vars)
6145
end
6246

63-
context 'when given a unicode-escaped series of bytes' do
64-
let(:js_string) { Rex::Text.to_unescape("ABCDEFG"*100).gsub!('%', '\\') }
65-
66-
it 'never splits in the middle of a unicode escape' do
67-
expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be_true
68-
end
69-
end
47+
it { should be random }
7048
end
7149
end
7250

0 commit comments

Comments
 (0)