#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?
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
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
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!
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!
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!
Also thanks again to @0x00pf for creating this fun challenge!