Summarize (rev)
All you have to do is find six numbers. How hard can that be?
This challenge is a crackme, which requires us to correctly enter 6 numbers in order for the flag to be shown.
The main function given by Ghidra looks like this:
undefined8 main(void)
{
char cVar1;
long in_FS_OFFSET;
uint local_60;
uint local_5c;
uint local_58;
uint local_54;
uint local_50;
uint local_4c;
char local_48 [56];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
puts(
"To get the flag, you must correctly enter six 9-digit positive integers: a, b, c, d, e, and f ."
);
putchar(10);
printf("a = ");
__isoc99_scanf(&DAT_0040206c,&local_60);
printf("b = ");
__isoc99_scanf(&DAT_0040206c,&local_5c);
printf("c = ");
__isoc99_scanf(&DAT_0040206c,&local_58);
printf("d = ");
__isoc99_scanf(&DAT_0040206c,&local_54);
printf("e = ");
__isoc99_scanf(&DAT_0040206c,&local_50);
printf("f = ");
__isoc99_scanf(&DAT_0040206c,&local_4c);
cVar1 = check(local_60,local_5c,local_58,local_54,local_50,local_4c);
if (cVar1 == '\0') {
puts("Wrong.");
}
else {
puts("Correct.");
sprintf(local_48,"uiuctf{%x%x%x%x%x%x}",(ulong)local_60,(ulong)local_5c,(ulong)local_58,
(ulong)local_54,(ulong)local_50,(ulong)local_4c);
puts(local_48);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}Our 6 inputs are first checked using check(). If it returns 1 we have passed the check, and we obtain the flag. Looking at the check() function:
undefined4 check(uint param_1,uint param_2,uint param_3,uint param_4,uint param_5,uint param_6)
{
undefined4 uVar1;
uint uVar2;
uint uVar3;
undefined4 uVar4;
uint uVar5;
uint uVar6;
uint uVar7;
uint uVar8;
uint uVar9;
uint uVar10;
ulong uVar11;
if (((((param_1 < 0x5f5e101) || (param_2 < 0x5f5e101)) || (param_3 < 0x5f5e101)) ||
((param_4 < 0x5f5e101 || (param_5 < 0x5f5e101)))) || (param_6 < 0x5f5e101)) {
uVar1 = 0;
}
else if (((param_1 < 1000000000) && (param_2 < 1000000000)) &&
((param_3 < 1000000000 &&
(((param_4 < 1000000000 && (param_5 < 1000000000)) && (param_6 < 1000000000)))))) {
uVar1 = fun1_2(param_1,param_2);
uVar2 = fun1(uVar1,param_3);
uVar3 = fun1(param_1,param_2);
uVar1 = fun2(2,param_2);
uVar4 = fun2(3,param_1);
uVar5 = fun1_2(uVar4,uVar1);
uVar6 = fun3(param_1,param_4);
uVar1 = fun1(param_3,param_1);
uVar7 = fun4(param_2,uVar1);
uVar11 = fun1(param_2,param_4);
uVar1 = fun1(param_4,param_6);
uVar8 = fun3(param_3,uVar1);
uVar9 = fun1_2(param_5,param_6);
uVar10 = fun1(param_5,param_6);
if ((((uVar2 % 0x10ae961 == 0x3f29b9) && (uVar3 % 0x1093a1d == 0x8bdcd2)) &&
((uVar5 % uVar6 == 0x212c944d &&
((uVar7 % 0x6e22 == 0x31be && ((int)((uVar11 & 0xffffffff) % (ulong)param_1) == 0x2038c4 3c)
))))) &&
((uVar8 % 0x1ce628 == 0x1386e2 &&
((uVar9 % 0x1172502 == 0x103cf4f && (uVar10 % 0x2e16f83 == 0x16ab0d7)))))) {
uVar1 = 1;
}
else {
uVar1 = 0;
}
}
else {
uVar1 = 0;
}
return uVar1;
}Let's look at fun1_2 and fun1 first.
void fun1_2(undefined4 param_1,int param_2)
{
fun1(param_1,-param_2);
return;
}
long fun1(uint param_1,uint param_2)
{
byte bVar1;
uint uVar2;
uint uVar3;
uint local_30;
uint local_2c;
uint local_20;
long local_10;
local_10 = 0;
local_20 = 0;
bVar1 = 0;
local_2c = param_1;
for (local_30 = param_2; (local_2c != 0 || (local_30 != 0)); local_30 = local_30 >> 1) {
uVar2 = local_2c & 1;
uVar3 = local_30 & 1;
local_2c = local_2c >> 1;
local_10 = local_10 + (ulong)((uVar2 ^ uVar3 ^ local_20) << (bVar1 & 0x1f));
local_20 = uVar3 & local_20 | uVar2 & (uVar3 | local_20);
bVar1 = bVar1 + 1;
}
return local_10 + ((ulong)local_20 << (bVar1 & 0x3f));
}fun1_2 is simply a wrapper for fun1, except that the second parameter is negated before calling fun1.
local_2c and local_30 are initialised to be param_1 and param_2 respectively at first. Then, with each iteration of the for loop, they are each right shifted by 1 bit. bVar1 is also incremented with each iteration. The loop stops if both of them are equivalent to 0. Within the loop, uVar2 is the rightmost bit of local_2c, while uVar3 is the rightmost bit of local_30. Then, local_10 is incremented by (uVar2 ^ uVar3 ^ local_20) << (bVar1 & 0x1f). Notice that (uVar2 ^ uVar3 ^ local_20) only evaluates to 1 if all 3 variables are 1, or only one of them is 1. This looks like how the result bit is obtained from addition of integers!
Let's think about the next line, local_20 = (uVar3 & local_20) | uVar2 & (uVar3 | local_20). In what scenarios would local_20 be set to 1?
Case 1:
local_20=1.(uVar3 & local_20)evaluates touVar3, and(uVar3 | local_20)evaluates to1here, so we are left withuVar3 | uVar2.Case 2:
local_20=0.(uVar3 & local_20)evaluates to0and(uVar3 | local_20)evaluates touVar3. So we are left withuVar2 & uVar3.
So if local_20 was already 1, as long as either uVar3 or uVar2 is 1, local_20 will be 1. If local_20 was 0, it will become 1 only if both uVar2 and uVar3 are 1. This reminds us of the carry bit during addition of integers! This can be further confirmed by the fact that local_20 is added to local_10 as the most significant bit once the loop exits.
With the above observations, we can deduce that fun1 is essentially doing integer addition of param_1 and param_2 . This means that fun1_2 would be doing subtraction of param_2 from param_1.
Now let's look at fun2.
int fun2(uint param_1,int param_2)
{
byte bVar1;
uint local_1c;
int local_14;
local_14 = 0;
bVar1 = 0;
for (local_1c = param_1; local_1c != 0; local_1c = local_1c >> 1) {
local_14 = local_14 + (param_2 << (bVar1 & 0x1f)) * (local_1c & 1);
bVar1 = bVar1 + 1;
}
return local_14;
}This looks similar to fun1 in that it is performing some bitwise operation, but our carry flag is gone. We are using param_1's bits, but param_2 is used without extracting its bits. Our loop is iterating through the bits in param_1 starting from the rightmost bit. Let's run through an example. Say param_1 = 2 = 0b10 and param_2 = 3 = 0b11.
In the first iteration, local_1c & 1 = 0, so local_14 remains as 0.
In the second iteration, bVar1 = 1. So param_2 is left-shifted by 1 bit, and this time local_1c & 1 = 1. So we get local_14 = local_14 + (3 << 1) * 1).
The result we get is 6. Notice that this is the result of doing 2 * 3!
In general, if bit i of param_1 = 1, then local_14 = local_14 + (param_2 << i), which is equivalent to local_14 = local_14 + param_2 * 2^i.
In another example if param_1 = 3 = 0b11 and param_2 = x, then we would get a result of x * 2^0 + x * 2^1 = x * 1 + x * 2 = param_2 * 3. This should convince you that fun2 is simply a multiplication function!
Now let's look at fun3.
int fun3(uint param_1,uint param_2)
{
byte bVar1;
uint uVar2;
uint local_20;
uint local_1c;
int local_18;
local_18 = 0;
bVar1 = 0;
local_1c = param_1;
for (local_20 = param_2; (local_1c != 0 || (local_20 != 0)); local_20 = local_20 >> 1) {
uVar2 = local_1c & 1;
local_1c = local_1c >> 1;
local_18 = local_18 + ((uVar2 ^ local_20 & 1) << (bVar1 & 0x1f));
bVar1 = bVar1 + 1;
}
return local_18;
}fun3 seems very similar to fun1, except that our carry bit is gone and now the bit being added in each iteration is given by (uVar2 ^ local_20 & 1). This is essentially an XOR operation between the bit we obtained from param_1 and the bit we obtained from param_2. So fun3 is an XOR function.
Moving on to fun4:
int fun4(uint param_1,uint param_2)
{
byte bVar1;
uint uVar2;
uint local_20;
uint local_1c;
int local_18;
local_18 = 0;
bVar1 = 0;
local_1c = param_1;
for (local_20 = param_2; (local_1c != 0 || (local_20 != 0)); local_20 = local_20 >> 1) {
uVar2 = local_1c & 1;
local_1c = local_1c >> 1;
local_18 = local_18 + ((uVar2 & local_20 & 1) << (bVar1 & 0x1f));
bVar1 = bVar1 + 1;
}
return local_18;
}It is similar to fun3, except that the bit being added is (uVar2 & local_20 & 1). That is the AND operation between the bit obtained from param_1 and the bit obtained from param_2. So fun4 is performing an AND operation on param_1 and param_2.
Looking back at the restrictions imposed by main(),
if (((((param_1 < 0x5f5e101) || (param_2 < 0x5f5e101)) || (param_3 < 0x5f5e101)) ||
((param_4 < 0x5f5e101 || (param_5 < 0x5f5e101)))) || (param_6 < 0x5f5e101)) {
uVar1 = 0;
}
else if (((param_1 < 1000000000) && (param_2 < 1000000000)) &&
((param_3 < 1000000000 &&
(((param_4 < 1000000000 && (param_5 < 1000000000)) && (param_6 < 1000000000)))))) {
uVar1 = fun1_2(param_1,param_2);
uVar2 = fun1(uVar1,param_3);
uVar3 = fun1(param_1,param_2);
uVar1 = fun2(2,param_2);
uVar4 = fun2(3,param_1);
uVar5 = fun1_2(uVar4,uVar1);
uVar6 = fun3(param_1,param_4);
uVar1 = fun1(param_3,param_1);
uVar7 = fun4(param_2,uVar1);
uVar11 = fun1(param_2,param_4);
uVar1 = fun1(param_4,param_6);
uVar8 = fun3(param_3,uVar1);
uVar9 = fun1_2(param_5,param_6);
uVar10 = fun1(param_5,param_6);
if ((((uVar2 % 0x10ae961 == 0x3f29b9) && (uVar3 % 0x1093a1d == 0x8bdcd2)) &&
((uVar5 % uVar6 == 0x212c944d &&
((uVar7 % 0x6e22 == 0x31be && ((int)((uVar11 & 0xffffffff) % (ulong)param_1) == 0x2038c4 3c)
))))) &&
((uVar8 % 0x1ce628 == 0x1386e2 &&
((uVar9 % 0x1172502 == 0x103cf4f && (uVar10 % 0x2e16f83 == 0x16ab0d7)))))) {
uVar1 = 1;
}Our 6 integers are restricted to be within the range (0x5f5e101, 1000000000) (which means they must all be positive and 9-digits long), and further modulo restrictions are imposed on the arithmetic results. Recall that fun1 is addition, fun1_2 is subtraction, fun2 is multiplication, fun3 is XOR, and fun4 is AND. We can use a z3 script to solve these constraints and obtain the 6 integers.
Below is my script:
from z3 import *
i1, i2, i3, i4, i5, i6 = BitVec('i1', 32), BitVec('i2', 32), BitVec('i3', 32), BitVec('i4', 32), BitVec('i5', 32), BitVec('i6', 32)
i1_cpy, i2_cpy, i3_cpy, i4_cpy, i5_cpy, i6_cpy = i1, i2, i3, i4, i5, i6
s = Solver()
s.add(And(i1_cpy >= 0x5f5e101, i2_cpy >= 0x5f5e101,i3_cpy >= 0x5f5e101,i4_cpy >= 0x5f5e101,i5_cpy >= 0x5f5e101))
s.add(And(i1_cpy < 1000000000, i2_cpy < 1000000000,i3_cpy < 1000000000,i4_cpy < 1000000000,i5_cpy < 1000000000))
uVar1 = i1_cpy - i2_cpy
uVar2 = uVar1 + i3_cpy
uVar3 = i1_cpy + i2_cpy
uVar1 = 2 * i2_cpy
uVar4 = 3 * i1_cpy
uVar5 = uVar4 - uVar1
uVar6 = i1_cpy ^ i4_cpy
uVar1 = i3_cpy + i1_cpy
uVar7 = i2_cpy & uVar1
uVar11 = i2_cpy + i4_cpy
uVar1 = i4_cpy + i6_cpy
uVar8 = i3_cpy ^ uVar1
uVar9 = i5_cpy - i6_cpy
uVar10 = i5_cpy + i6_cpy
s.add(uVar2 % 0x10ae961 == 0x3f29b9)
s.add(uVar3 % 0x1093a1d == 0x8bdcd2)
s.add(uVar5 % uVar6 == 0x212c944d)
s.add(uVar7 % 0x6e22 == 0x31be)
s.add(((uVar11 & 0xffffffff) % i1_cpy) == 0x2038c43c)
s.add(uVar8 % 0x1ce628 == 0x1386e2)
s.add(uVar9 % 0x1172502 == 0x103cf4f)
s.add(uVar10 % 0x2e16f83 == 0x16ab0d7)
print(s.check())
print(s.model())
"""
[i2 = 780663452,
i5 = 966221407,
i3 = 341222189,
i6 = 217433792,
i1 = 705965527,
i4 = 465893239]
uiuctf{2a142dd72e87fa9c1456a32d1bc4f77739975e5fcf5c6c0}
"""Last updated