This is just a small writeup I made for a shellcode payload encoder that I made for the the OSEP exam.

Goals:

  • Make sure the entropy is at a level similar to English

  • If the same shellcode is ran through the encoder multiple times, the result will be different but the result is the same

  • No key is needed to decrypt the shellcode. Just an algo

This technique was sponsored by the Pokemon Save Game Checksum Method. Check out this video by liveoverflow where I first saw it.

Long story short, in a pokemon save game file, it would check to see if the save game was corrupted, by initializing the checksum to 255 (0xFF), and for every byte between X and Y location, would subtract from the checksum value. The final checksum, if not equal to the values loaded, would announce that the save file is corrupted.

A hacker could edit the save file and make it valid by messing with the checksum.

So with that in mind, I wanted to make a shellcode encoder that uses this encoding method or a rough similarity to it. I wanted something that had the ability to morph using a single change but still be logical in inconsistent.

When I originally wrote this for the OSEP, I had to write it in nim, golang, python, and C# so the results had to stay consistent per lang. I wrote the original encoder in Go just because I was pretty comfortable with Go.

Before we begin

It is important to show the difference between encoding and encrypting.

We can mostly just refer to this stackoverflow answer

Now we begin

For this example, we can do this in python so its easier to understand.

The concept of my encoder is pretty easy. But to fully understand, we can start with the decoding side of it.

Lets take a random word. Let that word be apple and lets turn it into a byte value.

We start by capitalizing this word.

hiddenbyte = "apple".upper()

We start by iterating though each character in the string.

for char in hiddenbyte:
  print(char)

The resulting value:

A
P
P
L
E

We turn this character values into their ascii number:

for char in hiddenbyte:
  print(f"{char} -> {ord(char)}")

Output:

A -> 65
P -> 80
P -> 80
L -> 76
E -> 69


** Process exited - Return Code: 0 **
Press Enter to exit terminal

From here, it quite simple. We initialize a value of 0 as finalbyte.

If a letter is below or equal to 77 or its ascii value of M, it would add to the variable finalbyte. If it was above 77, it would subtract from it.

Because we used an unsigned 8 bit integer, we will never be below 0 or above 255. This is uniquely a problem only python has because its dynamically typed. In Golang, you would only need to initialize the var using var finalbyte uint8.

For us to emulate an unsigned 8 bit int, we can simple AND 255 the value of finalbyte after every subtraction or addition.

hiddenbyte = "apple".upper()
finalbyte = 0

for char in hiddenbyte:

#If it is under or equal to 77 or ascii char M
    if ord(char) <= 77:
        finalbyte += ord(char)
        finalbyte = finalbyte & 255
        print(f"{char} -> {ord(char)} (+) finalbyte: {finalbyte}")

#If it is over 77 or the acsii char M
    if ord(char) > 77:
        finalbyte -= ord(char)
        finalbyte = finalbyte & 255
        print(f"{char} -> {ord(char)} (-) finalbyte: {finalbyte}")


# Get the final value in hex
print(hex(finalbyte))

Output:

A -> 65 (+) finalbyte: 65
P -> 80 (-) finalbyte: 241
P -> 80 (-) finalbyte: 161
L -> 76 (+) finalbyte: 237
E -> 69 (+) finalbyte: 50


0x32

Playground of code

We see that since 80 is a greater value then 65, the final byte loops back to decimal value of 241.

We see that the final value of the word APPLE is the byte value of 0x32.

For the encoder, I used a bruteforce method of building these words substitutions. It would pick words at random from a wordlist, do the calculation until it found the word with the byte value it needed. Although it could be said that I could have just predetermined the words needed for each byte, I felt a little more giddy knowing it was truly random. The result of this was a payload where no bytes of the same value having the same word represent it. The chance of this happening, although low, is not zero.

For the final result, the payload windows/x64/exec cmd=calc.exe -f num as seen below. Note: I had to delete the all the whitespace chars when encoding it….

becomes:

"ALLOCATING,PUNNED,PLUNDERER,GARRULOUS,CONTINUUM,RECURSION,BAROMETERS,BOWDLERIZES,HEARKENING,SPOTTINESS,POLLYWOG,MONTHS,REBECCA,DELUGED,KAZAKHSTAN,OPTOMETRY,GAFFING,GOOSES,DENIS,KREMLIN,PROWS,SOOTHE,STREPTOMYCIN,ORATORY,BANDIEST,YOUNGSTER,DONKEYS,EXPERIMENTED,CUSHIONED,EDMONTON,NESTLINGS,CATALOGUES,WHITTLING,PUNNED,STICKUP,MUSKOGEE,MASHHAD,DABBLER,GREASIEST,SNAGGED,KORANS,ARBITRATED,BIRCHED,MUFFS,UKRAINIANS,TOCSIN,SLUMMER,AGREE,AGITATING,EXISTS,PARTICIPLE,BLURRIEST,MARIMBAS,HARVESTS,NASALLY,OLD,BAKER,COCCYX,BULKED,TENETS,CEAUSESCU,CLUEING,PINHOLES,POMPADOUR,BLEAKER,HUMORIST,HUNT,NARY,JABBERED,NORTHAMPTON,PITCHFORK,NONPROLIFERATION,TURTLE,CASTRO,BOOTSTRAP,POGROMS,GRAYER,TUNEFUL,NONRETURNABLES,FRITTER,AGRICOLA,HUMANITARIANS,EARTHQUAKES,ORKNEY,VATTING,EMBRYOLOGIST,FEINTING,PORTENTOUS,SEMBLANCE,BETAKING,BUSSED,SUPPLANTS,PANSIES,MOVERS,MALFUNCTION,GRAYBEARDS,HUMERUS,PLUNGERS,CHIROPRACTOR,REFRIGERATES,REGURGITATE,WESTWARDS,WASTERS,ALLEYWAY,MOVERS,BETCHA,INTRAMURAL,BEDRIDDEN,BOLL,CLIPT,PETUNIA,LORENZ,SPARROWS,MED,OXYCONTIN,SEQUESTRATION,OVERSTATEMENT,UNBOLT,PHYSIOLOGY,ESTELLE,LATERALLY,TUREEN,LEMURIA,FREEMASONS,LEADEN,BROWNE,SOLIDIFIES,LUCKIER,INTEL,RETOLD,DESERTERS,PROUDLY,RANSOM,SHELLACS,LYNDON,INFREQUENTLY,SEDATIVES,UPPERCUTS,SCREWS,MAELSTROMS,EXTREMITY,SPAMMERS,NOPE,JELLYFISHES,STAMENS,NASSER,REMOVED,WINNER,GIMMICK,CASTER,BRAHMANS,POSTMARK,SWINGER,ASHLEE,MIN,ZEST,YAQUI,PORNOGRAPHERS,CREDITS,THUNKS,OVERCHARGES,OBSOLETING,OUTSTRIPPED,PARTNERSHIPS,TABLED,TONSILLITIS,ROSIER,INNOVATORS,VIXENISH,CARTOONISTS,DEMITASSES,DISASSOCIATING,DALLIES,BIBLE,FORNICATED,PECULIARLY,SNARFS,KOWTOWS,RIVERS,UNLEAVENED,HIMALAYAS,ECSTATIC,JUNCOS,REEK,SYSTEMS,MILKER,NEUTER,PEDDLED,ROANS,FONDER,CANDIDNESS,BUSY,MIDDIES,REVIVIFIES,DAFFODILS,INDICTABLE,ORIZABA,EXPANSION,STEPCHILDREN,RAYLEIGH,RUNNING,CIVILIZE,GEOCENTRIC,BEADIER,BATCH,LOOKOUT,EXCLAIMS,EXTRACTED,EXTRACTED,PHILHARMONIC,LIFEBOAT,FUTON,COORS,TRANSEPTS,GAUSS,EXPERTNESS,USURPERS,NETHERLANDERS,PERPENDICULAR,INCONSIDERATE,AROUND,OFFENDING,ASSASSIN,GLUM,COLLIERY,CYCLOPS,CLAUDINE,OVERLIES,SALVADORAN,RESCHEDULE,HEATHER,TOYOTA,BENGALI,PETERS,TODDLED,CHAPARRAL,SLIPPER,ABERRATION,VOMIT,IRREPARABLY,ASSUMING,GOOSES,CATTY,JUBAL,PARKWAYS,COVETS,ROY,LITHER,FELLATIO,SANDPIPER,FULMINATED,CUES,REPEAL,FORTIFICATION,SIAMESE,INTUIT,NUMISMATICS,WAYLAYS,PIZZICATO,FINGERINGS,LUCRATIVELY,FORNICATED,RUSTPROOFED,SUBMITS,UNIFORMING,GRAVELLING,TYPOGRAPHER,ADHERENT,GUARDIAN,OSSIFIED,FIERCEST,STIFF,MOLEHILLS,INCHES,SQUELCHING,BUDDHISM"  

All the while having the entropy value stay low.

The other neat side effect of doing it this way, is that when encoding the payload, using this encoder, generates a completely different set of words.

During the examination, just to add an additional layer, I did add a small function that would XOR the byte with the previous byte before the encoding took place.

Head up:

For large payloads that are over 3mbs long, the encoder take a very long time. That could be solved by just premaking the byte to word key but I never got into it.

Next Goals:

My next goal with this project it write the encoder in ruby so that I can directly render it in metasploit.

You can view the python playground version of this code here.

I hope that anyone that finds this post will be inspired to make their own encoder as well!