CrossCTF Finals 2018 : Human Powered Flag Generator
First Blood by : Segfaulters
Keep cranking until the whole flag appears!
Creator - prokarius (@prokarius)
Static Analysis
We are given an APK file, which upon extracting and converting to its Java code, we get:
package com.a2018.crossctf.humanpoweredflaggenerator;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView flagDisplay;
private TextView percent;
private Status status;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView((int) C0185R.layout.activity_main);
this.status = new Status();
this.flagDisplay = (TextView) findViewById(C0185R.id.flagTextView);
this.percent = (TextView) findViewById(C0185R.id.percentTextView);
findViewById(C0185R.id.speedupButton).setOnClickListener(new MainActivity$$Lambda$0(this));
findViewById(C0185R.id.crankButton).setOnClickListener(new MainActivity$$Lambda$1(this));
}
final /* synthetic */ void lambda$onCreate$0$MainActivity(View view) {
this.status.speedUp();
}
final /* synthetic */ void lambda$onCreate$1$MainActivity(View view) {
crank();
}
private void crank() {
this.status.crank(3.8d);
Resources res = getResources();
this.flagDisplay.setText(res.getString(C0185R.string.flag, new Object[]{this.status.flag()}));
this.percent.setText(res.getString(C0185R.string.percent, new Object[]{Integer.valueOf(this.status.level()), this.status.percent()}));
}
}
Seems like the app has a display for the flag, a display for percent of work done, and a button to 'work' to flag generation, and a button to speed up the process.
package com.a2018.crossctf.humanpoweredflaggenerator;
import android.annotation.SuppressLint;
import java.math.BigInteger;
class Status {
private BigInteger curr;
private BigInteger f9f;
private StringBuilder flagBuilder = new StringBuilder();
private BigInteger fmax;
private int level = 0;
private BigInteger max;
private BigInteger prod;
Status() {
upLevel();
}
public void crank(double am) {
double amount = Math.pow(am, (double) this.level);
for (int i = 0; ((double) i) < amount; i++) {
if (this.max.compareTo(this.curr) == 0) {
if (this.level != 13) {
this.flagBuilder.append(extract());
upLevel();
} else {
return;
}
} else if (this.f9f.compareTo(this.fmax) == 0) {
this.curr = this.curr.add(BigInteger.ONE);
upF();
} else {
this.prod = this.prod.multiply(this.f9f);
this.f9f = this.f9f.add(BigInteger.ONE);
}
}
}
public String flag() {
return this.flagBuilder.toString();
}
public int level() {
return this.level;
}
@SuppressLint({"DefaultLocale"})
public String percent() {
double frac = BigInteger.valueOf(10000).divide(this.max.subtract(BigInteger.ONE)).doubleValue();
double x = this.curr.doubleValue() - 1.0d;
return String.format("%.2f", new Object[]{Double.valueOf(Math.min(100.0d, (((BigInteger.valueOf(10000).multiply(this.f9f).divide(this.fmax).doubleValue() / 10000.0d) + x) * frac) / 100.0d))});
}
private void upLevel() {
this.level++;
this.max = BigInteger.ONE.shiftLeft(this.level).add(BigInteger.ONE);
this.curr = BigInteger.ONE;
this.prod = BigInteger.ONE;
upF();
}
private void upF() {
this.fmax = fastExpo(BigInteger.valueOf(5), this.curr).add(BigInteger.ONE);
this.f9f = BigInteger.ONE;
}
public void speedUp() {
while (this.prod.mod(BigInteger.TEN).equals(BigInteger.ZERO)) {
this.prod = this.prod.divide(BigInteger.TEN);
}
}
private String extract() {
BigInteger thousand = BigInteger.valueOf(1000);
speedUp();
return this.prod.mod(thousand).add(thousand).toString().substring(1);
}
private static BigInteger fastExpo(BigInteger x, BigInteger y) {
BigInteger out = BigInteger.ONE;
while (y.compareTo(BigInteger.ONE) != 0) {
if (y.and(BigInteger.ONE).equals(BigInteger.ONE)) {
out = out.multiply(x);
}
x = x.multiply(x);
y = y.shiftRight(1);
}
return out.multiply(x);
}
}
(other useless files like lambda classes/resources files filtered out)
This seems like the meat of the app. Extracting this class out and running it on my local machine using a Java IDE, it seems that it takes a ... very long time ... yea no, running it won't solve the problem.
The flagBuilder
is a StringBuilder containing the flag. I started out by combining the methods together so to form as few methods as possible, then I converted this to Python so that my eyes don't bleed from the verbosity of Java.
class Status:
__init__():
level=1
max = 3
curr = 1
prod =1
fmax = 6
f9f = 1
flag = ''
def crank(am):
amount = am**level;
for i in range(amount):
if max == curr
if level == 13:
return
while prod%10==0:
prod//=10
flag += str((prod%1000)+1000)[1:]
level++
max = 1<<level +1
curr = 1
prod = 1
fmax = 6
f9f = 1
elif f9f == fmax:
curr++;
fmax = (5**curr) + 1
f9f = 1
else:
prod *= f9f;
f9f++;
The entire class can be replaced by just the crank method, as long as the state is stored within the function itself. We will do that, and things should get a lot simpler. The crank()
code still looks really bad, so we can begin by simplifying the code. The code looks suspiciously like disassembled nested while loops, so I started with that concept, then made crank()
return the flag instead of just shoving into a variable.
def crank():
flag = ''
for level in range(1,13):
prod=1
for curr in range(1, (1<<level)+1):
fmax = 5**curr+1
for f9f in range(fmax)
prod *= f9f
while prod % 10 == 0:
prod = prod // 10
flag+= str(prod%1000).rjust(3,'0')
return flag
The innermost for loop can be simplified, so the code can be:
def crank():
flag = ''
for level in range(1,13):
prod=1
for curr in range(1, (1<<level)+1):
prod *= factorial(curr)
while prod % 10 == 0:
prod = prod // 10
flag+= str(prod%1000).rjust(3,'0')
return flag
where factorial
is the factorial function, which I created to be a simple for-loop instead of recursion (python and its stack ;-;).
That's not the real problem of course. It took wayyyy to long.
Of course, it's because we are going to call factorial(5**(1<<level)), which will take toooooo long.
I was going to use Wolfram Alpha's API to get the factorial of those large numbers, when I realised there was a pattern in the factorials of (5*i): the last 3 digits are repeating (984, 088, 016, 912), except for factorial(5*1) which is 120.
Since the flag only cares about the last 3 digits of prod, which is the multiplication of all factorials of (5**i) in the range [1 to 1<<level], we can replace the factorial function with one that gives only the last 3 digits:
def factorial(g):
if g == 1:
return 120
else:
return [984, 88, 16, 912][(g-2)%4]
The final code is in here