247CTF - Several Reversing challenges
The Flag Bootloader
I found this challenge linked on the ReverseEngineering subreddit.
HxD => shows some strings:
- “Unlock code:”
- “Invalid code!”
- “247CTF{w!g0
5 xy.. rpu)+.\!pbce
…;;}”
Last one looks like a flag, but probably encrypted/encoded.
Ghidra doesn’t recognize the language => just select x86.
IDA does a significant better job though.
file .\flag.com
.\flag.com: DOS/MBR boot sector
Really a mbr bootloader (duh)
qemu to emulate whatever that is
.\qemu-system-x86_64.exe -drive format=raw,file=.\flag.com
We get this prompt
Booting from Hard Disk...
Unlock code:
Let’s debug this:
Just stole this command from here
qemu-system-x86_64 -s -S -m 512 -fda flag.com
Now attach gdb and break at the beginning
gdb
target remote localhost:1234
break *0x7c00
c
sub_10108
prints stuff
seg000:0115 int 10h ; - VIDEO - WRITE CHARACTER AND ADVANCE CURSOR (TTY WRITE)
; AL = character, BH = display page (alpha modes)
; BL = foreground color (graphics modes)
break *7c1d
sub_10102
reads keystrokes
seg000:0102 sub_10102 proc near ; CODE XREF: start+3A↓p
seg000:0102 mov ax, 1000h
seg000:0105 int 16h ; KEYBOARD - GET ENHANCED KEYSTROKE (AT model 339,XT2,XT286,PS)
seg000:0105 ; Return: AH = scan code, AL = character
seg000:0107 retn
seg000:0107 sub_10102 endp
Where al is the ascii char of the keystroke.
Using these two informations:
seg000:0137 loc_10137:
seg000:0137 mov si, 7DE6h
seg000:013A call sub_10102
seg000:013D mov ds:7DE6h, al
seg000:0140 call sub_10108
seg000:0143 mov bx, ds:7DEAh
seg000:0147 cmp bx, 7DFDh
seg000:014B jz loc_10284
seg000:014F mov [bx], al
seg000:0151 add bx, 1
seg000:0154 mov ds:7DEAh, bx
seg000:0158 cmp byte ptr ds:7DE6h, 0Dh
seg000:015D jnz short loc_10137
This is the read & write loop. And we can see that the input is saved into 0x7DE6
If we input more than 18 characters we get the message No memory!
.
Which we can see here (0x7DEA
is the input length).
seg000:0143 mov bx, ds:7DEAh
seg000:0147 cmp bx, 7DFDh
seg000:014B jz loc_10284
seg000:014F mov [bx], al
seg000:0151 add bx, 1
seg000:0154 mov ds:7DEAh, bx
We can see the exit jump here:
seg000:0158 cmp byte ptr ds:7DE6h, 0Dh
seg000:015D jnz short loc_10137
seg000:015F mov si, 7DD5h
seg000:0162 call sub_10108
seg000:0165 call sub_1016A
Checks 0x7DE6
for 0x0D
(which is the ascii code for enter).
sub_1016A
is the check function, which basically consists of this:
seg000:016B mov bx, 7DECh
seg000:016E mov si, 7DAAh
seg000:0171 add si, 7
seg000:0174 mov al, 4Bh
seg000:0176 xor al, 0Ch
seg000:0178 cmp [bx], al
seg000:017A jnz loc_1027C
Checks the character with an xor’ed value and quits if it’s not equal.
There is also sometimes a sneaky sub al,X
in there.
As the program uses not the user input to xor the key, there is no need for us to input that.
I just added break points at all the checks and changed the z flag there (to skip the jump).
Breakpoint list
break *0x7c7a
break *0x7c8b
break *0x7c9c
break *0x7cad
break *0x7cbe
break *0x7ccf
break *0x7ce0
break *0x7cf1
break *0x7d02
break *0x7d11
break *0x7d20
break *0x7d2f
break *0x7d3e
break *0x7d4d
break *0x7d5c
break *0x7d6b
To set the z flag:
(gdb) set $eflags |= (1 << 6)
If we’re at 0x7D74
, we can print the flag with
(gdb) x/s 0x7DAA
Or we just enter c
and let the program print it out (can’t copy though).
The more the merrier
file the_more_the_merrier
the_more_the_merrier: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=0f750d638337391328fa7432dd362189de908c1e, stripped
./the_more_the_merrier
Nothing to see here..
=> xref “Nothing to see here..” =>
undefined8 NothingHere(void)
{
puts("Nothing to see here..");
return 0;
}
Mhh this is the main function as we see in entry:
void entry(undefined8 param_1,undefined8 param_2,undefined8 param_3)
{
undefined8 in_stack_00000000;
undefined auStack8 [8];
__libc_start_main(NothingHere,in_stack_00000000,&stack0x00000008,FUN_00100660,&DAT_001006d0,
param_3,auStack8);
do {
} while( true );
}
The init function is empty too (FUN_00100660
).
If we take a look at the assembly in main():
LEA RAX,[DAT_001006e8]
MOV qword ptr [RBP + local_10],RAX=>DAT_001006e8
LEA RDI,[s_Nothing_to_see_here.._0010078c]
CALL puts
MOV EAX,0x0
LEAVE
RET
Huh? What’s up with DAT_001006e8
?
Let’s look at it:
DAT_001006e8
?? 32h 2
?? 00h
?? 00h
?? 00h
?? 34h 4
?? 00h
?? 00h
?? 00h
?? 37h 7
?? 00h
?? 00h
?? 00h
?? 43h C
?? 00h
?? 00h
?? 00h
?? 54h T
?? 00h
?? 00h
?? 00h
?? 46h F
?? 00h
?? 00h
?? 00h
?? 7Bh {
That looks like the flag…
Let’s just copy it, use some vscode multi cursor shit and get the flag.
Encrypted Password
file encrypted_password
encrypted_password: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e66644a82644c5ac6ab7a507cc5c6e84432ee0ad, stripped
The bytes
local_a8 = 0x3930343965353738;
local_a0 = 0x3861623131383966;
local_98 = 0x3665656562303635;
local_90 = 0x3264373763306266;
local_88 = 0;
local_78 = 0x5a53010106040309;
local_70 = 0x5c585354500a5b00;
local_68 = 0x555157570108520d;
local_60 = 0x5707530453040752;
local_58 = 0;
local_a8 can be converted to the string 875e9409f9811ba8560beee6fb0c77d2
.
The xor loop
i = 0;
while( true ) {
len = strlen((char *)&local_a8);
if (len <= (ulong)(long)i) break;
*(byte *)((long)&local_78 + (long)i) =
*(byte *)((long)&local_78 + (long)i) ^ *(byte *)((long)&local_a8 + (long)i);
i = i + 1;
}
Finally a strcmp
puts("Enter the secret password:");
fgets(local_48,0x21,stdin);
iVar1 = strcmp(local_48,(char *)&local_78);
if (iVar1 == 0) {
printf("You found the flag!\n247CTF{s}\n",&local_78);
}
A few options:
- Xor the bytes ourselves
- Print the flag after the xor (or get it through debugging)
- Just patch the strcmp check
Xor’ing ourself
Use 875e9409f9811ba8560beee6fb0c77d2
and xor with the bytes.
Watch out, as you need to reverse the bytes line-by-line (or just use the bytes directly and reverse afterwards).
“Sniffing” the password
While doing this I found out you can use
(gdb) layout asm
to get a really nice overview
gdb ./encrypted_password
(gdb) run
Ctrl + C
(gdb) info files
(gdb) break *0x8000700 # this is the entry point
In the asm we then can see
0x800071d lea 0xe6(%rip),%rdi # 0x800080a
Which is the main address.
Let’s break there
break *0x800080a
c
Let’s break right after the xor loop:
001008fe 48 8d 3d LEA RDI,[s_Enter_the_secret_password:_00100a48]
So at 0x80008fe
If we want to see the registers, we can disable TUI mode
tui disable
info regs
or enable the register layout
layout regs
At the end of each loop the string is saved
MOV byte ptr [RBP + RAX*0x1 + -0x70],DL
ADD dword ptr [RBP + i],0x1
Let’s look at rbp (which is where variables are saved) - 0x70:
(gdb) x/s $rbp - 0x70
0x7ffffffedcd0: "141c85XXXXXXXXXXXXXXXXXXXXXXXXXX"
x displays the address, s shows it as a string, with $ we can access the registers
Patching
Just change
00100935 85 c0 TEST len,len
00100937 75 18 JNZ LAB_00100951
00100939 48 8d 45 90 LEA len=>local_78,[RBP + -0x70]
to
00100937 74 18 JZ LAB_00100951
Run the program and enter any key that doesn’t match the original key.
Secret Lock
We get a html file?
It’s a fancy lock thingy.
We have no important external js.
onChange() {
this.code = this.getCode();
this.flag = this.checkFlag(this.code);
this.dom.status.textContent = this.flag;
}
Okay getCode:
getCode() {
let flag = {};
for (let i = 0, len = this.dom.rows.length; i < len; i++) {
flag[i] = this.dom.rows[i].querySelector('.is-selected .text').textContent;
}
return flag;
}
If we place a console.log(flag)
in there, we can see a object (I changed the second row)
Object { 0: "0", 1: "1", 2: "0", 3: "0", 4: "0", 5: "0", 6: "0", 7: "0", 8: "0", 9: "0", … }
Let’s look at checkFlag:
checkFlag(flag) {
let result = "LOCKED"
this.dom.lock.classList.remove('verified');
if (Object.keys(flag).length == 40 && ((flag[37] - flag[37]) * flag[15] == 0) && ...) {
result = "";
for (var idx in flag) {
result += (String.fromCharCode(flag[idx]));
}
this.dom.lock.classList.add('verified');
}
return result;
}
The if condition is a long boi (I cut out most of it).
Okay how do we solve this?
We know that
- the string is 40 chars long => 247CTF{}
- the numbers are ascii chars => 48 - 57, 65 - 90, 97 - 122
Let’s first get the ascii codes of the ones we know
for (let b of "247CTF{}") { console.log(b.charCodeAt(0)) }
50 52 55 67 84 70 123 125
Okay I tried solving it by hand, but only got like 2 values quickly.
Then I tried bruteforcing. Not my greatest idea as this takes way too long (~60 chars, 40 length).
Then I remembered something about like a solver for that shit.
Introducing: z3
z3? Wasn’t that this old calculator from some Zuse guy?
z3 is a theorem solver by Microsoft and it’s really easy to use and perfect for this kind of work.
To install the python bindings:
pip install z3-solver
Then just create a Solver with
from z3 import *
s = Solver()
Add some conditions with
x = Bool('x')
y = Bool('y')
s.add(x == y)
And let others do the hard work
if s.check == sat:
print(s.model())
And you get the model/solution.
You need to use those z3 types.
We actually need to use a 8-bit BitVec here, because infinite precision numbers can’t be xored.
flag = []
for i in range(40):
flag.append(BitVec(f'flag[{i}]', 8))
I just added conditions like this to set the values.
s.add(flag[0] == 50)
Then I could just replace the if’s && with \n and paste it into python.
After that use
lock.checkFlag(JSON.parse(`model json string here`))
in the browser console to get the flag.
Keygen
The challenge is also hosted online, so you can’t just patch the check or something like that.
Let’s start with main
:
undefined8 main(void)
{
puts("Enter a valid product key to gain access to the flag:");
fgets((char *)input,0x80,stdin);
len = strcspn((char *)input,"\n");
*(undefined *)((long)input + len) = 0;
iVar1 = Verify(input);
if (iVar1 == 0) {
puts("Invalid product key!");
}
else {
puts("Valid product key!");
printFlag();
}
Okay Verify:
- Length of 0x20/32
len = strlen(key); if (len == 0x20) { i = 0;
- Only chars between @ and Z (including both)
while (len = strlen(key), (ulong)(long)i < len) { if ((key[i] < '@') || ('Z' < key[i])) { return 0; } i = i + 1; }
- Adds all chars (except the first one) to total after calling weird func on them
total = 0xf7; j = 1; while (len = strlen(key), (ulong)(long)j < len) { iVar1 = Add0xB1orB5((ulong)(uint)(int)key[j]); total = total + (iVar1 - j) + 0xf7; j = j + 1; }
- First char through weird function == 0xf7 => ‘B’
iVar1 = Add0xB1orB5((ulong)(uint)(int)*key); if ((total % 0xf8 == iVar1) && (total % 0xf8 == 0xf7)) { uVar2 = 1; } else { uVar2 = 0; }
It wasn’t even necessary to write a keygen script because the check is so simple that you can change the chars by hand…
To debug:
gdb ./flag_keygen
break *0x8001243 (strlen)
break *0x80012b5 (before total calc)
I used gdb, to find out that the total calc starts at j = 1…
After you get a valid product key, use nc to connect and enter the key
nc XXXXXXXXXXXXXX.247ctf.com XXXXX
Enter a valid product key to gain access to the flag:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Valid product key!
247CTF{flag}
Bonus
The z3 solve code:
from z3 import *
import json
s = Solver()
flag = []
for i in range(40):
flag.append(BitVec(f'flag[{i}]', 8))
s.add(flag[0] == 50)
s.add(flag[1] == 52)
s.add(flag[2] == 55)
s.add(flag[3] == 67)
s.add(flag[4] == 84)
s.add(flag[5] == 70)
s.add(flag[6] == 123)
s.add(flag[39] == 125)
s.add((flag[37] - flag[37]) * flag[15] == 0)
s.add((flag[3] + flag[31]) ^ (flag[29] + flag[8]) == 234)
s.add((flag[32] - flag[12]) * flag[9] == -2332)
s.add((flag[24] - flag[27] + flag[13]) ^ flag[6] == 114)
s.add((flag[38] - flag[15]) * flag[33] == 800)
s.add((flag[34] - flag[21]) * flag[26] == 98)
s.add((flag[29] + flag[0]) ^ (flag[8] + flag[38]) == 248)
s.add((flag[21] * flag[18]) ^ (flag[7] - flag[15]) == 2694)
s.add((flag[28] * flag[23]) ^ (flag[19] - flag[5]) == -9813)
s.add((flag[34] + flag[30]) ^ (flag[37] + flag[6]) == 72)
s.add((flag[23] - flag[22]) * flag[12] == 4950)
s.add((flag[9] * flag[28]) ^ (flag[20] - flag[11]) == 5143)
s.add((flag[2] * flag[22]) ^ (flag[37] - flag[0]) == 2759)
s.add((flag[26] - flag[12]) * flag[3] == -3350)
s.add((flag[35] * flag[0]) ^ (flag[23] - flag[21]) == 2698)
s.add((flag[20] + flag[31]) ^ (flag[5] + flag[10]) == 22)
s.add((flag[31] * flag[19]) ^ (flag[1] - flag[2]) == -2655)
s.add((flag[38] - flag[14]) * flag[18] == 55)
s.add((flag[29] - flag[19] + flag[10]) ^ flag[2] == 93)
s.add((flag[13] - flag[25] + flag[30]) ^ flag[29] == 13)
s.add((flag[35] + flag[33]) ^ (flag[26] + flag[21]) == 249)
s.add((flag[17] + flag[24]) ^ (flag[34] + flag[1]) == 253)
s.add((flag[32] - flag[35] + flag[19]) ^ flag[1] == 0)
s.add((flag[22] - flag[11] + flag[3]) ^ flag[31] == 113)
s.add((flag[19] - flag[0]) * flag[13] == 108)
s.add((flag[19] - flag[17]) * flag[14] == -2475)
s.add((flag[31] - flag[35] + flag[16]) ^ flag[19] == 84)
s.add((flag[24] * flag[27]) ^ (flag[35] - flag[17]) == -5792)
s.add((flag[11] * flag[35]) ^ (flag[15] - flag[28]) == -2845)
s.add((flag[18] - flag[19] + flag[31]) ^ flag[5] == 112)
s.add((flag[20] - flag[6]) * flag[10] == -3933)
s.add((flag[39] - flag[33]) * flag[6] == 3075)
s.add((flag[22] + flag[1]) ^ (flag[39] + flag[14]) == 211)
s.add((flag[37] * flag[24]) ^ (flag[12] - flag[39]) == -5726)
s.add((flag[29] + flag[3]) ^ (flag[8] + flag[11]) == 195)
s.add((flag[26] * flag[7]) ^ (flag[10] - flag[17]) == -2375)
s.add((flag[11] - flag[12]) * flag[12] == -4653)
s.add((flag[13] * flag[5]) ^ (flag[12] - flag[25]) == 3829)
s.add((flag[24] * flag[0]) ^ (flag[13] - flag[23]) == -2829)
s.add((flag[17] + flag[12]) ^ (flag[8] + flag[14]) == 170)
s.add((flag[38] + flag[23]) ^ (flag[11] + flag[1]) == 245)
s.add((flag[22] + flag[5]) ^ (flag[21] + flag[24]) == 19)
s.add((flag[35] - flag[8] + flag[21]) ^ flag[30] == 85)
s.add((flag[18] - flag[31] + flag[28]) ^ flag[29] == 0)
s.add((flag[30] * flag[35]) ^ (flag[27] - flag[29]) == 5501)
s.add((flag[8] - flag[30] + flag[16]) ^ flag[36] == 81)
s.add((flag[13] * flag[18]) ^ (flag[35] - flag[38]) == -2971)
s.add((flag[27] - flag[14]) * flag[39] == 5875)
s.add((flag[34] - flag[33]) * flag[6] == -6027)
s.add((flag[38] * flag[1]) ^ (flag[20] - flag[10]) == -2915)
s.add((flag[1] - flag[1]) * flag[3] == 0)
s.add((flag[36] - flag[20]) * flag[8] == 2640)
s.add((flag[23] - flag[11] + flag[17]) ^ flag[33] == 246)
s.add((flag[13] - flag[38]) * flag[0] == -100)
s.add((flag[28] - flag[14]) * flag[31] == 2142)
s.add((flag[26] + flag[15]) ^ (flag[13] + flag[31]) == 8)
s.add((flag[36] - flag[15]) * flag[17] == 5238)
s.add((flag[16] - flag[30]) * flag[33] == 0)
s.add((flag[2] - flag[20] + flag[13]) ^ flag[6] == 76)
s.add((flag[10] - flag[14] + flag[31]) ^ flag[13] == 3)
s.add((flag[0] * flag[10]) ^ (flag[14] - flag[31]) == 2854)
s.add((flag[28] - flag[34] + flag[14]) ^ flag[14] == 82)
s.add((flag[28] - flag[25]) * flag[1] == 2444)
s.add((flag[34] - flag[12]) * flag[25] == -2400)
s.add((flag[28] * flag[38]) ^ (flag[17] - flag[4]) == 5429)
s.add((flag[21] - flag[21] + flag[26]) ^ flag[23] == 84)
s.add((flag[9] - flag[4] + flag[18]) ^ flag[35] == 47)
s.add((flag[28] - flag[21] + flag[1]) ^ flag[33] == 0)
s.add((flag[24] - flag[25] + flag[22]) ^ flag[0] == 8)
s.add((flag[28] - flag[25]) * flag[12] == 4653)
s.add((flag[1] * flag[15]) ^ (flag[10] - flag[8]) == 2498)
s.add((flag[5] * flag[7]) ^ (flag[15] - flag[34]) == -3429)
s.add((flag[8] * flag[3]) ^ (flag[23] - flag[22]) == 3671)
s.add((flag[25] - flag[33]) * flag[11] == -2600)
s.add((flag[21] + flag[12]) ^ (flag[37] + flag[28]) == 81)
s.add((flag[30] + flag[33]) ^ (flag[34] + flag[14]) == 162)
s.add((flag[6] - flag[25]) * flag[8] == 4015)
s.add((flag[24] - flag[7] + flag[12]) ^ flag[7] == 90)
s.add((flag[18] * flag[12]) ^ (flag[8] - flag[4]) == -5466)
s.add((flag[32] * flag[7]) ^ (flag[32] - flag[27]) == -2730)
s.add((flag[32] * flag[34]) ^ (flag[29] - flag[16]) == 2804)
s.add((flag[25] * flag[22]) ^ (flag[28] - flag[39]) == -2542)
s.add((flag[8] - flag[15]) * flag[6] == 861)
s.add((flag[20] + flag[18]) ^ (flag[25] + flag[36]) == 245)
s.add((flag[5] - flag[28] + flag[14]) ^ flag[39] == 97)
s.add((flag[30] * flag[11]) ^ (flag[16] - flag[11]) == 5216)
s.add((flag[11] + flag[18]) ^ (flag[7] + flag[9]) == 13)
s.add((flag[9] - flag[2]) * flag[30] == -200)
s.add((flag[12] + flag[37]) ^ (flag[9] + flag[4]) == 78)
s.add((flag[10] - flag[37]) * flag[38] == -2408)
s.add((flag[5] * flag[19]) ^ (flag[20] - flag[21]) == 3645)
s.add((flag[27] * flag[29]) ^ (flag[39] - flag[21]) == 10354)
s.add((flag[15] * flag[32]) ^ (flag[7] - flag[22]) == -2642)
s.add((flag[1] - flag[3] + flag[24]) ^ flag[31] == 25)
s.add((flag[13] - flag[0]) * flag[30] == 400)
s.add((flag[18] - flag[15] + flag[36]) ^ flag[28] == 12)
s.add((flag[34] + flag[21]) ^ (flag[12] + flag[37]) == 163)
s.add((flag[36] - flag[33]) * flag[14] == 110)
s.add((flag[2] - flag[3]) * flag[3] == -804)
s.add((flag[35] - flag[27] + flag[22]) ^ flag[4] == 80)
s.add((flag[10] + flag[9]) ^ (flag[17] + flag[2]) == 246)
s.add((flag[25] * flag[4]) ^ (flag[27] - flag[23]) == 4201)
s.add((flag[32] * flag[19]) ^ (flag[3] - flag[25]) == 2877)
s.add((flag[37] - flag[14]) * flag[23] == 4545)
s.add((flag[32] + flag[13]) ^ (flag[31] + flag[32]) == 7)
s.add((flag[11] - flag[25]) * flag[39] == 250)
s.add((flag[17] + flag[31]) ^ (flag[6] + flag[9]) == 36)
s.add((flag[4] + flag[27]) ^ (flag[2] + flag[31]) == 208)
s.add((flag[6] + flag[7]) ^ (flag[26] + flag[21]) == 206)
s.add((flag[19] + flag[25]) ^ (flag[22] + flag[10]) == 10)
s.add((flag[34] + flag[2]) ^ (flag[8] + flag[26]) == 2)
s.add((flag[7] + flag[5]) ^ (flag[12] + flag[14]) == 237)
s.add((flag[1] - flag[13]) * flag[38] == -112)
s.add((flag[0] - flag[19] + flag[16]) ^ flag[0] == 80)
s.add((flag[31] + flag[36]) ^ (flag[3] + flag[2]) == 227)
s.add((flag[32] - flag[3] + flag[26]) ^ flag[4] == 113)
s.add((flag[3] * flag[6]) ^ (flag[16] - flag[27]) == -8241)
s.add((flag[24] + flag[15]) ^ (flag[2] + flag[30]) == 242)
s.add((flag[11] + flag[21]) ^ (flag[31] + flag[20]) == 12)
s.add((flag[9] - flag[26] + flag[23]) ^ flag[30] == 13)
obj = {}
if s.check() == sat:
model = s.model()
for m in model:
obj[int(str(m)[5:-1])] = int(str(model[m]))
print(json.dumps(obj))
Key check script:
# 0x20/32 chars
# between @ and Z
# first char + 0xb5 || char + 0xb1 == f7
def fuck(input):
if input < 'N':
return ord(input) + 0xb5
else:
return ord(input) + 0xb1
def checkInRange(key):
for c in key:
if c < '@' or c > 'Z':
return False
return True
def getTotal(key):
total = 0xf7
for i in range(1, len(key)):
total = total + (fuck(key[i]) - i) + 0xf7
return total
key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
if len(key) != 0x20:
print("wrong key length")
exit(0)
if not checkInRange(key):
print("some char out of range (@ - Z)")
exit(0)
total = getTotal(key)
print(f"Total: {total}")
if total % 0xf8 != fuck(key[0]) or total % 0xf8 != 0xf7:
print("not valid")
print(f"{total} % 0xf8 = {total % 0xf8} != {0xf7}")
exit(0)
print("valid")