Log in

View Full Version : Fileopen Pdf encryption


jennyc
April 10th, 2003, 03:40
Hi,
This is my first post on tsehp's cool board.
Recently i have been studying for some computer networking exams
and i came across some encrypted pdf's which i could not open.
Being quite desperate to open them i looked for standard tools
and found none(ElcomSofts pdf decrypter did not work).

I decided to learn something more about pdf encryption and how to break it. I am a newbie to reversing so if this sounds silly my
apologies. Also i have not been succesfull in breaking the encryption because i am a newbie and i need to study for exams.
I am hoping that someone will be intrested after reading this and
help me with the encryption routines.

Adobe-Acroread(pdf viewer) has support for 3rd party plugins to do
the encryption/decryption. Open a pdf file using a HexEditor.
I used Xemacs (much nicer raw pdf view) and go to the end of the file.
You will see a /Encrypt line. This means that the file is Encrypted.
Go a few pages up. You will see a /INFO,/V,/FILTER line.
/Filter Standard implies file was encrypted using standard Adobe Encryption. This uses a 40 bit key which can be brute forced.
To learn more about Adobe's standard pdf encryption and format
about a pdf download PDFReference.pdf from adobe.com.
This is not about Adobe but about FileOpen.
So the /Filter line will be /Filter FOPN_foweb.
it could also be FOPN_fLock this means the file was encrypted using another product of www.fileopen.com called personal-publisher.
/V 1 means doc is protected by version-1.
/SVID Service-ID /DUID Document-ID.
/INFO contains contact-info-of-document-publishers-server.

All this info can be obtained from the Webpublisher2 manual.
/INFO is encoded by some special proprietary algorithm.
When the PDF file is opened the plugin unencodes /INFO
and uses the info to contact the publisher's server to
authenticate etc...but first the plugin checks in
C:\Windows\Application Data\some_configurable_dir
for a .sek LICENSE file.
some_configurable_dir is specified in the pdf's /INFO
structure.
The exact name of the .sek file is built from /INFO data.
It consists of the Publishers-ID.SVID.sek
Publisher-ID is alloted to you by fileopen once you buy the
package which costs 4000$$$ approxi!!.
For the demo the ID is 10006.
The .sek file contains a KEY to decrypt our PDF. It also contains
PDF open/print/how-many-days-left-to-expiry etc etc permissions.
And it is encrypted with the machine-id which is got from System-info/volume-info etc i think. This encryption serves 2 purposes. One we cant modify the license file and give ourselves
permissions after getting a valid KEY.
Two we cant transfer the license to another machine since the
machine-id on the new machine will be calulated and will not match
the old machine-key.
The .sek file is encrypted using RC4/MD5 and some proprietary
algorithm!!.
I took the machine key and md5sum(on linux) and then passed it to rc4 and tried decrypting the .sek file but it didnt work.
I tried with 5 byte md5 key and 5byte md5hash.
So i hope some one can help me understand this part!!.

The pdf contents are encrypted using KEY which is downloaded
from the publisher's server encrypted and stored in the license file. Since i dont have acess on the publishers server without
paying 3000$ i dont have the pdf keys.....
HOWEVER the pdf keys are exactly 5 bytes in LENGTH
Not 4 not 3 exactly 5 bytes.
Which means it is VERY VERY easy to brute force this and
obtain a valid key!. It mostly contains a-z and 0-9.
Once we understand how exactly the pdf encryption is done
we can brute force rc4 directly since the key length is 40bits
and directly extract the rc4 key instead of breaking our head
about the Publishers-key-string or trying to reverse md5.

One more thing..only the pdf string/image objects are encrypted not the whole pdf.

Also this PDFEncrypt.exe program works on Linux/Solaris
and Siul-hackey might very very very easily
figure all the encryption because
THE LINUX BINARY HAS NOT BEEN STRIPPED!!!!!!
this means that gdb works nicely, Functionnames are there
objdump also works and prints all function names

The machine key was found out by running netcat...nc.exe on port 80 and putting a entry in hosts.sam so that plugin contacts my
localhost machine and prints the machineid.
The exact format of what to send to the plugin is given in the
manual for the product...or mail me and ill send it..

Adobe-standard-encryption uses 2 keys a user and a owner key.
I dont think the plugin is using 2 keys just 1.
So some special problems occur. With adobe because it used
2 keys we could verify the validity of the key by comparing
one key against a hashed version of the other...

Howevre here because the pdf is crypted with only 1 key how
do we verify??????

Disabling the license checks within the plugin also seems very
easy once we can brute force and get the KEY.
They have lots of simple test/jne pairs

#########################################################
The plugin name is FileOpen.api. There is another plugin
called FileOpenTest.api not sure about the name since i am typing
all this from memory...that comes with the package.
This was used by the fileopen developers for debugging the client
side and is filled with string references!!!!.
It should be too easy for someone who knows even a little
to figure out the whole scheme of things.
The log file is called dsomething.txt not sure forgotten
##############################################################

Grrr!!!! if only i knew a bit more

So theese are the highlights that i could figure out. Please remeber i am very very new to all this!.

If anyone is intrested in helping me break this encryption
please contact me.

Kilby
April 10th, 2003, 04:19
You seem to have done quite a bit of work already, however this would probably get more responces in the crypto section.

Kilby...

evlncrn8
April 10th, 2003, 04:59
that is of course assuming it isnt a starforce protected pdf file (i've never encountered one personally so i have no idea what it looks like) ...

peterg70
April 10th, 2003, 06:31
Just a quick read.

Do you have the full PDF that is completly encrypted??? if so maybe post or send me a link to the PDF.

If the PDF requires the Plugin to work send me a link for the plugin as well.

I have done a similar document control system for something I use myself.

I am interested in further information as well.

Catch ya

jennyc
April 10th, 2003, 07:33
The package is 2126709 bytes(2Mb) so i cant upload.
Its available on mail requests.
You can use it to create encrypted pdf's and test things out.
The 2 plugins come as part of the package.
Copy FileOpen.api to the plug_ins diectory in Adobe.
Then start up adobe to be able to READ crypted plugins.
PDFEncrypt is used to encrypt the pdf's.
FileOpentrace.api is also there. You cant copy
both plugins to plu_in directory and use. Copy either one and use it.

A short note on encryption modes used. Its all there in the manual
or in the config_wahtever.fow files in TestFiles/Mode_f/encript_before/definition.fow

The plugin can authenticate via
Username/Password: here adobe-reader prompts u to enter a
password/login pair. this is sent via a cgi get request using http/https to the pdf-publishers server.
EG: Session Verification Request https://site.com/cgi-bin/script.pl?Request=DocPerm&ServiceID=594&DocumentID=4576&Stamp=1007398586&
ProdVer=1.1.0.0&Session=87654321&Machine=3S2EE27N

EG: Username/password verification request
https://site.com/cgi-bin/script.pl?Request=DocPerm&ServiceID=594&DocumentID=4576&Stamp=1007398586&
ProdVer=1.1.0.0&UserName=myusername&Password=mypassword&Machine=3S2EE27N


The server authenticates you using perl/asp whatevr and sends back
EG: retval=0 fail;retval=1 success;retval=2 success and offline perms follow
RetVal=1&ServId=594&Docuid=4576&Perms=1&Code=12345&NotifyClose=1
EG: notifyclose=1 menas when u close doc plugin notifies server.
RetVal=2&ServId=594&Docuid=4576&Perms=1&Code=12345&NotifyClose=1&OfflinePrints=c&Offline
Expire=100d


100d=100days

After the data from the server is retrived it gets encrypted and
then added or a new license file.sek is generated.
NOTE: before contacting the server the plugin checks for a license.sek to see if u have offline perms etc..

jennyc
April 10th, 2003, 08:03
objdump -xS PDFEncryptor>diss

strace -s 2000 -o strace.defU-RST.foe PDFEncryptor -f unenc/U-RST-D-100.pdf -i D-100 -d defU-RST.foe

strace -s 2000 -o strace.D-100 PDFEncryptor -f unenc/C-ABC-D-100.pdf -i D-100

Attachment is linux.zip which has the output of above commands.

jennyc
April 13th, 2003, 20:44
Anyone written a plugin for adobe..maybe there is a way to extract the key like that...

peterg70
April 14th, 2003, 02:14
This is someone elses work.
But it may help or lead you down the right path.

*********************************
/* fileSaveAs.c

This program reads a pdf encrypted with the fileOpen plugin,
and attempts to write a plain pdf.

Compile with:
gcc -g -o fileSaveAs -lssl -lcrypto fileSaveAs.c

To compile, you'll need packages libssl09-dev and libCrypto.
This is a list of things that must be worked out:

1) It does not decrypt strings, so there is no navigation
bar on the left, no author, no additional info and such.
2) Often in the resulting pdf there are many white pages.
3) I had only one ebook to work with, so the salt that
I have blatantly hardcoded may be different.
4) The ebook I have contains the strings '/FOPN_fLock /V 1',
I guess that it is the version of the plugin. There
certainly are other versions of the plugin, I don't
know about them.
5) When printing, sometimes acrobat reader says that there
are illegal characters.

So much to do...
*/

Code:
#include <stdio.h>
#include <openssl/md5.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <getopt.h>

#define MAX_OBJ_LEN 2000000
#define STREAM_STR "stream"
#define END_STREAM_STR "endstream"
#define OBJ_STR " obj"
#define END_OBJ_DELIM "endobj"
#define ENCRYPTION_STRING "/Encrypt"

/* I know this is ugly, but hey, I said there is much
to do! Begin here! */
unsigned char pObjBuf[MAX_OBJ_LEN];


#define SHUFFLEUP(YY, XX, ZZ, AA) { \
YY=0; \
YY=(char)*ZZ; \
YY=YY << XX; \
AA |= YY; \
ZZ++; }

unsigned char S[256];
unsigned char K[256];

/* This is the only thing they added on top of plain RC4. */
void superSecret
(
unsigned char *pData,
int nIntParam,
MD5_CTX *pMd5Ctx
)
{
unsigned long lTmp1,
lTmp2,
lTmp3,
lTmp4,
lTmp5,
lTmp6,
lTmp7;
unsigned long *plPoint1;
unsigned char *plMainPnt;

plMainPnt=pData;
if(0 == nIntParam)
return;
lTmp4=pMd5Ctx->Nl;
lTmp7=nIntParam;
lTmp7=lTmp7<<3;
lTmp4+= lTmp7;
lTmp4 &= 0xFF;
if(lTmp4<pMd5Ctx->Nl)
pMd5Ctx->Nh++;
lTmp6=nIntParam;
lTmp6=lTmp6 >> 0x1d;
pMd5Ctx->Nh+= lTmp6;
pMd5Ctx->Nl=lTmp4;

if(0!= pMd5Ctx->num) {
plPoint1=(unsigned long *)pMd5Ctx->data;
lTmp5=pMd5Ctx->num ;
lTmp5=lTmp5 >> 2;
lTmp7=pMd5Ctx->num;
lTmp7&= 3;
lTmp1=lTmp7;
lTmp4=pMd5Ctx->num;
lTmp4+= nIntParam;
if(lTmp4>= 0x40) {
lTmp4=plPoint1[lTmp5];
switch(lTmp1) {
case 0:
SHUFFLEUP(lTmp4, 0, plMainPnt, lTmp4);
case 1:
SHUFFLEUP(lTmp7, 8, plMainPnt, lTmp4);
case 2:
SHUFFLEUP(lTmp6,0x10, plMainPnt, lTmp4);
case 3:
SHUFFLEUP(lTmp7, 0x18, plMainPnt, lTmp4);
}
plPoint1[lTmp5]=lTmp4;

lTmp5++;
if(lTmp5 <0x10) {
do {
SHUFFLEUP(lTmp4, 0, plMainPnt, lTmp4);
SHUFFLEUP(lTmp6, 8, plMainPnt, lTmp4);
SHUFFLEUP(lTmp7, 0x10, plMainPnt, lTmp4);
SHUFFLEUP(lTmp6, 0x18, plMainPnt, lTmp4);
plPoint1[lTmp5]=lTmp4;
} while(lTmp5<10);
}
lTmp7=0x40-pMd5Ctx->num;
nIntParam-= lTmp7;
MD5_Update (pMd5Ctx, (unsigned char *)plPoint1, 0x40);
pMd5Ctx->num;
} else {
pMd5Ctx->num+= nIntParam;
if(lTmp1+nIntParam<4) {
lTmp4=plPoint1[lTmp5];
switch(lTmp1) {
case 0:
SHUFFLEUP(lTmp4, 0, plMainPnt, lTmp4);
nIntParam--;
if(0 == nIntParam)
break;
case 1:
SHUFFLEUP(lTmp7, 8, plMainPnt, lTmp4);
nIntParam--;
if(0 == nIntParam)
break;
case 2:
SHUFFLEUP(lTmp6, 0x10, plMainPnt, lTmp4);
}
plPoint1[lTmp5]=lTmp4;
return;
}
lTmp7=pMd5Ctx->num;
lTmp7=lTmp7 >> 2;
lTmp3=lTmp7;
lTmp2=pMd5Ctx->num & 3;
lTmp4=((unsigned char *)plPoint1)[4*lTmp5];

switch(lTmp1) {
case 0:
SHUFFLEUP(lTmp4, 0, plMainPnt, lTmp4);
case 1:
SHUFFLEUP(lTmp7, 8, plMainPnt, lTmp4);
case 2:
SHUFFLEUP(lTmp6, 0x10, plMainPnt, lTmp4);
case 3:
SHUFFLEUP(lTmp7, 0x18, plMainPnt, lTmp4);
}
plPoint1[lTmp5++]=lTmp4;
if(lTmp5<lTmp3) {
do {
SHUFFLEUP(lTmp4, 0, plMainPnt, lTmp4);
SHUFFLEUP(lTmp6, 8, plMainPnt, lTmp4);
SHUFFLEUP(lTmp7, 0x10, plMainPnt, lTmp4);
SHUFFLEUP(lTmp6, 0x18, plMainPnt, lTmp4);
plPoint1[lTmp5++]=lTmp4;
} while(lTmp5<lTmp3);
}
if(0 == lTmp2)
return;
lTmp4=0;
plMainPnt+= lTmp2;
switch(lTmp2) {
case 0:
plMainPnt--;
lTmp4=0;
lTmp4=(char)*plMainPnt;
lTmp4 << 0x10;
case 2:
plMainPnt--;
lTmp6=0;
lTmp6=(char)*plMainPnt;
lTmp6<<8;
lTmp4|= lTmp6;
case 1:
plMainPnt--;
lTmp7=0;
lTmp7=(char)*plMainPnt;
lTmp4|= lTmp7;
case 3:
plPoint1[lTmp5]=lTmp4;
return;
}
}
}
if(0 == ((unsigned long)plMainPnt & 0x3)) {
lTmp5=nIntParam;
if ((lTmp5>> 6) >0) {
lTmp5=lTmp5<<6;
MD5_Update (pMd5Ctx, (unsigned char *)plMainPnt, lTmp5);
plMainPnt+= lTmp5;
nIntParam-= lTmp5;
}
}
plPoint1=(unsigned long *)pMd5Ctx->data;
if(nIntParam>= 0x40) {
do {
if(plMainPnt!= (unsigned char *)plPoint1) {
memcpy((unsigned char *)plPoint1, plMainPnt, 0x40);
}
plMainPnt+= 0x40;
MD5_Update (pMd5Ctx, (unsigned char *)pMd5Ctx->data, 0x40);
} while(nIntParam>= 0x40);
}
lTmp1=nIntParam;
pMd5Ctx->num=lTmp1;
if(0 == lTmp1)
return;
lTmp5=lTmp1>>2;
plPoint1[lTmp5]=0;
memcpy((unsigned char *)plPoint1, plMainPnt, lTmp1);
}

peterg70
April 14th, 2003, 02:14
/* From 'Applied Cryptography'.
Yes, they used an algorithm from a book.
I can understand: after all, it is a good algorithm.
But If I wanted to distribute some encrypted data, I wouldn't
put the algorithm _AND_ the key in the same place. */

Code:

void CreateRc4Sbox
(
unsigned char *pKeySeed
)
{
int i=0;
int j=0;
int nRealIndex;
unsigned char nTemp;

for (i=0; i<256; i++)
S[I]=i;

for (i=0; i<256; i++)
K[I]=pKeySeed[i%16];
nRealIndex=0;
for (i=0; i<256; i++) {
j=(j + S[I] + K[nRealIndex])%256;
nRealIndex++;
if(nRealIndex>9)
nRealIndex=0;
nTemp=S[I];
S[I]=S[j];
S[j]=nTemp;
}
}


/* Also from the book */
int Rc4Decrypt
(
char *pCryptoData,
int nCryptoBufLen,
char *pResults,
int nResultsBufSize,
unsigned char *SBox
)
{
short nByte;
int nClearPos=0,
nBuf=0,
nBuf2=0,
nCount,
nSwap,
nLength=nCryptoBufLen,
nAdv;
while(0!= nLength) {
nBuf++;
nBuf &= 0xFF;
nCount=SBox[nBuf];
nBuf2 += nCount;
nBuf2 &= 0xFF;
nSwap=SBox[nBuf2];
nAdv=nSwap;
SBox[nBuf]=nSwap;
SBox[nBuf2]=nCount;
nCount += nAdv;
nCount &= 0xFF;
nByte=SBox[nCount];
nByte ^= *pCryptoData;
nClearPos++;
*pResults=nByte;
++pCryptoData;
++pResults;
nLength--;
}
return nClearPos;
}


/* this function calculates the key and executes the Rc4
decrypt loop. Here I have hardcoded the salt: 'DgNab'.
It may change in other ebooks but I only have one... */
int mainDecryptLoop
(
unsigned char * pCryptText,
int nCryptSize,
unsigned char * pClearBuffer,
int nClearBufferSize,
int nNumObj
)
{
unsigned char rc4Key[16],
keyBuf[8];
int nClearLength,
nIdx;
MD5_CTX md5Ctx;
unsigned char salt[20];

for(nIdx=0; nIdx<8; nIdx++)
keyBuf[nIdx]=0;

*(int*)keyBuf=nNumObj;
strcpy(salt, "DgNab";
MD5_Init(&md5Ctx);
superSecret(salt, 5, &md5Ctx);
superSecret(keyBuf, 3, &md5Ctx);
superSecret(keyBuf+4, 2, &md5Ctx);
MD5_Final(rc4Key, &md5Ctx);
CreateRc4Sbox(rc4Key);
nClearLength=Rc4Decrypt(pCryptText, nCryptSize, pClearBuffer, nClearBufferSize, S);
return nClearLength;
}


/* the following is just some PDF parsing.
The juicy part is over, I'm afraid... */
int removeSecurityString
(
unsigned char *pBeginBlock,
unsigned char *pEndBlock
)
{
unsigned char *pTemp;
for(
pTemp=pBeginBlock;
pTemp!=pEndBlock;
pTemp++
) {
if(!strncmp(pTemp, ENCRYPTION_STRING, strlen(ENCRYPTION_STRING))) {
while(
'\n'!=*pTemp &&
'\r'!=*pTemp
) {
*pTemp=' ';
pTemp++;
}
}
}

return 0;
}


int scanFile
(
const int fh,
const int ofh,
int *pnWrote
)
{
int numObject,
nCryptLen,
nClearTextLength,
bStayThere;
unsigned char *pStart,
*pStartCrypted,
*pBeginBlock,
*pCurrent;

for(; {
bStayThere=1;
pCurrent=pObjBuf;
pCurrent+=strlen(OBJ_STR);
pStart=pCurrent;

while(bStayThere) {
if(1>read(fh,pCurrent,1)) {
removeSecurityString(pBeginBlock, pCurrent);
write(ofh, pBeginBlock, pCurrent-pBeginBlock);
return 0;
}
if(!strncmp(pCurrent-strlen(OBJ_STR), OBJ_STR, strlen(OBJ_STR))) {
pCurrent++;
write(ofh, pStart, pCurrent-pStart);
while(*pCurrent!=' ')
pCurrent--;
pCurrent--;
while(*pCurrent!=' ')
pCurrent--;
*pCurrent=0;
numObject=atoi(pStart);
bStayThere=0;
}
pCurrent++;
}

pCurrent=pObjBuf;
pCurrent+=strlen(END_OBJ_DELIM);
pBeginBlock=pCurrent;
bStayThere=1;
while(bStayThere) {
if(1>read(fh,pCurrent,1)) {
write(ofh, pBeginBlock, pCurrent-pBeginBlock);
return 0;
}
if(strncmp(pCurrent-strlen(END_OBJ_DELIM), END_OBJ_DELIM, strlen(END_OBJ_DELIM))) {
pCurrent++;
continue;
}
for(pStart=pObjBuf-strlen(END_OBJ_DELIM); pStart!=pCurrent ; pStart++ ) {
if(!bStayThere)
break;
if(strncmp(pStart, STREAM_STR, strlen(STREAM_STR)))
continue;
pStartCrypted=pStart+strlen(STREAM_STR)+2;
for(pStart=pStartCrypted; pStart!=pCurrent ; pStart++) {
if(strncmp(pStart, END_STREAM_STR, strlen(END_STREAM_STR)))
continue;
nCryptLen=pStart-pStartCrypted-1;
nClearTextLength=mainDecryptLoop(pStartCrypted,nCryptLen,pStartCrypted,nCryptLen,numObject);
bStayThere=0;
break;
}
}
write(ofh, pBeginBlock, 1+pCurrent-pBeginBlock);
bStayThere=0;
}
}
return 0;
}


int Usage
(
const char *szMessage
)
{
fprintf(stderr, "%s\n", szMessage);
fprintf(stderr, "Usage: \n";
fprintf(stderr, "-h : this message\n";
fprintf(stderr, "-i file : input file\n";
fprintf(stderr, "-o file : output file\n";
return 255;
}



int main
(
int argc,
char **argv
)
{
int ifh,
ofh,
nWrote,
nOffs;
char *strInputFileName,
*strOutputFileName,
ch;
struct stat statStruct;
strInputFileName=NULL;
strOutputFileName=NULL;

while((ch=getopt(argc, argv, "hi:")!= -1) {
switch(ch) {
case 'h':
return Usage("Help message requested";
break;
case 'i':
strInputFileName=optarg;
break;
case 'o':
strOutputFileName=optarg;
break;
}
}

if(NULL == strInputFileName)
return Usage("Must give input file";

if(NULL == strOutputFileName)
return Usage("Must give output file";

if(stat(strInputFileName, &statStruct))
return Usage("Error opening input file";

ifh=open(strInputFileName, O_RDONLY);

ofh=open(strOutputFileName, O_WRONLY|O_CREAT|O_TRUNC, 0666);

nWrote=0;
if(scanFile(ifh, ofh, &nWrote))
return Usage("Problems scanning file";

close(ifh);
close(ofh);
return 0;
}

jennyc
April 14th, 2003, 23:49
Ive already taken a look at that file. I got it from Rusty mae's site..or is that Kyler larids site...
Anyway yeah it was a big help!. They havent changed the crypt
routines that much since the elcomsoft debacle.
Trouble was when i started it in gdb and tried to trace through
it refused to enter the MD5Routines..i think this was because
i dont have a E-book pdf file...anyway since my last post
ive changed my tactics....

1. I am first going to bypass all the save/print disable routines.
That is pretty trivial to achieve...just patch the je's and test..
2. They use the fucntion InternetReadFile to do the job of reading
the config info via https..im thinking of reading the info from
its buffer..trouble is this will be encrypted(https)..
so ill try and figure out where his decryption routine is and
read it there...then ill try and figure out where he passes
the key(40bit RC4 key) to adobe. This will be after all the bit mangling and md5summing has been done.

The advantage of this is that i can then directly bruteforce RC4.
Instead of breaking my head against the md5 wall...
Lets see how things go...figuring out the https routine
and reading the plaintext key seems to be the hardest...

peterg70
April 15th, 2003, 07:18
I assume you already have the PDF file that is encrypted and unencrypted.

Any chance of getting an encrypted file.

Peterg70

joblack
May 1st, 2010, 19:14
Any news on that topic? An encrypted pdf shouldn´t be a problem.

Darkelf
May 1st, 2010, 20:53
Holy cow! That must be a new forum record.
7 years, 2 weeks and tatatata... 3 days after the last post. What is it? Some kind of grave desecration

joblack
May 1st, 2010, 21:53
I found the text a few hours ago. Fileopen RE seems to be a 'black listed' web topic.

Silkut
May 2nd, 2010, 10:09
Hi,

I took the liberty to edit two posts containing code to format them.

BR

Blah
May 21st, 2010, 23:20
I would also like to know if anyone has got any further with this..
Seems elcomsoft and the fileSaveAs only work for the fopen_fLock and not the fopen_foweb.
Presumably the "salt" in the fileSaveAs is the 5-byte key the original poster described.

joblack
July 11th, 2010, 20:36
A guy named Tetrachroma released a script which seems to solve the problem.