Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let asm/disasm support more architectures #150

Merged
merged 10 commits into from
Feb 22, 2019
121 changes: 80 additions & 41 deletions lib/pwnlib/asm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,52 +37,62 @@ module Asm
#
# @raise [Pwnlib::Errors::DependencyError]
# If libcapstone is not installed.
# @raise [Pwnlib::Errors::UnsupportedArchError]
# If disassembling of +context.arch+ is not supported.
#
# @example
# context.arch = 'i386'
# print disasm("\xb8\x5d\x00\x00\x00")
# # 0: b8 5d 00 00 00 mov eax, 0x5d
# # 0: b8 5d 00 00 00 mov eax, 0x5d
#
# context.arch = 'amd64'
# print disasm("\xb8\x17\x00\x00\x00")
# # 0: b8 17 00 00 00 mov eax, 0x17
# print disasm("jhH\xb8/bin///sPH\x89\xe71\xd21\xf6j;X\x0f\x05", vma: 0x1000)
# # 1000: 6a 68 push 0x68
# # 1002: 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax, 0x732f2f2f6e69622f
# # 100c: 50 push rax
# # 100d: 48 89 e7 mov rdi, rsp
# # 1010: 31 d2 xor edx, edx
# # 1012: 31 f6 xor esi, esi
# # 1014: 6a 3b push 0x3b
# # 1016: 58 pop rax
# # 1017: 0f 05 syscall
# # 1000: 6a 68 push 0x68
# # 1002: 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax, 0x732f2f2f6e69622f
# # 100c: 50 push rax
# # 100d: 48 89 e7 mov rdi, rsp
# # 1010: 31 d2 xor edx, edx
# # 1012: 31 f6 xor esi, esi
# # 1014: 6a 3b push 0x3b
# # 1016: 58 pop rax
# # 1017: 0f 05 syscall
def disasm(data, vma: 0)
require_message('crabstone', install_crabstone_guide) # will raise error if require fail.
cs = Crabstone::Disassembler.new(cap_arch, cap_mode)
cs = Crabstone::Disassembler.new(cs_arch, cs_mode)
insts = cs.disasm(data, vma).map do |ins|
[ins.address, ins.bytes.pack('C*'), ins.mnemonic, ins.op_str.to_s]
[ins.address, ins.bytes, ins.mnemonic.to_s, ins.op_str.to_s]
end
max_dlen = format('%x', insts.last.first).size + 2
max_hlen = insts.map { |ins| ins[1].size }.max * 3
max_ilen = insts.map { |ins| ins[2].size }.max
insts.reduce('') do |s, ins|
hex_code = ins[1].bytes.map { |c| format('%02x', c) }.join(' ')
hex_code = ins[1].map { |c| format('%02x', c) }.join(' ')
inst = if ins[3].empty?
ins[2]
else
format('%-7s %s', ins[2], ins[3])
format("%-#{max_ilen}s %s", ins[2], ins[3])
end
s + format("%#{max_dlen}x: %-#{max_hlen}s%s\n", ins[0], hex_code, inst)
s + format("%#{max_dlen}x: %-#{max_hlen}s %s\n", ins[0], hex_code, inst)
end
end

# Convert assembly code to machine code.
#
# @param [String] code
# The assembly code to be converted.
# @param [Integer] vma
# Virtual memory address.
#
# @return [String]
# The result.
#
# @raise [Pwnlib::Errors::DependencyError]
# If libkeystone is not installed.
# @raise [Pwnlib::Errors::UnsupportedArchError]
# If assembling of +context.arch+ is not supported.
#
# @example
# assembly = shellcraft.amd64.linux.sh
# context.local(arch: 'amd64') { asm(assembly) }
Expand All @@ -93,9 +103,9 @@ def disasm(data, vma: 0)
#
# @diff
# Not support +asm('mov eax, SYS_execve')+.
def asm(code)
def asm(code, vma: 0)
require_message('keystone_engine', install_keystone_guide)
KeystoneEngine::Ks.new(ks_arch, ks_mode).asm(code)[0]
KeystoneEngine::Ks.new(ks_arch, ks_mode).asm(code, vma)[0]
end

# Builds an ELF file from executable code.
Expand Down Expand Up @@ -174,32 +184,59 @@ def make_elf(data, vma: nil, to_file: false)
end

::Pwnlib::Util::Ruby.private_class_method_block do
def cap_arch
{
'i386' => Crabstone::ARCH_X86,
'amd64' => Crabstone::ARCH_X86
}[context.arch]
def cs_arch
case context.arch
when 'aarch64' then Crabstone::ARCH_ARM64
when 'amd64', 'i386' then Crabstone::ARCH_X86
when 'arm', 'thumb' then Crabstone::ARCH_ARM
when 'mips', 'mips64' then Crabstone::ARCH_MIPS
when 'powerpc64' then Crabstone::ARCH_PPC
when 'sparc', 'sparc64' then Crabstone::ARCH_SPARC
else unsupported!("Disasm on architecture #{context.arch.inspect} is not supported yet.")
end
end

def cap_mode
{
32 => Crabstone::MODE_32,
64 => Crabstone::MODE_64
}[context.bits]
def cs_mode
case context.arch
when 'aarch64' then Crabstone::MODE_ARM
when 'amd64' then Crabstone::MODE_64
when 'arm' then Crabstone::MODE_ARM
when 'i386' then Crabstone::MODE_32
when 'mips' then Crabstone::MODE_MIPS32
when 'mips64' then Crabstone::MODE_MIPS64
when 'powerpc64' then Crabstone::MODE_64
when 'sparc' then 0 # default mode
when 'sparc64' then Crabstone::MODE_V9
when 'thumb' then Crabstone::MODE_THUMB
end | (context.endian == 'big' ? Crabstone::MODE_BIG_ENDIAN : Crabstone::MODE_LITTLE_ENDIAN)
end

def ks_arch
{
'i386' => KeystoneEngine::KS_ARCH_X86,
'amd64' => KeystoneEngine::KS_ARCH_X86
}[context.arch]
case context.arch
when 'aarch64' then KeystoneEngine::KS_ARCH_ARM64
when 'amd64', 'i386' then KeystoneEngine::KS_ARCH_X86
when 'arm', 'thumb' then KeystoneEngine::KS_ARCH_ARM
when 'mips', 'mips64' then KeystoneEngine::KS_ARCH_MIPS
when 'powerpc', 'powerpc64' then KeystoneEngine::KS_ARCH_PPC
when 'sparc', 'sparc64' then KeystoneEngine::KS_ARCH_SPARC
else unsupported!("Asm on architecture #{context.arch.inspect} is not supported yet.")
end
end

def ks_mode
{
32 => KeystoneEngine::KS_MODE_32,
64 => KeystoneEngine::KS_MODE_64
}[context.bits]
case context.arch
when 'aarch64' then 0 # default mode
when 'amd64' then KeystoneEngine::KS_MODE_64
when 'arm' then KeystoneEngine::KS_MODE_ARM
when 'i386' then KeystoneEngine::KS_MODE_32
when 'mips' then KeystoneEngine::KS_MODE_MIPS32
when 'mips64' then KeystoneEngine::KS_MODE_MIPS64
when 'powerpc' then KeystoneEngine::KS_MODE_PPC32
when 'powerpc64' then KeystoneEngine::KS_MODE_PPC64
when 'sparc' then KeystoneEngine::KS_MODE_SPARC32
when 'sparc64' then KeystoneEngine::KS_MODE_SPARC64
when 'thumb' then KeystoneEngine::KS_MODE_THUMB
end | (context.endian == 'big' ? KeystoneEngine::KS_MODE_BIG_ENDIAN : KeystoneEngine::KS_MODE_LITTLE_ENDIAN)
end

# FFI is used in keystone and capstone binding gems, this method handles when libraries not installed yet.
Expand All @@ -211,7 +248,7 @@ def require_message(lib, msg)

def install_crabstone_guide
<<-EOS
#disasm dependes on capstone, which is detected not installed yet.
#disasm depends on capstone, which is detected not installed yet.
Checkout the following link for installation guide:

http://www.capstone-engine.org/documentation.html
Expand All @@ -221,7 +258,7 @@ def install_crabstone_guide

def install_keystone_guide
<<-EOS
#asm dependes on keystone, which is detected not installed yet.
#asm depends on keystone, which is detected not installed yet.
Checkout the following link for installation guide:

https://github.com/keystone-engine/keystone/tree/master/docs
Expand Down Expand Up @@ -300,17 +337,19 @@ def arch_align

def e_machine
const = ARCH_EM[context.arch.to_sym]
if const.nil?
raise ::Pwnlib::Errors::UnsupportedArchError,
"Unknown machine type of architecture #{context.arch.inspect}."
end
unsupported!("Unknown machine type of architecture #{context.arch.inspect}.") if const.nil?

::ELFTools::Constants::EM.const_get("EM_#{const}")
end

def endian
context.endian.to_sym
end

def unsupported!(msg)
raise ::Pwnlib::Errors::UnsupportedArchError, msg
end

include ::Pwnlib::Context
end
end
Expand Down
146 changes: 68 additions & 78 deletions test/asm_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,98 +14,88 @@ def setup
@shellcraft = ::Pwnlib::Shellcraft::Shellcraft.instance
end

def skip_windows
skip 'Not test asm/disasm on Windows' if TTY::Platform.new.windows?
end
def parse_sfile(filename)
File.read(filename).split("\n\n").each do |it|
lines = it.lines
metadata = {}
# First line of +lines+ might be the extra context setting
if lines.first.start_with?('# context: ')
# "# context: arch: a, endian: big"
# => { arch: 'a', endian: 'big' }
metadata = lines.shift.slice(11..-1)
.split(',').map { |c| c.split(':', 2).map(&:strip) }
.map { |k, v| [k.to_sym, v] }.to_h
end
comment, output = lines.partition { |l| l =~ /^\s*[;#]/ }.map(&:join)
next if output.empty?

def linux_only
skip 'ELF can only be executed on Linux' unless TTY::Platform.new.linux?
output << "\n" unless output.end_with?("\n")
tests = output.lines.map do |l|
vma, hex_code, _dummy, inst = l.scan(/^\s*(\w+):\s{3}(([\da-f]{2}\s)+)\s+(.*)$/).first
[vma.to_i(16), hex_code.split.join, inst.strip]
end

vma = tests.first.first
bytes = [tests.map { |l| l[1] }.join].pack('H*')
insts = tests.map(&:last)
yield(bytes, vma, insts, output, comment, **metadata)
end
end

def test_i386_asm
skip_windows
context.local(arch: 'i386') do
assert_equal("\x90", Asm.asm('nop'))
assert_equal("\xeb\xfe", Asm.asm(@shellcraft.infloop))
assert_equal("jhh///sh/binj\x0bX\x89\xe31\xc9\x99\xcd\x80", Asm.asm(@shellcraft.sh))
# issue #51
assert_equal("j\x01\xfe\x0c$h\x01\x01\x01\x01\x814$\xf2\xf3\x0b\xfe",
Asm.asm(@shellcraft.pushstr("\xf3\xf2\x0a\xff")))
# All tests of asm can be found under test/data/assembly/<arch>.s.
%w[aarch64 amd64 arm i386 mips mips64 powerpc powerpc64 sparc sparc64 thumb].each do |arch|
file = File.join(__dir__, 'data', 'assembly', arch + '.s')
# Defining methods dynamically makes proper error message shown when tests failed.
__send__(:define_method, "test_asm_#{arch}") do
skip_windows

context.local(arch: arch) do
parse_sfile(file) do |bytes, vma, insts, _output, comment, **ctx|
next if comment.include?('!skip asm')

context.local(**ctx) do
assert_equal(bytes, Asm.asm(insts.join("\n"), vma: vma))
end
end
end
end
end

def test_amd64_asm
def test_asm_unsupported
skip_windows
context.local(arch: 'amd64') do
assert_equal("\x90", Asm.asm('nop'))
assert_equal("\xeb\xfe", Asm.asm(@shellcraft.infloop))
assert_equal("jhH\xb8/bin///sPj;XH\x89\xe71\xf6\x99\x0f\x05", Asm.asm(@shellcraft.sh))
assert_equal("j\x01\xfe\x0c$H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8\xfe\xfe\xfe\xfe\xfe\xfe\x0b\xfeH1\x04$",
Asm.asm(@shellcraft.pushstr("\xff\xff\xff\xff\xff\xff\x0a\xff")))

err = context.local(arch: :vax) do
assert_raises(::Pwnlib::Errors::UnsupportedArchError) { Asm.asm('') }
end
assert_equal('Asm on architecture "vax" is not supported yet.', err.message)
end

def test_i386_disasm
skip_windows
context.local(arch: 'i386') do
str = Asm.disasm("h\x01\x01\x01\x01\x814$ri\x01\x011\xd2"\
"Rj\x04Z\x01\xe2R\x89\xe2jhh///sh/binj\x0bX\x89\xe3\x89\xd1\x99\xcd\x80")
assert_equal(<<-EOS, str)
0: 68 01 01 01 01 push 0x1010101
5: 81 34 24 72 69 01 01 xor dword ptr [esp], 0x1016972
c: 31 d2 xor edx, edx
e: 52 push edx
f: 6a 04 push 4
11: 5a pop edx
12: 01 e2 add edx, esp
14: 52 push edx
15: 89 e2 mov edx, esp
17: 6a 68 push 0x68
19: 68 2f 2f 2f 73 push 0x732f2f2f
1e: 68 2f 62 69 6e push 0x6e69622f
23: 6a 0b push 0xb
25: 58 pop eax
26: 89 e3 mov ebx, esp
28: 89 d1 mov ecx, edx
2a: 99 cdq
2b: cd 80 int 0x80
EOS
assert_equal(<<-EOS, Asm.disasm("\xb8\x5d\x00\x00\x00"))
0: b8 5d 00 00 00 mov eax, 0x5d
EOS
# All tests of disasm can be found under test/data/assembly/<arch>.s.
%w[aarch64 amd64 arm i386 mips mips64 powerpc64 sparc sparc64 thumb].each do |arch|
file = File.join(__dir__, 'data', 'assembly', arch + '.s')
# Defining methods dynamically makes proper error message shown when tests failed.
__send__(:define_method, "test_disasm_#{arch}") do
skip_windows

context.local(arch: arch) do
parse_sfile(file) do |bytes, vma, _insts, output, comment, **ctx|
next if comment.include?('!skip disasm')

context.local(**ctx) do
assert_equal(output, Asm.disasm(bytes, vma: vma))
end
end
end
end
end

def test_amd64_disasm
def test_disasm_unsupported
skip_windows
context.local(arch: 'amd64') do
str = Asm.disasm("hri\x01\x01\x814$\x01\x01\x01\x011\xd2" \
"Rj\x08ZH\x01\xe2RH\x89\xe2jhH\xb8/bin///sPj;XH\x89\xe7H\x89\xd6\x99\x0f\x05", vma: 0xfff)

assert_equal(<<-EOS, str)
fff: 68 72 69 01 01 push 0x1016972
1004: 81 34 24 01 01 01 01 xor dword ptr [rsp], 0x1010101
100b: 31 d2 xor edx, edx
100d: 52 push rdx
100e: 6a 08 push 8
1010: 5a pop rdx
1011: 48 01 e2 add rdx, rsp
1014: 52 push rdx
1015: 48 89 e2 mov rdx, rsp
1018: 6a 68 push 0x68
101a: 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax, 0x732f2f2f6e69622f
1024: 50 push rax
1025: 6a 3b push 0x3b
1027: 58 pop rax
1028: 48 89 e7 mov rdi, rsp
102b: 48 89 d6 mov rsi, rdx
102e: 99 cdq
102f: 0f 05 syscall
EOS
assert_equal(<<-EOS, Asm.disasm("\xb8\x17\x00\x00\x00"))
0: b8 17 00 00 00 mov eax, 0x17
EOS

err = context.local(arch: :vax) do
assert_raises(::Pwnlib::Errors::UnsupportedArchError) { Asm.disasm('') }
end
assert_equal('Disasm on architecture "vax" is not supported yet.', err.message)
end

# To ensure coverage
Expand Down Expand Up @@ -168,7 +158,7 @@ def test_make_elf
# this test can be removed after method +run_shellcode+ being implemented
def test_make_elf_and_run
# run the ELF we created to make sure it works.
linux_only
linux_only 'ELF can only be executed on Linux'

# test supported architecture
{
Expand Down
Loading