hsctf 2019 A-Byte
Let's take a look at the binary:
$ file a-byte
a-byte: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=88fe0ee8aed1a070d6555c7e9866e364a40f686c, stripped
$ ./a-byte 159
u do not know da wae
So we can see that we are dealing with a 64
bit function, that takes in data by passing arguments to the program. Looking through the functions, we find FUN_0010073a
which appears to hold most of the code that is relevant to us.
undefined8 FUN_0010073a(int argc,long argv)
{
long lVar1;
int iVar2;
undefined8 uVar3;
size_t inputLen;
long in_FS_OFFSET;
int i;
char desiredOutput;
char *inputPtr;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
if (argc == 2) {
inputPtr = *(char **)(argv + 8);
inputLen = strlen(inputPtr);
if ((int)inputLen == 0x23) {
i = 0;
while (i < 0x23) {
inputPtr[(long)i] = inputPtr[(long)i] ^ 1;
i = i + 1;
}
desiredOutput = 'i';
iVar2 = strcmp(&desiredOutput,inputPtr);
if (iVar2 == 0) {
puts("Oof, ur too good");
uVar3 = 0;
goto LAB_00100891;
}
}
}
puts("u do not know da wae");
uVar3 = 0xffffffff;
LAB_00100891:
if (lVar1 == *(long *)(in_FS_OFFSET + 0x28)) {
return uVar3;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
So we can see that it only wants a single argument in addition to the program name (argc has to be two). Then it checks to see if our input that we gave it via and argument is 0x23
bytes long. If so it will then go through and set all of the bytes equal to the byte xored by 1. It then checks to see if our input is equal to desiredOutput
, and if it is it looks like we solved the challenge. Looking at the decompiled code, it looks like desiredOutput
is set equal to just the character i
. The decompilation got that wrong, and looking at the assembly code shows us what it is actually set equal to:
001007d5 c6 45 d0 69 MOV byte ptr [RBP + desiredOutput],0x69
001007d9 c6 45 d1 72 MOV byte ptr [RBP + local_37],0x72
001007dd c6 45 d2 62 MOV byte ptr [RBP + local_36],0x62
001007e1 c6 45 d3 75 MOV byte ptr [RBP + local_35],0x75
001007e5 c6 45 d4 67 MOV byte ptr [RBP + local_34],0x67
001007e9 c6 45 d5 7a MOV byte ptr [RBP + local_33],0x7a
001007ed c6 45 d6 76 MOV byte ptr [RBP + local_32],0x76
001007f1 c6 45 d7 31 MOV byte ptr [RBP + local_31],0x31
001007f5 c6 45 d8 76 MOV byte ptr [RBP + local_30],0x76
001007f9 c6 45 d9 5e MOV byte ptr [RBP + local_2f],0x5e
001007fd c6 45 da 78 MOV byte ptr [RBP + local_2e],0x78
00100801 c6 45 db 31 MOV byte ptr [RBP + local_2d],0x31
00100805 c6 45 dc 74 MOV byte ptr [RBP + local_2c],0x74
00100809 c6 45 dd 5e MOV byte ptr [RBP + local_2b],0x5e
0010080d c6 45 de 6a MOV byte ptr [RBP + local_2a],0x6a
00100811 c6 45 df 6f MOV byte ptr [RBP + local_29],0x6f
00100815 c6 45 e0 31 MOV byte ptr [RBP + local_28],0x31
00100819 c6 45 e1 76 MOV byte ptr [RBP + local_27],0x76
0010081d c6 45 e2 5e MOV byte ptr [RBP + local_26],0x5e
00100821 c6 45 e3 65 MOV byte ptr [RBP + local_25],0x65
00100825 c6 45 e4 35 MOV byte ptr [RBP + local_24],0x35
00100829 c6 45 e5 5e MOV byte ptr [RBP + local_23],0x5e
0010082d c6 45 e6 76 MOV byte ptr [RBP + local_22],0x76
00100831 c6 45 e7 40 MOV byte ptr [RBP + local_21],0x40
00100835 c6 45 e8 32 MOV byte ptr [RBP + local_20],0x32
00100839 c6 45 e9 5e MOV byte ptr [RBP + local_1f],0x5e
0010083d c6 45 ea 39 MOV byte ptr [RBP + local_1e],0x39
00100841 c6 45 eb 69 MOV byte ptr [RBP + local_1d],0x69
00100845 c6 45 ec 33 MOV byte ptr [RBP + local_1c],0x33
00100849 c6 45 ed 63 MOV byte ptr [RBP + local_1b],0x63
0010084d c6 45 ee 40 MOV byte ptr [RBP + local_1a],0x40
00100851 c6 45 ef 31 MOV byte ptr [RBP + local_19],0x31
00100855 c6 45 f0 33 MOV byte ptr [RBP + local_18],0x33
00100859 c6 45 f1 38 MOV byte ptr [RBP + local_17],0x38
0010085d c6 45 f2 7c MOV byte ptr [RBP + local_16],0x7c
00100861 c6 45 f3 00 MOV byte ptr [RBP + local_15],0x0
So we can see that we are dealing with a char array on the stack, that it moves in input one byte at a time. We can see that the amount of bytes it moves in is 35
(excluding the null byte terminator at the end), the same amount for the length of the data we pass in as an argument. So we know what input we control, we know the algorithm that it is passed through, and we know what the end result will need to be. This is everything we need to make a simple Z3 script to find the solution for us:
from z3 import *
# Designate the desired output
desiredOutput = [0x69, 0x72, 0x62, 0x75, 0x67, 0x7a, 0x76, 0x31, 0x76, 0x5e, 0x78, 0x31, 0x74, 0x5e, 0x6a, 0x6f, 0x31, 0x76, 0x5e, 0x65, 0x35, 0x5e, 0x76, 0x40, 0x32, 0x5e, 0x39, 0x69, 0x33, 0x63, 0x40, 0x31, 0x33, 0x38, 0x7c]
# Designate the input z3 will have control of
inp = []
for i in xrange(0x23):
byte = BitVec("%s" % i, 8)
inp.append(byte)
z = Solver()
for i in xrange(0x23):
z.add((inp[i] ^ 1) == desiredOutput[i])
#Check if z3 can solve it, and if it can print out the solution
if z.check() == sat:
# print z
print "Condition is satisfied, would still recommend crying: " + str(z.check())
solution = z.model()
flag = ""
for i in range(0, 0x23):
flag += chr(int(str(solution[inp[i]])))
print flag
#Check if z3 can't solve it
elif z.check() == unsat:
print "Condition is not satisfied, would recommend crying: " + str(z.check())
When we run it:
$ python reverent.py
Condition is satisfied, would still recommend crying: sat
hsctf{w0w_y0u_kn0w_d4_wA3_8h2bA029}
$ ./a-byte hsctf{w0w_y0u_kn0w_d4_wA3_8h2bA029}
Oof, ur too good
Just like that, we solved the challenge!