Writeup - Look Inside

#Writeup - Look Inside

So yeah, a writeup for @0x00pf’s challenge Look Inside (and also for the sub-challenge Look Deeper Inside).
Considering you are interested in this article I guess you spent some time with the challenges (if not I recommend doing so before reading this as it becomes way more relatable that way).

Challenges can be found here:

This writeup will be split up into:

  • Getting the binary from the image
  • Getting the binary to run correctly
  • Figuring out how the credential checking works
  • Writing a keygen for it (and my keygen code)
  • Patching the no custom boot image limitation
  • Looking at picos assembly format and execution (picoASM)
  • Writing some custom boot images

So if you want to just know how to get the binary stop after that part and you shouldn’t be spoilered beyond.
Also note that this is just the way I solved it which may have been a bad way to go on about this so if you have better (or just other) approaches go on and share them!

So let’s begin:

##Image -> Binary

The challenge gives us a GIF image for which we should write a keygen. Sounds weird doesn’t it?

https://0x00sec.s3.amazonaws.com/optimized/2X/8/860ecd3009fda09f0e6982a25e75c0f9a9c7af1a_1_690x414.gif

Looking into the image with a text editor I found two interesting base64 strings:

Hint1: Y29udmVydCBhLmdpZiAtZGVwdGggOCAtYWxwaGEgb2ZmIC1jb21wcmVzcyBOT05FIC1jb2xvcnMyNTYgQk1QOmEuYm1wCg==
=> 'convert a.gif -depth 8 -alpha off -compress NONE -colors256 BMP:a.bmp'
Hint2: VGFrZSBhIGxvb2sgdG8gdGhhdCBzaWduYWwgaGFuZGxlcgo=
=> 'Take a look to that signal handler'

Looking at the first hint I saw that the image has to be converted to a bitmap with some special parameters so I just did that:

convert LookInside.gif -depth 8 -alpha off -compress NONE -colors 256 BMP:LookInside.bmp

I then opened the bitmap within a hex editor and I again saw two pretty long base64 strings in it.

file01:

H4sIAN/m9FkAA+3OTQ7CIBAF4MdokcGNNeEYJvYGpfVv4aGwatVbeTSxMdC1C93MFyY8GEjm2DbV.mwV...

file02:

H4sIANHm9FkAA+y9fXwTVdY4PknTNn0hU960KErUIlSgNgWVStGmTelEghTKmyCWNEkhS5vUZKa0.iFBM...

After that I tried to decode them (or at least the first few bytes) to find out what kind of files they are by looking at their signature.
The first 3 bytes of both files were “1F 8B 08” so I looked at this page and found out that they are both gzip compressed files.
(After thinking about it just running the file command on the decoded base64 strings would have told me that they are gzip files)

leeky@backbox:/media/sf_SharedSpace/current$ base64 -d -i base1.txt > file01.gz
leeky@backbox:/media/sf_SharedSpace/current$ base64 -d -i base2.txt > file02.gz
leeky@backbox:/media/sf_SharedSpace/current$ gunzip file01.gz 
leeky@backbox:/media/sf_SharedSpace/current$ gunzip file02.gz 
leeky@backbox:/media/sf_SharedSpace/current$ file file01
file01: data
leeky@backbox:/media/sf_SharedSpace/current$ file file02
file02: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=b145afa52dc565cfc91340c4ba03ed39f6e848be, not stripped

Guessing from that, file02 is the LookInside binary and file01 some file relevant to the challenge.

If you want to try your luck on the binary yourself just use the convert command and extract the base64 strings :smiley:
Edit: I wanted to include the full base64 strings but I wasn’t able to post it because of the character limit

##Binary -> Getting it Running

Executing the binary results in the following output:

leeky@backbox:/media/sf_SharedSpace/current$ ./file02
DAMS v 1.0 Alpha Code
(c) GBCorp 2017

GBC-212 Server Login

*****************************************
**   This program is an alpha version. **
**   Some features not yet implemented **
*****************************************
Customer ID : Leeky
Customer ID must be 10 characters long

Running it with a 10 character long CustomerID:

leeky@backbox:/media/sf_SharedSpace/current$ ./file02
DAMS v 1.0 Alpha Code
(c) GBCorp 2017

GBC-212 Server Login

*****************************************
**   This program is an alpha version. **
**   Some features not yet implemented **
*****************************************
Customer ID : 1234567890
Enter Key   : password
************************************
Initialising LCB v1.0...
- Load Error

Invalid boot code... Aborting

Interesting. It requires a 10 character CustomerID and some kind of key but then falls back to some kind of loading error.

Let’s look at this in gdb to understand it better:

leeky@backbox:/media/sf_SharedSpace/current$ gdbPeda file02
Reading symbols from /media/sf_SharedSpace/current/file02...(no debugging symbols found)...done.
(gdb-peda) info functions 
All defined functions:

Non-debugging symbols:
0x0000000000400150  main
0x00000000004002ab  _start
0x00000000004002ca  elfi_find_section
0x0000000000400342  xor_block
0x000000000040035d  vm_get_ptr
0x000000000040039c  vm_save
0x00000000004003e9  vm_load
0x0000000000400436  vm_init
0x00000000004004a1  vm_run
0x0000000000400b44  handler
...
0x00000000004030a3  vm_dis
0x000000000040323a  vm_dump_reg
0x0000000000403275  vm_dump
0x00000000004032d5  vm_monitor

Investigating the main function (I removed not interesting lines):

   0x0000000000400150 <main+0>:  push   rbx
   0x0000000000400151 <main+1>:  mov    edi,0xa
   ...
   0x0000000000400159 <main+9>:  mov    esi,0x400b44                 #0x400b44 = function "handler"
   ...
   0x0000000000400165 <main+21>: call   0x400ff2 <signal>
   ...
   0x00000000004001c9 <main+121>: xor    edi,edi
   0x00000000004001cb <main+123>: mov    edx,0x10
   0x00000000004001d0 <main+128>: mov    rsi,rsp
   0x00000000004001d3 <main+131>: call   0x400c75 <read>
   0x00000000004001d8 <main+136>: cmp    eax,0xb
   0x00000000004001db <main+139>: je     0x4001f1 <main+161>
   0x00000000004001dd <main+141>: mov    edi,0x403693                #0x403693 = "Customer ID mus"...
   0x00000000004001e2 <main+146>: call   0x401923 <puts>
   0x00000000004001e7 <main+151>: mov    edi,0x1
   0x00000000004001ec <main+156>: call   0x401a30 <exit>
   0x00000000004001f1 <main+161>: mov    edx,0xe
   ...
   0x000000000040020a <main+186>: lea    rsi,[rsp+0x80]
   0x0000000000400212 <main+194>: mov    edx,0x10
   0x0000000000400217 <main+199>: xor    edi,edi
   0x0000000000400219 <main+201>: call   0x400c75 <read>
   0x000000000040021e <main+206>: dec    eax
   ...
   0x000000000040022f <main+223>: call   0x400436 <vm_init>
   ...
   0x000000000040028a <main+314>: call   0x4004a1 <vm_run>
   0x000000000040028f <main+319>: test   eax,eax
   0x0000000000400291 <main+321>: mov    edi,0x403719                  #0x403719 = "Well done!"
   0x0000000000400296 <main+326>: jne    0x40029d <main+333>
   0x0000000000400298 <main+328>: mov    edi,0x403724                  #0x403724 = "Invalid key..."
   0x000000000040029d <main+333>: call   0x401923 <puts>
   ...
   0x00000000004002a9 <main+345>: pop    rbx
   0x00000000004002aa <main+346>: ret

Simplified:
(main+1 - main+21): The program registers a new signal handler for the signal 0xA. The signal 0xA is the SIGUSR1 signal which is a user defined signal and normally triggered from within program code. The assigned handler is the function listed under defined functions.
(main+121 - main+161): The program requires a Customer ID and exits if the entered string is shorted than 11 letters (where the last letter is the line break)
(main+186 - main+206): It now wants the key as input
(main+223): Some kind of VM gets initialized
(main+314 - main+333): Now this VM gets run and depending on the output of it either returns a “Well done!” or “Invalid key”

Sounds reasonable, but why does it say “- Load Error Invalid boot code… Aborting”?

Looking at vm_init(again I removed unnecessary disassembly):

   ...
   0x000000000040045a <vm_init+36>: mov    esi,0x4034e1                # "./lcb.boot"
   0x000000000040045f <vm_init+41>: mov    rdi,rbx
   0x0000000000400462 <vm_init+44>: call   0x4003e9 <vm_load>
   0x0000000000400467 <vm_init+49>: cmp    BYTE PTR [rbx],0x4c         # 0x4C = 'L'
   0x000000000040046a <vm_init+52>: jne    0x400478 <vm_init+66>
   0x000000000040046c <vm_init+54>: cmp    BYTE PTR [rbx+0x1],0x43     # 0x43 = 'C'
   0x0000000000400470 <vm_init+58>: jne    0x400478 <vm_init+66>
   0x0000000000400472 <vm_init+60>: cmp    BYTE PTR [rbx+0x2],0x42     # 0x42 = 'B'
   0x0000000000400476 <vm_init+64>: je     0x40048c <vm_init+86>
   0x0000000000400478 <vm_init+66>: mov    edi,0x4034ec                # "Invalid boot code..."
   0x000000000040047d <vm_init+71>: call   0x401923 <puts>
   0x0000000000400482 <vm_init+76>: mov    edi,0x1
   0x0000000000400487 <vm_init+81>: call   0x401a30 <exit>
   0x000000000040048c <vm_init+86>: mov    DWORD PTR [rbx+0x1400],0x3
   ...

So vm_init loads something and takes the path “./lcb.boot” as an argument for it and then compares some successive bytes with ‘L’,‘C’ and ‘B’ and if it they don’t match it prints out the “Invalid boot code” message
Wait there was another file I didn’t know the purpose for! The first few bytes look like this:
“4C 43 42 31 31 31 31 31 0A 00 00 00 0A 06 00 00” => “LCB11111”
Considering the first three bytes match with the bytes the function is searching for I renamed the file01 to lcb.boot and restarted the binary

leeky@backbox:/media/sf_SharedSpace/current$ ./file02
DAMS v 1.0 Alpha Code
(c) GBCorp 2017

GBC-212 Server Login

*****************************************
**   This program is an alpha version. **
**   Some features not yet implemented **
*****************************************
Customer ID : 1234567890
Enter Key   : password
************************************
Initialising LCB v1.0...
Boot Code initialised... Ready


Welcome to the GBCorp Dirty Affairs Management System

Checking credentials...

Invalid key...

Great it doesn’t return any errors anymore! So now it’s checking the entered Customer ID and Key somehow and returns either “Invalid Key” or “Well done” depending on the result of vm_run.

##Figuring out how the credential checking works

Next I continued with the signal handler because the 2nd hint said so :stuck_out_tongue:

As earlier said the program registers the signal handler function “handler” to handle SIGUSR1 which is a signal that is normally triggered by software, we will do that from within gdb:

First let’s break before the vm is initialized and then trigger the signal:

(gdb-peda) b *0x000000000040022f
(gdb-peda) run
(gdb-peda) signal SIGUSR1
Entering monitor mode...
Password:password
[Inferior 1 (process 4628) exited with code 01]

Huh, it requests a password and exits after I entered something. Monitor mode? This sounds interesting
Looking at the disassembly of the function I saw calls to both read and strncmp:

   ...
   0x0000000000400b59 <handler+21>:  mov    edi,0x40357a               # "Password:"
   0x0000000000400b5e <handler+26>:  xor    eax,eax
   0x0000000000400b60 <handler+28>:  call   0x401864 <printf>
   ...
   0x0000000000400b7b <handler+55>:  call   0x400c75 <read>
   0x0000000000400b80 <handler+60>:  mov    edx,0x8
   0x0000000000400b85 <handler+65>:  mov    esi,0x607000               # "GBC-x212"
   0x0000000000400b8a <handler+70>:  mov    rdi,rsp
   0x0000000000400b8d <handler+73>:  call   0x4012cc <strncmp>      
   0x0000000000400b92 <handler+78>:  test   eax,eax
   0x0000000000400b94 <handler+80>:  jne    0x400bb8 <handler+116>
   ...
   0x0000000000400bb8 <handler+116>: mov    edi,0x1
   0x0000000000400bbd <handler+121>: call   0x401a30 <exit>
   ...

Guessing from the strncmp the password might be “GBC-x212” , that would also explain why it closed before as I entered the wrong password.

(gdb-peda) signal SIGUSR1

Entering monitor mode...
Password:GBC-x212
+ 13 section in file. Looking for section '.t3xt'
Text (0x4030a3 329) Data (0x400c48 40358a)
$ $ help
help quit dump init run

Ok the password was right and now I have something that requests input from me. The $ made me assume that it’s something like a console so the first thing I thought of was trying to enter ‘help’ and that worked as well!
Trying to dump only returned zeroed values, so I ran init before it and dumped again after that:

$ dump    
R_00:[00000000] R_01:[00000000] R_02:[00000000] R_03:[00000000] R_04:[00000000] R_05:[00000000] R_06:[00000001] R_07:[00000000] 
R_08:[00000000] R_09:[00000000] R_10:[00000000] R_11:[00000000] R_12:[00000000] R_13:[00000000] R_14:[00000000] R_15:[00000000] 
R_16:[00000000] R_17:[00000040] R_18:[00000080] R_19:[00000000] R_20:[00000000] R_21:[00000000] R_22:[00000000] R_23:[00000000] 
R_24:[00000000] R_25:[00000000] R_26:[00000000] R_27:[00000000] R_28:[00000000] R_29:[00000000] R_30:[00000000] R_31:[00000000] 
LBC Code disassembly...
[003]  0a 06 00 00   JMP      0x0006  
[004]  06 00 00 00   SFLAG    R0        0x0000  
[005]  07 00 00 00   RET      
[006]  01 10 00 00   MOVI     RD1       0x0000  
[007]  01 11 40 00   MOVI     RD2       0x0040  
[008]  01 12 80 00   MOVI     RD3       0x0080  
[009]  01 06 01 00   MOVI     R6        0x0001  
[00a]  02 02 40 00   MOV      R2        RD1[0x00]  
[00b]  02 03 80 00   MOV      R3        RD2[0x00]  
[00c]  02 05 c0 00   MOV      R5        RD3[0x00]  
[00d]  15 82 2d 00   CMPI     RD2[0x02]  0x002d  
[00e]  09 01 02 00   J        Z +0x02     <0x0010>  
[00f]  0a 04 00 00   JMP      0x0004  
[010]  08 42 81 00   CMP      RD1[0x02]  RD2[0x01]  
[011]  09 01 02 00   J        Z +0x02     <0x0013>  
[012]  0a 04 00 00   JMP      0x0004  
[013]  08 48 80 00   CMP      RD1[0x08]  RD2[0x00]  
[014]  09 01 02 00   J        Z +0x02     <0x0016>  
[015]  0a 04 00 00   JMP      0x0004  
[016]  08 89 83 00   CMP      RD2[0x09]  RD2[0x03]  
[017]  09 01 02 00   J        Z +0x02     <0x0019>  
[018]  0a 04 00 00   JMP      0x0004  
[019]  08 41 44 00   CMP      RD1[0x01]  RD1[0x04]  
[01a]  09 01 02 00   J        Z +0x02     <0x001c>  
[01b]  15 86 41 00   CMPI     RD2[0x06]  0x0041  
[01c]  09 01 05 00   J        Z +0x05     <0x0021>  
[01d]  0a 04 00 00   JMP      0x0004  
[01e]  15 86 47 00   CMPI     RD2[0x06]  0x0047  
[01f]  09 01 02 00   J        Z +0x02     <0x0021>  
[020]  0a 04 00 00   JMP      0x0004  
[021]  0b 09 43 46   ADD      R9        RD1[0x03]  RD1[0x06]  
[022]  11 09 30 00   SUBI     R9        0x0030  
[023]  08 09 87 00   CMP      R9        RD2[0x07]  
[024]  09 01 02 00   J        Z +0x02     <0x0026>  
[025]  0a 04 00 00   JMP      0x0004  
[026]  08 89 85 00   CMP      RD2[0x09]  RD2[0x05]  
[027]  09 02 02 00   J        NZ +0x02     <0x0029>  
[028]  0a 04 00 00   JMP      0x0004  
[029]  08 45 88 00   CMP      RD1[0x05]  RD2[0x08]  
[02a]  09 01 02 00   J        Z +0x02     <0x002c>  
[02b]  0a 04 00 00   JMP      0x0004  
[02c]  0b 09 41 06   ADD      R9        RD1[0x01]  R6        
[02d]  08 09 84 00   CMP      R9        RD2[0x04]  
[02e]  09 01 02 00   J        Z +0x02     <0x0030>  
[02f]  0a 04 00 00   JMP      0x0004  
[030]  06 01 00 00   SFLAG    R1        0x0000  
Data: 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Guessing from this disassembly and the behaviour I’ve already seen I was able to conclude how the credential checking works without poking around in the binary any further.
Considering I don’t think that’s too hard I recommend trying it yourself as it was quite fun.
Some small tips that make understanding picos assembly easier:

  • RD1 points to the Customer ID
  • RD2 points to the Key
  • SFLAG R0 makes vm_run return false
  • SFLAG R1 makes vm_run return true

So if you intend to try it yourself stop reading here.

##Writing a Keygen

Let me start by explaining what instructions do and how they work (mostly guessed from context of the disassembly):

JMP <address>               - jump the given <address>
SFLAG <reg> <value>         - defines returned value (R0 - false ; R1 - true)
RET                         - returns/ends vm_run
MOVI <reg> <value>          - move immediate value <value> to register <reg>
MOV  <reg> <mreg[offset]>   - move 8bit value read from memory at <mreg+offset> to <reg>
CMPI <mreg[offset]> <value> - compares 8bit value read from memory at <mreg+offset> with <value> and sets zero flag depending on result (Z = equal, NZ = not equal)
J    <condition> <offset>   - jumps <offset> bytes if condition is fullified
CMP <reg1[o1]> <reg2[o2]>   - compares memory at <reg1+o1> with memory at <reg2+o2> and sets zero flag depending on result (Z = equal, NZ = not equal)
ADD <reg> <r1[o1]> <r2[o2]> - adds content of <r1+o1> and <r2+o2> and puts result into <reg>
SUBI <reg> <value>          - subtract <value> from <reg> and puts result into <reg>

The first JMP jumps to the start of credential checking, after that RD1 and RD2 are defined to point to the Customer ID and the Key.
The following conditions must be fulfilled to make it return true or else it will jump back to the beginning, set the return value to false and ends the vm:

  • Key[2] == ‘-’
  • ID[2] == Key[1]
  • ID[8] == Key[0]
  • Key[9] == Key[3]
  • (ID[1] == ID[4] || Key[6] == ‘A’)
  • Key[6] == ‘G’
  • with r9 = ID[3] + ID[6] - 0x30: r9 == Key[7]
  • Key[9] != Key[5]
  • ID[5] == Key[8]
  • with r6 = 1 and r9 = ID[1]+r6: r9 == Key[4]

(Note: you might have noticed that Key[6] == ‘G’ makes the second part of (ID[1] == ID[4] || Key[6] == ‘A’) not applicable and thus redundant)

Following these conditions I wrote a keygen:

import string
bruteForce = string.ascii_letters+string.digits+"{}().,-_+!&*#=?%/\\<>^"
def keygen(name):
    while len(name) < 10:
        name = name + " "
    while len(name) > 10:
        name = name[:10]   
    rd1 = [ord(c) for c in name]
    rd2 = [ord(c) for c in bruteForce[0]*10]
    r6  = 1
    if(rd1[1] != rd1[4]):
        print("1st and 4th character have to be the same!")
    rd1[4] = rd1[1]
    rd2[9] = ord('$')
    rd2[5] = ord('Z')
    rd2[0] = rd1[8]
    rd2[1] = rd1[2]
    rd2[2] = 0x2D
    rd2[3] = rd2[9]
    rd2[4] = rd1[1]+r6
    rd2[6] = 0x47
    rd2[7] = rd1[0x03] + rd1[0x06] - 0x30
    while chr(rd2[7]) not in bruteForce:
        rd1[0x06] -=1                        #one of these two must be active
        #rd1[0x03] -=1
        rd2[7] = rd1[0x03] + rd1[0x06] - 0x30
        print("Correcting 6th character")
    rd2[8] = rd1[5]
    return ''.join([chr(i) for i in rd1]), ''.join([chr(i) for i in rd2])
def verify(rd1, rd2):
    r6  = 1;
    rd1 = [ord(c) for c in rd1]
    rd2 = [ord(c) for c in rd2]
    if(rd2[2] != 0x2D): return -1;
    if(rd1[2] != rd2[1]): return -2;
    if(rd1[8] != rd2[0]): return -3;
    if(rd2[9] != rd2[3]): return -4;
    if(not (rd1[1] == rd1[4] or rd2[6] == 0x41)): return -5;
    if(rd2[6] != 0x47): return -6;
    r9 = rd1[0x03] + rd1[0x06]
    r9 = r9 - 0x30
    if(r9 != rd2[7]): return -7;
    if(rd2[9] == rd2[5]): return -8;
    if(rd1[5] != rd2[8]): return -9;
    r9 = rd1[1]+r6
    if(r9 != rd2[4]): return -10;
    return 1;
pair = keygen("ydoIdodis$")
print(pair)
print("Verified Key" if verify(pair[0], pair[1]) else "Key couldn't be verified")

Looking Deeper Inside

Note that if you replace the lcb.boot file with any other file with the same signature bytes (‘L’,‘C’,‘B’) it will default to picos lcb.boot as it’s hard coded to load it from somewhere within the binary! :open_mouth:

Challenge of the Looking Deeper Inside Challenge was to fix this by modifying the binary:

After realising that the binary actually loads the binary and then overwrites the loaded binary with something from memory I searched for a memcpy call or REP MOVS instruction (as it’s a method of coping memory I’ve seen quite a few times already).
Not much looking around was required and I’ve found a few of those. One that caught my eye was the following one:

   0x400276 <main+294>: mov    esi,0x606000
   0x40027b <main+299>: mov    ecx,0x100
   0x400280 <main+304>: mov    rdi,rax
   0x400283 <main+307>: rep movs DWORD PTR es:[rdi],DWORD PTR ds:[rsi]

Looking at 0x606000 and comparing it to the original lcb.boot I’ve seen that they match after the first few bytes so I’ve concluded that this is the overwrite.
Testing it within gdb and a custom lcb.boot confirms it:

(gdb-peda) set *((unsigned short*)0x400283) = 0x9090
(gdb-peda) run

But after trying to look at the disassembly of my custom lcb.boot I had to realise that the monitor has the hard coded memory loading code as well.
After looking at the <vm_monitor> function before running it to search for it I also noticed that the vm debugging functions are encrypted and only decrypted by a call to <xor_block> within the function before giving me the ability to input commands.

<vm_monitor> before decryption:

   0x00000000004032d5 <vm_monitor+0>:  adc    BYTE PTR [rbp-0xf],ah
   0x00000000004032d8 <vm_monitor+3>:  leave  
   0x00000000004032d9 <vm_monitor+4>:  jns    0x40328e <vm_dump+25>
   0x00000000004032db <vm_monitor+6>:  stos   DWORD PTR es:[rdi],eax
   0x00000000004032dc <vm_monitor+7>:  rex.X
   0x00000000004032dd <vm_monitor+8>:  rex.RXB sub eax,0x71d8d78
   0x00000000004032e3 <vm_monitor+14>: (bad)  
   0x00000000004032e4 <vm_monitor+15>: rex.X jb 0x4032d4 <vm_dump+95>

<vm_monitor> after decryption:

   0x00000000004032d5 <vm_monitor+0>:  adc    BYTE PTR [rax-0x77],cl
   0x00000000004032d8 <vm_monitor+3>:  sti    
   0x00000000004032d9 <vm_monitor+4>:  sub    rsp,0x400
   0x00000000004032e0 <vm_monitor+11>: mov    edi,0x40352c
   0x00000000004032e5 <vm_monitor+16>: xor    eax,eax

After skimming over the <xor_block> functions I’ve confirmed my hope that it just encrypts by xoring with an byte array which makes everything quite easy.
So I found the REP MOVS instruction within the <vm_monitor> at 0x40337b by setting a breakpoint after the decryption and confirmed that it loads the custom image but now I had to make those changes permanent which doesn’t work directly because of the encrypted memory.

<vm_monitor> hard coded overwrite after decryption:

   0x000000000040336e <vm_monitor+153>: mov    esi,0x606000
   0x0000000000403373 <vm_monitor+158>: mov    ecx,0x100
   0x0000000000403378 <vm_monitor+163>: mov    rdi,rax
   0x000000000040337b <vm_monitor+166>: rep movs DWORD PTR es:[rdi],DWORD PTR ds:[rsi]

quick-patch from within gdb:

(gdb-peda) set *((unsigned short*)0x40337b) = 0x9090
(gdb-peda) continue

But after all it wasn’t too hard thanks to the simple encryption:

First I looked at the REP MOVS instruction which is made out of the bytes 0xf3 and 0xa5 and searched for them within a hex editor and found only one occurrence at offset 643 which meant this must be the REP MOVS within the function.
After that I looked at the encrypted memory of the function in gdb and found the 2 bytes 0xb4 and 0xe7 at 0x40337b and searched for them in a hex editor and found again only once occurrence at offset 13179.
I then xored 0xb4 with 0xf3 and 0xe7 with 0xa5 to get the values they were xored with and got 0x47 and 0x42 as the bytes used to encrypt the original instructions.
Considering I want to make them do nothing I replaced them with NOP instructions (which is encoded the byte 0x90).
So I exchanged the 0xf3,0xa5 with 0x90,0x90 and the 0xb4,0xe7 with the same but encrypted bytes 0x90^0x47,0x90^0x42.

After running both

echo -ne "\x90\x90" | dd of=LookInside seek=643   bs=1 count=2 conv=notrunc

and

echo -ne "\xd7\xd2" | dd of=LookInside seek=13179 bs=1 count=2 conv=notrunc

I’ve confirmed that it’s loading custom boot images. Nice! :smiley:

Other optional changes:

“echo -ne “\xae” | dd of=LookInside seek=644 bs=1 count=1 conv=notrunc”
Fixes the overwrite within function with just a 1 byte patch by changing the REP MOVSB instruction to a REP SCASB instruction

echo -ne “\xec” | dd of=LookInside seek=13180 bs=1 count=1 conv=notrunc
Fixes the overwrite within <vm_monitor> function with just a 1 byte patch by changing the REP MOVSB instruction to a REP SCASB instruction

echo -ne “\x02” | dd of=LookInside seek=338 bs=1 count=1 conv=notrunc
Sets the signal the function is associated with to SIGINT (Control+C) to make debugging picoASM code easier without needing to trigger an interrupt within gdb

echo -ne “\x29” | dd of=LookInside seek=2962 bs=1 count=1 conv=notrunc
Makes any password within the function correct by replacing the TEST instruction with a SUB instruction

PicoASM

After writing the keygen and fixing the hard coded boot image limitation I had some fun with picos assembly and the way the instructions were encoded and executed:

First of all the following 32bit registers are available:

  • R0 - R14, R16 (there is no R15)
  • RD1,RD2,RD3
  • R20 - R32
  • RFLAG (is equal to 2 if zeroflag is set and equal to 0 if it’s not set)

References to memory can be done by pointing the RD1, RD2 or RD3 register to the memory address to write to or read from.
It’s also possible to add an additional hard coded offset of up to 4bits (0-15) to the memory the registers point to.

All instructions have 4 bytes and start with the instruction ID as the first byte.

Read/Writable and Executable Memory are divided and don’t overlap.

Registers are encoded into a single byte by the ID they are associated with.
Memory References are also encoded into a single byte following the following format depending on the register used for reference:

  • RD1: 0x40 | (offset&0xF)
  • RD2: 0x80 | (offset&0xF)
  • RD3: 0xC0 | (offset&0xF)
    Immediate values are limited to 8bit (0-255) and also encoded into a single byte.
    Not used bytes are not parsed but exist as padding for the 4 byte size of each instruction.

The VM returns either True or False which is determined by the RFLAG Registers first bit (RFLAG&1)

The Zeroflag is saved within the RFLAG Registers second bit (RFLAG&2)

Starting address for code is [03] which means that JMP 0x3 jumps to the beginning of customizable code

The following instructions are available:

[01]  MOVI  <r0> <i8>
  => r0 = r0 + i8
[02]  MOV   <r0> <r1>
  => r0 = r1
[03]  XOR   <r0> <r1> <r2>
  => r0 = r1^r2
[04]  AND   <r0> <r1> <r2>
  => r0 = r1&r2
[05]  HEX   <i8> <r0> <r1>
  => Does nothing
[06]  SFLAG <r0> <i8>
  => Zeroflag = Zeroflag | IDOfRegister(<r0>)
[07]  RET
  => Ends program
[08] CMP   <r0> <r1>
  => Zeroflag is set if r0 == r1 and unset otherwise
[09] J     <COND> <OFFSET>
  <COND>   either N[0x01] or NZ[0x02]
  <OFFSET> is an 8bit offset to an instruction following this instruction (1 -> next instruction)
[0A] JMP   <ADDRESS>
  <ADDRESS> is an 8bit address of an instruction to jump to
[0B] ADD   <r0> <r1> <r2>
  => r0 = r1 + r2
[0C] SUB   <r0> <r1> <r2>
  => r0 = r1 - r2
[0D] OR    <r0> <r1> <r2>
  => r0 = r1 | r2
[0E] NOP
  => Does nothing
[0F] NOP2
  => Does nothing (actually decoded as NOP by the disassembler as well)
[10] ADDI  <r0> <i8>
  => r0 = r0 + i8
[11] SUBI  <r0> <i8>
  => r0 = r0 - i8
[12] ANDI  <r0> <i8>
  => r0 = r0 & i8
[13] ORI   <r0> <i8>
  => r0 = r0 | i8
[14] XORI  <r0> <i80
  => r0 = r0 ^ i8
[15] CMPI  <r0> <i8>
  => Zeroflag is set if (r0 == i8) and unset otherwise
[40] SIGSEGV
  => SIGSEGV program

Notable behaviour:

  • Instructions over 0x39 SIGSEGV application.
  • Monitor mode registers aren’t named correctly (RD1, RD2, RD3, R15, RFLAG)
  • Instructions can’t encode more than 1 byte in immediate values although bytes are free for that
  • Although Instructions can’t encode more than 1 byte registers have 32bit size
  • Instruction immediate values are not signed -> no conditional backwards jumps, no MOVI R0, -1
  • Two NOP instructions with the same name within the disassembler (0x0E, 0x0F)
  • MOVI doesn’t work with direct memory writing (MOVI RD[0x00], 0x13 doesn’t work) where as the other immediate instructions do
  • SFLAG has a unused immediate argument
  • HEX is a NOP instructions with unused immediate and register arguments

Writing some custom boot images

Ok to be honest I was too lazy to write a proper assembler so I just glued some python code together to create custom images.
Anyways the following snippets show some example usages of picoASM.

As an example the default LookDeeper code:

    JMP(0x6)
    SFLAG(R0, 0x0)
    RET()
    MOVI(RD1,0x00)
    MOVI(RD2,0x40)
    MOVI(RD3,0x80)
    MOVI(R6, 0x01)
    MOV(R2, (RD1, 0x00))
    MOV(R3, (RD2, 0x00))
    MOV(R5, (RD3, 0x00))
    CMPI((RD2,0x02), 0x2D)
    J(Z, 0x02)
    JMP(0x04)
    CMP((RD1,0x02),(RD2,0x01))
    J(Z, 0x02)
    JMP(0x04)
    CMP((RD1,0x08),(RD2,0x00))
    J(Z,0x02)
    JMP(0x04)
    CMP((RD2,0x09),(RD2,0x03))
    J(Z,0x02)
    JMP(0x04)
    CMP((RD1,0x01),(RD1,0x04))
    J(Z,0x02)
    CMPI((RD2,0x06), 0x41)
    J(Z,0x05)
    JMP(0x04)
    CMPI((RD2,0x06), 0x47)
    J(Z,0x02)
    JMP(0x04)
    ADD(R9, (RD1,0x03), (RD1,0x06))
    SUBI(R9, 0x30)
    CMP(R9,(RD2, 0x07))
    J(Z,0x02)
    JMP(0x04)
    CMP((RD2,0x09),(RD2,0x05))
    J(NZ,0x02)
    JMP(0x04)
    CMP((RD1,0x05),(RD2,0x08))
    J(Z,0x02)
    JMP(0x04)
    ADD(R9, (RD1, 0x01), R6)
    CMP(R9, (RD2, 0x04))
    J(Z,0x02)
    JMP(0x04)
    SFLAG(R1, 0x00)

Or a small accept everything code:

    SFLAG(R1, 0x00)

A bit of a Fibonacci sequence:

    MOVI(R0, 1)
    MOVI(R1, 1)
    MOVI(R3, 12)
    MOVI(RD1, 0)
    MOV((RD1, 0), R0)
    ADDI(RD1, 1)
    MOV((RD1, 0), R0)
    MOV(R2,R1)
    MOV(R1,R0)
    ADD(R0,R0,R2)
    SUBI(R3, 1)
    CMPI(R3, 0)
    J(Z,2)
    JMP(8)
    RET()

The following script creates a new lcb.boot (watch out to not overwrite your original one) with the content of the <hardcoded_picoasm> function:

#Assembler thingy for picoASM
R0   = 0x0
R1   = 0x1
R2   = 0x2
R3   = 0x3
R4   = 0x4
R5   = 0x5
R6   = 0x6
R7   = 0x7
R8   = 0x8
R9   = 0x9
R10  = 0xA
R11  = 0xB
R12  = 0xC
R13  = 0xD
R14  = 0xE
#There is no R15
R16  = 0xF
RD1  = 0x10  #this can be used for addressing memory
RD2  = 0x11  #this can be used for addressing memory
RD3  = 0x12  #this can be used for addressing memory
R20  = 0x13
R21  = 0x13
R22  = 0x14
R23  = 0x15
R24  = 0x16
R25  = 0x17
R26  = 0x18
R27  = 0x19
R28  = 0x1a
R29  = 0x1b
R30  = 0x1c
R31  = 0x1d
R32  = 0x1e
RFLAG  = 0x1f
Z   = 1
NZ  = 2
codeList = []
def encodeReg(reg):
    return (reg&0xFF) if type(reg) == int else ((((reg[0]&3)+1)*0x40) | (reg[1]&0xF))
def MOVI(target, value):
    codeList.extend([0x01, encodeReg(target), (value&0xFF), ((value>>8)&0xFF)])
def MOV(target, reg1):
    codeList.extend([0x02, encodeReg(target), encodeReg(reg1), 0x00])
def XOR(target, reg1, reg2):
    codeList.extend([0x03, encodeReg(target), encodeReg(reg1), encodeReg(reg2)])
def AND(target, reg1, reg2):
    codeList.extend([0x04, encodeReg(target), encodeReg(reg1), encodeReg(reg2)])
def HEX(value, reg1, reg2): #?
    codeList.extend([0x05, value&0xFF, encodeReg(reg1), encodeReg(reg2)])
def SFLAG(reg, value):
    codeList.extend([0x06, encodeReg(reg), (value&0xFF), ((value>>8)&0xFF)])
def RET():
    codeList.extend([0x07, 0x00, 0x00, 0x00])
def CMP(reg1, reg2):
    codeList.extend([0x08, encodeReg(reg1), encodeReg(reg2), 0x00])
def J(condition, offset): 
    codeList.extend([0x09, condition&0xFF ,(offset&0xFF), 0x00])
def JMP(address):
    codeList.extend([0x0A, (address&0xFF), ((address>>8)&0xFF), ((address>>16)&0xFF)])
def ADD(target, reg1, reg2):
    codeList.extend([0x0B, encodeReg(target), encodeReg(reg1), encodeReg(reg2)])
def SUB(target, reg1, reg2):
    codeList.extend([0x0C, encodeReg(target), encodeReg(reg1), encodeReg(reg2)])
def OR(target, reg1, reg2):
    codeList.extend([0x0D, encodeReg(target), encodeReg(reg1), encodeReg(reg2)])
def NOP():
    codeList.extend([0x0E, 0x00, 0x00, 0x00])
def NOP2():
    codeList.extend([0x0F, 0x00, 0x00, 0x00])
def ADDI(target, value):
    codeList.extend([0x10, encodeReg(target), (value&0xFF), ((value>>8)&0xFF)])
def SUBI(target, value):
    codeList.extend([0x11, encodeReg(target), (value&0xFF), ((value>>8)&0xFF)])
def ANDI(target, value):
    codeList.extend([0x12, encodeReg(target), (value&0xFF), ((value>>8)&0xFF)])
def ORI(target, value):
    codeList.extend([0x13, encodeReg(target), (value&0xFF), ((value>>8)&0xFF)])
def XORI(target, value):
    codeList.extend([0x14, encodeReg(target), (value&0xFF), ((value>>8)&0xFF)])
def CMPI(reg, value):
    codeList.extend([0x15, encodeReg(reg), (value&0xFF), ((value>>8)&0xFF)])
def SIGSEGV():
    codeList.extend([0x40, 0x00, 0x00, 0x00])
    
#Hardcode your picoASM here
#example: MOVI(R1, 0x13)
#         MOV(R2, (RD1, 0x1))
def hardcoded_picoasm():
    #Code here
    return
#End of hardcoded picoASM

print("Creating new binary...")
prefix = [0x4C,0x43,0x42,0x31,0x31,0x31,0x31,0x31,0x0A,0x00,0x00,0x00]
middle = []
codeList = []
hardcoded_picoasm()
middle += codeList
if len(middle) > 0x3F4:
    print("Code to long! %d/1012" % len(middle))
else:
    middle = middle + ([0x00] * (0x3F4-len(middle)))
suffix = ([0x31] * 0xA) + ([0x00]*0x3F6)
code = prefix + middle + suffix
code = ''.join([chr(i) for i in code])
file = open("lcb.boot", "wb")
file.write(code)
file.close()
print("Done creating binary.")

##Last few words

No clue how you read this far without sleeping in but thanks for reading my writeup! :smiley:
Also thanks again to @0x00pf for creating this fun challenge!

11 Likes

Thanks for this Leeky! I had given up on this challenge a while ago. I didn’t realize that the lcb.boot file was also in the image and made my own file with the “LCB” string in it :joy: I’ll be extra careful for the next challenge tho.

2 Likes

Excellent write-up @Leeky.

Extra Kudos for the 2 bytes patch

2 Likes

@0x00_Jinx Yeah I did this as well. I got pretty far but I got stuck on vm_run. I saw the hint about signals but I never even bothered about it, pretty ignorant of me :sweat_smile:. This challenge was exceptionally well designed and had a lot of depth to it, please continue creating these wonderful programs like this. I want to thank @0x00pf for creating this challenge and @Leeky for showing what I wasn’t able to find myself.

2 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.