콘텐츠로 건너뛰기

[LACTF2024] technically-correct

file / readelf

ELF 32-bit?

seo@ubuntu:~/study/LACTF2024/technically-correct$ file technically_correct
technically_correct: ELF 32-bit MSB *unknown arch 0x3e00*

elf 헤더가 깨져있음.

seo@ubuntu:~/study/LACTF2024/technically-correct$ readelf -a ./technically_correct
ELF Header:
  Magic:   7f 45 4c 46 01 02 a8 9e b6 21 74 80 06 55 b8 e5
  Class:                             ELF32
  Data:                              2's complement, big endian
  Version:                           168 <unknown>
  OS/ABI:                            <unknown: 9e>
  ABI Version:                       182
  Type:                              <unknown>: 200
  Machine:                           <unknown>: 0x3e00
  Version:                           0x6ed7b4c7
  Entry point address:               0x37c184d0
  Start of program headers:          3338993664 (bytes into file)
  Start of section headers:          973078528 (bytes into file)
  Flags:                             0x0
  Size of this header:               36406 (bytes)
  Size of program headers:           8300 (bytes)
  Number of program headers:         15801
  Size of section headers:           35328 (bytes)
  Number of section headers:         56772
  Section header string table index: 5298
readelf: Warning: The e_shentsize field in the ELF header is larger than the size of an ELF section header
readelf: Error: Reading 2005641216 bytes extends past end of file for section headers
readelf: Error: Section headers are not available!
readelf: Error: Too many program headers - 0x3db9 - the file is not that big

There is no dynamic section in this file.
readelf: Error: Too many program headers - 0x3db9 - the file is not that big

IDA Pro / Ghidra로도 열리지 않음.

strace

ptrace 안티 디버깅이 들어가있음.

seo@ubuntu:~/study/LACTF2024/technically-correct$ strace -f ./technically_correct
execve("./technically_correct", ["./technically_correct"], 0x7ffcae644878 /* 24 vars */) = 0
ptrace(PTRACE_TRACEME)                  = -1 EPERM (Operation not permitted)
exit(0)                                 = ?
+++ exited with 0 +++

gdb

ELF 파일 포맷이 이상해서 gdb로도 안붙여짐.

seo@ubuntu:~/study/LACTF2024/technically-correct$ gdb ./technically_correct
...
GEF for linux ready, type `gef' to start, `gef config' to configure
90 commands loaded and 5 functions added for GDB 9.2 in 1.37ms using Python engine 3.8
"/home/seo/study/LACTF2024/technically-correct/./technically_correct": not in executable format: file format not recognized

gdbserver

디버거 붙이기전에 바이너리 내에서 ptrace(PTRACE_TRACEME) 를 호출하기에 디버깅이 안붙여지는거 같음.

seo@ubuntu:~/study/LACTF2024/technically-correct$ gdbserver :1234 ./technically_correct
Process ./technically_correct created; pid = 4052
Listening on port 1234
...

seo@ubuntu:~/study/LACTF2024/technically-correct/ftrace-hook$ gdb -p 4052
warning: process 4052 is already traced by process 4048
ptrace: Operation not permitted.

Run

놀랍게도, ELF 구조 꺠져도 실행은 됨. no 출력.

seo@ubuntu:~/study/LACTF2024/technically-correct$ ./technically_correct
no

디버깅 방법

https://github.com/ilammy/ftrace-hook

ftrace-hook 프로젝트를 이용하여 ptrace 커널 함수 후킹 코드 추가.

ptrace 후킹 함수에 다음 조건을 추가함.

  • 프로세스 이름이 문제 파일 이름인 “prob”일 경우
    • “/tmp/exit” 파일이 존재하지 않을 경우, 계속 무한 루프
    • 파일이 존재할 경우, real_sys_ptrace 호출하지 않고 return 0; 반환.
--- ./ftrace_hook.c.1	2025-04-21 05:10:09.857303366 -0700
+++ ./ftrace_hook.c	2025-04-21 05:09:38.213003904 -0700
@@ -15,6 +15,7 @@
 #include <linux/uaccess.h>
 #include <linux/version.h>
 #include <linux/kprobes.h>
+#include <linux/delay.h>  
 
 MODULE_DESCRIPTION("Example module hooking clone() and execve() via ftrace");
 MODULE_AUTHOR("ilammy <[email protected]>");
@@ -310,6 +311,42 @@
 
 	return ret;
 }
+
+/* 원본 syscall 함수 포인터 */
+static asmlinkage long (*real_sys_ptrace)(struct pt_regs *regs);
+
+/* 후킹 함수 */
+static asmlinkage long fh_sys_ptrace(struct pt_regs *regs)
+{
+    long ret;
+    unsigned long request = regs->di; /* 첫 번째 인자: 요청 타입 */
+    pid_t target = (pid_t)regs->si;   /* 두 번째 인자: 대상 PID */
+
+    pr_info("ptrace called by %s (pid:%d), req:%lu, target:%d\n",
+            current->comm, current->pid,
+            request, target);
+	
+	if (strcmp(current->comm, "prob") == 0) {
+		struct file *file;
+	
+	    while (1) {
+		    file = filp_open("/tmp/exit", O_RDONLY, 0);
+		    if (!IS_ERR(file)) {
+		        /* 파일을 찾았으면 닫고 루프 탈출 */
+		        filp_close(file, NULL);
+		        pr_info("fh_sys_exit: /tmp/exit found, proceeding exit\n");
+		        return 0;
+		    }
+		    /* 없으면 1초 대기 후 다시 확인 */
+		    msleep(1000);
+		}
+	}
+
+    /* 원본 ptrace 호출 */
+    ret = real_sys_ptrace(regs);
+
+    return ret;
+}
 #else
 static asmlinkage long (*real_sys_execve)(const char __user *filename,
 		const char __user *const __user *argv,
@@ -356,6 +393,7 @@
 static struct ftrace_hook demo_hooks[] = {
 	HOOK("sys_clone",  fh_sys_clone,  &real_sys_clone),
 	HOOK("sys_execve", fh_sys_execve, &real_sys_execve),
+	HOOK("sys_ptrace", fh_sys_ptrace, &real_sys_ptrace),
 };
 
 static int fh_init(void)

후킹 모듈을 설치하면, ptrace에서 멈춰있다가

seo@ubuntu:~/study/LACTF2024/technically-correct$ strace ./prob
execve("./prob", ["./prob"], 0x7ffe501d2cb0 /* 24 vars */) = 0
ptrace(PTRACE_TRACEME

/tmp/exit 파일 생성시, 계속 진행되어 안티 디버깅이 우회됨.

seo@ubuntu:~/study/LACTF2024/technically-correct$ strace ./prob
execve("./prob", ["./prob"], 0x7ffe501d2cb0 /* 24 vars */) = 0
ptrace(PTRACE_TRACEME)                  = 0
write(1, "no\n", 3no
)                     = 3
exit(0)                                 = ?
+++ exited with 0 +++

gdbserver로 다시 디버깅 붙여보면, 그래도 안됨.

/build/gdb-gAcuhh/gdb-9.2/gdb/gdbserver/regcache.c:257: A problem internal to GDBserver has been detected.
Unknown register pkru requested
...

gef➤  target remote :1234
Remote debugging using :1234
Remote connection closed

/tmp/exit 파일을 지우고, ./prob 실행. ptrace 단계에 멈춰있는 상태에서 gdb -p <pid>로 붙임.

그러면, gdb 창에서 Attaching to process 5333 메시지와 함께 멈춤.

이 상태에서 /tmp/exit 파일 생성하면, gdb 창에서 디버거가 성공적으로 붙여짐.

seo@ubuntu:~/study/LACTF2024/technically-correct/ftrace-hook$ gdb -p 5333
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.2) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
GEF for linux ready, type `gef' to start, `gef config' to configure
90 commands loaded and 5 functions added for GDB 9.2 in 2.02ms using Python engine 3.8
Attaching to process 5333

warning: "/home/seo/study/LACTF2024/technically-correct/prob": not in executable format: file format not recognized

warning: `/home/seo/study/LACTF2024/technically-correct/prob': can't read symbols: file format not recognized.

warning: Could not load vsyscall page because no executable was specified
0x000005c7d084c577 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
[!] Command 'registers' failed to execute properly, reason: max() arg is an empty sequence
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x007fff0b643ca0│+0x0000: 0x0000000000000001
0x007fff0b643ca8│+0x0008: 0x007fff0b64573f  →  0x5300626f72702f2e ("./prob"?)
0x007fff0b643cb0│+0x0010: 0x0000000000000000
0x007fff0b643cb8│+0x0018: 0x007fff0b645746  →  "SHELL=/bin/bash"
0x007fff0b643cc0│+0x0020: 0x007fff0b645756  →  "PWD=/home/seo/study/LACTF2024/technically-correct"
0x007fff0b643cc8│+0x0028: 0x007fff0b645788  →  "LOGNAME=seo"
0x007fff0b643cd0│+0x0030: 0x007fff0b645794  →  "XDG_SESSION_TYPE=tty"
0x007fff0b643cd8│+0x0038: 0x007fff0b6457a9  →  "MOTD_SHOWN=pam"
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:generic: ────
   0x5c7d084c56a                  test   DWORD PTR [rdi+0x3da33687], ecx
   0x5c7d084c570                  mov    eax, 0x65
   0x5c7d084c575                  syscall
[!] Command 'context' failed to execute properly, reason:
gef➤

Analysis

쉘코드는 다음과 같음.

gef➤  x/50i $rip-7
   0x5c7d084c570:       mov    eax,0x65
   0x5c7d084c575:       syscall
=> 0x5c7d084c577:       test   eax,eax
   0x5c7d084c579:       jne    0x5c7d084c603
   0x5c7d084c57f:       pop    rsi
   0x5c7d084c580:       cmp    rsi,0x2
   0x5c7d084c584:       jl     0x5c7d084c5de
   0x5c7d084c586:       pop    rsi
   0x5c7d084c587:       pop    rsi
   0x5c7d084c588:       movabs rbx,0xf84bc1f88e8
   0x5c7d084c592:       movzx  eax,BYTE PTR [rsi]
   0x5c7d084c595:       cmp    al,0xa
   0x5c7d084c597:       je     0x5c7d084c5cf
   0x5c7d084c599:       cmp    al,0x0
   0x5c7d084c59b:       je     0x5c7d084c5cf
   0x5c7d084c59d:       cmp    al,0x7e
   0x5c7d084c59f:       ja     0x5c7d084c5de
   0x5c7d084c5a1:       sub    al,0x20
   0x5c7d084c5a3:       jb     0x5c7d084c5de
   0x5c7d084c5a5:       lea    rdx,[rbx+rax*8]
   0x5c7d084c5a9:       mov    rbx,QWORD PTR [rdx]
   0x5c7d084c5ac:       xor    rbx,rdx
   0x5c7d084c5af:       movabs r8,0xb216cb3c48c1e693
   0x5c7d084c5b9:       imul   rbx,r8
   0x5c7d084c5bd:       movabs r8,0xc200c6d3267c529d
   0x5c7d084c5c7:       add    rbx,r8
   0x5c7d084c5ca:       inc    rsi
   0x5c7d084c5cd:       jmp    0x5c7d084c592
   0x5c7d084c5cf:       movabs rcx,0x7038fc00be0
   0x5c7d084c5d9:       cmp    rbx,rcx
   0x5c7d084c5dc:       je     0x5c7d084c5ea
   0x5c7d084c5de:       push   0xa6f6e
   0x5c7d084c5e3:       mov    edx,0x3
   0x5c7d084c5e8:       jmp    0x5c7d084c5f4
   0x5c7d084c5ea:       push   0xa736579
   0x5c7d084c5ef:       mov    edx,0x4
   0x5c7d084c5f4:       mov    eax,0x1
   0x5c7d084c5f9:       mov    edi,0x1
   0x5c7d084c5fe:       mov    rsi,rsp
   0x5c7d084c601:       syscall
   0x5c7d084c603:       mov    eax,0x3c
   0x5c7d084c608:       xor    edi,edi
   0x5c7d084c60a:       syscall

쉘코드 덤프.

dump binary memory sc.bin 0x5c7d084c570 0x5c7d084c60c

검사

rsp에 있는 값을 pop해서 rsi 값이 2보다 작으면 분기.

seg000:000000000000000F                 pop     rsi
seg000:0000000000000010                 cmp     rsi, 2
seg000:0000000000000014                 jl      short loc_6E    ; 'no\n'

여기서 rsi는 곧 argc를 의미함. 즉, 프로그램 실행시킬 때 매개변수 필요.

gef➤  x/10i $rip
=> 0x5c7d084c57f:       pop    rsi
   0x5c7d084c580:       cmp    rsi,0x2
   0x5c7d084c584:       jl     0x5c7d084c5de
   0x5c7d084c586:       pop    rsi
   0x5c7d084c587:       pop    rsi
   0x5c7d084c588:       movabs rbx,0xf84bc1f88e8
   0x5c7d084c592:       movzx  eax,BYTE PTR [rsi]
   0x5c7d084c595:       cmp    al,0xa
   0x5c7d084c597:       je     0x5c7d084c5cf
   0x5c7d084c599:       cmp    al,0x0
gef➤  x/4gx $rsp
0x7ffe4800aab0: 0x0000000000000001      0x00007ffe4800b73f
0x7ffe4800aac0: 0x0000000000000000      0x00007ffe4800b746
gef➤  x/gx 0x00007ffe4800b73f
0x7ffe4800b73f: 0x5300626f72702f2e
gef➤  x/4s 0x00007ffe4800b73f
0x7ffe4800b73f: "./prob"
0x7ffe4800b746: "SHELL=/bin/bash"
0x7ffe4800b756: "PWD=/home/seo/study/LACTF2024/technically-correct"
0x7ffe4800b788: "LOGNAME=seo"

loc_22 (0x5c7d084c592)

실행:

./prob ABCDEFGHIJKLMNOPQRSTUVWXYZ

al = 0x41 al이 0xa일 경우, loc_5F 분기.

gef➤  x/15i $rip
=> 0x5c7d084c592:       movzx  eax,BYTE PTR [rsi]
   0x5c7d084c595:       cmp    al,0xa
   0x5c7d084c597:       je     0x5c7d084c5cf
gef➤  info reg rsi
rsi            0x7fff4e2ae72b      0x7fff4e2ae72b
gef➤  x/gx 0x7fff4e2ae72b
0x7fff4e2ae72b: 0x4847464544434241
gef➤  si
gef➤  x/15i $rip
=> 0x5c7d084c595:       cmp    al,0xa
gef➤  info reg al
al             0x41                0x41

al = 0x41 al이 0일 경우, loc_5F 분기.

gef➤  x/10i $rip
=> 0x5c7d084c599:       cmp    al,0x0
   0x5c7d084c59b:       je     0x5c7d084c5cf
   0x5c7d084c59d:       cmp    al,0x7e
gef➤  info reg al
al             0x41                0x41
  • loc_5F

rcx가 7038FC00BE0값이면, “yes\n”

seg000:000000000000005F loc_5F:                                 ; CODE XREF: sub_0+27↑j
seg000:000000000000005F                                         ; sub_0+2B↑j
seg000:000000000000005F                 mov     rcx, 7038FC00BE0h
seg000:0000000000000069                 cmp     rbx, rcx
seg000:000000000000006C                 jz      short loc_7A    ; 'yes\n'

al = 0x41 if al > 0x7E → jump “no\n” if al < 0x20 → jump “no\n”

sub al, 20h에 의해 al 값은 0x20 감소.

cmp     al, 7Eh         ; '~'
ja      short loc_6E    ; if al > 0x7E → jump
sub     al, 20h         ; ' '
jb      short loc_6E    ; if al < 0x20 → jump

전체 분기문 어셈블리 코드는 다음과 같음.

seg000:0000000000000022
seg000:0000000000000022 loc_22:                                 ; CODE XREF: sub_0+5D↓j
seg000:0000000000000022                 movzx   eax, byte ptr [rsi]
seg000:0000000000000025                 cmp     al, 0Ah
seg000:0000000000000027                 jz      short loc_5F
seg000:0000000000000029                 cmp     al, 0
seg000:000000000000002B                 jz      short loc_5F
seg000:000000000000002D                 cmp     al, 7Eh ; '~'
seg000:000000000000002F                 ja      short loc_6E    ; 'no\n'
seg000:0000000000000031                 sub     al, 20h ; ' '
seg000:0000000000000033                 jb      short loc_6E    ; 'no\n'
seg000:0000000000000035                 lea     rdx, [rbx+rax*8]
seg000:0000000000000039                 mov     rbx, [rdx]
seg000:000000000000003C                 xor     rbx, rdx
seg000:000000000000003F                 mov     r8, 0B216CB3C48C1E693h
seg000:0000000000000049                 imul    rbx, r8
seg000:000000000000004D                 mov     r8, 0C200C6D3267C529Dh
seg000:0000000000000057                 add     rbx, r8
seg000:000000000000005A                 inc     rsi
seg000:000000000000005D                 jmp     short loc_22

전체적으로, c언어로 재구성하면, 다음과 같음.

#include <stdio.h>
#include <stdint.h>

int readbuf(uint64_t addr, void* output, size_t size) {
    memcpy((void*)output, addr, size);
    return 0;
}

uint64_t read64(uint64_t what) {
    uint64_t value = 0;
    readbuf(what, &value, sizeof(value));
    return value;
}

int main(int argc, char *argv[], char *envp[]) {
    uint64_t rbx = 0x0F84BC1F88E8;
    //loop_21
    char *input = argv[1];
    printf("argv[1]: %s\n", input);
    int rsi = 0;
    while (1) {
        if(input[rsi] == '\n')
            goto loc_5F;
        if(input[rsi] == '\0')
            goto loc_5F;
        if(input[rsi] > 0x7E)
            goto loc_6E;
        if(input[rsi] < 0x20)
            goto loc_6E;
        input[rsi] -= 0x20;

        uint64_t rdx = rbx + input[rsi]*8;
        rbx = read64(rdx) ^ rdx;
        uint64_t r8 = 0x0B216CB3C48C1E693;
        rbx *= r8;
        r8 = 0x0C200C6D3267C529D;
        rbx += r8;

        rsi+=1;
    }

    loc_5F:
    printf("rbx: 0x%lx\n", rbx);
    if(rbx == 0x7038FC00BE0) {
        puts("yes\n");
        return 0;
    }

    loc_6E:
    puts("no\n");

    return 0;
}

문제 환경을 재구성시키기 위해 덤프함

  • dump_all.py
import gdb
import re
import os

class DumpAllMappings(gdb.Command):
    """dump-all: info proc mappings를 통해 모든 메모리 영역을 파일로 덤프합니다.
    사용법: (gdb) dump-all
    """

    def __init__(self):
        super(DumpAllMappings, self).__init__("dump-all", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        inferior = gdb.selected_inferior()
        try:
            mappings = gdb.execute("info proc mappings", to_string=True)
        except gdb.error as e:
            gdb.write(f"info proc mappings 명령을 사용할 수 없습니다: {e}\n", gdb.STDERR)
            return

        # 정규 표현식을 사용하여 각 메모리 영역의 시작 주소와 끝 주소를 추출합니다.
        pattern = re.compile(r'^\s*([0-9a-fA-Fx]+)\s+([0-9a-fA-Fx]+)\s+.*$')
        for line in mappings.splitlines():
            match = pattern.match(line)
            if not match:
                continue
            start_str, end_str = match.groups()
            try:
                start = int(start_str, 16)
                end = int(end_str, 16)
                size = end - start
                # 메모리 읽기
                try:
                    mem = inferior.read_memory(start, size)
                except gdb.MemoryError:
                    gdb.write(f"메모리 읽기 실패: {start_str} - {end_str}\n", gdb.STDERR)
                    continue
                # 파일명 생성 및 쓰기
                filename = f"{start_str}_{size}.bin"
                try:
                    with open(filename, "wb") as f:
                        f.write(mem.tobytes())
                    gdb.write(f"메모리 덤프 완료: {filename}\n")
                except IOError as e:
                    gdb.write(f"파일 쓰기 실패: {filename}: {e}\n", gdb.STDERR)
            except ValueError:
                continue

DumpAllMappings()
  • Result
gef➤  dump-all
메모리 덤프 완료: 0x1bd4eff000_4096.bin
메모리 덤프 완료: 0x387efe4000_4096.bin
메모리 덤프 완료: 0xe634a9f000_4096.bin
...

덤프시킨 여러 bin 파일들을 ./dumped 폴더에 놓고 컴파일해서 실행시키면
문제 환경을 재구성시킬 수 있음.

  • prob_self.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/mman.h>
#include <errno.h>
#include <stdint.h>

#define DUMP_DIR "./dumped"

int mmap_rwx_from_dumped() {
    DIR *dir = opendir(DUMP_DIR);
    if (!dir) {
        perror("opendir");
        return 1;
    }

    struct dirent *entry;
    char path[1024];

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_type != DT_REG) continue; // skip non-regular files

        // 파일명 파싱: 0x주소_크기.bin
        uint64_t base_addr = 0;
        size_t size = 0;
        if (sscanf(entry->d_name, "0x%lx_%lu.bin", &base_addr, &size) != 2) {
            fprintf(stderr, "파일명 파싱 실패: %s\n", entry->d_name);
            continue;
        }

        snprintf(path, sizeof(path), "%s/%s", DUMP_DIR, entry->d_name);

        // 파일 열기
        int fd = open(path, O_RDONLY);
        if (fd < 0) {
            perror("open");
            continue;
        }

        // mmap
        void *mapped = mmap((void *)base_addr, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                            MAP_PRIVATE | MAP_FIXED, fd, 0);
        if (mapped == MAP_FAILED) {
            fprintf(stderr, "mmap 실패: %s - %s\n", entry->d_name, strerror(errno));
            close(fd);
            continue;
        }

        printf("매핑 성공: %s -> %p (%lu bytes)\n", entry->d_name, mapped, size);
        close(fd);
    }

    closedir(dir);
    return 0;
}

int readbuf(uint64_t addr, void* output, size_t size) {
    memcpy((void*)output, addr, size);
    return 0;
}

uint64_t read64(uint64_t what) {
    uint64_t value = 0;
    readbuf(what, &value, sizeof(value));
    return value;
}

int main(int argc, char *argv[], char *envp[]) {

    mmap_rwx_from_dumped();

    uint64_t rbx = 0x0F84BC1F88E8;
    //loop_21
    char *input = argv[1];
    printf("argv[1]: %s\n", input);
    int rsi = 0;
    while (1) {
        if(input[rsi] == '\n')
            goto loc_5F;
        if(input[rsi] == '\0')
            goto loc_5F;
        if(input[rsi] > 0x7E)
            goto loc_6E;
        if(input[rsi] < 0x20)
            goto loc_6E;
        input[rsi] -= 0x20;

        uint64_t rdx = rbx + input[rsi]*8;
        rbx = read64(rdx) ^ rdx;
        uint64_t r8 = 0x0B216CB3C48C1E693;
        rbx *= r8;
        r8 = 0x0C200C6D3267C529D;
        rbx += r8;

        rsi+=1;
    }

    loc_5F:
    printf("rbx: 0x%lx\n", rbx);
    if(rbx == 0x7038FC00BE0) {
        puts("yes\n");
        return 0;
    }

    loc_6E:
    puts("no\n");

    return 0;
}
  • Result
seo@ubuntu:~/study/LACTF2024/technically-correct$ gcc -o prob_self prob_self.c
...
seo@ubuntu:~/study/LACTF2024/technically-correct$ ./prob_self AA
...
매핑 성공: 0x1b5cc2b1000_4096.bin -> 0x1b5cc2b1000 (4096 bytes)
argv[1]: AA
rbx: 0xe58854d6000
no

solve.c

덤프된 메모리 구간에서 특정 암호화된 패턴을 복호화해 flag를 찾음.

  • ./dumped 폴더에 저장된 0x주소_크기.bin 파일들을 원래의 주소로 RWX 권한으로 mmap()

역연산 파트: prob_self.c / solve.c

  • prob_self.c
...
rbx = read64(rdx) ^ rdx;
uint64_t r8 = 0x0B216CB3C48C1E693;
rbx *= r8;
r8 = 0x0C200C6D3267C529D;
rbx += r8;
  • solve.c
    • rdx 값을 찾아내기 위해 맵핑된 rwx 영역들 중에 8바이트를 읽어 상응하는 rdx 값을 찾아냄.
uint64_t rbx = 0x7038FC00BE0;
rbx -= 0x0C200C6D3267C529D
rbx *= modinv_newton(0x0B216CB3C48C1E693);
uint64_t rdx = find_matching_rdx_in_mapped_regions(rbx);
...
  • solve.c

이후 rbx 값을 key 값 범위 0~0x5e까지 브루트포싱하여
find_matching_rdx_in_mapped_regions 에서 상응하는 rdx 값을 찾지 못할시 계속 key 값을 늘림.

찾을 경우, +0x20을 더해 문자 하나씩 붙임.

이후에, 초기 rbx값이 0x0F84BC1F88E8라면, 복호화가 끝났기에 success 구문으로 이동.

uint64_t saved_rdx = rdx;
int key = 0;

while(key < 0x5e) {
        rbx = saved_rdx - key*8;
        // printf("rbx_a: 0x%lx\n", rbx);
        if(rbx == 0x0F84BC1F88E8)
            goto success;
        rbx -= 0x0C200C6D3267C529D;
        // printf("rbx_b: 0x%lx\n", rbx);
        rbx *= modinv_newton(0x0B216CB3C48C1E693);
        // printf("rbx_c: 0x%lx\n", rbx);
        rdx = find_matching_rdx_in_mapped_regions(rbx);
        if(rdx == 0) {
            key++;
            continue;
        }
        success:
        // printf("for rdx: 0x%lx\n", rdx);
        saved_rdx = rdx;
        char ch = (char)key+0x20;
        // printf("key: %c\n", ch);
        
        char temp[2];
        temp[0] = ch;
        temp[1] = '\0'; 
        strcat(flag, temp);

        key = 0;
    }

solve.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/mman.h>
#include <errno.h>
#include <stdint.h>

#define DUMP_DIR "./dumped"

int mmap_rwx_from_dumped() {
    DIR *dir = opendir(DUMP_DIR);
    if (!dir) {
        perror("opendir");
        return 1;
    }

    struct dirent *entry;
    char path[1024];

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_type != DT_REG) continue; // skip non-regular files

        // 파일명 파싱: 0x주소_크기.bin
        uint64_t base_addr = 0;
        size_t size = 0;
        if (sscanf(entry->d_name, "0x%lx_%lu.bin", &base_addr, &size) != 2) {
            fprintf(stderr, "파일명 파싱 실패: %s\n", entry->d_name);
            continue;
        }

        snprintf(path, sizeof(path), "%s/%s", DUMP_DIR, entry->d_name);

        // 파일 열기
        int fd = open(path, O_RDONLY);
        if (fd < 0) {
            perror("open");
            continue;
        }

        // mmap
        void *mapped = mmap((void *)base_addr, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                            MAP_PRIVATE | MAP_FIXED, fd, 0);
        if (mapped == MAP_FAILED) {
            fprintf(stderr, "mmap 실패: %s - %s\n", entry->d_name, strerror(errno));
            close(fd);
            continue;
        }

        // printf("매핑 성공: %s -> %p (%lu bytes)\n", entry->d_name, mapped, size);
        close(fd);
    }

    closedir(dir);
    return 0;
}

int readbuf(uint64_t addr, void* output, size_t size) {
    memcpy((void*)output, addr, size);
    return 0;
}

uint64_t read64(uint64_t what) {
    uint64_t value = 0;
    readbuf(what, &value, sizeof(value));
    return value;
}

uint64_t find_matching_rdx_in_mapped_regions(uint64_t target_xor_result) {
    uint64_t saved_addr = 0;
    FILE *maps = fopen("/proc/self/maps", "r");
    if (!maps) {
        perror("fopen /proc/self/maps");
        return 1;
    }

    char line[256];
    while (fgets(line, sizeof(line), maps)) {
        unsigned long start, end;
        char perms[5];
        // maps 파일 한 줄에서 시작주소-끝주소 권한 부분만 파싱
        if (sscanf(line, "%lx-%lx %4s", &start, &end, perms) != 3)
            continue;

        // RWX 권한을 가진 구간인지 확인
        if (perms[0] != 'r' || perms[1] != 'w' || perms[2] != 'x')
            continue;

        // 구간을 8바이트 단위로 순회
        for (uint64_t addr = start; addr + 8 <= end; addr += 8) {
            uint64_t val = read64(addr);
            // printf("val: 0x%lx\n", val);
            if ((val ^ addr) == target_xor_result) {
                // printf("찾음! rdx = 0x%016lx (read64=0x%016lx)\n",
                    //    addr, val);
                saved_addr = addr;
            }
        }
        // printf("start: 0x%lx, end: 0x%lx\n", start, end);
    }

    fclose(maps);
    return saved_addr;
}

// 1) 뉴턴 방법으로 모듈러 역원 구하기
uint64_t modinv_newton(uint64_t a) {
    uint64_t x = 1;  // a mod 2 = 1 이므로 역원도 1
    for (int i = 0; i < 6; i++) {
        x = x * (2 - a * x);  // x_{n+1} = x_n * (2 - a*x_n)
    }
    return x;
}

void print_reverse(const char* str) {
    int len = strlen(str);
    for (int i = len - 1; i >= 0; i--) {
        putchar(str[i]);
    }
    putchar('\n');  // 줄 바꿈
}

int main(int argc, char *argv[], char *envp[]) {
    mmap_rwx_from_dumped();
    char flag[128];
    memset(flag, 0, 128);

    uint64_t rbx = 0x7038FC00BE0;
    rbx -= 0x0C200C6D3267C529D;
    // printf("rbx2: 0x%lx\n", rbx);
    rbx *= modinv_newton(0x0B216CB3C48C1E693);
    // printf("rbx: 0x%lx\n", rbx);
    uint64_t rdx = find_matching_rdx_in_mapped_regions(rbx);
    // printf("rdx: 0x%lx\n", rdx);

    uint64_t saved_rdx = rdx;
    int key = 0;

    while(key < 0x5e) {
        rbx = saved_rdx - key*8;
        // printf("rbx_a: 0x%lx\n", rbx);
        if(rbx == 0x0F84BC1F88E8)
            goto success;
        rbx -= 0x0C200C6D3267C529D;
        // printf("rbx_b: 0x%lx\n", rbx);
        rbx *= modinv_newton(0x0B216CB3C48C1E693);
        // printf("rbx_c: 0x%lx\n", rbx);
        rdx = find_matching_rdx_in_mapped_regions(rbx);
        if(rdx == 0) {
            key++;
            continue;
        }
        success:
        // printf("for rdx: 0x%lx\n", rdx);
        saved_rdx = rdx;
        char ch = (char)key+0x20;
        // printf("key: %c\n", ch);
        
        char temp[2];
        temp[0] = ch;
        temp[1] = '\0'; 
        strcat(flag, temp);

        key = 0;
    }
    printf("flag: ");
    print_reverse(flag);
    printf("\n");

    return 0;
}

Result

seo@ubuntu:~/study/LACTF2024/technically-correct$ ./solve
flag: lactf{i_l0v3_l1nux_elf_p4rs1ng}
seo@ubuntu:~/study/LACTF2024/technically-correct$ ./technically_correct lactf{i_l0v3_l1nux_elf_p4rs1ng}
yes
태그: