Skip to content

Commit c01a52e

Browse files
JonathanBeverleydavid942j
authored andcommitted
Feature/serialtube (#90)
* Add Tubes::Serialtube
1 parent 1dd331a commit c01a52e

File tree

7 files changed

+288
-0
lines changed

7 files changed

+288
-0
lines changed

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ PATH
99
method_source (~> 0.9)
1010
rainbow (>= 2.2, < 4.0)
1111
ruby2ruby (~> 2.4)
12+
rubyserial (~> 0.5)
1213

1314
GEM
1415
remote: https://www.rubygems.org/
@@ -50,6 +51,8 @@ GEM
5051
sexp_processor (~> 4.6)
5152
ruby_parser (3.11.0)
5253
sexp_processor (~> 4.9)
54+
rubyserial (0.5.0)
55+
ffi (~> 1.9, >= 1.9.3)
5356
sexp_processor (4.10.1)
5457
simplecov (0.16.1)
5558
docile (~> 1.1)

lib/pwnlib/pwn.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require 'pwnlib/reg_sort'
1414
require 'pwnlib/shellcraft/shellcraft'
1515
require 'pwnlib/tubes/process'
16+
require 'pwnlib/tubes/serialtube'
1617
require 'pwnlib/tubes/sock'
1718

1819
require 'pwnlib/util/cyclic'

lib/pwnlib/tubes/serialtube.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# encoding: ASCII-8BIT
2+
3+
require 'rubyserial'
4+
5+
require 'pwnlib/tubes/tube'
6+
7+
module Pwnlib
8+
module Tubes
9+
# @!macro [new] raise_eof
10+
# @raise [Pwnlib::Errors::EndOfTubeError]
11+
# If the request is not satisfied when all data is received.
12+
13+
# Serial Connections
14+
class SerialTube < Tube
15+
# Instantiate a {Pwnlib::Tubes::SerialTube} object.
16+
#
17+
# @param [String] port
18+
# A device name for rubyserial to open, e.g. /dev/ttypUSB0
19+
# @param [Integer] baudrate
20+
# Baud rate.
21+
# @param [Boolean] convert_newlines
22+
# If +true+, convert any +context.newline+s to +"\\r\\n"+ before
23+
# sending to remote. Has no effect on bytes received.
24+
# @param [Integer] bytesize
25+
# Serial character byte size. The '8' in '8N1'.
26+
# @param [Symbol] parity
27+
# Serial character parity. The 'N' in '8N1'.
28+
def initialize(port = nil, baudrate: 115_200,
29+
convert_newlines: true,
30+
bytesize: 8, parity: :none)
31+
super()
32+
33+
# go hunting for a port
34+
port ||= Dir.glob('/dev/tty.usbserial*').first
35+
port ||= '/dev/ttyUSB0'
36+
37+
@convert_newlines = convert_newlines
38+
@conn = Serial.new(port, baudrate, bytesize, parity)
39+
@serial_timer = Timer.new
40+
end
41+
42+
# Closes the active connection
43+
def close
44+
@conn.close if @conn && !@conn.closed?
45+
@conn = nil
46+
end
47+
48+
# Implementation of the methods required for tube
49+
private
50+
51+
# Gets bytes over the serial connection until some bytes are received, or
52+
# +@timeout+ has passed. It is an error for it to return no data in less
53+
# than +@timeout+ seconds. It is ok for it to return some data in less
54+
# time.
55+
#
56+
# @param [Integer] numbytes
57+
# An upper limit on the number of bytes to get.
58+
#
59+
# @return [String]
60+
# A string containing read bytes.
61+
#
62+
# @!macro raise_eof
63+
def recv_raw(numbytes)
64+
raise ::Pwnlib::Errors::EndOfTubeError if @conn.nil?
65+
66+
@serial_timer.countdown do
67+
data = ''
68+
begin
69+
while @serial_timer.active?
70+
data += @conn.read(numbytes - data.length)
71+
break unless data.empty?
72+
sleep 0.1
73+
end
74+
# XXX(JonathanBeverley): should we reverse @convert_newlines here?
75+
return data
76+
rescue RubySerial::Error
77+
close
78+
raise ::Pwnlib::Errors::EndOfTubeError
79+
end
80+
end
81+
end
82+
83+
# Sends bytes over the serial connection. This call will block until all the bytes are sent or an error occurs.
84+
#
85+
# @param [String] data
86+
# A string of the bytes to send.
87+
#
88+
# @return [Integer]
89+
# The number of bytes successfully written.
90+
#
91+
# @!macro raise_eof
92+
def send_raw(data)
93+
raise ::Pwnlib::Errors::EndOfTubeError if @conn.nil?
94+
95+
data.gsub!(context.newline, "\r\n") if @convert_newlines
96+
begin
97+
return @conn.write(data)
98+
rescue RubySerial::Error
99+
close
100+
raise ::Pwnlib::Errors::EndOfTubeError
101+
end
102+
end
103+
104+
# Sets the +timeout+ to use for subsequent +recv_raw+ calls.
105+
#
106+
# @param [Float] timeout
107+
def timeout_raw=(timeout)
108+
@serial_timer.timeout = timeout
109+
end
110+
end
111+
end
112+
end

lib/pwnlib/tubes/tube.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def initialize(timeout: nil)
5454
# @return [String]
5555
# A string contains bytes received from the tube, or +''+ if a timeout occurred while
5656
# waiting.
57+
#
58+
# @!macro raise_eof
59+
# @!macro raise_timeout
5760
def recv(num_bytes = nil, timeout: nil)
5861
return '' if @buffer.empty? && !fillbuffer(timeout: timeout)
5962
@buffer.get(num_bytes)

pwntools.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
3333
s.add_runtime_dependency 'method_source', '~> 0.9'
3434
s.add_runtime_dependency 'rainbow', '>= 2.2', '< 4.0'
3535
s.add_runtime_dependency 'ruby2ruby', '~> 2.4'
36+
s.add_runtime_dependency 'rubyserial', '~> 0.5'
3637

3738
# TODO(david942j): check why ruby crash during testing if upgrade minitest to 5.10.2/3
3839
s.add_development_dependency 'minitest', '= 5.10.1'

test/tubes/serialtube_test.rb

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# encoding: ASCII-8BIT
2+
3+
require 'open3'
4+
5+
require 'test_helper'
6+
7+
require 'pwnlib/tubes/serialtube'
8+
9+
module Pwnlib
10+
module Tubes
11+
class SerialTube
12+
def break_encapsulation
13+
@conn.close
14+
end
15+
end
16+
end
17+
end
18+
19+
class SerialTest < MiniTest::Test
20+
include ::Pwnlib::Tubes
21+
22+
def skip_windows
23+
skip 'Not test tube/serialtube on Windows' if TTY::Platform.new.windows?
24+
end
25+
26+
def open_pair
27+
Open3.popen3('socat -d -d pty,raw,echo=0 pty,raw,echo=0') do |_i, _o, stderr, thread|
28+
devs = []
29+
2.times do
30+
devs << stderr.readline.chomp.split.last
31+
# First pattern matches Linux, second is macOS
32+
raise IOError, 'Could not create serial crosslink' if devs.last !~ %r{^(/dev/pts/[0-9]+|/dev/ttys[0-9]+)$}
33+
end
34+
# To ensure socat have finished setup
35+
stderr.gets('starting data transfer loop')
36+
37+
serial = SerialTube.new devs[1], convert_newlines: false
38+
39+
begin
40+
File.open devs[0], 'r+' do |file|
41+
file.set_encoding 'default'.encoding
42+
yield file, serial, thread
43+
end
44+
ensure
45+
::Process.kill('SIGTERM', thread.pid) if thread.alive?
46+
end
47+
end
48+
end
49+
50+
def random_string(length)
51+
Random.rand(36**length).to_s(36).rjust(length, '0')
52+
end
53+
54+
def test_raise
55+
skip_windows
56+
open_pair do |_file, serial, thread|
57+
::Process.kill('SIGTERM', thread.pid)
58+
# ensure the process has been killed
59+
thread.value
60+
assert_raises(Pwnlib::Errors::EndOfTubeError) { serial.puts('a') }
61+
end
62+
open_pair do |_file, serial|
63+
serial.break_encapsulation
64+
assert_raises(Pwnlib::Errors::EndOfTubeError) { serial.recv(1, timeout: 2) }
65+
end
66+
end
67+
68+
def test_recv
69+
skip_windows
70+
open_pair do |file, serial|
71+
# recv, recvline
72+
rs = random_string 24
73+
file.puts rs
74+
result = serial.recv 8, timeout: 1
75+
76+
assert_equal(rs[0...8], result)
77+
result = serial.recv 8
78+
assert_equal(rs[8...16], result)
79+
result = serial.recvline.chomp
80+
assert_equal(rs[16..-1], result)
81+
82+
assert_raises(Pwnlib::Errors::TimeoutError) { serial.recv(1, timeout: 0.2) }
83+
84+
# recvpred
85+
rs = random_string 12
86+
file.print rs
87+
result = serial.recvpred do |data|
88+
data[-6..-1] == rs[-6..-1]
89+
end
90+
assert_equal rs, result
91+
92+
assert_raises(Pwnlib::Errors::TimeoutError) { serial.recv(1, timeout: 0.2) }
93+
94+
# recvn
95+
rs = random_string 6
96+
file.print rs
97+
result = ''
98+
assert_raises(Pwnlib::Errors::TimeoutError) do
99+
result = serial.recvn 120, timeout: 1
100+
end
101+
assert_empty result
102+
file.print rs
103+
result = serial.recvn 12
104+
assert_equal rs * 2, result
105+
106+
assert_raises(Pwnlib::Errors::TimeoutError) { serial.recv(1, timeout: 0.2) }
107+
108+
# recvuntil
109+
rs = random_string 12
110+
file.print rs + '|'
111+
result = serial.recvuntil('|').chomp('|')
112+
assert_equal rs, result
113+
114+
assert_raises(Pwnlib::Errors::TimeoutError) { serial.recv(1, timeout: 0.2) }
115+
116+
# gets
117+
rs = random_string 24
118+
file.puts rs
119+
result = serial.gets 12
120+
assert_equal rs[0...12], result
121+
result = serial.gets.chomp
122+
assert_equal rs[12..-1], result
123+
124+
assert_raises(Pwnlib::Errors::TimeoutError) { serial.recv(1, timeout: 0.2) }
125+
end
126+
end
127+
128+
def test_send
129+
skip_windows
130+
open_pair do |file, serial|
131+
# send, sendline
132+
rs = random_string 24
133+
# rubocop:disable Style/Send
134+
# Justification: This isn't Object#send, false positive.
135+
serial.send rs[0...12]
136+
# rubocop:enable Style/Send
137+
serial.sendline rs[12...24]
138+
result = file.readline.chomp
139+
assert_equal rs, result
140+
141+
# puts
142+
r1 = random_string 4
143+
r2 = random_string 4
144+
r3 = random_string 4
145+
serial.puts r1, r2, r3
146+
result = ''
147+
3.times do
148+
result += file.readline.chomp
149+
end
150+
assert_equal r1 + r2 + r3, result
151+
end
152+
end
153+
154+
def test_close
155+
skip_windows
156+
open_pair do |_file, serial|
157+
serial.close
158+
assert_raises(::Pwnlib::Errors::EndOfTubeError) { serial.puts(514) }
159+
assert_raises(::Pwnlib::Errors::EndOfTubeError) { serial.puts(514) }
160+
assert_raises(::Pwnlib::Errors::EndOfTubeError) { serial.recv }
161+
assert_raises(::Pwnlib::Errors::EndOfTubeError) { serial.recv }
162+
assert_raises(ArgumentError) { serial.close(:hh) }
163+
end
164+
end
165+
end

travis/install.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ setup_osx()
5050
install_keystone_from_source
5151
ln -s keystone/build/llvm/lib/libkeystone.dylib libkeystone.dylib # hack, don't know why next line has no effect
5252
# export DYLD_LIBRARY_PATH=$TRAVIS_BUILD_DIR/keystone/build/llvm/lib:$DYLD_LIBRARY_PATH
53+
54+
# install socat
55+
brew install socat
5356
}
5457

5558
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then

0 commit comments

Comments
 (0)