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

Bug in shellcraft.execve() for AArch64 when specifying argv #2160

Closed
nicolaipre opened this issue Jan 18, 2023 · 6 comments · Fixed by #2161
Closed

Bug in shellcraft.execve() for AArch64 when specifying argv #2160

nicolaipre opened this issue Jan 18, 2023 · 6 comments · Fixed by #2161

Comments

@nicolaipre
Copy link

nicolaipre commented Jan 18, 2023

There seems to be a bug in the shellcraft.execve() module for aarch64 when specifying arguments.

For some reason when pushing the null terminator string and pointers it uses 8 bytes instead of 16.

pwntools version:

$ pip freeze | grep pwntools
pwntools==4.9.0

Example program (I am using busybox, and executing the command /bin/busybox sh as an example):

from pwn import *

context.arch = "aarch64"

path = '/bin/busybox'
argv = ["sh"]
envp = 0

sc = shellcraft.execve(path, argv, envp)

print(sc)

io = run_assembly(sc)
io.interactive()

The generated shellcode does not seem to be 16 byte aligned (the way AArch64 wants it to be), and therefore crashes.

By adding the two lines to modify the assembly to be 16 byte aligned, it works fine:

from pwn import *

context.arch = "aarch64"

path = '/bin/busybox'
argv = ["sh"]
envp = 0

sc = shellcraft.execve(path, argv, envp)
sc = sc.replace("[sp, #-8]!", "[sp, #-16]!")
sc = sc.replace("mov  x14, #8", "mov  x14, #16")

print(sc)

io = run_assembly(sc)
io.interactive()

Full log file:

================================================================================
= Started at 2023-01-18T01:21:24                                               =
= sys.argv = [                                                                 =
=   'bug.py',                                                                  =
= ]                                                                            =
================================================================================
2023-01-18T01:21:25:DEBUG:pwnlib.asm:cpp -C -nostdinc -undef -P -I/home/pi/.local/lib/python3.9/site-packages/pwnlib/data/includes /dev/stdin
2023-01-18T01:21:25:DEBUG:pwnlib.asm:Assembling
.section .shellcode,"awx"
.global _start
.global __start
.p2align 2
_start:
__start:
    /* execve(path='/bin/busybox', argv=['sh'], envp=0) */
    /* push b'/bin/busybox\x00' */
    /* Set x14 = 8319663842492244527 = 0x7375622f6e69622f */
    mov x14, #25135
    movk x14, #28265, lsl #16
    movk x14, #25135, lsl #0x20
    movk x14, #29557, lsl #0x30
    /* Set x15 = 2020565625 = 0x786f6279 */
    mov x15, #25209
    movk x15, #30831, lsl #16
    stp x14, x15, [sp, #-16]!
    mov x0, sp
    /* push argument array [b'sh\x00'] */
    /* push b'sh\x00' */
    mov x14, #26739
    str x14, [sp, #-16]!
    /* push null terminator */
    mov x14, xzr
    str x14, [sp, #-8]!
    /* push pointers onto the stack */
    mov x14, #8
    add x14, sp, x14
    str x14, [sp, #-8]! /* b'sh\x00' */
    /* set x1 to the current top of the stack */
    mov x1, sp
    mov x2, xzr
    /* call execve() */
    mov x8, #221
    svc 0

2023-01-18T01:21:25:DEBUG:pwnlib.asm:/usr/bin/aarch64-linux-gnu-as -EL -o /tmp/pwn-asm-j2ihde5l/step2 /tmp/pwn-asm-j2ihde5l/step1
2023-01-18T01:21:25:DEBUG:pwnlib.asm:/usr/bin/aarch64-linux-gnu-ld --oformat=elf64-littleaarch64 -EL -z execstack -o /tmp/pwn-asm-j2ihde5l/step3 /tmp/pwn-asm-j2ihde5l/step2 --section-start=.shellcode=0x10000000 --entry=0x10000000 -z max-page-size=4096 -z common-page-size=4096
2023-01-18T01:21:25:DEBUG:pwnlib.elf.elf:'/tmp/pwn-asm-j2ihde5l/step3' is statically linked, skipping GOT/PLT symbols
2023-01-18T01:21:25:INFO:pwnlib.elf.elf:'/tmp/pwn-asm-j2ihde5l/step3'
Arch:     aarch64-64-little
RELRO:    No RELRO
Stack:    No canary found
NX:       NX disabled
PIE:      No PIE (0xffff000)
RWX:      Has RWX segments
================================================================================
= Started at 2023-01-18T01:21:25                                               =
= sys.argv = [                                                                 =
=   'bug.py',                                                                  =
= ]                                                                            =
================================================================================
2023-01-18T01:21:25:INFO:pwnlib.tubes.process.process.548144842880:Starting local process '/tmp/pwn-asm-j2ihde5l/step3'
2023-01-18T01:21:25:INFO:pwnlib.tubes.process.process.548144842880:Starting local process '/tmp/pwn-asm-j2ihde5l/step3': pid 76107
2023-01-18T01:21:25:INFO:pwnlib.tubes.process.process.548144842880:Switching to interactive mode
================================================================================
= Started at 2023-01-18T01:21:25                                               =
= sys.argv = [                                                                 =
=   'bug.py',                                                                  =
= ]                                                                            =
================================================================================
2023-01-18T01:21:25:INFO:pwnlib.tubes.process.process.548144842880:Process '/tmp/pwn-asm-j2ihde5l/step3' stopped with exit code -7 (SIGBUS) (pid 76107)
2023-01-18T01:21:25:INFO:pwnlib.tubes.process.process.548144842880:Got EOF while reading in interactive
2023-01-18T01:21:26:DEBUG:pwnlib.tubes.process.process.548144842880:Sent 0x1 bytes:
2023-01-18T01:21:26:DEBUG:pwnlib.tubes.process.process.548144842880:b'\n'
2023-01-18T01:21:26:INFO:pwnlib.tubes.process.process.548144842880:Got EOF while sending in interactive
================================================================================
= Started at 2023-01-18T01:21:26                                               =
= sys.argv = [                                                                 =
=   'bug.py',                                                                  =
= ]                                                                            =
================================================================================
@zachriggle
Copy link
Member

Busybox segfaults if argv is empty, can you try with a non-empty envp? It might be the same issue.

@nicolaipre
Copy link
Author

nicolaipre commented Jan 19, 2023

Busybox segfaults if argv is empty, can you try with a non-empty envp? It might be the same issue.

Just tested with the following envp:

from pwn import *

context.arch = "aarch64"

path = '/bin/busybox'
argv = ["sh"]
envp = {"SOME_ENV": "DEBUG"}

sc = shellcraft.execve(path, argv, envp)
#sc = sc.replace("[sp, #-8]!", "[sp, #-16]!")
#sc = sc.replace("mov  x14, #8", "mov  x14, #16")

print(sc)

io = run_assembly(sc)
io.interactive()

Still getting SIGBUS. If I enable commented out lines (10 & 11) it works again. I have also tested with other binaries, and I am getting SIGBUS with those too, so it is not because of busybox.

@patryk4815
Copy link
Contributor

In [6]: print(pwnlib.shellcraft.mov('x0', 2**32))
    eor  x0, x0, x0


In [7]: print(pwnlib.shellcraft.mov('x0', 2**32 * 2))
    eor  x0, x0, x0


In [8]: print(pwnlib.shellcraft.mov('x0', 2**32 * 2 * 2))
    eor  x0, x0, x0


In [9]: print(pwnlib.shellcraft.mov('x0', 2**32 * 2 * 2 * 2))
    eor  x0, x0, x0

Maybe related, broken mov

@patryk4815
Copy link
Contributor

@Arusekk If you want apply patches.

Fixed pushstr_array.asm:

<%
    from pwnlib import shellcraft
    from pwnlib.shellcraft import pretty
    from pwnlib.util.iters import group
    from pwnlib.util.packing import _need_bytes
    from six import text_type, binary_type
%>
<%docstring>
Pushes an array/envp-style array of pointers onto the stack.

Arguments:
    reg(str):
        Destination register to hold the pointer.
    array(str,list):
        Single argument or list of arguments to push.
        NULL termination is normalized so that each argument
        ends with exactly one NULL byte.

Example:
    >>> assembly = shellcraft.execve("/bin/sh", ["sh", "-c", "echo Hello string $WORLD"], {"WORLD": "World!"})
    >>> ELF.from_assembly(assembly).process().recvall()
    b'Hello string World!\n'
</%docstring>
<%page args="reg, array, register1='x14', register2='x15'"/>
<%
if isinstance(array, (binary_type, text_type)):
    array = [array]

# Convert all items to strings
array = [_need_bytes(x, 2, 0x80) for x in array]

# Normalize line endings for each item
array = [arg.rstrip(b'\x00') + b'\x00' for arg in array]

# Join everything in the string-to-be-pushed
string = b''.join(array)

# Maximum amount that we can adjust SP by at once is 4095,
# which seems like a safe maximum.
if len(array) * 8 > 4095:
    raise Exception("Array size is too large (%i), max=4095" % len(array))

need_fix_alligment = len(array) % 2 == 1
%>\
    /* push argument array ${shellcraft.pretty(array, False)} */
    ${shellcraft.pushstr(string, register1=register1, register2=register2)}

    /* push null terminator */
    ${shellcraft.mov(register2, 0)}
    str ${register2}, [sp, #-16]!

    /* push pointers onto the stack */
%for i, value in enumerate(reversed(array)):
   ${shellcraft.mov(register1, 8 + ((i+1)*8 + string.index(value)))}
   add ${register1}, sp, ${register1}
 %if i % 2 == 0:
   str ${register2}, [sp, #-16]!  /* allocate zeros */
   str ${register1}, [sp, #8]!
 %else:
   sub sp, sp, #8
   str ${register1}, [sp, #0]!
 %endif
%endfor

    /* set ${reg} to the current top of the stack */
    ${shellcraft.mov(reg,'sp')}

  %if need_fix_alligment:
    /* fix alligment */
    sub sp, sp, #8
  %endif

Fixed mov.asm:

<%
  from pwnlib import shellcraft as SC
  from pwnlib import constants
  from pwnlib.context import context as ctx # Ugly hack, mako will not let it be called context
  from pwnlib.log import getLogger
  from pwnlib.util.lists import group
  from pwnlib.util.packing import p16, u16, pack, unpack
  from pwnlib.util.fiddling import xor_pair
  from pwnlib.shellcraft import pretty
  from pwnlib.shellcraft.registers import aarch64 as regs
  import six
  log = getLogger('pwnlib.shellcraft.arm.mov')
%>
<%page args="dst, src"/>
<%docstring>
Move src into dest.

Support for automatically avoiding newline and null bytes has to be done.

If src is a string that is not a register, then it will locally set
`context.arch` to `'arm'` and use :func:`pwnlib.constants.eval` to evaluate the
string. Note that this means that this shellcode can change behavior depending
on the value of `context.os`.

Examples:

    >>> print(shellcraft.mov('x0','x1').rstrip())
        mov  x0, x1
    >>> print(shellcraft.mov('x0','0').rstrip())
        mov  x0, xzr
    >>> print(shellcraft.mov('x0', 5).rstrip())
        mov  x0, #5
    >>> print(shellcraft.mov('x0', 0x34532).rstrip())
        /* Set x0 = 214322 = 0x34532 */
        mov  x0, #17714
        movk x0, #3, lsl #16

Args:
  dest (str): The destination register.
  src (str): Either the input register, or an immediate value.
</%docstring>
<%
if dst not in regs:
    log.error('%r is not a register' % str(dst))

if not src in regs:
    src = SC.eval(src)

mov_x0_x15 = False
xor        = None

# if isinstance(src, six.integer_types):
#     # Moving an immediate into x0 emits a null byte.
#     # Moving a register into x0 does not.
#     # Use x15 as a scratch register.
#     if dst == 'x0':
#         mov_x0_x15 = True
#         dst = 'x15'
#
#     packed = pack(src)
#     words  = group(2, packed)
#     xor    = ['\x00\x00'] * 4
#     okay   = False
#
#     for i, word in enumerate(list(words)):
#         # If an entire word is zero, we can work around it.
#         # However, if any of the individual bytes are '\n', or only
#         # one of the bytes is a zero, we must do an XOR.
#         if '\n' not in word or word == '\x00\x00' or '\x00' not in word:
#             continue
#
#         a, b = xor_pair(word)
#         words[i] = a
#         xor[i]   = b
#
#     src = unpack(''.join(words))
#     xor = unpack(''.join(xor))

%>
%if not isinstance(src, six.integer_types):
    mov  ${dst}, ${src}
%else:
  %if src & 0xffff == 0:
    mov  ${dst}, xzr
  %endif
  %if src == 0:
    mov  ${dst}, xzr
  %elif src & 0xffff == src:
    mov  ${dst}, #${src}
  %else:
    /* Set4 ${dst} = ${src} = ${pretty(src)} */
    %if src & 0x000000000000ffff:
    mov  ${dst}, #${(src >> 0x00) & 0xffff}
    %endif
    %if src & 0x00000000ffff0000:
    movk ${dst}, #${(src >> 0x10) & 0xffff}, lsl #16
    %endif
    %if src & 0x0000ffff00000000:
    movk ${dst}, #${(src >> 0x20) & 0xffff}, lsl #0x20
    %endif
    %if src & 0xffff000000000000:
    movk ${dst}, #${(src >> 0x30) & 0xffff}, lsl #0x30
    %endif
    %if xor:
    ${SC.mov('x14', xor)}
    eor ${dst}, ${dst}, x14
    %endif
    %if mov_x0_x15:
    ${SC.mov('x0','x15')}
    %endif
  %endif
%endif

@heapcrash
Copy link
Collaborator

Please create a pull request ❤️

@nicolaipre
Copy link
Author

@patryk4815 - Could you please submit a PR with your fixes?

@Arusekk Arusekk linked a pull request Oct 2, 2023 that will close this issue
patryk4815 added a commit to patryk4815/pwntools that referenced this issue Jan 3, 2024
patryk4815 added a commit to patryk4815/pwntools that referenced this issue Jan 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants