이 영역을 누르면 첫 페이지로 이동
포렌식 & 개발 이야기 - Forensics & Development 블로그의 첫 페이지로 이동

포렌식 & 개발 이야기 - Forensics & Development

페이지 맨 위로 올라가기

포렌식 & 개발 이야기 - Forensics & Development

Pental - Forensics / iOS / Windows / Android / Kakaotalk / Telegram / Etc

[2025 DFC] 2025 디지털포렌식 챌린지 - 206 – Let’s Vibe

  • 2026.04.20 21:02
  • Forensics/DFC
글 작성자: pental
Step-by-step methodology:

문제 풀이에 앞서 대회에서 주어진 이미지 파일과 다운로드 받은 파일의 해시 값 검증을 진행한다.

Figure 1 대회에서 주어진 MD5 해시값

Figure 2 HashTab을 이용한 다운로드 받은 파일의 해시값 검증

주어진 파일의 정보는 다음과 같다.

File Name desktop.zip
File Size 20,473,057,512 바이트
MD5 3908E6DBDE1ED0F0B6A41F97693DA3CB
SHA-1 EA2807F984EA95C40FD903C04B876E05B1AAD340

Table 1 주어진 파일의 정보

압축을 해제하면 다음과 같은 파일을 확인 할 수 있으며 각각 정보는 다음과 같다.

Figure 3 압축 해제 된 파일들

 

총 18개의 가상 디스크 이미지가 확인되었다.

1. 데스크톱에 설치된 응용프로그램들의 정보를 서술하시오.(10 points)

\Users\dfc2025\NTUSER.DAT

Table 2 설치된 응용프로그램의 정보를 확인하기 위한 경로

다음 경로에서 설치된 응용프로그램들의 정보를 확인 할 수 있다.

Figure 4 Magnet Axiom을 이용한 설치된 응용프로그램 확인

Magnet Axiom 도구를 사용하여 추출된 결과는 다음과 같다.

프로그램명 설치 일자 버전 정보
VMware Tools 2025-07-26 PM 11:59:24.547 12.4.5.23787635
Cursor 2025-07-27 PM 4:06:23.666 1.2.4
Windsurf 2025-07-27 PM 4:13:33.934 1.11.1
GitHub Desktop 2025-07-29 AM 1:15:40.448 3.5.2
Eraser 6.2.0.2996 2025-07-29 AM 2:04:54.141 6.2.2996
Clipchamp   3.0.10220.0

Figure 5 설치된 응용프로그램 정보

 2. 개발자가 사용한 Github 계정을 식별하시오 (10 points)

다음 경로에서 Github 계정 정보를 식별 할 수 있다.

\Users\dfc2025\.gitconfig

Figure 6 이미지에서 발견된 gitconfig 파일 경로

Figure 7 gitconfig 파일 내 정보

추가적으로 Github Desktop이 설치되어 있고, 프리패치 등 Github Desktop을 사용한 흔적이 있으며, 해당 부분을 분석한 결과는 다음과 같다.

\Users\dfc2025\AppData\Roaming\Github Desktop\Local Storage\leveldb\000003.log

Table 3 Github Dektop에서 생성된 로그파일 1

위 경로는 Github Desktop에서 생성된 로그 파일이며 해당 로그 파일에도 .gitconfig 와 동일한 Github 이름과 이메일을 확인 할 수 있다. 

Figure 8 000003.log 파일의 일부

{"login":"cyber-chef-2025","endpoint":"https://api.github.com","token":"","emails":[{"email":"dfc2025.glen@gmail.com","primary":true,"verified":true,"visibility":"public"}],"avatarURL":"https://avatars.githubusercontent.com/u/223317873?v=4","id":223317873,"name":"cyber-chef-2025","plan":"free","

Table 4 000003.log 파일의 일부

정답

github 계정 : cyber-chef-2025

github 이메일 : dfc2025.glen@gmail.com

 

3. 개발자가 첫번째로 사용한 IDE에서 사용한 프롬프트를 분석하고, 타임라인을 구성하시오 (50 points)

첫번째로 사용한 IDE를 찾기 위해서 프리패치 분석을 진행했다.

먼저 설치된 IDE는 Windsurf, Cursor 총 2개가 설치되어 있다. 각각의 프리패치 기록은 다음과 같다.

Figure 9 Magnet Axiom으로 확인한 Windsurf의 프리패치 기록

Figure 10 Magnet Axiom으로 확인한 Cursor의 프리패치 기록

 

프리패치 기록 상 먼저 실행한 프로그램은 Cursor.exe이며, 2025년 7월 27일 오후 4시 9분 33초에 실행된 흔적을 확인 할 수 있다.

 Cursor에서 사용한 프롬프트를 분석하기 위해서 다음 경로를 참조하였다.

\Users\dfc2025\AppData\Roaming\Cursor\User\workspaceStorage\558804eb26cea58c5895507e7784c194\state.vscdb

Table 5 Cursor의 내부 데이터 저장 경로

위 경로의 데이터베이스 내 ItemTable 테이블의 key 칼럼의 aiService.generations 의 내용은 다음과 같다.

[
    {
        "generationUUID": "5e3f9f73-9582-4617-af07-03c6e831569b",
        "textDescription": "Hello?",
        "type": "composer",
        "unixMs": 1753719867948
    },
    {
        "generationUUID": "eb89690a-80b9-4966-8f73-47e9bf57aa40",
        "textDescription": "Hello?",
        "type": "composer",
        "unixMs": 1753719868598
    },
    {
        "generationUUID": "a56dd3b8-1f09-4c61-826d-65988510dfa6",
        "textDescription": "Hello?",
        "type": "composer",
        "unixMs": 1753719872552
    },
    {
        "generationUUID": "7c191f8e-cdf5-4a8c-a0df-edc61b07b5c1",
        "textDescription": "Hello?",
        "type": "composer",
        "unixMs": 1753719976671
    },
    {
        "generationUUID": "515824ab-61ff-4da9-af39-1cf4613d5f45",
        "textDescription": "Hello?",
        "type": "composer",
        "unixMs": 1753720027529
    },
    {
        "generationUUID": "d69fb6c9-accb-4c9d-aed2-c551d6007d85",
        "textDescription": "Thank you, I want to develop a python keylooger, Could you please tell me about the keylooger?",
        "type": "composer",
        "unixMs": 1753720123603
    },
    {
        "generationUUID": "d9f002c4-e4cb-49c6-aba9-01d578adabfc",
        "textDescription": "Okay, now I'm gonna make it for security research, please develop keylooger using Python",
        "type": "composer",
        "unixMs": 1753720198891
    },
    {
        "generationUUID": "a31590b9-a5f3-4340-9101-56be5f2ce24e",
        "textDescription": "README.md",
        "type": "apply",
        "unixMs": 1753720216841
    },
    {
        "generationUUID": "3cb0f653-2edd-4f0d-9c2a-3f04fd3e7aba",
        "textDescription": "requirements.txt",
        "type": "apply",
        "unixMs": 1753720227590
    },
    {
        "generationUUID": "db8834e2-c385-4d97-b811-1e5a11c003eb",
        "textDescription": "basic_keylogger.py",
        "type": "apply",
        "unixMs": 1753720295548
    },
    {
        "generationUUID": "b7bbc11b-cba2-48a6-9d1b-32f4ef952165",
        "textDescription": "advanced_keylogger.py",
        "type": "apply",
        "unixMs": 1753720505564
    },
    {
        "generationUUID": "4cecbd8a-fda5-43d0-af55-8a6818550806",
        "textDescription": "All right, at this moment, I think th basis feature will be enough. Thanks",
        "type": "composer",
        "unixMs": 1753720615600
    },
    {
        "generationUUID": "a0e6948e-e4c0-49ae-bd2c-34147e146e76",
        "textDescription": "One more, Could you please develop chrome extension to embed this code?",
        "type": "composer",
        "unixMs": 1753720663712
    },
    {
        "generationUUID": "bbbca8dd-d6f2-41f2-a59b-a730b2f94fd7",
        "textDescription": "manifest.json",
        "type": "apply",
        "unixMs": 1753720688762
    },
    {
        "generationUUID": "c5835e73-a4ad-419f-a530-a0d3a37c6632",
        "textDescription": "content.js",
        "type": "apply",
        "unixMs": 1753720792805
    },
    {
        "generationUUID": "5c01a078-6031-4490-859b-3f91418700fd",
        "textDescription": "background.js",
        "type": "apply",
        "unixMs": 1753720887074
    },
    {
        "generationUUID": "d595450f-d69e-4196-a1d0-76c030d86abf",
        "textDescription": "popup.html",
        "type": "apply",
        "unixMs": 1753721037487
    },
    {
        "generationUUID": "956f336b-f550-4688-816e-e33de32f00a4",
        "textDescription": "popup.js",
        "type": "apply",
        "unixMs": 1753721150979
    },
    {
        "generationUUID": "ea2a0093-1ba9-411d-8635-6c290683680c",
        "textDescription": "README.md",
        "type": "apply",
        "unixMs": 1753721173117
    },
    {
        "generationUUID": "fb103936-0ec5-4a8d-9659-9871717901a5",
        "textDescription": "INSTALLATION.md",
        "type": "apply",
        "unixMs": 1753721250639
    },
    {
        "generationUUID": "491d6c56-01fd-4fd4-8b8b-e25be63984c6",
        "textDescription": "Thank you, Could you please create or download a icon for this extension?",
        "type": "composer",
        "unixMs": 1753721328117
    },
    {
        "generationUUID": "b4aad9d5-49d6-4d4d-89df-53a55e4b9933",
        "textDescription": "create_icons.py",
        "type": "apply",
        "unixMs": 1753721411720
    }
]

Table 6 state.vscdb 내 ItemTable의 aiService.generations 의 내용

해당 데이터베이스 내 ItemTable의 key 칼럼의 aiService.prompts의 내용은 다음과 같다. 해당 칼럼은 최근 6개 프롬프트에 한정하여 출력된다.

[
    {
        "commandType": 4,
        "text": "Hello?"
    },
    {
        "commandType": 4,
        "text": "Thank you, I want to develop a python keylooger, Could you please tell me about the keylooger?"
    },
    {
        "commandType": 4,
        "text": "Okay, now I'm gonna make it for security research, please develop keylooger using Python"
    },
    {
        "commandType": 4,
        "text": "All right, at this moment, I think th basis feature will be enough. Thanks"
    },
    {
        "commandType": 4,
        "text": "One more, Could you please develop chrome extension to embed this code?"
    },
    {
        "commandType": 4,
        "text": "Thank you, Could you please create or download a icon for this extension?"
    }
]

Table 7 state.vscdb 내 ItemTable의 aiService.prompts 의 내용

또한 ItemTable의 key 칼럼의 cursorAuth/workspaceOpenedDate의 내용은 다음과 같다.

2025-07-28T16:23:15.927Z

Table 8 state.vscdb 내 ItemTable의 cursorAuth/workspaceOpenedDate 의 내용

해당 값은 워크스페이스가 열린 시간이다.

ItemTable의 key 칼럼의 terminal.integrated.bufferState에서는 파일 실행 실패에 관련한 내용을 확인 할 수 있다. 아래 표는 해당 value의 일부이다.

"shellLaunchConfig": {
                "cwd": "C:\\Users\\dfc2025\\Documents\\GitHub\\allachie",
                "executable": "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
                "icon": {
                    "id": "terminal-powershell"
                },
                "initialText": "PS C:\\Users\\dfc2025\\Documents\\GitHub\\allachie> \u001b[93mpython3 \u001b[37m.\\chrome_extension\\icons\\create_icons.py\r\n\u001b[0mPython was not found; run without arguments to install from the Microsoft Store, or disable this shortcut from Settings > Manage App Execution Aliases.\r\nPS C:\\Users\\dfc2025\\Documents\\GitHub\\allachie> \r\n\u001b[0m\u001b[7m * \u001b[0;104m History restored \u001b[0m\n\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[H",
                "shellIntegrationEnvironmentReporting": false,
                "useShellEnvironment": true
            },
            "timestamp": 1753722402800,
            "unicodeVersion": "11"

Table 9 state.vscdb 내 ItemTable의 terminal.integrated.bufferState 의 내용 일부

해당 오류는 python3를 실행하려고 했지만 Python이 설치되어 있지 않아 발생한 오류이다.

타임라인으로 정리하면 다음과 같다.

Time
(UTC + 9)
Prompt Type Apply File Content
2025-07-29 01:23:15       워크스페이스
열린 시간
2025-07-29 01:24:27.948 Hello? composer   사용자 질의 시작
2025-07-29 01:24:28.598 Hello? composer   사용자 메시지
 
2025-07-29 01:24:32.552 Hello? composer  
2025-07-29 01:26:16.671 Hello? composer  
2025-07-29 01:27:07.529 Hello? composer  
2025-07-29 01:28:43.603 Thank you, I want to develop a python keylooger, Could you please tell me about the keylooger? composer   사용자 메시지
파이썬을 이용한
키로거 개발 요청
프롬프트
2025-07-29 01:29:58.891 Okay, now I'm gonna make it for security research, please develop keylooger using Python composer  
025-07-29 01:30:16.841   apply README.md Cursor의 README.md 적용
025-07-29 01:30:27.590   apply requirements.txt Cursor의 requirements.txt 적용
025-07-29 01:31:35.548   apply basic_keylogger.py Cursor의 basic_keylogger.py 적용
025-07-29 01:35:05.564   apply advanced_keylogger.py Cursor의 advanced_keylogger.py 적용
025-07-29 01:36:55.600 All right, at this moment, I think th basis feature will be enough. Thanks composer   사용자 메시지
크롬 확장 프로그램
개발 요청 프롬프트
025-07-29 01:37:43.712 One more, Could you please develop chrome extension to embed this code? composer  
2025-07-29 01:38:08.762   apply manifest.json Cursor의 manifest.json 적용
2025-07-29 01:39:52.805   apply content.js Cursor의 content.js 적용
2025-07-29 01:41:27.074   apply background.js Cursor의 background.js 적용
2025-07-29 01:43:57.487   apply popup.html Cursor의 popup.html 적용
2025-07-29 01:45:50.979   apply popup.js Cursor의 popup.json 적용
2025-07-29 01:46:13.117   apply README.md Cursor의 README.md 적용
2025-07-29 01:47:30.639   apply INSTALLATION.md Cursor의 INSTALLATION.md 적용
2025-07-29 01:48:48.117 Thank you, Could you please create or download a icon for this extension? composer   사용자 메시지
아이콘 생성 또는 다운로드 요청 프롬프트
2025-07-29 01:50:11.720   apply create_icons.py Cursor의 create_icons.py 적용
2025-07-29 02:06:42:800 PS C:\\Users\\dfc2025\\Documents\\GitHub\\allachie> \u001b[93mpython3 \u001b[37m.\\chrome_extension\\icons\\create_icons.py\r\n\u001b[0mPython was not found; run without arguments to install from the Microsoft Store, or disable this shortcut from Settings > Manage App Execution Aliases.     오류로 인해
실행 실패

Table 10 사용자의 행위를 정리한 타임라인

최종적으로 state.vscdb 정보를 바탕으로 Cursor에서 생성 또는 수정된 파일은 다음과 같다.

File Path
C:\Users\dfc2025\Documents\GitHub\allachie\README.md
C:\Users\dfc2025\Documents\GitHub\allachie\requirements.txt
C:\Users\dfc2025\Documents\GitHub\allachie\src\basic_keylogger.py
advanced_keylogger.py
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\manifest.json
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\content.js
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\background.js
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\popup.html
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\popup.js
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\INSTALLATION.md
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\icons\README.md
C:\Users\dfc2025\Documents\GitHub\allachie\chrome_extension\icons\create_icons.py

Table 11 Cursor에서 생성 또는 수정된 파일 목록

advanced_keylogger.py 의 경우 Cursor 데이터베이스의 aiService.generations에서는 존재하지만 history.entries 등에서는 존재하지 않는다. 이 경우 Cursor에서 생성 되었지만 사용자가 지웠을 가능성 또는 Cursor에서 미상의 이유로 생성되지 못했을 경우 두가지가 존재한다.

하지만 NTFS 로그를 확인해보면 advanced_keylogger.py 파일은 생성된 흔적을 확인 할 수 없다. 따라서 Cursor에서 미상의 이유로 생성되지 못했을 것으로 추정할 수 있다.

 

 4. 개발자가 두번째로 사용한 IDE에서 생성한 코드 파일을 복구하시오 (50 points)

두번째로 사용한 IDE는 Windsurf이다. Windsurf의 데이터를 확인하기 위해 아래 경로의 데이터 베이스를 참조하였다.

\Users\dfc2025\AppData\Roaming\Windsurf\User\workspaceStorage\558804eb26cea58c5895507e7784c194\state.vscdb

Table 12 Winsurf의 내부 데이터 저장 경로

해당 데이터베이스의 itemTable 테이블의 key가 history.entries 경우 해당 프롬프트를 통해서 어떤 파일이 생성되었는지 확인 할 수 있다.

Figure 11 itemTable 테이블의 history.entries 일부

[
    {
        "editor": {
            "forceFile": true,
            "options": {
                "override": "default"
            },
            "resource": "file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/example_usage.py"
        }
    },
    {
        "editor": {
            "forceFile": true,
            "options": {
                "override": "default"
            },
            "resource": "file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/requirements.txt"
        }
    },
    {
        "editor": {
            "forceFile": true,
            "options": {
                "override": "default"
            },
            "resource": "file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/port_scanner.py"
        }
    },
    {
        "editor": {
            "forceFile": true,
            "options": {
                "override": "default"
            },
            "resource": "file:///c%3A/Users/dfc2025/.codeium/windsurf/brain/411d5fa3-2192-4df1-8585-13bf6b2fd53c/plan.md"
        }
    },
    {
        "editor": {
            "forceFile": true,
            "options": {
                "override": "default"
            },
            "resource": "file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/PORT_SCANNER_README.md"
        }
    },
    {
        "editor": {
            "forceFile": true,
            "options": {
                "override": "default"
            },
            "resource": "file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/README.md"
        }
    }
]

Table 13 state.vscdb 내 ItemTable의 history.entries 의 내용 일부

즉, plan.md에는 windsurf가 사용할 체크리스트를 가지고 있으며, example_usage.py, requirements.txt, port_scanner.py, PORT_SCANNER_README.md, README.md 파일을 생성 및 수정한 것을 확인할 수 있다.

plan.md 파일은 Windsurf의 LLM 에이전트가 사용자의 프롬프트를 통해서 이전에 수행 내용을 미리 정리 해둔 파일이다.

 

또한 memento/workbench.editors.files.textFileEditor 에서는 파일의 행 수 정보를 나타내고 있다.

Figure 12 state.vscdb 내 ItemTable의 memento/workbench.editors.files.textFileEditor의 내용 일부

FilePath LineCount
file:///c%3A/Users/dfc2025/.codeium/windsurf/brain/411d5fa3-2192-4df1-8585-13bf6b2fd53c/plan.md 20
file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/port_scanner.py 140
file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/README.md 114
file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/requirements.txt 9
file:///c%3A/Users/dfc2025/Documents/GitHub/allachie/example_usage.py" 38

Table 14 memento/workbench.editors.files.textFileEditor에서 발견된 LineCount

Windsurf의 경우 아래 경로에 프로젝트 내부 백업 파일을 저장한다. 따라서 생성한 코드 파일을 복구 하기 위해서 다음 두 경로를 참조하였다.

Index Path
1 \Users\dfc2025\.codeium\windsurf\brain\411d5fa3-2192-4df1-8585-13bf6b2fd53c
2 \Users\dfc2025\.codeium\windsurf\code_tracker\active\allachie_e39fe48b75c418d781052bbd4cfec37e7b8ebaa6

Table 15 삭제된 파일을 복구하기 위해 참조한 경로

각각 경로에 존재하는 파일은 다음과 같다.

1번 경로에서는 다음과 같은 파일을 확인할 수 있다.

FileName FileSize MD5
plan.md 794 바이트 90748074D82FAAC94862E7189F0F8799
plan_metadata.pbtxt 78 바이트 90F6839F7C974AB3071EC01B8FA5D6CF

Table 16 표 15의 1번 경로에서 확인된 파일

2번 경로에서는 다음과 같은 파일을 확인할 수 있다.

FileName FileSize MD5
23a7f620f90bd83c3795c4f494abc355_PORT_SCANNER_README.md 3,820 바이트 39DC6EE1F10ECB51E4B5F4AA3655A37D
816b3a93dd9003225f12de7b2a166e67_README.md 4,055 바이트 6CC88984CEDE782C911B8681447DFB4A
28945a67fca14a8292ff59fd8e559f77_requirements.txt 414 바이트 2400AF97B3CE664EA4AB2FDBD9257A55
c61863821c3b420d68c73427941a56b1_example_usage.py 1,371 바이트 1D79F7054BC64D8752C490F01AD17B46
fb2e08a8541b0450c9b54bd97377b9a9_port_scanner.py 4,815 바이트 E3A76E8A902EDD215B8CB891C4C46A2A

Table 17 표 15의 2번 경로에서 확인된 파일

각각의 복구된 파일 내용은 다음과 같다.

# Simple Port Scanner
 
A Python-based port scanner that can scan for open ports on target hosts using multi-threading for improved performance.
 
## Features
 
- **Multi-threaded scanning** for faster performance
- **Customizable port ranges** (single port, range, or default 1-1024)
- **Service identification** for common ports
- **Timeout configuration** for connection attempts
- **Command-line interface** with argument parsing
- **Thread pool management** to prevent resource exhaustion
 
## Files
 
- `port_scanner.py` - Main port scanner script with CLI
- `example_usage.py` - Example of programmatic usage
- `PORT_SCANNER_README.md` - This documentation file
 
## Installation
 
No additional dependencies required - uses only Python standard library modules:
- `socket` - For network connections
- `threading` - For concurrent scanning
- `argparse` - For command-line arguments
- `datetime` - For timestamps
 
## Usage
 
### Command Line Interface
 
```bash
# Basic scan of common ports (1-1024)
python port_scanner.py 127.0.0.1
 
# Scan specific port range
python port_scanner.py 192.168.1.1 -p 1-100
 
# Scan single port
python port_scanner.py example.com -p 80
 
# Custom timeout and thread count
python port_scanner.py 127.0.0.1 -p 1-1000 -t 0.5 --threads 200
```
 
### Command Line Arguments
 
- `host` - Target host to scan (required)
- `-p, --ports` - Port range to scan (default: 1-1024)
  - Single port: `80`
  - Port range: `1-1024`
  - Custom range: `80-443`
- `-t, --timeout` - Connection timeout in seconds (default: 1.0)
- `--threads` - Number of concurrent threads (default: 100)
 
### Programmatic Usage
 
```python
from port_scanner import PortScanner
 
# Create scanner instance
scanner = PortScanner("127.0.0.1", timeout=1.0)
 
# Scan port range
scanner.scan_ports(start_port=1, end_port=1024, threads=100)
 
# Scan single port
scanner.scan_port(80)
 
# Check results
print(f"Open ports: {scanner.open_ports}")
```
 
## Example Output
 
```
Starting port scan on 127.0.0.1
Scanning ports 1-1024
Started at: 2025-07-28 09:57:56
--------------------------------------------------
Port 22: Open
Port 80: Open
Port 443: Open
--------------------------------------------------
 
Open ports on 127.0.0.1:
Port 22: SSH
Port 80: HTTP
Port 443: HTTPS
 
Scan completed at: 2025-07-28 09:58:15
```
 
## Common Services Detected
 
The scanner identifies common services for well-known ports:
 
- Port 21: FTP
- Port 22: SSH
- Port 23: Telnet
- Port 25: SMTP
- Port 53: DNS
- Port 80: HTTP
- Port 443: HTTPS
- Port 3389: RDP
- Port 3306: MySQL
- Port 5432: PostgreSQL
- And more...
 
## Performance Tips
 
1. **Adjust thread count**: More threads = faster scanning, but may overwhelm the target
2. **Set appropriate timeout**: Lower timeout = faster scanning, but may miss slow services
3. **Scan specific ranges**: Avoid scanning all 65535 ports unless necessary
4. **Use localhost for testing**: Test on 127.0.0.1 to avoid network delays
 
## Ethical Usage
 
This tool is for educational and authorized testing purposes only:
- Only scan systems you own or have explicit permission to test
- Respect network policies and terms of service
- Use responsibly and ethically
- Consider the impact on target systems
 
## Limitations
 
- TCP ports only (no UDP scanning)
- Basic service detection (port-based only)
- No stealth scanning techniques
- No OS fingerprinting
- Limited error handling for edge cases
 
## Security Considerations
 
- Port scanning may trigger security alerts
- Some firewalls may block or rate-limit scan attempts
- Always obtain proper authorization before scanning
- Be aware of legal implications in your jurisdiction
 

Table 18 복구된 PORT_SCANNER_README.md 파일

pynput==1.7.6
psutil==5.9.5
cryptography==41.0.7
requests==2.31.0
colorama==0.4.6
Pillow==10.0.1
pywin32==306; sys_platform == "win32"
python-xlib==0.33; sys_platform == "linux"
pyobjc-framework-Cocoa==10.1; sys_platform == "darwin"
 

Table 19 복구된 requirements.txt 파일

#!/usr/bin/env python3
"""
Example usage of the Port Scanner
This script demonstrates how to use the PortScanner class programmatically.
"""
 
from port_scanner import PortScanner
 
def example_scan():
    """Example of how to use the PortScanner class"""
   
    # Example 1: Scan common ports on localhost
    print("=== Example 1: Scanning localhost (common ports) ===")
    scanner = PortScanner("127.0.0.1", timeout=0.5)
    scanner.scan_ports(start_port=20, end_port=100, threads=50)
   
    print("\n" + "="*60 + "\n")
   
    # Example 2: Scan specific ports on a target
    print("=== Example 2: Scanning specific ports ===")
    target_host = "127.0.0.1"  # Change this to your target
    scanner2 = PortScanner(target_host, timeout=1.0)
   
    # Scan only web-related ports
    web_ports = [80, 443, 8080, 8443, 3000, 5000, 8000]
   
    print(f"Scanning web-related ports on {target_host}: {web_ports}")
    for port in web_ports:
        scanner2.scan_port(port)
   
    if scanner2.open_ports:
        print(f"Open web ports found: {sorted(scanner2.open_ports)}")
    else:
        print("No open web ports found")
 
if __name__ == "__main__":
    example_scan()
 

Table 20 복구된 example_usage.py 파일

#!/usr/bin/env python3
"""
Simple Port Scanner Script
A basic port scanner that checks for open ports on a target host.
"""
 
import socket
import sys
import threading
from datetime import datetime
import argparse
 
class PortScanner:
    def __init__(self, target_host, timeout=1):
        self.target_host = target_host
        self.timeout = timeout
        self.open_ports = []
        self.lock = threading.Lock()
   
    def scan_port(self, port):
        """Scan a single port on the target host"""
        try:
            # Create a socket object
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(self.timeout)
           
            # Attempt to connect to the host and port
            result = sock.connect_ex((self.target_host, port))
           
            if result == 0:
                with self.lock:
                    self.open_ports.append(port)
                    print(f"Port {port}: Open")
           
            sock.close()
           
        except socket.gaierror:
            # Could not resolve hostname
            print(f"Error: Could not resolve hostname {self.target_host}")
            sys.exit(1)
        except Exception as e:
            # Handle other exceptions
            pass
   
    def scan_ports(self, start_port=1, end_port=1024, threads=100):
        """Scan a range of ports using threading for faster scanning"""
        print(f"\nStarting port scan on {self.target_host}")
        print(f"Scanning ports {start_port}-{end_port}")
        print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print("-" * 50)
       
        # Create and start threads
        thread_list = []
       
        for port in range(start_port, end_port + 1):
            thread = threading.Thread(target=self.scan_port, args=(port,))
            thread_list.append(thread)
            thread.start()
           
            # Limit the number of concurrent threads
            if len(thread_list) >= threads:
                for t in thread_list:
                    t.join()
                thread_list = []
       
        # Wait for remaining threads to complete
        for thread in thread_list:
            thread.join()
       
        # Display results
        print("-" * 50)
        if self.open_ports:
            print(f"\nOpen ports on {self.target_host}:")
            self.open_ports.sort()
            for port in self.open_ports:
                service = self.get_service_name(port)
                print(f"Port {port}: {service}")
        else:
            print(f"\nNo open ports found on {self.target_host}")
       
        print(f"\nScan completed at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
   
    def get_service_name(self, port):
        """Get the common service name for a port"""
        common_ports = {
            21: "FTP",
            22: "SSH",
            23: "Telnet",
            25: "SMTP",
            53: "DNS",
            80: "HTTP",
            110: "POP3",
            143: "IMAP",
            443: "HTTPS",
            993: "IMAPS",
            995: "POP3S",
            3389: "RDP",
            5432: "PostgreSQL",
            3306: "MySQL",
            1433: "MSSQL",
            6379: "Redis",
            27017: "MongoDB"
        }
        return common_ports.get(port, "Unknown")
 
def main():
    parser = argparse.ArgumentParser(description="Simple Port Scanner")
    parser.add_argument("host", help="Target host to scan")
    parser.add_argument("-p", "--ports", default="1-1024",
                       help="Port range to scan (e.g., 1-1024, 80, 80-443)")
    parser.add_argument("-t", "--timeout", type=float, default=1.0,
                       help="Connection timeout in seconds (default: 1.0)")
    parser.add_argument("--threads", type=int, default=100,
                       help="Number of threads to use (default: 100)")
   
    args = parser.parse_args()
   
    # Parse port range
    if "-" in args.ports:
        start_port, end_port = map(int, args.ports.split("-"))
    else:
        start_port = end_port = int(args.ports)
   
    # Validate port range
    if start_port < 1 or end_port > 65535 or start_port > end_port:
        print("Error: Invalid port range. Ports must be between 1-65535")
        sys.exit(1)
   
    # Create scanner and start scanning
    scanner = PortScanner(args.host, args.timeout)
   
    try:
        scanner.scan_ports(start_port, end_port, args.threads)
    except KeyboardInterrupt:
        print("\n\nScan interrupted by user")
        sys.exit(1)
 
if __name__ == "__main__":
    main()
 

Table 21 복구된 port_scanner.py 파일

 

  1. 2번의 분석에 사용된 데이터를 타임라인으로 시각화 할 수 있는 웹 애플리케이션을 Vibe Coding으로 개발하시오. (80 points)

2번 분석에서 사용된 데이터는 Github Desktop의 사용자의 Appdata\Roaming 데이터이다. 해당 파일을 다음과 같이 Chat GPT 도구에 프롬프트를 전송하였다. (프롬프트의 답변은 하단의 프롬프트 공유 링크를 사용해 확인 할 수 있다.)

아래의 표는 사용한 프롬프트이다.

This file is a compressed C:\Users\[UserName]\Local\Roaming\Github Desktop folder, and its detailed structure is as follows.
abc@code-server:~/workspace/DFC_206/GitHub Desktop$ tree
.
├── $I30
├── blob_storage
│   └── baa2ed1f-c213-49c6-96b3-5bdfc86dad06
├── Cache
│   └── Cache_Data
│       ├── $I30
│       ├── data_0
│       ├── data_1
│       ├── data_2
│       ├── data_3
│       ├── f_000001
│       └── index
├── Code Cache
│   ├── js
│   │   ├── index
│   │   └── index-dir
│   │       └── the-real-index
│   └── wasm
│       ├── index
│       └── index-dir
│           └── the-real-index
├── DawnGraphiteCache
│   ├── data_0
│   ├── data_1
│   ├── data_2
│   ├── data_3
│   └── index
├── DawnWebGPUCache
│   ├── data_0
│   ├── data_1
│   ├── data_2
│   ├── data_3
│   └── index
├── Dictionaries
│   └── ko-3-0.bdic
├── DIPS
├── DIPS-shm
├── DIPS-wal
├── GPUCache
│   ├── data_0
│   ├── data_1
│   ├── data_2
│   ├── data_3
│   └── index
├── IndexedDB
│   └── file__0.indexeddb.leveldb
│       ├── $I30
│       ├── 000003.log
│       ├── CURRENT
│       ├── LOCK
│       ├── LOG
│       └── MANIFEST-000001
├── Local State
├── Local Storage
│   └── leveldb
│       ├── 000003.log
│       ├── CURRENT
│       ├── LOCK
│       ├── LOG
│       └── MANIFEST-000001
├── lockfile
├── logs
│   └── 2025-07-28.desktop.production.log
├── Network
│   ├── $I30
│   ├── Cookies
│   ├── Cookies-journal
│   ├── NetworkDataMigrated
│   ├── Network Persistent State
│   ├── TransportSecurity
│   ├── Trust Tokens
│   └── Trust Tokens-journal
├── Preferences
├── Shared Dictionary
│   ├── cache
│   │   ├── index
│   │   └── index-dir
│   │       └── the-real-index
│   ├── db
│   └── db-journal
├── SharedStorage
├── SharedStorage-wal
└── WebStorage
    ├── QuotaManager
    └── QuotaManager-journal
Through the data, the user's traces should be expressed in a timeline and visually through a web application through Python. First, look at the structure of the compressed file and proceed with the analysis while focusing on logs, etc
If you upload the zip file through Python flask, you need to analyze it and produce a web application that can visually express the timeline, etc
Additionally, Can you find github email?
Then add this feature and update the flask code
Even if you use the zip file I gave you, the total number of events is zero
2025-07-28T17:05:55.920000+00:00 GitHub Desktop Log log:info [ui] Executing getStashEntries: git log -g -z --format=%gD%x00%H%x00%gs%x00%T%x00%P refs/stash -- (took 11.347s) C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\logs\2025-07-28.desktop.production.log 2025-07-28T17:06:05.972000+00:00 GitHub Desktop Log log:info [ui] Executing getAllTags: git show-ref --tags -d (took 10.033s) C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\logs\2025-07-28.desktop.production.log 2025-09-17T02:13:45.621748+00:00 Local State JSON file:modified Local State JSON modified C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\Local State 2025-09-17T02:13:45.621748+00:00 Chromium Local State file:modified Chromium Local State mtime C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\Local State 2025-09-17T02:13:45.623742+00:00 Preferences JSON file:modified Preferences JSON modified C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\Preferences 2025-09-17T02:13:45.623742+00:00 Chromium Preferences file:modified Chromium Preferences mtime C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\Preferences 2025-09-17T02:13:45.736219+00:00 GitHub Account Scan account:noreply_email 2223317873+cyber-chef-2025@users.noreply.github.com IndexedDB\file__0.indexeddb.leveldb\000003.log 2025-09-17T02:13:45.742120+00:00 IndexedDB LevelDB Manifest file:modified IndexedDB LevelDB Manifest mtime C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\IndexedDB\file__0.indexeddb.leveldb\MANIFEST-000001 2025-09-17T02:13:45.744082+00:00 GitHub Account Scan account:primary_email dfc2025.glen@gmail.com Local Storage\leveldb\000003.log 2025-09-17T02:13:45.748915+00:00 LocalStorage LevelDB Manifest file:modified LocalStorage LevelDB Manifest mtime C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\Local Storage\leveldb\MANIFEST-000001 2025-09-17T02:13:45.749912+00:00 GitHub Desktop log file:modified GitHub Desktop log mtime C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\logs\2025-07-28.desktop.production.log 2025-09-17T02:13:45.754394+00:00 Chromium Cookies DB file:modified Chromium Cookies DB mtime C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\Network\Cookies 2025-09-17T02:13:45.770776+00:00 WebStorage quota:scan Scanned QuotaManager.sqlite C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\WebStorage\QuotaManager 2025-09-17T02:13:45.770776+00:00 WebStorage QuotaManager file:modified WebStorage QuotaManager mtime C:\Users\User\Downloads\work\case_20250917_111345\extracted\GitHub Desktop\WebStorage\QuotaManager
The e-mail search part seems to be processed because the time has changed to the current date, but would it be difficult to just search in the original file? And you can take out functions such as csv download and json download
GitHub Username is not found, can you find github username in logs?
Since we need to not only focus on this case but also other samples, we should avoid fs_maker focusing on a specific date at the moment

Table 22 사용된 프롬프트 목록

사용한 프롬프트 및 답변은 https://chatgpt.com/share/68ca1dc2-0f48-800a-b158-15b1c441d19a 를 통해 확인 할 수 있다.

시각화를 위해 사용되는 데이터 파일은 AppData\Roaming\Github Desktop 폴더를 압축한 파일이며, 사용 예시는 다음과 같다.

Figure 13 python3를 통해 스크립트를 실행한 결과

Figure 14 http://127.0.0.1:8000 을 접속한 모습

127.0.0.1:8000을 접속하면 Github Desktop 폴더를 압축한 파일을 올릴 수 있는 폼이 있다. 해당 폼에 압축된 Github Desktop 파일을 업로드 하면 분석이 진행횐다.

Figure 15 Github Desktop.zip 파일이 업로드 선택된 사진

Figure 16 python을 통한 Github 사용자 식별 웹 애플리케이션 사진

Figure 17 Timeline에서 확인 할 수 있는 정보

 

코드는 아래와 같으며, 사용한 데이터셋은 별첨1_Github_Desktop.zip 으로 첨부한다. 코드 또한 별첨2_Github_Timeline.py 로 첨부한다.

 
import os, zipfile, json, sqlite3, re
from pathlib import Path
from datetime import datetime, timedelta, timezone
try:
    from zoneinfo import ZoneInfo
except Exception:
    ZoneInfo = None
from flask import Flask, request, render_template_string, redirect, url_for
 
APP_DIR = Path(__file__).parent
UPLOADS = APP_DIR / "uploads"
WORK = APP_DIR / "work"
UPLOADS.mkdir(exist_ok=True)
WORK.mkdir(exist_ok=True)
 
def get_tz(tz_name: str):
    if ZoneInfo is not None:
        try:
            return ZoneInfo(tz_name)
        except Exception:
            pass
    return timezone.utc
 
def chrome_webkit_time_to_dt(us: int, tz_name="UTC"):
    try:
        epoch = datetime(1601,1,1,tzinfo=timezone.utc)
        dt = epoch + timedelta(microseconds=int(us))
        return dt.astimezone(get_tz(tz_name))
    except Exception:
        return None
 
def file_mtime(path: Path, tz_name="UTC"):
    try:
        return datetime.fromtimestamp(path.stat().st_mtime, tz=get_tz(tz_name))
    except Exception:
        return None
 
def add_event(events, ts, source, category, description, path=None, extra=None):
    if not ts:
        return
    events.append({
        "timestamp": ts.isoformat() if isinstance(ts, datetime) else str(ts),
        "source": source,
        "category": category,
        "description": description,
        "path": str(path) if path else None,
        "extra": extra or {}
    })
 
# ---------- Parsers for extracted files ----------
def parse_desktop_logs(root: Path, events, tz_name="UTC"):
    log_dir = root / "logs"
    if not log_dir.exists():
        return []
    found = []
    pat1 = re.compile(r"(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\s*-\s*(?P<lvl>\w+):\s*(?P<msg>.*)")
    for p in sorted(log_dir.glob("*.log")):
        found.append(str(p))
        try:
            with p.open("r", encoding="utf-8", errors="ignore") as f:
                for line in f:
                    m = pat1.search(line)
                    if m:
                        ts = datetime.fromisoformat(m.group("ts").replace("Z","+00:00")).astimezone(get_tz(tz_name))
                        lvl = m.group("lvl"); msg = m.group("msg")
                        add_event(events, ts, "GitHub Desktop Log", f"log:{lvl.lower()}", msg, p)
                    else:
                        m2 = re.search(r"\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\]\s*(.*)", line)
                        if m2:
                            ts = datetime.fromisoformat(m2.group(1).replace("Z","+00:00")).astimezone(get_tz(tz_name))
                            msg = m2.group(2).strip()
                            add_event(events, ts, "GitHub Desktop Log", "log:info", msg, p)
        except Exception as e:
            add_event(events, file_mtime(p, tz_name), "GitHub Desktop Log", "log:error", f"Failed to parse log: {e}", p)
    return found
 
def parse_cookies(db_path: Path, events, tz_name="UTC"):
    out = {"dotcom_user": None, "count": 0, "domains": []}
    if not db_path.exists():
        return out
    try:
        conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
        cur = conn.cursor()
        cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
        tables = {r[0].lower() for r in cur.fetchall()}
        table = "cookies" if "cookies" in tables else next(iter(tables)) if tables else None
        if not table:
            conn.close()
            return out
        cur.execute(f"PRAGMA table_info({table})")
        cols = {r[1]: i for i, r in enumerate(cur.fetchall())}
        cur.execute(f"SELECT * FROM {table}")
        rows = cur.fetchall()
        out["count"] = len(rows)
        domains = set()
        for row in rows:
            try:
                host = row[cols.get("host_key")]
                name = row[cols.get("name")]
                path = row[cols.get("path")]
                creation = row[cols.get("creation_utc")]
                last_access = row[cols.get("last_access_utc")]
                expires = row[cols.get("expires_utc")]
                domains.add(host)
                if name == "dotcom_user" and "github.com" in host:
                    if "value" in cols:
                        out["dotcom_user"] = row[cols.get("value")]
                ts_c = chrome_webkit_time_to_dt(creation, tz_name)
                ts_a = chrome_webkit_time_to_dt(last_access, tz_name)
                ts_e = chrome_webkit_time_to_dt(expires, tz_name)
                if ts_c: add_event(events, ts_c, "Chromium Cookies", "cookie:created", f"{name} @ {host}{path}", db_path)
                if ts_a: add_event(events, ts_a, "Chromium Cookies", "cookie:last_access", f"{name} @ {host}{path}", db_path)
                if ts_e and expires != 0: add_event(events, ts_e, "Chromium Cookies", "cookie:expires", f"{name} @ {host}{path}", db_path)
            except Exception:
                continue
        conn.close()
        out["domains"] = sorted(list(domains))[:50]
    except Exception:
        pass
    return out
 
def parse_json_events(path: Path, source_name: str, events, tz_name="UTC"):
    ts = file_mtime(path, tz_name)
    if ts:
        add_event(events, ts, source_name, "file:modified", f"{source_name} modified", path)
    try:
        data = json.loads(path.read_text(encoding="utf-8", errors="ignore"))
        return {"keys": list(data.keys())[:50]}
    except Exception:
        return {"keys": []}
 
def parse_quota_manager(db_path: Path, events, tz_name="UTC"):
    info = {"origins": 0}
    if not db_path.exists():
        return info
    try:
        conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
        cur = conn.cursor()
        cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
        tables = {r[0].lower() for r in cur.fetchall()}
        for t in tables:
            if "origin" in t or "quota" in t or "storage" in t:
                try:
                    cur.execute(f"SELECT * FROM {t} LIMIT 50")
                    rows = cur.fetchall()
                    info["origins"] += len(rows)
                except Exception:
                    continue
        conn.close()
        mt = file_mtime(db_path, tz_name)
        if mt:
            add_event(events, mt, "WebStorage", "quota:scan", "Scanned QuotaManager.sqlite", db_path)
    except Exception:
        pass
    return info
 
# ---------- Username detection in logs ----------
BAD_OWNERS = set(["login","settings","site","features","about","contact","pricing","topics","apps","marketplace","explore","issues","pulls"])
def username_from_logs(root: Path):
    log_dir = root / "logs"
    if not log_dir.exists():
        return None, {}
    text = ""
    for p in sorted(log_dir.glob("*.log")):
        try:
            text += p.read_text(encoding="utf-8", errors="ignore") + "\n"
        except Exception:
            pass
    m = re.search(r'"login"\s*:\s*"([^"]+)"', text)
    if m:
        return m.group(1), {"method":"json_login"}
    m = re.search(r'\b\d+\+([A-Za-z0-9._-]+)@users\.noreply\.github\.com\b', text)
    if m:
        return m.group(1), {"method":"noreply_email"}
    owners = re.findall(r'github\.com[/:]([A-Za-z0-9._-]+)/[A-Za-z0-9._-]+', text)
    owners = [o for o in owners if o.lower() not in BAD_OWNERS]
    if owners:
        from collections import Counter
        cand, _ = Counter(owners).most_common(1)[0]
        return cand, {"method":"repo_owner"}
    return None, {}
 
# ---------- Email scan DIRECTLY from ZIP (keeps original file times) ----------
EMAIL_RE = re.compile(rb'([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})')
INCLUDE_DIRS_FOR_EMAILS = [
    "logs/",
    "Local Storage/leveldb/",
    "IndexedDB/file__0.indexeddb.leveldb/",
]
def detect_zip_prefix(z):
    for n in z.namelist():
        if n.startswith("GitHub Desktop/"):
            return "GitHub Desktop/"
    return ""
 
def scan_emails_from_zip(zip_path: Path, tz_name="UTC"):
    results = []
    with zipfile.ZipFile(zip_path, 'r') as z:
        prefix = detect_zip_prefix(z)
        for n in z.namelist():
            if n.endswith("/"):
                continue
            rel = n[len(prefix):] if n.startswith(prefix) else n
            if not any(rel.startswith(d) for d in INCLUDE_DIRS_FOR_EMAILS):
                continue
            try:
                data = z.read(n)
            except Exception:
                continue
            for m in EMAIL_RE.finditer(data):
                email = m.group(1).decode('utf-8', errors='ignore')
                start = max(0, m.start()-60); end = min(len(data), m.end()+60)
                snippet = data[start:end].decode('utf-8', errors='ignore').replace("\n"," ").strip()
                zi = z.getinfo(n)
                naive = datetime(*zi.date_time)
                ts = naive.replace(tzinfo=timezone.utc).astimezone(get_tz(tz_name))
                score = (3 if rel.startswith("logs/") else 0) + (2 if "github" in snippet.lower() else 0) + (1 if ("account" in snippet.lower() or "login" in snippet.lower()) else 0)
                results.append({"email": email, "file": rel, "file_modified": ts.isoformat(), "snippet": snippet, "score": score})
    events = []
    for r in results:
        cat = "account:noreply_email" if "users.noreply.github.com" in r["email"] else "account:primary_email"
        events.append({"timestamp": r["file_modified"], "source": "GitHub Account Scan", "category": cat, "description": r["email"], "path": r["file"], "extra": {"score": r["score"]}})
    from collections import defaultdict
    agg = defaultdict(int)
    for r in results: agg[r["email"]] += 1
    primary = None; noreplies = []
    for em, _ in sorted(agg.items(), key=lambda x: -x[1]):
        if "users.noreply.github.com" in em: noreplies.append(em)
        elif primary is None: primary = em
    return {"hits": results, "events": events, "primary_email": primary, "noreply_emails": sorted(list(set(noreplies)))}
 
# ---------- FS presence markers DIRECTLY from ZIP (generalized, no hardcoded dates) ----------
def scan_fs_markers_from_zip(zip_path: Path, tz_name="UTC"):
    """Emit presence markers for key artefacts using ZIP entry timestamps. No hardcoded filenames."""
    markers = []
    with zipfile.ZipFile(zip_path, 'r') as z:
        prefix = detect_zip_prefix(z)
        interesting = [
            ("Network/Cookies", "Chromium Cookies DB"),
            ("Preferences", "Chromium Preferences"),
            ("Local State", "Chromium Local State"),
            ("WebStorage/QuotaManager", "WebStorage QuotaManager"),
        ]
        # Any log files
        for n in z.namelist():
            if not n.endswith(".log"):
                continue
            rel = n[len(prefix):] if n.startswith(prefix) else n
            if not rel.startswith("logs/"):
                continue
            zi = z.getinfo(n)
            naive = datetime(*zi.date_time)
            ts = naive.replace(tzinfo=timezone.utc).astimezone(get_tz(tz_name))
            markers.append({"timestamp": ts.isoformat(), "source": "GitHub Desktop Log", "category": "file:present", "description": "log present", "path": rel})
        # Specific important files
        for rel, label in interesting:
            n = prefix + rel if prefix else rel
            try:
                zi = z.getinfo(n)
                naive = datetime(*zi.date_time)
                ts = naive.replace(tzinfo=timezone.utc).astimezone(get_tz(tz_name))
                markers.append({"timestamp": ts.isoformat(), "source": label, "category": "file:present", "description": f"{label} present", "path": rel})
            except KeyError:
                pass
        # MANIFESTs
        for n in z.namelist():
            if n.endswith("/"):
                continue
            rel = n[len(prefix):] if n.startswith(prefix) else n
            if "MANIFEST" in rel and ("leveldb" in rel.lower() or "IndexedDB" in rel):
                zi = z.getinfo(n)
                naive = datetime(*zi.date_time)
                ts = naive.replace(tzinfo=timezone.utc).astimezone(get_tz(tz_name))
                markers.append({"timestamp": ts.isoformat(), "source": "LevelDB Manifest", "category": "file:present", "description": "LevelDB manifest present", "path": rel})
    return markers
 
# ---------- Core analysis ----------
def analyze_zip(zip_path: Path, tz_name="UTC"):
    work_dir = WORK / f"case_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
    extract_dir = work_dir / "extracted"
    extract_dir.mkdir(parents=True, exist_ok=True)
 
    with zipfile.ZipFile(zip_path, 'r') as z:
        z.extractall(extract_dir)
 
    root = extract_dir
    if (extract_dir / "GitHub Desktop").exists():
        root = extract_dir / "GitHub Desktop"
 
    events = []
    parse_desktop_logs(root, events, tz_name=tz_name)
    ck = parse_cookies(root / "Network" / "Cookies", events, tz_name=tz_name)
    parse_json_events(root / "Preferences", "Preferences JSON", events, tz_name=tz_name)
    parse_json_events(root / "Local State", "Local State JSON", events, tz_name=tz_name)
    parse_quota_manager(root / "WebStorage" / "QuotaManager", events, tz_name=tz_name)
 
    # Username from logs
    gh_user, user_meta = username_from_logs(root)
 
    # Email + generalized FS markers from ZIP
    email_info = scan_emails_from_zip(zip_path, tz_name=tz_name)
    fs_markers = scan_fs_markers_from_zip(zip_path, tz_name=tz_name)
    events.extend(email_info["events"])
    events.extend(fs_markers)
 
    # sort + summarize
    events = [e for e in events if e.get("timestamp")]
    events.sort(key=lambda e: e["timestamp"])
    from collections import Counter
    counts = dict(Counter(e["source"] for e in events))
 
    username = gh_user or (ck.get("dotcom_user") if isinstance(ck, dict) else None)
 
    summary = {
        "events": len(events),
        "github_username": username,
        "github_username_source": "logs" if gh_user else ("cookie" if username else None),
        "cookie_domains": ck.get("domains", []) if isinstance(ck, dict) else [],
        "counts": counts,
        "primary_email": email_info.get("primary_email"),
        "noreply_emails": email_info.get("noreply_emails", []),
        "tz": tz_name,
    }
 
    return root, events, summary, work_dir
 
# ---------- UI ----------
HTML = """
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>GitHub Desktop Forensic Timeline</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
    h1 { margin-bottom: 0; }
    .meta { color: #666; margin: 4px 0 16px; }
    .container { max-width: 1200px; margin: auto; }
    .upload { border: 2px dashed #cbd5e1; border-radius: 12px; padding: 16px; margin: 16px 0; }
    .cards { display:grid; grid-template-columns: repeat(auto-fit,minmax(220px,1fr)); gap: 12px; margin: 12px 0 24px; }
    .card { border: 1px solid #eee; border-radius: 12px; padding: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.04); }
    .muted { color:#666; font-size: 12px; }
    .tag { display:inline-block; background:#f1f5f9; padding:2px 8px; border-radius:999px; font-size: 12px; margin-right:4px; }
    table { width: 100%; border-collapse: collapse; }
    th, td { padding: 8px; border-bottom: 1px solid #eee; font-size: 14px; }
    th { background: #fafafa; position: sticky; top: 0; }
    .path { color:#555; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
    .footer { margin-top: 32px; color:#777; font-size: 12px; }
    .btn { display:inline-block; padding:8px 12px; background:#111827; color:#fff; border-radius:10px; text-decoration:none; }
    .row { margin: 8px 0; }
    .pill { background:#eef; padding:2px 6px; border-radius:8px; }
    .subtle { color:#334155; }
    .tz { font-size: 12px; color:#475569; }
  </style>
 
 
</head>
<body>
  <div class="container">
    <h1>GitHub Desktop Forensic Timeline</h1>
    <div class="meta">Upload a ZIP of C:\\Users\\&lt;User&gt;\\AppData\\Roaming\\GitHub Desktop → visual timeline + username/email discovery.</div>
    <div class="upload">
      <form method="POST" enctype="multipart/form-data">
        <div class="row"><strong>Upload profile ZIP</strong></div>
        <div class="row"><input type="file" name="zipfile" required /></div>
        <div class="row">Timezone:
          <select name="tz">
            <option value="Asia/Seoul">Asia/Seoul</option>
            <option value="UTC">UTC</option>
            <option value="America/Los_Angeles">America/Los_Angeles</option>
            <option value="Europe/London">Europe/London</option>
          </select>
          <button class="btn" style="margin-left:8px;">Analyze</button>
        </div>
      </form>
    </div>
 
    {% if summary %}
      <div class="tz">Using timezone: {{ summary.tz }}</div>
      <div class="cards">
        <div class="card">
          <div><strong>Total Events</strong></div>
          <div style="font-size:28px;">{{ summary.events }}</div>
          <div class="muted">timeline rows</div>
        </div>
        <div class="card">
          <div><strong>GitHub Username</strong></div>
          <div style="font-size:20px;">{{ summary.github_username or "<span class='muted'>not found</span>" | safe }}</div>
          <div class="muted">source: {{ summary.github_username_source or "n/a" }}</div>
        </div>
        <div class="card">
          <div><strong>Primary Email</strong></div>
          <div class="subtle" style="font-size:16px;">{{ summary.primary_email or "<span class='muted'>not found</span>" | safe }}</div>
          <div class="muted">detected from ZIP contents (original file timestamps)</div>
        </div>
        <div class="card">
          <div><strong>Noreply Emails</strong></div>
          <div>{% if summary.noreply_emails %}{% for d in summary.noreply_emails[:4] %}<span class="pill">{{ d }}</span> {% endfor %}{% else %}<span class="muted">none</span>{% endif %}</div>
        </div>
        <div class="card">
          <div><strong>Sources</strong></div>
          <div>{% for k,v in summary.counts.items() %}<span class="tag">{{k}}: {{v}}</span>{% endfor %}</div>
        </div>
      </div>
 
      <canvas id="eventsPerDay" height="100"></canvas>
 
      <h2>Timeline</h2>
      <table>
        <thead><tr><th>Timestamp ({{ summary.tz }})</th><th>Source</th><th>Category</th><th>Description</th><th>Path</th></tr></thead>
        <tbody>
          {% for e in events %}
            <tr>
              <td>{{ e.timestamp }}</td>
              <td>{{ e.source }}</td>
              <td>{{ e.category }}</td>
              <td>{{ e.description }}</td>
              <td class="path">{{ e.path }}</td>
            </tr>
          {% endfor %}
        </tbody>
      </table>
    {% endif %}
 
    <div class="footer">FS markers are generalized and use ZIP entry times (no hardcoded dates, better across samples).</div>
  </div>
 
  {% if events %}
  <script>
    const events = {{ events|tojson }};
    const counts = {};
    for (const e of events) {
      const d = e.timestamp.slice(0,10);
      counts[d] = (counts[d] || 0) + 1;
    }
    const labels = Object.keys(counts).sort();
    const data = labels.map(k => counts[k]);
    new Chart(document.getElementById('eventsPerDay').getContext('2d'), {
      type: 'bar',
      data: { labels, datasets: [{ label: 'Events per day', data }] },
      options: { responsive: true, scales: { x: { ticks: { autoSkip: true, maxRotation: 0 }}}}
    });
  </script>
  {% endif %}
</body>
</html>
"""
 
app = Flask(__name__)
 
@app.route("/", methods=["GET","POST"])
def index():
    if request.method == "POST":
        f = request.files.get("zipfile")
        tz = request.form.get("tz", "UTC")
        if not f:
            return "No file", 400
        save_path = UPLOADS / f.filename
        f.save(save_path)
        return redirect(url_for("case", filename=f.filename, tz=tz))
    return render_template_string(HTML, summary=None, events=None)
 
@app.route("/case/<filename>")
def case(filename):
    tz = request.args.get("tz", "UTC")
    zip_path = UPLOADS / filename
    if not zip_path.exists():
        return "Not found", 404
    root, events, summary, _ = analyze_zip(zip_path, tz_name=tz)
    return render_template_string(HTML, summary=type("Obj",(object,),summary), events=events)
 
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=False)

Table 23 바이브 코딩을 통한 타임라인 분석 소스코드

저작자표시 비영리 (새창열림)

'Forensics > DFC' 카테고리의 다른 글

[2025 DFC] 2025 디지털포렌식 챌린지 - 301 - 404 VMK Not Found  (0) 2026.04.20
[2025 DFC] 2025 디지털포렌식 챌린지 - 207 – iBackup Trace  (0) 2026.04.20
[2025 DFC] 2025 디지털포렌식 챌린지 - 203 - Ooops, your files have ben encrypted!  (0) 2026.04.20
[2025 DFC] 2025 디지털포렌식 챌린지 - 201 - Look at the Quote  (1) 2026.04.20
[2021 DFC] 2021 디지털포렌식 챌린지 - 301 - What is the secret information  (4) 2021.10.29

댓글

이 글 공유하기

  • 구독하기

    구독하기

  • 카카오톡

    카카오톡

  • 라인

    라인

  • 트위터

    트위터

  • Facebook

    Facebook

  • 카카오스토리

    카카오스토리

  • 밴드

    밴드

  • 네이버 블로그

    네이버 블로그

  • Pocket

    Pocket

  • Evernote

    Evernote

다른 글

  • [2025 DFC] 2025 디지털포렌식 챌린지 - 301 - 404 VMK Not Found

    [2025 DFC] 2025 디지털포렌식 챌린지 - 301 - 404 VMK Not Found

    2026.04.20
  • [2025 DFC] 2025 디지털포렌식 챌린지 - 207 – iBackup Trace

    [2025 DFC] 2025 디지털포렌식 챌린지 - 207 – iBackup Trace

    2026.04.20
  • [2025 DFC] 2025 디지털포렌식 챌린지 - 203 - Ooops, your files have ben encrypted!

    [2025 DFC] 2025 디지털포렌식 챌린지 - 203 - Ooops, your files have ben encrypted!

    2026.04.20
  • [2025 DFC] 2025 디지털포렌식 챌린지 - 201 - Look at the Quote

    [2025 DFC] 2025 디지털포렌식 챌린지 - 201 - Look at the Quote

    2026.04.20
다른 글 더 둘러보기

정보

포렌식 & 개발 이야기 - Forensics & Development 블로그의 첫 페이지로 이동

포렌식 & 개발 이야기 - Forensics & Development

  • 포렌식 & 개발 이야기 - Forensics & Development의 첫 페이지로 이동

검색

메뉴

  • 홈
  • 태그
  • 미디어로그
  • 위치로그
  • 방명록

카테고리

  • Category (527) N
    • Forensics (112) N
      • Magnet AXIOM (28)
      • Digital Forensics Informati.. (9)
      • Iphone Forensics (26)
      • DFC (12) N
      • 디지털포렌식전문가2급 자격증 (10)
      • FTK ACE 자격증 (7)
    • 이것저것 (12)
      • Ubuntu (6)
      • 디스코드 봇 (4)
      • Volatility GUI (2)
    • CTF (32)
      • NEWSECU (14)
      • CTF-d (5)
      • Puzzel - Network Forensics (2)
      • Security Traps (2)
      • system32.kr (5)
      • HMCTF (4)
    • Programming (322)
      • C (10)
      • Python (11)
      • 백준 (264)
      • 프로그래머스 (32)
      • 빅데이터분석기사 실기 (4)
    • 그냥 개발 및 잡담 (17)
      • Docker (2)
      • Google Cloud (3)
      • OS 개발 (3)
    • Best of Best (20)

최근 글

인기 글

댓글

공지사항

아카이브

태그

  • 포렌식
  • axiom
  • 디지털포렌식
  • 백준
  • 파이썬
  • 프로그래머스
  • Forensics
  • pental
  • 전체 보기…

정보

pental의 포렌식 & 개발 이야기 - Forensics & Development

포렌식 & 개발 이야기 - Forensics & Development

pental

블로그 구독하기

  • 구독하기
  • RSS 피드

방문자

  • 전체 방문자
  • 오늘
  • 어제

티스토리

  • 티스토리 홈
  • 이 블로그 관리하기
  • 글쓰기
Powered by Tistory / AXZ. Copyright © pental.

티스토리툴바