# b1c's HSCTF Writeups
## Glad Bags
> Poortho made some hard pwn for y'all. It's so hard, you don't get any input.
In the ELF, there was a 7zip file. Upon extracting that 7zip file, there was the source code of angr. Hidden in that source code was another 7zip file, with the source code of pwntools. Diffing the source with the actual source downloaded from github, we find that setup.py has a crap ton of hex in it. Decoding that hex, we find yet another 7zip file, this time with audacity inside. Diffing with audacity's download, we find 7zip itself, 7zipped (how meta). Diffing with the actual 7zip, we find that "MZWGCZ33MRXW45C7M5SXIX3NMFSF6Z3FORPWO3DBMRPWK6T5" has been added in. Using cyberchef's magic function, we find out it's base 32 and we get the flag. tl;dr: poortho's challenges are bad.
Flag: `flag{dont_get_mad_get_glad_ez}`
## Unitled Audio Challenge
> Untitled Description Message.
>
> Warning: the audio file is very loud.
>
> Hint: I hid an image into the waveform... recognize the magic numbers?
We can open the file with the `scipy.io.wavfile` module. We can analyze the data and see that all of the data points are very close to multiples of 2048. So, we divide every value by 2048 and round to get the values `8,9,5,0,...`. Using the hint, we make the connection that these are 4-bit values which, when concatenated, form the PNG header (`89 50 4b ...`). So, we write these values to a file to get the flag. Solve script below:
```python=
from scipy.io import wavfile
fs, data = wavfile.read('UntitledAudioChallenge.wav')
print(fs,len(data))
with open("out.png","wb") as f:
b = []
i = 0
t = 0
for n in data:
if i%2:
b.append(t*16+int(round(n/2048.0)))
else:
t = int(round(n/2048.0))
i+=1
f.write(bytes(b))
```
Output image:

Flag: `flag{sudo cat flag.txt}`
Note: After the end of the PNG we saw the following plaintext: `Hey, you've found a secret! Message AC#4637 on Discord and tell him you found this message. :eyes:`. Unfortunately we did not get points for this.

## Traffic Lights A
> We need your help to optimize our traffic routines!
>
> Connect with nc algo.hsctf.com 4001.
>
> There are 10 test cases. You have 120 seconds total. Good luck. Remember to have a terminating newline.
>
> Hint: Network flow.
GabeWu said that this is a min-cost max flow problem and that he copied one of Benjamin Qi's templates for the code below. I trust his judgement.
```cpp=
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef double db; //
typedef string str; //
//
typedef pair<int,int> pi;
typedef pair<ll,ll> pl; //
typedef pair<db,db> pd; //
//
typedef vector<int> vi;
typedef vector<ll> vl; //
typedef vector<db> vd; //
typedef vector<str> vs; //
typedef vector<pi> vpi;
typedef vector<pl> vpl; //
typedef vector<pd> vpd; //
#define mp make_pair
#define forn(i, n) for(int i=0; i<(n); i++)
#define f first
#define s second
#define sz(x) (int)x.size()
#define all(x) begin(x), end(x)
#define rall(x) (x).rbegin(), (x).rend() //
#define rsz resize
#define ins insert //
#define ft front() //
#define bk back()
#define pf push_front //
#define pb push_back
#define eb emplace_back //
#define lb lower_bound //
#define ub upper_bound //
template<class T> bool ckmin(T& a, const T& b) {
return b < a ? a = b, 1 : 0; }
template<class T> bool ckmax(T& a, const T& b) { //
return a < b ? a = b, 1 : 0; } //
#define FOR(i,a,b) for (int i = (a); i < (b); ++i)
#define F0R(i,a) FOR(i,0,a)
#define ROF(i,a,b) for (int i = (b)-1; i >= (a); --i)
#define R0F(i,a) ROF(i,0,a)
#define trav(a,x) for (auto& a: x)
/**
*From Ben Qi.
* Description: Minimum-cost maximum flow, assumes no negative cycles.
* Edges may be negative only during first run of SPFA. To get flow
* through original edges, assign ID's during \texttt{ae}.
* Time: $O(FM\log M)$ if caps are integers and $F$ is max flow
* Source: GeeksForGeeks
* https://courses.csail.mit.edu/6.854/06/scribe/s12-minCostFlowAlg.pdf
* running time is only pseudo-polynomial; see https://codeforces.com/blog/entry/70740
* Verification: https://codeforces.com/contest/164/problem/C
*/
template<int SZ> struct MCMF {
int N,s,t; // # verts, source, sink
typedef ll F; typedef ll C; // flow type, cost type
struct Edge { int to, rev; F flow, cap; C cost; };
vector<Edge> adj[SZ]; // use asserts, don't try smth dumb
void ae(int u, int v, F cap, C cost) { assert(cap >= 0);
Edge a{v,sz(adj[v]),0,cap,cost}, b{u,sz(adj[u]),0,0,-cost};
adj[u].pb(a), adj[v].pb(b); }
pi pre[SZ]; // previous vertex, edge label on path
pair<C,F> cost[SZ]; // tot cost of path, amount of flow
C totCost = 0, curCost = 0; F totFlow = 0;
bool spfa() { // find lowest cost path to send flow through
F0R(i,N) cost[i] = {numeric_limits<C>::max(),0};
cost[s] = {0,numeric_limits<F>::max()};
typedef pair<C,int> T;
priority_queue<T,vector<T>,greater<T>> todo;
todo.push({0,s});
while (sz(todo)) {
T x = todo.top(); todo.pop();
if (x.f > cost[x.s].f) continue;
trav(a,adj[x.s]) if (a.flow < a.cap
&& ckmin(cost[a.to].f,x.f+a.cost)) {
pre[a.to] = {x.s,a.rev};
cost[a.to].s = min(a.cap-a.flow,cost[x.s].s);
todo.push({cost[a.to].f,a.to});
} // if costs are doubles, add some EPS so you
} // don't traverse ~0-weight cycle repeatedly
return cost[t].s; // return flow
}
void backtrack() {
F df = cost[t].s; totFlow += df;
curCost += cost[t].f; totCost += curCost*df;
for (int x = t; x != s; x = pre[x].f) {
adj[x][pre[x].s].flow -= df;
adj[pre[x].f][adj[x][pre[x].s].rev].flow += df;
}
F0R(i,N) trav(p,adj[i]) p.cost += cost[i].f-cost[p.to].f;
} // all reduced costs non-negative if flow > 0
// edges on shortest path become 0
pair<F,C> calc(int _N, int _s, int _t) {
N = _N, s = _s, t = _t; assert(s != t);
while (spfa()) backtrack();
return {totFlow,totCost};
}
};
const int size = 1005;
int main(){
int n, m, k, l;
cin >> n >> m >> k >> l;
MCMF<size> x;
forn(I, m){
int u, v, i, f;
cin >> u >> v >> i >> f;
x.ae(u, v, f, i);
}
ll tot_spots;
forn(i, k){
int u, p;
cin >> u >> p;
x.ae(0, u, p, 0);
}
forn(i, l){
int u, c;
cin >> u >> c;
x.ae(u, n+1, c, 0);
tot_spots += c;
}
pl fc = x.calc(n+2, 0, n+1);
if(fc.f < tot_spots){
cout << "Impossible" << endl;
}else{
cout << fc.s;
}
}
```
We can compiled this bad boy up with `g++ solver.cpp`, giving us an `a.out` executable. We can then just feed it our netcat input:
```py=
from subprocess import Popen, PIPE, STDOUT
from pwn import *
p = remote("algo.hsctf.com", 4001)
p.recvline()
for count in range(10):
for i in range(2):
p.recvline()
t = p.recvline()
#print(t)
n,m,k,l = map(int, t.strip().split())
for i in range(k+m+l):
t += p.recvline()
cal = Popen(["./a.out"], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
dat = cal.communicate(input=t.encode())[0]
#print(t)
#print(dat.decode())
p.sendline(dat.decode().strip())
p.interactive()
```
Flag: `flag{n0_u_c4n7_ju57_u53_n37w0rk_51mpl3x_h4h4_c4r_60_vr00m_69bb3a80}`
## Generic Flag Checker
> https://twitter.com/mcclure111/status/1002648636516282368
For some reason I solved a decently-worth rev chall this CTF instead of getting aplet to do it ("not on Windows").
Just running the exe gives us a `bad input`. So we reason that it must be passed in with the command line. It gives back, "checking..." and then depending on the length of the input, either a "Incorrect!" or it just exits.
Obviously there must be some memcmp happening, and using `strings memes.exe` we see a memcmp at the end.
Opening it in Ghidra makes the disassembly look scary so we opt for dynamic analysis.
Open it in x64db. Configure it to send cmd line input with `File > Change Command Line`. For now, let's just use `AAAABBBBCCCCDDDD` as our input.
In the beginning there are just some Windows DLL stuff so we can just skip to memes.EntryPoint with the call stack after it appears.
We can keep going until we see "checking..." inputted. After some annoying breakpointing we can see that the call to whatever outputs "checking..." is here:

Skipping through it, we end up at a function call to what appears to be the flag checking function.
Eventually, we see our own input at:

A few more F8s... What's this?

It seems to have taken off 5 characters (`AAAAB`) from the front and 1 character (`D`) from the back, which perfectly fits the flag format! Now we know it must be checking the contents of our flag.
After holding down F8 through a loop, we can see our modified flag being built, and our original input ends up as:

Now, you may have to squint a bit, but you'll see that the new character is simply the old character minus the index of it in the string. If you're not convinced, you can simply input a `flag{ABCDEF}`. The registers will end up as `AAAAAA`.
Great, so now we know what's happening to our input. What's it comparing to?
Holding down F8 again, we see a suspicious string,

stored in the registers near 00007FF6CBC012DE. Moreover, we can even see a `memcmp` call soon after!

So, let's just reverse the subtraction of the index by adding the index back.
```py=
s = "con\\i`g^k"
for i in range(len(x)):
print(chr(ord(x[i])+i), end="")
```
This gives us the flag.
Flag: `flag{cpp_memes}`
## xkcd.com/2247 v2
> Why pick a weird hill to die on when you could pick a soft hill to lie on?
>
> Note: this challenge has been revised with a new flag. It was previously taken down and all solves for that challenge have been rolled back.
>
> The flag format will be flag{a_bunch_of_words_with_underscores}. The flag is hidden inside the plaintext, and does not include a flag{}.
The hill cipher is a simple matrix system modulo `26`, which we can solve with sage's `solve_right` method.
```python=
k = [[ord(j)-ord('a') for j in k[i:i+100]] for i in range(0,10000,100)]
c = [[ord(j)-ord('a') for j in c[i:i+100]] for i in range(0,10000,100)]
M = MatrixSpace(Zmod(26),100,100)
K = M(k)
C = M(c).transpose()
S = K.solve_right(C)
S = S.transpose()
for i in S.rows():
print(''.join([chr(int(j)+97) for j in i]))
```
This gives us the output in about 45 seconds. The result seems like gibberish, but plugging this into `dcode.fr`'s word search feature can give us the flag.

Flag: `flag{imagine_giving_the_plaintext_and_not_the_ciphertext_rip_lmao}`