보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

socat daemonize

자료 2019.07.22 02:00

동아리 신입생 여름방학 교육 마지막을 장식할 CTF를 여는데, 서버관리를 전부 내가 다하게 됐다. (할 사람이 없음)

 

docker로 환경 구축하고 적당히 문제 설정까지 완벽하게 한다음 올린다.

 

아래와 같이 이전에 사용하려고 했던 방식은 얼마든지 사용자가 권한을 얻었을 경우, socat parent를 종료시킬 수 있는 문제가 발생했다.

su - [user] -c "socat ..."

 

그래서 socat에 setuid, setgid가 있었으면 좋겠다 싶어서 구글링해보니 있었다. 간단하게 python으로 코드 짜서 올린다.

 

from os import system
import sys
import argparse

parser = argparse.ArgumentParser(description='socat daemonize')
parser.add_argument("P", metavar="homepath", type=str, help="daemon home path")
parser.add_argument("p", metavar="port", type=int, help="port")
parser.add_argument("g", metavar="gid",type=str, help="setgid")
parser.add_argument("u", metavar="uid",type=str, help="setuid")
parser.add_argument("b", metavar="binary",type=str, help="binary")

args = parser.parse_args()
home_path = args.P
port = args.p
gid = args.g
uid = args.u
binary = args.b

system('cd {}; socat TCP-LISTEN:{},setgid={},setuid={},reuseaddr,fork EXEC:"{}" 2>/dev/null &'.format(home_path, port, gid, uid, binary))

 

socat에서 setgid, setuid를 통해 적당히 권한을 낮추고 바이너리 실행하면 된다.

'자료' 카테고리의 다른 글

socat daemonize  (0) 2019.07.22
[Conoha/VPS] KernelPanic 복구  (0) 2018.12.16
[Conoha] letsencrypt wildcard 인증서 발급  (0) 2018.12.09
[Pwntools] pyserial uninstall fail시  (0) 2018.07.17
[Python] Mutation Fuzzer  (0) 2018.05.16
Abusing File Structure  (0) 2018.01.04
블로그 이미지

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

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

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

KuroNeko_

KuroNeko

[Grotesque] elf(100pt)

2019.07.06 16:38

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

[Grotesque] sudoku(111pt)

2019.07.06 01:47

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

[Rookiss] note (200pt)

2019.06.24 23:57

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

#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 제거)

 

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

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

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
[nodejs] mongoose를 이용한 로그인 구현  (0) 2019.05.08
[Windows Kernel Driver] 개발환경 구성  (0) 2018.11.25
유저 영역 Stack Canary 분석  (0) 2018.08.16
python AES  (0) 2018.08.10
블로그 이미지

KuroNeko_

KuroNeko