#include <stdio.h>
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32")
//#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define BUFSIZE 1024
int main()
{
/* WSAStartup 함수의 인자로 들어가는 변수입니다. */
WSADATA wsaData;
/* socket 함수의 반환값을 저장할 UINT_PTR 변수입니다. */
SOCKET client;
/* SOCKADDR_IN https://docs.microsoft.com/ko-kr/windows/win32/api/winsock/ns-winsock-sockaddr_in
* 소켓의 주소를 담는 기본 구조체 역할을 합니다.
* 선택한 프로토콜에 따라 구조가 달라지며, 네트워크 바이트 순서대로 표현됩니다.
* 2바이트의 family 부분과 14바이트의 data부분으로 나눠집니다.
*/
SOCKADDR_IN server_addr;
/* 보낼 문자열과 받은 문자열을 저장할 변수입니다. */
char buffer[BUFSIZE];
int size;
/* WSAStartup https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsastartup
* WSACleanup 함수와 같이 사용되며 소켓 프로그램의 시작을 나타냅니다.
*
* 첫번째 인자에는 winsock 버전을 넣습니다.
* 상위 2바이트에는 주 버전 번호, 하위 2바이트에는 부 버전 번호를 넣습니다.
* 2.2버전을 사용할려면 0x0202 또는 MAKEWORD(2,2)를 사용하면 됩니다.
*
* 두번째 인자에는 WSADATA 구조체의 포인터를 넣습니다.
* 이 포인터는 윈도우 소켓 구현에 대한 세부정보를 받는 WSADATA 타입 구조체의 포인터입니다.
*
* 성공하면 0을 반환하고, 실패하면 나열된 오류 코드중 하나를 반환합니다.
* 오류코드를 반환하기 때문에 WSAGetLastError 함수의 호출은 필요하지 않으며, 사용하면 안됩니다.
*/
WSAStartup(0x0202, &wsaData);
/* socket https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
* 소켓을 만듭니다.
*
* 첫번째 인자에서는 주소 계열을 지정해줍니다.
* AF_* 상수와 PF_* 상수의 값이 동일하기 때문에 원하는 상수를 쓰면 됩니다.
*
* 두번째 인자에서는 소켓의 유형을 지정해줍니다.
*
* 세번째 인자에서는 소켓의 프로토콜을 지정해줍니다.
* 값을 0으로 지정하면 프로토콜을 지정하지 않고 서버가 사용할 프로토콜을 선택합니다.
*
* 오류가 발생하지 않으면 새 소켓을 참조하는 descriptor 를 반환합니다.
* 그렇지 않으면 INVALID_SOCKET 값이 반환되고 WSAGetLastError를 호출하여 오류코드를 반환받을 수 있습니다.
*/
client = socket(AF_INET, SOCK_STREAM, 0);
if (client == INVALID_SOCKET) { //에러 핸들링
printf("socket error, error code : %d", WSAGetLastError());
system("pause");
return 1;
}
printf("socket descriptor : %d\n", client);
// server_addr 변수를 0으로 초기화 해줍니다.
memset(&server_addr, 0, sizeof(server_addr));
// 주소 체계를 AF_INET(IPv4)로 지정합니다.
server_addr.sin_family = AF_INET;
// 주소를 152.70.238.188로 지정합니다.
server_addr.sin_addr.S_un.S_addr = inet_addr("152.70.238.188");
/* 포트를 3000으로 지정해주는데,
* htons 함수는 2바이트 데이터를 네트워크 바이트 정렬 방식으로 변경합니다.
*/
server_addr.sin_port = htons(3000);
/* connect https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect
* SOCKET 변수와 주소 변수를 받아서 소켓을 연결합니다.
*
* 첫번째 인자는 연결되지 않은 소켓의 descriptor을 넣습니다.
* 두번째 인자는 SOCKADDR_IN 변수를 sockaddr 구조체의 포인터로 변경하여 넣어줍니다.
* 세번째 인자는 sockaddr 구조체의 크기를 넣어줍니다.
*
* 오류가 발생하지 않으면 0을 반환하고, 아니면 SOCKET_ERROR를 반환합니다.
* socket 함수와 같이 WSAGetLastError 를 통해서 오류코드를 반환받을 수 있습니다.
* 비 블로킹 소켓(비동기)을 사용하면 SOCKET_ERROR를 반환하고,
* WSAGetLastError에서 WSAEWOULDBLOCK를 반환합니다.
*/
int con = connect(client, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (con == SOCKET_ERROR) {
printf("connect error, error code : %d\n", WSAGetLastError());
system("pause");
return 1;
}
// 연결이 성공됨을 출력합니다.
printf("----------------------connect success----------------------\n\n");
// buffer를 비웁니다.
memset(buffer, 0, BUFSIZE);
// buffer에 "10215" 문자열을 복사합니다.
strcpy(buffer, "10215");
// 소켓에 전송합니다.
/* send https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-send
* 연결된 소켓에 데이터를 전송합니다.
*
* 첫번째 인자엔 연결된 소켓의 descriptor가 들어갑니다.
* 두번째 인자엔 전송할 데이터를 포함하는 버퍼의 포인터가 들어갑니다.
* 세번째 인자엔 버퍼의 크기가 들어갑니다.
* 네번째 인자엔 플래그가 들어갑니다.
*
* MSG_DONTROUTE, MSG_OOB 가 비트연산자를 통해 들어갑니다.(0은 지정 안함)
* 오류가 없으면 전송된 총 바이트 수를 반환하고, 그렇지 않으면 SOCKET_ERROR 값이 반환됩니다.
* WSAGetLastError를 통해 오류코드를 반환받을 수 있습니다.
*/
size = send(client, buffer, BUFSIZE, 0);
if (size == SOCKET_ERROR){
printf("send error, error code : %d\n", WSAGetLastError());
system("pause");
return 1;
}
// 전송한 메세지를 출력합니다.
printf("sent message: \"%s\"\n", buffer);
// 버퍼를 0으로 비웁니다.
memset(buffer, 0, BUFSIZE);
/* recv https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-recv
* 연결된 소켓에서 데이터를 수신합니다.
*
* 첫번째 인자엔 연결된 소켓의 descriptor가 들어갑니다.
* 두번째 인자엔 수신할 데이터를 받을 버퍼의 포인터가 들어갑니다.
* 세번째 인자엔 버퍼의 크기가 들어갑니다.
* 네번째 인자엔 플래그가 들어갑니다.
*
* MSG_PEEK, MSG_OOB, MSG_WAITALL가 비트연산자를 통해 들어갑니다.(0은 지정 안함)
* 오류가 없으면 수신된 총 바이트 수를 반환하고, 그렇지 않으면 SOCKET_ERROR 값이 반환됩니다.
* WSAGetLastError를 통해 오류코드를 반환받을 수 있습니다.
*/
size = recv(client, buffer, BUFSIZE, 0);
if (size == SOCKET_ERROR){
printf("receive error, error code : %d\n", WSAGetLastError());
system("pause");
return 1;
}
// 수신한 메세지를 출력합니다.
printf("received message: \"%s\"\n", buffer);
/* WSACleanup https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsacleanup
* WSAStartup 함수와 같이 사용되며 소켓 프로그램의 끝을 나타냅니다.
*
* 작업에 성공하면 0을 반환하고, 그렇지 않으면 SOCKET_ERROR 값을 반환합니다.
* WSAGetLastError를 통해 오류코드를 반환받을 수 있습니다.
*/
if (WSACleanup() == SOCKET_ERROR){
printf("socket terminate error, error code : %d\n", WSAGetLastError());
}
system("pause");
}
client는 주석에서 설명을 거의 다 해놨기 때문에, server 부분만 약간 설명을 하겠습니다.
mov eax, 0보다 xor eax, eax나 sub eax, eax가 더 작기 때문에 쓰는것 같다.
lea rdi, [rip + 0xde1] (main+27)
rdi에 [rip + 0xde1]의 주소를 넣는데 이 위치엔 "Enter Num : " 문자열이 위치해 있다.
mov eax, 0 (main+34)
함수를 호출하기 전에 반환값을 0으로 초기화 해 주는거 같다.
call printf@plt (main+39)
printf 함수를 호출한다.
lea rax, [rbp - 0x10] (main+44)
mov rsi, rax (main+48)
rsi: 시스템 V AMD64 ABI 기준 함수의 2번째 인자
lea rdi, [rip + 0xdd6]
rdi: 시스템 V AMD64 ABI 기준 함수의 1번째 인자
mov eax, 0
반환값 0으로 초기화
call __isoc99_scanf@plt
scanf 함수 호출, 일단 5984 넣어보겠다.
이걸 C 코드로 바꿔서 생각을 해 보면
int x;
scanf("%d", &x);
가 되겠다.
int x가 위치한 스택에서의 위치는 rbp - 0x10이 되겠다.
mov eax, dword ptr [rbp - 0x10]
rbp - 0x10은 아까 위에 c 코드로 보면 int x가 위치한 주소고, eax는 그냥 레지스터다.
eax에 scanf에서 입력받은 값을 저장한다.
mov edi, eax
edi가 첫번째 인자이고 아래에 함수가 오므로 아래 함수의 첫번째 인자에 eax의 값을 넣는다.
call test
test 함수를 호출한다. si 명령어를 통해서 내부로 들어가보겠다.
push rbp (test+4)
mov rbp, rsp (test+5)
스택 프레임을 생성해주는 기본적인 코드다. (함수 프롤로그)
mov dword ptr [rbp - 4], edi (test+8)
첫번째 인자를 스택에 저장한다. (레지스터는 다른곳에서도 많이 써야 하기 때문에)
cmp dword ptr [rbp - 4], 0x64 (test+11)
jne test+24 (test+15)
분기문이 나왔다.
cmp dword ptr [rbp - 4], 0x64
rbp - 4와 0x64를 비교한다.
jne test+24
rbp - 4가 0x64가 아니면 test+24로 점프한다.
일단 인자로 들어온 값(rbp - 4)가 0x64가 아니라고 가정하고 생각을 해보겠다.
jne test+24 (test+15)
mov eax, 0 (test+24)
pop rbp (test+29)
ret (test+30)
mov eax, 0 을 해주고 바로 함수 에필로그가 실행된다.
pop rbp
ret
(함수 에필로그)
한번 c 코드로 만들어보자
int test(int x){
if (x != 0x64){
return 0;
}
...
}
이렇게 될 것이다.
그러면 만약 인자로 들어온 값이 0x64라고 생각을 하고 코드를 해석해보자
cmp dword ptr [rbp - 4], 0x64 (test+11)
jne test+24 (test+15) //점프 안함
mov eax, 1 (test+17)
jmp test+29 (test+22)
pop rbp (test+29)
ret (test+30)
mov eax, 1은 eax를 1로 설정해준다. (eax는 반환값을 저장하는 용도로도 사용되는 레지스터이다.)
jmp test+29는 test+29줄로 무조건 점프한다.
test+29로 점프했으니
pop rbp 부분부터 실행한다.
pop rbp
ret
함수의 에필로그이다.
C 코드로 한번 만들어보자
int test(int x){
if (x != 0x64){
...
}
else{
return 1;
}
}
이렇게 될 것이다.
한번 두 코드를 합쳐보면
int test(int x){
if (x != 0x64){
return 0;
}
else{
return 1;
}
}
이렇게 될 것이다.
그러면 test 함수는 분석을 다 했다.
위에서 함수 분석을 한번 해봤으니까 두번째부턴 간단히 하고 넘어가겠다.
mov dword ptr [rbp - 0xc], eax (main+78)
eax(반환값)을 rbp - 0xc 위치에 저장한다.
cmp dword ptr [rbp - 0xc], 1 (main+81)
jne main+120 (main+85)
[rbp - 0xc]와 1을 비교해서
[rbp - 0xc]가 1이 아니면 main+120으로 점프한다.
rbp - 0xc가 1일때
main+120로 넘어가지 않고 계속 진행한다
mov r8d, 5
mov ecx, 4
mov edx, 3
mov esi, 2
mov edi, 1
인자 순서가
edi -> esi -> edx -> ecx -> r8 -> r9 -> 스택
순이므로
아래에 오는 함수의 인자에
(1, 2, 3, 4, 5) 순서대로 넣는다.
call win
win 함수를 호출한다.
push rbp
mov rbp, rsp
함수 프롤로그
sub rsp, 0x20
스택 프레임에 0x20만큼 공간 생성
mov dword ptr [rbp - 4], edi
mov dword ptr [rbp - 8], esi
mov dword ptr [rbp - 0xc], edx
mov dword ptr [rbp - 0x10], ecx
mov dword ptr [rbp - 0x14], r8d
함수의 인자들을 차례대로 스택에 넣어준다.
lea rdi, [rip + 0xe19]
rip + 0xe19의 주소를 계산해서 rdi에 넣어주는데, 이 값은 "good" 이다.
그리고 rdi는 함수의 첫번째 인자에 사용되는 레지스터이다.
call puts@plt
puts 함수를 실행한다. puts 함수가 뭘 하는진 알거라고 생각한다.
nop
파이프라인에서 한 사이클 쉬라는 뜻이다.
leave
현재까지 썻던 메모리 스택을 깔끔히 비우고, 자신을 호출했던 메모리의 베이스 주소를 ebp에 다시 채운다.
ret
함수 종료
eax를 설정하지 않는걸 보아 아마도 이 함수의 반환형은 void일 것이다.
jmp main+136 (main+118)
main+136 위치로 무조건 점프한다.
rbp - 0xc가 1이 아닐때
main+120으로 점프해 main+120부터 실행한다.
cmp dword ptr [rbp - 0xc], 0 (main+120)
jne main+136 (main+124)
rbp - 0xc가 0이 아니면 136번줄로 이동한다.
test 함수에서는 0과 1만 반환하기 때문에 메모리에 오류가 생긴게 아닌 이상 이 분기를 탈 일은 없다.
mov eax, 0
함수의 반환값을 0으로 초기화 한다.
call lose
lose 함수를 호출한다.
push rbp
mov rbp rsp
함수 프롤로그다.
lea rdi, [rip + 0xe07]
rdi(첫번째 인자) 에 [rip + 0xe07]의 주소를 넣는다.
nop
파이프라인에서 한 사이클 쉰다.
pop rbp
ret
함수 에필로그
eax를 설정하지 않는걸 보아 아마도 이 함수의 반환형은 void일 것이다.
mov eax, 0
함수의 반환값을 0으로 설정한다.
mov rdx, qword ptr [rbp - 8]
rbp - 8의 있는 값을 rdx로 옮긴다.
정확히 무슨 값인진 잘 모르겠지만 아마도 canary 값으로 보인다.
(스택 오버플로우가 일어났는지 검증하기 위한 값)
아래 __stack_chk_fail 함수에 필요한 인자인것 같다.
xor rdx, qword ptr fs:[0x28]
보호된 코드
je main+161
main+161로 점프한다.
call __stack_chk_fail@plt
스택 오버플로우가 났을시에 실행되는 함수인것 같다.
leave
ret
함수 에필로그
이 전체 코드를 하나의 c 코드로 바꿔보자면
#include <stdio.h>
int test(int x){
if (x != 0x64){
return 0;
}
else{
return 1;
}
}
void win(int a, int b, int c, int d, int e){
puts("good");
}
void lose(){
puts("bad");
}
int main(){
int x;
printf("Enter Num : ");
scanf("%d", &x);
int c = test(x);
if (c == 1){
win(1, 2, 3, 4, 5);
}
else if (c == 0){
lose();
}
return 0;
}
이렇게 될 것 이다.
그러면 good 이 나오게 하고 싶으면 0x64에 해당되는 정수 100을 넣으면 될 것이고,