pwn 0.5.562 → 0.5.587

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.
@@ -4,6 +4,7 @@ require 'curses'
4
4
  require 'fileutils'
5
5
  require 'meshtastic'
6
6
  require 'pry'
7
+ require 'reline'
7
8
  require 'tty-prompt'
8
9
  require 'unicode/display_width'
9
10
  require 'yaml'
@@ -12,10 +13,57 @@ module PWN
12
13
  module Plugins
13
14
  # This module contains methods related to the pwn REPL Driver.
14
15
  module REPL
15
- # Supported Method Parameters::
16
- # PWN::Plugins::REPL.refresh_ps1_proc(
17
- # mode: 'required - :splat or nil'
18
- # )
16
+ # Custom input handler for pwn-ai to support multi-line submissions:
17
+ # - SHIFT+ENTER inserts a newline (continue editing)
18
+ # - plain ENTER submits the full prompt (possibly multi-line) to the AI
19
+ # - Multi-line pastes are supported (Reline handles \n in buffer; submit with ENTER)
20
+ class PwnAIInput
21
+ attr_reader :line_buffer
22
+
23
+ def initialize
24
+ @line_buffer = ''
25
+ end
26
+
27
+ def readline(prompt)
28
+ # Common escape sequences for SHIFT+ENTER across terminals (xterm, modern, etc.)
29
+ shift_enter_seqs = [
30
+ # Only SHIFT+ENTER (user requirement). Plain ENTER = submit.
31
+ [27, 91, 49, 51, 59, 50, 126], # \e[13;2~
32
+ [27, 91, 49, 59, 50, 126], # \e[1;2~
33
+ [27, 13], # \e\r
34
+ [27, 10], # \e\n
35
+ [27, 91, 49, 51, 59, 50, 117], # \e[13;2u
36
+ [27, 91, 50, 55, 59, 50, 59, 49, 51, 126], # \e[27;2;13~
37
+ [27, 91, 50, 55, 59, 50, 59, 49, 51, 117], # \e[27;2;13u
38
+ [27, 91, 49, 51, 59, 50, 117],
39
+ [27, 91, 49, 59, 50, 126],
40
+ [27, 91, 50, 55, 59, 50, 59, 49, 51, 126]
41
+ ]
42
+ shift_enter_seqs.each do |seq|
43
+ Reline.config.add_oneshot_key_binding(seq.bytes, :key_newline)
44
+ end
45
+
46
+ begin
47
+ # readmultiline with confirm block that *always* returns true:
48
+ # => normal ENTER triggers finish/submit of the (multi-line) buffer
49
+ # SHIFT+ENTER bytes trigger key_newline (insert \n, stay in edit)
50
+ # Reline in multiline mode also handles multi-line pastes by splitting on \n in the buffer.
51
+ @line_buffer = Reline.readmultiline(prompt, true) { |_buffer| true } || ''
52
+ ensure
53
+ Reline.config.reset_oneshot_key_bindings
54
+ end
55
+ @line_buffer
56
+ end
57
+
58
+ # Compatibility with Pry input expectations (used by hooks for line_buffer, and possibly completer/tty checks)
59
+ def tty?
60
+ true
61
+ end
62
+
63
+ def winsize
64
+ [TTY::Screen.rows || 24, TTY::Screen.columns || 80]
65
+ end
66
+ end
19
67
 
20
68
  public_class_method def self.refresh_ps1_proc(opts = {})
21
69
  mode = opts[:mode]
@@ -117,13 +165,147 @@ module PWN
117
165
  end
118
166
 
119
167
  Pry::Commands.create_command 'pwn-ai' do
120
- description 'Initiate pwn.ai chat interface.'
168
+ description 'Initiate pwn.ai autonomous agent TUI (instruct tasks using PWN modules + CLI tools; memory/sessions/agents/cron/skills-aware from PWN::Config/PWN::Memory etc).'
121
169
 
122
170
  def process
123
171
  pi = pry_instance
124
172
  pi.config.pwn_ai = true
173
+ pi.config.pwn_ai_agent = true
125
174
  pi.config.color = false if pi.config.pwn_ai
126
- pi.config.color = true unless pi.config.pwn_ai
175
+
176
+ # Switch to custom multi-line input for pwn-ai (SHIFT+ENTER newline, ENTER submit)
177
+ pi.config.pwn_ai_original_input ||= Pry.config.input
178
+ Pry.config.input = PwnAIInput.new
179
+
180
+ # Load and make aware of skills folder (scaled in PWN::Config per user pwn_env_path parent)
181
+ skills_path = begin
182
+ PWN::Config.pwn_skills_path
183
+ rescue StandardError
184
+ "#{Dir.home}/.pwn/skills"
185
+ end
186
+ PWN::Config.load_skills(pwn_skills_path: skills_path)
187
+ skills_count = (PWN.const_defined?(:Skills) ? PWN::Skills.keys.length : 0)
188
+
189
+ # Hermes-equivalent memory/sessions/cron init for this pwn-ai activation
190
+ PWN::Config.load_memory
191
+ mem_count = (PWN.const_defined?(:Memory) ? PWN::Memory.load.keys.length : 0)
192
+ sess = begin
193
+ PWN::Sessions.create(title: "pwn-ai #{Time.now.strftime('%Y-%m-%d %H:%M')}", source: 'pwn-ai-repl')
194
+ rescue StandardError
195
+ nil
196
+ end
197
+ pi.config.pwn_ai_session_id = sess[:id] if sess
198
+ cron_count = (PWN.const_defined?(:Cron) ? PWN::Cron.list.keys.length : 0)
199
+
200
+ puts "\
201
+ [*] pwn-ai agent TUI activated (PWN REPL driver w/ memory, sessions, delegation, cron)."
202
+ puts "[*] Memory facts: #{mem_count} | Session: #{pi.config.pwn_ai_session_id} | Cron jobs: #{cron_count} | Skills: #{skills_count}"
203
+ puts '[*] Instruct the AI agent to carry out a task, e.g.:'
204
+ puts " 'Use NmapIt to port scan target.com then use TransparentBrowser to spider and SAST::TestCaseEngine to analyze code if cloned. Generate report with PWN::Reports.'"
205
+ puts " 'Execute CLI nmap -sV target.com and summarize findings using PWN modules.'"
206
+ puts "[*] Skills loaded from #{skills_path} (#{skills_count} available) + memory/sessions/cron to expand autonomous capabilities."
207
+ puts "[*] Type 'toggle-pwn-ai' or normal pwn commands to exit agent mode.
208
+ "
209
+ end
210
+ end
211
+
212
+ Pry::Commands.create_command 'pwn-ai-memory' do
213
+ description 'Manage pwn-ai persistent memory (Hermes equiv).'
214
+
215
+ def process
216
+ cmd = args[0]
217
+ case cmd
218
+ when 'list', 'recall', nil
219
+ q = args[1]
220
+ res = PWN::Memory.recall(query: q)
221
+ puts res.inspect
222
+ when 'remember'
223
+ key = args[1]
224
+ val = args[2..-1].join(' ')
225
+ PWN::Memory.remember(key: key, value: val)
226
+ puts "Remembered #{key}"
227
+ when 'forget'
228
+ PWN::Memory.forget(args[1])
229
+ puts "Forgot #{args[1]}"
230
+ when 'clear'
231
+ PWN::Memory.clear
232
+ puts 'Memory cleared'
233
+ else
234
+ puts PWN::Memory.help
235
+ end
236
+ end
237
+ end
238
+
239
+ Pry::Commands.create_command 'pwn-ai-sessions' do
240
+ description 'List/resume/delete pwn-ai sessions (Hermes equiv).'
241
+
242
+ def process
243
+ cmd = args[0]
244
+ case cmd
245
+ when 'list', nil
246
+ puts PWN::Sessions.list.inspect
247
+ when 'resume'
248
+ sid = args[1]
249
+ hist = PWN::Sessions.to_response_history(session_id: sid)
250
+ puts "Loaded session #{sid} with #{hist[:choices].size} entries (set manually into response_history if needed)"
251
+ when 'delete'
252
+ PWN::Sessions.delete(session_id: args[1])
253
+ puts "Deleted #{args[1]}"
254
+ when 'stats'
255
+ puts PWN::Sessions.stats
256
+ else
257
+ puts PWN::Sessions.help
258
+ end
259
+ end
260
+ end
261
+
262
+ Pry::Commands.create_command 'pwn-ai-cron' do
263
+ description 'Manage scheduled pwn-ai / cron jobs (Hermes equiv).'
264
+
265
+ def process
266
+ cmd = args[0]
267
+ case cmd
268
+ when 'list', nil
269
+ puts PWN::Cron.list.inspect
270
+ when 'create'
271
+ # simplistic: pwn-ai-cron create '0 * * * *' 'prompt here'
272
+ sched = args[1]
273
+ pr = args[2..-1].join(' ')
274
+ job = PWN::Cron.create(schedule: sched, prompt: pr)
275
+ puts "Created #{job}"
276
+ when 'run'
277
+ res = PWN::Cron.run(id: args[1])
278
+ puts res
279
+ when 'remove'
280
+ PWN::Cron.remove(id: args[1])
281
+ puts 'Removed'
282
+ else
283
+ puts PWN::Cron.help
284
+ end
285
+ end
286
+ end
287
+
288
+ Pry::Commands.create_command 'pwn-ai-delegate' do
289
+ description 'Delegate sub-task to a PWN::AI::Agent or simple sub-chat (Hermes delegation equiv).'
290
+
291
+ def process
292
+ goal = args.join(' ')
293
+ puts "[*] Delegating: #{goal}"
294
+ # Simple delegation: use a specialized agent if matches, else another chat turn
295
+ if goal =~ /sast|code|scan/i
296
+ res = PWN::AI::Agent::SAST.analyze(request: goal)
297
+ elsif goal =~ /vuln|report/i
298
+ res = PWN::AI::Agent::VulnGen.analyze(request: goal)
299
+ else
300
+ # fallback sub call to active engine (no full loop here)
301
+ engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
302
+ case engine
303
+ when :anthropic then res = PWN::AI::Anthropic.chat(request: goal)
304
+ when :grok then res = PWN::AI::Grok.chat(request: goal)
305
+ else res = PWN::AI::Ollama.chat(request: goal)
306
+ end
307
+ end
308
+ puts res
127
309
  end
128
310
  end
129
311
 
@@ -339,6 +521,12 @@ module PWN
339
521
  response_history: response_history,
340
522
  spinner: false
341
523
  )
524
+ when :anthropic
525
+ response = PWN::AI::Anthropic.chat(
526
+ request: request,
527
+ response_history: response_history,
528
+ spinner: false
529
+ )
342
530
  end
343
531
 
344
532
  response_history = {
@@ -742,9 +930,14 @@ module PWN
742
930
  pi.config.color = true
743
931
  pi.config.pwn_asm = false if pi.config.pwn_asm
744
932
  pi.config.pwn_ai = false if pi.config.pwn_ai
933
+ pi.config.pwn_ai_agent = false if pi.config.pwn_ai_agent
745
934
  pi.config.pwn_ai_debug = false if pi.config.pwn_ai_debug
746
935
  pi.config.pwn_ai_speak = false if pi.config.pwn_ai_speak
747
936
  pi.config.completer = Pry::InputCompleter
937
+ if pi.config.pwn_ai_original_input
938
+ Pry.config.input = pi.config.pwn_ai_original_input
939
+ pi.config.pwn_ai_original_input = nil
940
+ end
748
941
  return unless pi.config.pwn_mesh
749
942
 
750
943
  pi.config.pwn_mesh = false
@@ -834,65 +1027,215 @@ module PWN
834
1027
 
835
1028
  Pry.config.hooks.add_hook(:after_read, :pwn_ai_hook) do |request, pi|
836
1029
  if pi.config.pwn_ai && !request.chomp.empty?
837
- request = pi.input.line_buffer.to_s
1030
+ orig_request = pi.input.line_buffer.to_s
1031
+ request = orig_request
838
1032
  debug = pi.config.pwn_ai_debug
839
1033
  engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
840
1034
  response_history = PWN::Env[:ai][engine][:response_history]
841
1035
  speak_answer = pi.config.pwn_ai_speak
1036
+ is_agent = (pi.config.pwn_ai_agent == true)
842
1037
 
843
- case engine
844
- when :grok
845
- response = PWN::AI::Grok.chat(
846
- request: request.chomp,
847
- response_history: response_history,
848
- speak_answer: speak_answer,
849
- spinner: true
850
- )
851
- when :ollama
852
- response = PWN::AI::Ollama.chat(
853
- request: request.chomp,
854
- response_history: response_history,
855
- speak_answer: speak_answer,
856
- spinner: true
857
- )
858
- when :openai
859
- response = PWN::AI::OpenAI.chat(
860
- request: request.chomp,
1038
+ # pwn-ai agent mode (Hermes TUI equiv): load skills context for autonomous task carrying
1039
+ skills_context = ''
1040
+ PWN::Skills.each { |n, m| skills_context += "\n--- SKILL #{n} ---\n#{m[:content].to_s[0, 1200]}\n" } if is_agent && PWN.const_defined?(:Skills) && PWN::Skills.is_a?(Hash)
1041
+
1042
+ memory_context = ''
1043
+ memory_context = PWN::Memory.to_context(limit: 25) if is_agent && PWN.const_defined?(:Memory)
1044
+
1045
+ sess_id = begin
1046
+ pi.config.pwn_ai_session_id
1047
+ rescue StandardError
1048
+ nil
1049
+ end
1050
+
1051
+ # Pre-process for clear CLI execution intent (e.g. "what does `id` return?")
1052
+ # This makes the agent actually *run* commands instead of just explaining them.
1053
+ curr_req = request.chomp
1054
+ if is_agent && sess_id && PWN.const_defined?(:Sessions)
1055
+ begin
1056
+ PWN::Sessions.append(session_id: sess_id, role: 'user', content: orig_request)
1057
+ rescue StandardError
1058
+ nil
1059
+ end
1060
+ end
1061
+ if is_agent && request =~ /`([^`]+)`/
1062
+ potential = ::Regexp.last_match(1).strip
1063
+ # Looks like a shell command (not PWN ruby)
1064
+ unless potential =~ /^(PWN::|def |class |require |puts |pp )/
1065
+ curr_req = "The user wants the *actual raw output* of this command (do not just describe it): `#{potential}`. " \
1066
+ 'To fulfill the request accurately, you MUST immediately output ONLY a bash code block with the exact command. ' \
1067
+ "Example format: ```bash\n#{potential}\n``` . After the host executes it, you will receive the OBSERVATION with the real output."
1068
+ end
1069
+ end
1070
+
1071
+ # Strict system prompt for agent mode (forces tool use over explanation)
1072
+ system_role = nil
1073
+ if is_agent
1074
+ base = PWN::Env[:ai][engine][:system_role_content] || 'You are an ethical hacker.'
1075
+ system_role = base + <<~PROMPT
1076
+
1077
+ You are operating as a Hermes-style autonomous agent inside the PWN REPL driver.
1078
+
1079
+ PRIMARY RULE FOR CLI AND TOOLS: When the user asks for the output of a command, "what does X return?", "run X", or anything that requires real execution, you MUST use a tool call.#{' '}
1080
+ NEVER just explain what a command does or what its output "would be".#{' '}
1081
+ To execute anything:
1082
+ - Output *exactly and only* a fenced code block.
1083
+ - For shell/CLI: ```bash
1084
+ <exact command here>
1085
+ ```
1086
+ - For PWN Ruby modules: ```ruby
1087
+ PWN::Plugins::NmapIt.port_scan(...)
1088
+ ```
1089
+ The host will execute it (Ruby in full PWN context, bash via shell) and reply with an OBSERVATION containing the real result.#{' '}
1090
+ Then continue or give the final answer.
1091
+
1092
+ Available tools include all PWN::Plugins (NmapIt, TransparentBrowser, etc.), SAST, Reports, and any CLI via bash blocks.
1093
+ Skills available this session:#{skills_context}
1094
+ #{memory_context}
1095
+
1096
+ PERSISTENT CAPABILITIES (use via ruby code blocks or direct calls):
1097
+ - Memory (cross-session): PWN::Memory.remember(key: :key, value: val, category: :fact|:preference|:lesson)
1098
+ PWN::Memory.recall(query: 'foo'), PWN::Memory.forget(key)
1099
+ - Sessions: current session id = #{sess_id}; PWN::Sessions.append(session_id: '#{sess_id}', role: 'observation', content: obs)
1100
+ - Cron: PWN::Cron.create(schedule: '0 * * * *', prompt: 'task here', name: 'foo')
1101
+ PWN::Cron.run(id: 'id'); list with PWN::Cron.list
1102
+ - Agents/Delegation: PWN::AI::Agent::SAST.analyze(request: ...); PWN::AI::Agent::VulnGen etc.
1103
+ For sub-agents use threads or separate eval calls and feed results back as OBS.
1104
+
1105
+ After receiving an observation, decide the next step or conclude.
1106
+ If you output text without a code block, it will be treated as your final answer to the user.
1107
+ PROMPT
1108
+ end
1109
+
1110
+ max_turns = is_agent ? 7 : 1
1111
+ turn = 0
1112
+ last_response = ''
1113
+ tool_was_executed_this_turn = false
1114
+
1115
+ while turn < max_turns
1116
+ chat_opts = {
1117
+ request: curr_req,
861
1118
  response_history: response_history,
862
1119
  speak_answer: speak_answer,
863
1120
  spinner: true
864
- )
865
- else
866
- raise "ERROR: Unsupported AI Engine: #{engine}"
867
- end
868
- # puts response.inspect
1121
+ }
1122
+ chat_opts[:system_role_content] = system_role if system_role
1123
+
1124
+ case engine
1125
+ when :grok
1126
+ response = PWN::AI::Grok.chat(**chat_opts)
1127
+ when :ollama
1128
+ response = PWN::AI::Ollama.chat(**chat_opts)
1129
+ when :openai
1130
+ response = PWN::AI::OpenAI.chat(**chat_opts)
1131
+ when :anthropic
1132
+ response = PWN::AI::Anthropic.chat(**chat_opts)
1133
+ else
1134
+ raise "ERROR: Unsupported AI Engine: #{engine}"
1135
+ end
869
1136
 
870
- last_response = ''
871
- if response.nil?
872
- last_response = "Model: #{model} not currently supported with API key."
873
- else
874
- if response[:choices].last.keys.include?(:text)
875
- last_response = response[:choices].last[:text]
1137
+ if response.nil?
1138
+ last_response = 'Model not currently supported with API key.'
876
1139
  else
877
- last_response = response[:choices].last[:content]
1140
+ if response[:choices].last.keys.include?(:text)
1141
+ last_response = response[:choices].last[:text].to_s
1142
+ else
1143
+ last_response = response[:choices].last[:content].to_s
1144
+ end
1145
+ response_history = {
1146
+ id: response[:id],
1147
+ object: response[:object],
1148
+ model: response[:model],
1149
+ usage: response[:usage]
1150
+ }
1151
+ response_history[:choices] ||= response[:choices]
878
1152
  end
879
1153
 
880
- response_history = {
881
- id: response[:id],
882
- object: response[:object],
883
- model: response[:model],
884
- usage: response[:usage]
885
- }
886
- response_history[:choices] ||= response[:choices]
1154
+ puts "\n\001\e[32m\002#{last_response}\001\e[0m\002\n\n"
1155
+ if is_agent && sess_id && PWN.const_defined?(:Sessions)
1156
+ begin
1157
+ PWN::Sessions.append(session_id: sess_id, role: 'assistant', content: last_response)
1158
+ rescue StandardError
1159
+ nil
1160
+ end
1161
+ end
1162
+
1163
+ if debug
1164
+ puts 'DEBUG: response_history => '
1165
+ pp response_history
1166
+ end
1167
+ PWN::Env[:ai][engine][:response_history] = response_history
1168
+
1169
+ # === Agent tool execution: parse code blocks from *this* response and actually run them ===
1170
+ tool_was_executed_this_turn = false
1171
+ if is_agent
1172
+ # Robust regex: tolerate language specifier, extra whitespace, and text around the block
1173
+ last_response.scan(/```(?:\s*(ruby|bash|sh|shell|zsh))?\s*\n?(.*?)\n?```/m).each do |lang, code|
1174
+ code = code.strip
1175
+ next if code.empty? || tool_was_executed_this_turn
1176
+
1177
+ lang = (lang || 'bash').downcase
1178
+ puts "\001\e[33m\002[ pwn-ai AGENT EXEC #{lang} ]\e[0m\002 #{code[0..90]}..."
1179
+
1180
+ obs = ''
1181
+ begin
1182
+ if lang == 'ruby'
1183
+ require 'stringio'
1184
+ old_stdout = $stdout
1185
+ $stdout = StringIO.new
1186
+ res = eval(code, TOPLEVEL_BINDING) # rubocop:disable Security/Eval -- intentional for pwn-ai agent to run PWN Ruby modules/tools in REPL context
1187
+ captured = $stdout.string
1188
+ $stdout = old_stdout
1189
+ obs = (captured + "\n=> #{res.inspect}").strip
1190
+ else
1191
+ # CLI execution - use Open3 for cleaner capture (no extra shell if possible, but backticks are simple and work)
1192
+ require 'open3'
1193
+ stdout, stderr, status = Open3.capture3(code)
1194
+ obs = stdout
1195
+ obs += "\n[stderr]\n#{stderr}" unless stderr.to_s.strip.empty?
1196
+ obs += "\n[exit: #{status.exitstatus}]" unless status.success?
1197
+ obs = obs.strip
1198
+ end
1199
+ rescue StandardError => e
1200
+ obs = "ERROR executing #{lang} block: #{e.class} - #{e.message}"
1201
+ end
1202
+
1203
+ puts "\001\e[36m\002[OBSERVATION from #{lang}]\001\e[0m\002\n#{obs[0..700]}\n"
1204
+ if is_agent && sess_id && PWN.const_defined?(:Sessions)
1205
+ begin
1206
+ PWN::Sessions.append(session_id: sess_id, role: 'observation', content: obs)
1207
+ rescue StandardError
1208
+ nil
1209
+ end
1210
+ end
1211
+
1212
+ # Feed real result back to the model as the next "user" message in the loop
1213
+ curr_req = "OBSERVATION (#{lang} execution result for previous block):\n#{obs}\n\n" \
1214
+ "Now continue fulfilling the original user request: #{orig_request}. " \
1215
+ 'If the task is complete, give the final answer (no more code blocks). Otherwise output the next needed tool block.'
1216
+
1217
+ tool_was_executed_this_turn = true
1218
+ turn += 1
1219
+ break # one execution per model turn for controlled pacing
1220
+ end
1221
+ end
1222
+
1223
+ # If we executed something, loop to let the model react to the OBS
1224
+ next if tool_was_executed_this_turn
1225
+
1226
+ # No tool executed this turn -> this last_response is the final answer
1227
+ break
887
1228
  end
888
- puts "\n\001\e[32m\002#{last_response}\001\e[0m\002\n\n"
889
1229
 
890
- if debug
891
- puts 'DEBUG: response_history => '
892
- pp response_history
893
- puts "\nresponse_history[:choices] Length: #{response_history[:choices].length}\n" unless response_history.nil?
1230
+ # If in agent mode and the model never produced an executable block but the query clearly wanted execution,
1231
+ # give one last chance with a strong reminder (helps weaker models like some Ollama ones)
1232
+ if is_agent && !tool_was_executed_this_turn && orig_request =~ /`[^`]+`/ && turn < max_turns
1233
+ reminder = 'The user explicitly asked about the output of a command in backticks. ' \
1234
+ 'Do not describe the command. Output *only* the corresponding ```bash block now so the host can run it and give you the real result.'
1235
+ curr_req = "#{reminder}\nOriginal: #{orig_request}"
1236
+ # One final direct call (no full re-loop to avoid complexity)
1237
+ # (The main loop already handled most cases; this is a safety net)
894
1238
  end
895
- PWN::Env[:ai][engine][:response_history] = response_history
896
1239
  end
897
1240
  end
898
1241
 
@@ -144,6 +144,25 @@ module PWN
144
144
  )
145
145
  end
146
146
 
147
+ # Supported Method Parameters::
148
+ # PWN::Plugins::XXD.reverse_dump(
149
+ # string: 'required - continuous hex string to reverse (i.e. "68656c6c6f")',
150
+ # file: 'required - path to binary file to dump'
151
+ # )
152
+
153
+ def self.reverse_hex_string(opts = {})
154
+ string = opts[:string]
155
+ file = opts[:file]
156
+
157
+ raise ArgumentError, 'string is required' if string.nil?
158
+
159
+ raise ArgumentError, 'output file is required' if file.nil?
160
+
161
+ File.binwrite(file, [string].pack('H*'))
162
+ rescue StandardError => e
163
+ raise e
164
+ end
165
+
147
166
  # Supported Method Parameters::
148
167
  # PWN::Plugins::XXD.reverse_dump(
149
168
  # hexdump: 'required - hexdump returned from #dump method',
@@ -284,6 +303,11 @@ module PWN
284
303
  # <step through via F7, F8, F9, etc. to get to desired instruction>
285
304
  # ```
286
305
 
306
+ #{self}.reverse_hex_string(
307
+ string: 'required - continuous hex string to reverse (i.e. \"68656c6f\")',
308
+ file: 'required - path to binary file to dump'
309
+ )
310
+
287
311
  #{self}.reverse_dump(
288
312
  hexdump: 'required - hexdump returned from #dump method',
289
313
  file: 'required - path to binary file to dump',