Below are the Reverse Engineering writeups of all the challenges that were being asked in the VishwaCTF’22, organised by Cyber cell VIIT, Pune.
Aggresive vault
First dry run: No output
After analizing the binary found:
[ExE argument]:
-> Exe accpets an argument
-> The string is opened as a file
-> The file needs to exist
-> The argument is used to work upon the arra of bytes
[Array of bytes]:
-> Are being processed upon and finally written to a file.
-> Output is in an image file
[An ascii string just there]
-> Points to VishwaCTF website
-> The site only has one image
Steps to solve:
-> Dowload img on site, keep exe and img in same directory
-> Open run exe with img name
[ Output ]
program leaks memory, before exiting abnormally
the img file is changed (possibly corrupted or empty)
-> After more analyzing, you find the memory leak, block that function call, remake the exe
-> Open with the img name with img in same directory, img is changed to the flag
[Incase img not found]
-> Experienced solvers may realize its an encrypted img right away, after reversing the encryption steps,
Keys can be tried out to get the expected headder bytes, correct format can be identified from them by looking
at the meaningful key string
creating a file with that name, blocking the mem leak call, the file will be built with your flag in it!
--- More about encryption ---
-> It's just exoring bits of a bmp image with the key string
Confusion
Simple keygen that takes a username and creates a password the user must enter, the Username has already been given to the user, all the user had to do was reverse engineer the program, and based on the Username find the suitable Password. There are multiple debugger checks that will force the application to exit if it is being debugged.
Analysis
The main function is found by finding the _cexit
function. This is the function that returns control from the user code back to the C runtime environment. The function call at instruction 14000310b
seen below is the main function.
1400030f6 e8 93 0a CALL API-MS-WIN-CRT-RUNTIME-L1-1-0.DLL::__p___argv 00 00 1400030fb 48 8b 18 MOV RBX,qword ptr [RAX] 1400030fe e8 85 0a CALL API-MS-WIN-CRT-RUNTIME-L1-1-0.DLL::__p___argc 00 00 140003103 4c 8b c7 MOV R8,RDI 140003106 48 8b d3 MOV RDX,RBX 140003109 8b 08 MOV ECX,dword ptr [RAX] 14000310b e8 f0 e1 CALL main <----------------- ff ff 140003110 8b d8 MOV EBX,EAX 140003112 e8 05 07 CALL FUN_14000381c 00 00 140003117 84 c0 TEST AL,AL 140003119 74 55 JZ LAB_140003170 14000311b 40 84 f6 TEST SIL,SIL 14000311e 75 05 JNZ LAB_140003125 140003120 e8 6f 0a CALL API-MS-WIN-CRT-RUNTIME-L1-1-0.DLL::_cexit 00 00
Inside main there are three debugger checking functions. These are found at 14000138c
, 14000139f
, and 1400013ef
. If the user wants to bypass the logic checks for the presence of a debugger, the zflag needs to be set to 1, 1, and 0 respectively for the logic checks at addresses 14000138c
, 14000139f
, and 1400013ef
.
The program then prompts the user to either enter ‘continue’ or ‘rules’ to either continue or see a list of rules. Entering continue will take the user to a function called at address 140001591
. The function starts at address 140002120
and is the function that generates the key, prompts the user for their name and password, and checks if the password is valid. Another debugger check occurs at 140002157
. This requires the zflag set to 1 to bypass. The console screen is cleared and the username is prompted for their username. Data is encountered at 1400021dc
to 140002201
that is the base data used to generate the password.
1400021dc c7 45 db MOV dword ptr [RBP + -0x25]=>local_88[4],0xff9cffa0 a0 ff 9c ff 1400021e3 c7 45 e7 MOV dword ptr [RBP + -0x19]=>local_78[0],0xffb2ffe2 e2 ff b2 ff 1400021ea c7 45 d7 MOV dword ptr [RBP + -0x29]=>local_88[0],0xff98ffb1 b1 ff 98 ff 1400021f1 c7 45 e3 MOV dword ptr [RBP + -0x1d]=>local_88[12],0xffe0ffa2 a2 ff e0 ff 1400021f8 b8 ff ff MOV EAX,0xffff 00 00 1400021fd 66 89 45 eb MOV word ptr [RBP + -0x15]=>local_78[4],AX 140002201 c7 45 df MOV dword ptr [RBP + -0x21]=>local_88[8],0xff99ffb8 b8 ff 99 ff
A loop is then entered that will begin the process of transforming the data from the assembly above.
encrypt_sub 140002210 0f b7 0a MOVZX ECX,word ptr [RDX]=>local_88 140002213 66 2b c8 SUB CX,AX 140002216 66 f7 d1 NOT CX 140002219 66 33 c8 XOR CX,AX 14000221c 66 89 0a MOV word ptr [RDX]=>local_88,CX 14000221f ff c0 INC EAX 140002221 48 8d 52 02 LEA RDX=>local_88[2],[RDX + 0x2] 140002225 83 f8 0b CMP EAX,0xb 140002228 72 e6 JC encrypt_sub
The loop takes 2 byte chunks from the data “buffer” and subtracts it from the index of the loop, toggles every bit, xor’s it with the index of the loop, and then places the value back in the same portion of the data “buffer.” This is done 11 times.
A loop is then entered that prompts the user for the password. The input stream accepts an integer value as input. This is done 11 times.
The final transform is taking the values from the “encryption” loop and xor’ing each 2 byte zero extended chunk by the length of the username entered. A comparison is performed with the input password integers, if the two values are not equal a counter is incremented.
Note, the 2 byte chunks start at index 0 and incremented to the next 2 byte chunk. The input password integers are indexed by their order in the password prompting above. The counter is checked after all the comparisons are performed and if it is zero the success message is output to the user.
Is this even Hashing?
#include <iostream>
#include <chrono>
#include <thread>
#define ll long long
using namespace std;
ll hashIt(ll n, ll x, ll m){
ll ans = 0;
for (int i=0; i<x; i++){
ans += n;
ans %=m;
}
return ans;
}
ll hashHash(ll n){
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return (n << 32) + (n >> 32);
}
int main(int argc, char *argv[]){
if (argc == 1) cout << "Enter atleast 1 argument \n";
else {
for (int i=1; i<argc; i++){
// cout << hashHash(hashIt(stoll(argv[i]), 961798719, 1000000000)) << "\n";
cout << hashIt(hashHash(stoll(argv[i])), 279954879, 1000000000) << "\n";
}
}
return 0;
}
One way enigma
from string import ascii_uppercase, digits
class rotors:
saved = {
1:('{K8UMNZBGS20D3IR_5CEFYOQPX4JH1VA}96TW7L',[9]),
2:('EFXRSBQ1MC3GYW_9{47AVT8JP5ON6U20ZK}HDIL',[23]),
3:('WUXQGVJHT2YO043MK17LC9ANZBP{_S6D}I5EFR8',[7]),
4:('W0K2NBVRPJTZY1XHD9478L{QUMG6I_O3FAS5}CE',[30]),
5:('V3F_1QA}L4WOZIM8SY7P9XG20CDNKJUH5B6ERT{',[13])
}
rotorSequence = {}
def __init__(self, **kwargs):
# default values
self.preset = 1
self.offset = 0
self.position = 0
# switching to given values
if "rotor_num" in kwargs: self.rotor_num = kwargs["rotor_num"]
if "preset" in kwargs: self.preset = kwargs["preset"]
if "offset" in kwargs: self.offset = kwargs["offset"]
if "position" in kwargs: self.position = kwargs["position"]
if "notches" in kwargs: self.notches = kwargs["notches"]
else: self.notches = rotors.saved[self.preset][1]
# setting up remaining properties
rotors.rotorSequence[self.rotor_num] = self
self.mapper(self.preset)
self.notch_found = False
def __repr__(self):
return f'<Rotor no. {self.rotor_num}, Preset: {self.preset}, Position: {self.position}, Offset: {self.offset}>'
def mapper(self, preset=1):
'''takes preset number as an argument and maps the\n
rotor wiring accordingly'''
statorKey = ascii_uppercase + digits + "_{}"
rotorKey = rotors.saved[preset][0]
map = {}
rotorMap = {}
for i,char in enumerate(statorKey):
map[char]=i
map[i]=char
for i in range(len(statorKey)):
rotorMap[i] = [map[rotorKey[i]]]
for i in range(len(statorKey)):
temp = rotorMap[map[rotorKey[i]]]
rotorMap[map[rotorKey[i]]] = (temp[0], i)
rotorMap["wiring"] = rotorKey
self.wiring = rotorMap
def rotate(self):
'''rotates the rotorset once (for each keypress)'''
# should this rotor make the next rotor rotate?
if self.position in self.notches and self.rotor_num < len(rotors.rotorSequence):
rotors.rotorSequence[self.rotor_num + 1].notch_found = True
# to rotate or not to rotate
if self.rotor_num == 1:
self.position +=1
elif self.notch_found:
self.position += 1
self.notch_found = False
# Completion of rotation >>> Reset
self.position %= 39
def passLeft(self, contact):
'''takes contact from right and sends to left\n
meanwhile taking care of rotation in the\n
FORWARD CYCLE'''
self.rotate() # Rotation prior to connection
LSCN = (self.wiring[(self.position-self.offset+contact)%39][0] - (self.position-self.offset))%39
return LSCN
def passRight(self, contact):
'''Hmmm...'''
RSCN = (self.wiring[(self.position-self.offset+contact)%39][1] - (self.position-self.offset))%39
return RSCN
# pass
class steckerbrett:
def __init__(self):
self.mapper()
def __repr__(self):
return f'Steckerbrett : {self.swaps}'
def mapper(self):
statorKey = ascii_uppercase + digits + "_{}"
self.pairs = {}
self.map = {}
for i,char in enumerate(statorKey):
self.map[char]=i
self.map[i]=char
self.pairs[char]= char
def connect(self, char):
return self.map[self.pairs[char]]
def disconnect(self, contact):
return self.pairs[self.map[contact]]
def getSettings():
num = int(input('How many rotors do you want? >>> '))
settings['rotors']['sequence'] = []
for i in range(num):
rotor_num = int(input(f'Enter number of rotor {i+1} from 1-5 >>> '))
settings['rotors']['sequence'].append(rotor_num)
settings['rotors']['positions'] = []
for i in range(num):
position = int(input(f'Enter position of rotor {i+1} from 0-38 >>> '))
settings['rotors']['positions'].append(position)
return settings
def Enigma(settings):
plugboard = steckerbrett()
sequence = settings['rotors']['sequence']
positions = settings['rotors']['positions']
rotorSet = {}
for i in range(len(sequence)):
rotorSet[i] = rotors(rotor_num=i+1, preset=sequence[i], position=positions[i])
return plugboard, rotorSet
def Encrypt(message, enigma):
plugboard, rotorSet = enigma
encrypted = ''
for char in message:
contact = plugboard.connect(char)
for i in range(1, len(rotors.rotorSequence)+1):
contact = rotors.rotorSequence[i].passLeft(contact)
char = plugboard.disconnect(contact)
encrypted += char
return encrypted
def Decrypt(message, enigma):
plugboard, rotorSet = enigma
decrypted = ''
for char in message:
for i in range(1, len(rotors.rotorSequence)+1):
rotors.rotorSequence[i].rotate()
contact = plugboard.connect(char)
for i in range(len(rotors.rotorSequence), 0, -1):
contact = rotors.rotorSequence[i].passRight(contact)
char = plugboard.disconnect(contact)
decrypted += char
return decrypted
# Default Settings for The Enigma
settings = {'version':1,
'phrase':"Anyth1nG RanD0M Go3S HeR=E 69 And HWa+t CtuaLFck39875/",
'rotors':{'sequence':[1,1,1],
'positions':[11,22,33],
'offset':[15,2,31]}}
Overcook_
python -c “print(‘a’*50+’b’*12+’\x3c\x62\x55\x56’)” |
114 051 118 101 114 115 049 110 103 095 100 117 100 051 |
VishwaCTF{r3vers1ng_dud3}
Uzamaki Game
- Extract code from exe file
- Among garbage find spiralOrder function
- You will get the matrix which you have to access all elements in a
spiral order - For each pair just multiply 1st no. by 26 and add second no and
convert this integer ascii value to character - Do this for all elements to get flag
Bet Game
We are given an ELF and a .py file named game and sage.py respectively.
On running game we see game instructions:
![[Pasted image 20220323130725.png]] While playing, we notice it is almost impossible to earn the flag amount. So we give the player a hint in the form of sage.py
when we run sage.py it says “The biggest clue lies in your input” and asks for input.
Let’s try some random input.
![[Pasted image 20220323130738.png]]
It’s saying “t@k3_f3w_5t3p5_b@<k”.
If we look into code we can see a weird looking string and some functions. Let’s try to input that string.
![[Pasted image 20220323130745.png]] Hmm, so the output was same.
We reversed those functions and tried to print the output given by functions we get:-
![[Pasted image 20220323130754.png]]
So we got the hint which is “try_thinking_negatively”.
The flag amount can be earned directly in one bet if the player bets negative amount (hence the hint try_thinking_negatively) only this time we have to guess incorrectly (which is pretty easy).
![[Pasted image 20220323130800.png]]
Flag
$tr!k3_!t_lu<ky
Brutal taskmaster
Compiling task.ll in ELF format using clang with the following command
_clang task.ll -mllvm -W -g -W1,-pie -o task_
After compiling the task.ll with clang, we run executable file and type string as 0123456789, but receive fail notification as below:
![[Pasted image 20220323130931.png]] Let’s open task![[Pasted image 20220323130941.png]]in Ghidra.
Pseudo code for check() function.
![[Pasted image 20220323130948.png]] We easy conclude that the return value( initially, its value is 1) of this function depends input string and what string:
· if it’s 1 then all elements of input must satisfy above equation( choose this case)
· if it’s 0, we will have a lot’s of potential input which can happen( ignore this case)
main() function
![[Pasted image 20220323130954.png]] ![[Pasted image 20220323130959.png]]
- As mentioning above, I guess that the return value of check() function is 1, so I will pay my attention to the second branch of program: each element of input will be XOR-ed secret string before printing to screen.
- Because there are no additional hints about original flag, so I brute-forced all possible characters of flag( in range 32-127, in ASCII). We also noticed that, when I have the first character of input, I easily can get its original form( xor with secret[i]) and second character’s original form( xor with what[i])
This is the sample script which will do the brute forcing.
secret = ‘B\n|“\x06\x1bg7#\F\n)\t0Q8_{Y\x13\x18\rP’_
what = “\x17/’\x17\x1dJy\x03,\x11\x1e&\nexjONacA-&\x01LANH’.&\x12>#’Z\x0fO\x0b%:(&HI\x0cJylL’\x1emtdC”
alphabet =[]
for i in range(30, 127):
alphabet.append(i)
flag = [0]*56
for i in alphabet:
print( ‘i = ‘, chr(i))
flag[0] = i
for j in range(0, len(flag) – 1):
#flag[i] ^ secret
tmp = flag[j] ^ ord(secret[j %len(secret)])
#ori_flag[i] ^ what –> ord_flag[i + 1]
ori_flag_i_1 = tmp ^ ord(what[j])
flag[j + 1] = ori_flag_i_1 ^ ord(secret[(j + 1) % len(secret)])
print(‘flag{‘, end=”)
for k in flag:
print(chr(k), end=”)
print(‘}’)
print(‘\n**********************************************************\n’)
print()
Sensible xor is found as i=7.
Flag
7h15_f14g_15_v3ry_v3ry_l0ng_4nd_1_h0p3_th3r3_4r3_n0_7yp0
Take your time
#include <stdio.h>
#include <stdlib.h>
#include<time.h>
int main(void)
{
char str[80];
int T = time(0);
srand(T);
int num1 = rand();
uint num2 = (num1 - 3) * 3;
if(num2 & 1 != 0)
{
num2 = num2 - 1;
}
int num3 = (int) num2 /2 + 7;
sprintf(str,"%d %d %d %d",T,num1,num2,num3);
puts(str);
return 0;
}
Taking over the world
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <map>
#include <algorithm>
#include <climits>
#include "offsets.h"
using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::vector;
using std::queue;
using std::multimap;
using std::min;
struct Node {
int to, cap, rev;
Node(int t, int c, int r) : to(t), cap(c), rev(r) {}
};
void die() {
puts("Incorrect!");
exit(1);
}
int vid(const int v, const bool o) {
return v * 2 + (o ? 1 : 0);
}
void add_edge(const int i,
const int j,
const int c,
vector<vector<Node>> *adj) {
(*adj)[i].emplace_back(j, c, (*adj)[j].size());
(*adj)[j].emplace_back(i, 0, (*adj)[i].size() - 1);
}
// Time: O(MlogN)
// Space: O(N)
vector<int> dijkstra(const vector<bool>& guard,
const vector<vector<int>>& A,
const int s) {
vector<int> dst(A.size(), INT_MAX);
multimap<int, int> que;
que.emplace(0, s);
dst[s] = 0;
while (!que.empty()) {
const int c = que.begin()->first;
const int v = que.begin()->second;
que.erase(que.begin());
if (dst[v] == c) {
for (int tv = 0; tv < A[v].size(); ++tv) {
if (A[v][tv]) {
const int tc = dst[v] + 1 + (guard[v] ? 1 : 0);
if (tc < dst[tv]) {
dst[tv] = tc;
que.emplace(tc, tv);
}
}
}
}
}
return dst;
}
bool levelize(const int V, const int S, const int T,
vector<vector<Node>> *adj,
vector<int> *lev) {
*lev = vector<int>(V, -1);
queue<int> que;
(*lev)[S] = 0;
que.emplace(S);
while (!que.empty()) {
int v = que.front();
que.pop();
for (int i = 0; i < (*adj)[v].size(); ++i) {
const Node &e = (*adj)[v][i];
if (e.cap && (*lev)[e.to] == -1) {
(*lev)[e.to] = (*lev)[v] + 1;
que.emplace(e.to);
}
}
}
return (*lev)[T] != -1;
}
int augment(const int S, const int T,
const int v, const int f,
const vector<int>& lev,
vector<vector<Node>> *adj,
vector<int> *done) {
if (v == T || !f) {
return f;
}
for (; (*done)[v] < (*adj)[v].size(); ++(*done)[v]) {
Node &e = (*adj)[v][(*done)[v]];
if (lev[e.to] > lev[v]) {
const int t = augment(S, T, e.to, min(f, e.cap), lev, adj, done);
if (t > 0) {
e.cap -= t;
(*adj)[e.to][e.rev].cap += t;
return t;
}
}
}
return 0;
}
// Time: O(N * M^2)
// Space: O(N)
int max_flow(const int V, const int S, const int T,
vector<vector<Node>> *adj) {
int f = 0, t;
vector<int> lev;
while (levelize(V, S, T, adj, &lev)) {
vector<int> done(V);
while ((t = augment(S, T, S, INT_MAX, lev, adj, &done))) {
f += t;
}
}
return f;
}
vector<bool> min_cut(const int V, const int S,
const vector<vector<Node>>& adj) {
vector<bool> vis(V);
queue<int> que;
que.emplace(S);
vis[S] = true;
while (!que.empty()) {
int v = que.front();
que.pop();
for (const Node &e : adj[v]) {
if (e.cap && !vis[e.to]) {
que.emplace(e.to);
vis[e.to] = true;
}
}
}
return vis;
}
int taking_over_the_world() {
int N, M, K; cin >> N >> M >> K;
vector<vector<int>> A(N, vector<int>(N));
for (int i = 0; i < M; ++i) {
int u, v;
cin >> u >> v;
A[u][v] = A[v][u] = true;
}
const int GUARD = 1000;
vector<bool> guard(N);
while (true) {
const int V = N * 2;
const int S = vid(0, false);
const int T = vid(N - 1, false);
vector<vector<Node>> adj(V);
for (int v = 0; v < N; ++v) {
add_edge(vid(v, false), vid(v, true), guard[v] ? GUARD : 1,
&adj);
}
const vector<int> ds = dijkstra(guard, A, 0);
const vector<int> dt = dijkstra(guard, A, N - 1);
for (int u = 0; u < N; ++u) {
for (int v = 0; v < N; ++v) {
if (A[u][v]) {
if (ds[u] != -1 && dt[v] != -1) {
// Edge (u, v)
int td = ds[u] +
(guard[u] ? 1 : 0) + 1 +
(v == N - 1 ? 0 : (guard[v] ? 1 : 0) + dt[v]);
if (td == ds[N - 1]) {
add_edge(vid(u, true), vid(v, false), GUARD, &adj);
}
}
}
}
}
if (max_flow(V, S, T, &adj) <= K) {
const vector<bool> mc = min_cut(V, S, adj);
for (int v = 0; v < N; ++v) {
if (mc[vid(v, false)] && !mc[vid(v, true)]) {
guard[v] = true;
--K; // At most K loops
}
}
} else {
break;
}
}
return dijkstra(guard, A, 0)[N - 1];
}
/*
int array_to_num(int arr[],int n){
char str[10][2];
int i;
char number[13] = {'\n'};
for(i=0;i<n;i++) sprintf(str[i],"%d",arr[i]);
for(i=0;i<n;i++)strcat(number,str[i]);
i = atoi(number);
return i;
}
*/
/*
int num[6] = {13,20,6,4,3,55};
int real_num;
real_num = array_to_num(num,6);
*/
int main() {
int T = 5;
int decrypt[10] = {123421, 212, 3234, 23234, 55755, 4566, 464517, 464688, 2579, 152629};
/*
FILE* fp = fopen(argv[0], "r");
if (!fp){
return 1;
}
*/
matrix newMatrix;
int *magic_numbers = newMatrix.magic_numbers_();
int *num_array1 = newMatrix.num_array_1_();
int *num_array2 = newMatrix.num_array_2_();
// fread(decrypt, sizeof(char), sizeof(decrypt), fp);
int add_by = 1;
for (int i = 0; i < 10; i++)
{
if (magic_numbers[i] == 0)
{
magic_numbers[i] = add_by;
}
add_by++;
}
for (int i = 0; i < 10; i++){
magic_numbers[i] = magic_numbers[i] ^ decrypt[i];
}
for (int test = 0; test < T; ++test) {
int x = taking_over_the_world();
if(num_array1[test] != x){
die();
}
if(num_array2[test] != x){
die();
}
}
// false flag
cout << "VishwaCTF{";
for(int i = 0; i < T*2; i++){
cout << magic_numbers[i];
}
cout << "}";
}
Get the latest tech news and updates, ethical hacking tutorials and cybersecurity tips and tricks. Check out MeuSec for more.
Sometimes we include links to online retail stores and/or online campaigns. If you click on one and make a purchase we may receive a small commission.
Comments: