/* * Non-compilable C-like reconstruction of /games/pokemon_pro/conagent. * * This is not a Ghidra C export. Ghidra imported the binary but did not create * normal functions for most stripped text during this pass. The code below is * manually reconstructed from Ghidra imports, objdump ranges, and strings. */ typedef struct AgentConfig AgentConfig; typedef struct Log Log; typedef struct GameClient GameClient; typedef struct HttpClient HttpClient; typedef struct GameMessage GameMessage; typedef struct HttpRequest HttpRequest; typedef struct TransferRequest TransferRequest; static volatile bool g_shutdown; int conagent_main(int argc, char **argv) { AgentConfig cfg; Log *log; GameClient *game; cfg = default_agent_config(); parse_agent_args(argc, argv, &cfg); install_signal_handler(SIGTERM, request_shutdown); install_signal_handler(SIGINT, request_shutdown); log = create_log_channel(&cfg); if (log == NULL) { if (!cfg.silent) { fwrite("WARNING: Failed to create agent log file.\n", 1, 42, stderr); fwrite("WARNING: Failed to create agent log channel.\n", 1, 45, stderr); } } log_info(log, "Stern Pinball Connectivity Agent %s", "1.0.16 (Linux,ARM64,GNU)"); log_info(log, "Using libagentcmn %s", "libagent 1.0.16 (Linux,ARM64,GNU)"); log_agent_configuration(log, &cfg); game = game_client_create(&cfg, log); if (game == NULL) { log_fatal(log, "Failed to create Game process interface."); destroy_log_channel(log); return 0; } game_client_start(game, log); while (!g_shutdown) { game_client_update(game, log); usleep(16000); } game_client_stop(game, log); destroy_log_channel(log); return 0; } GameClient *game_client_create(const AgentConfig *cfg, Log *log) { GameClient *gc; gc = allocate_game_client_state(); if (gc == NULL) { log_error(log, "Failed to allocate memory for game_client_state_t."); return NULL; } gc->socket_path = cfg->game_process_host ? cfg->game_process_host : "/usr/local/spike/agent.uds"; gc->state = NOT_CONNECTED; gc->next_rx_id = 0; gc->next_tx_id = 0; gc->http = http_client_create(cfg, log); if (gc->http == NULL) { log_error(log, "Failed to create HTTP client to access back-end server."); free(gc); return NULL; } init_arena(&gc->json_arena, "game_client_json"); init_arena(&gc->action_arena, "game_client_action"); init_arena(&gc->config_arena, "game_client_config"); return gc; } void game_client_update(GameClient *gc, Log *log) { GameMessage msg; if (gc->state == NOT_CONNECTED) { if (agent_client_connect(gc->socket_path)) { log_info(log, "Successfully connected to Game process. Resetting state."); gc->state = NEGOTIATE_PROTOCOL; send_protocol_request(gc); } return; } while (receive_game_message(gc, &msg)) { GameAction action = decode_game_message(gc, &msg); process_game_action(gc, log, action); } if (gc->state == COMMS_ENABLED) { maybe_send_heartbeat(gc, log); http_client_update(gc->http, log); } } void process_game_action(GameClient *gc, Log *log, GameAction action) { switch (action.type) { case AGREE_PROTOCOL_VERSION: if (action.protocol_version == EXPECTED_PROTOCOL_VERSION) { log_info(log, "Negotiated protocol version %d.", action.protocol_version); gc->state = AWAIT_CONFIG; } else { log_error(log, "Expected protocol version %d, got unsupported version %d.", EXPECTED_PROTOCOL_VERSION, action.protocol_version); disconnect_game(gc); } break; case CONFIG_SECURITY: require(action.has_device_credentials, "CONFIG_SECURITY message missing device credentials."); store_security_config(gc, action.security); break; case CONFIG_NETWORK: validate_network_config(action.network); http_client_configure(gc->http, action.network); break; case CONFIG_TITLE: require(action.serial_number, "CONFIG_TITLE message missing machine serial number."); require(action.machine_configuration_number, "CONFIG_TITLE message missing machine configuration number."); require(action.code_version, "CONFIG_TITLE message missing code version identifier."); require(action.game_title, "CONFIG_TITLE message missing game title identifier."); require(action.game_model, "CONFIG_TITLE message missing game model identifier."); store_title_config(gc, action.title); break; case COMMS_ENABLE: if (gc->state == NEGOTIATE_PROTOCOL) { log_error(log, "Cannot enable communications during protocol version negotiation"); break; } if (!have_required_config(gc)) { log_error(log, "Cannot enable communications while waiting for required configuration data"); break; } if (!http_sync_api_token(gc->http, log)) { log_error(log, "Failed to retrieve the API token from the backend server"); break; } gc->state = COMMS_ENABLED; break; case COMMS_DISABLE: gc->state = COMMS_DISABLED; break; case MACHINE_REGISTER: api_game_register(gc, log, &action.register_machine); break; case SET_MACHINE_UUID: gc->machine_uuid = action.machine_uuid; log_info(log, "Machine UUID updated to %s.", gc->machine_uuid); break; case PLAYER_AUTHENTICATE: api_player_auth(gc, log, action.user_uuid); break; case PING_REQUEST: api_ping(gc, log); break; case GAME_TO_SERVER_POST: if (!valid_game_api_endpoint(action.endpoint)) { nack(action.id, "The API endpoint supplied with GAME_TO_SERVER_POST request is invalid"); break; } api_game_to_server_post(gc, log, action.endpoint, action.json_payload); break; case GAME_TO_SERVER_GET: if (!valid_game_api_endpoint(action.endpoint)) { nack(action.id, "The API endpoint supplied with GAME_TO_SERVER_GET request is invalid"); break; } api_game_to_server_get(gc, log, action.endpoint, action.args); break; case FILE_TRANSFER_REQUEST: http_client_queue_file_transfer(gc->http, &action.transfer); break; case DISABLE_INTERFACE_REQUEST: disable_network_interface(log, action.interface_name); break; case CONFIGURE_INTERFACE_REQUEST: reconfigure_network_interface(log, &action.network_interface); break; case QUERY_SERVER_TIME: http_client_query_server_time(gc->http, log, action.server_host); break; case CHECK_FOR_UPDATES: api_version_upgrades_available(gc, log); break; } } bool http_sync_api_token(HttpClient *http, Log *log) { char url[MAX_URL]; char body[MAX_JSON]; char response[MAX_RESPONSE]; json_t *json; if (!http->server_host) { log_error(log, "http_sync_api_token missing back-end server hostname or IP address."); return false; } if (!http->api_username) { log_error(log, "http_sync_api_token missing API token username."); return false; } if (!http->api_password) { log_error(log, "http_sync_api_token missing API token password."); return false; } snprintf(url, sizeof(url), "https://%s/api-token-auth/", http->server_host); snprintf(body, sizeof(body), "{\"username\":\"%s\",\"password\":\"%s\"}", http->api_username, http->api_password); CURL *curl = curl_easy_init(); if (curl == NULL) { log_error(log, "Failed to allocate cURL request object."); return false; } curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, strlen(body)); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, "Content-Type: application/json"); curl_easy_setopt(curl, CURLOPT_USERAGENT, "Spike-Connectivity-Agent/1.0"); if (curl_easy_perform(curl) != CURLE_OK) { log_error(log, "http_sync_api_token: Failed to retrieve API token"); curl_easy_cleanup(curl); return false; } json = parse_json(response); if (!json_is_object(json)) { log_error(log, "http_sync_api_token: Expected json_type_object"); curl_easy_cleanup(curl); return false; } if (!json_get_string(json, "token", http->api_token, sizeof(http->api_token))) { log_error(log, "http_sync_api_token: Failed to find the token field."); curl_easy_cleanup(curl); return false; } curl_easy_cleanup(curl); return true; } HttpResult http_execute_api(HttpClient *http, HttpRequest *req, Log *log) { char url[MAX_URL]; char auth[MAX_HEADER]; char device_token[MAX_HEADER]; char device_refresh[MAX_HEADER]; if (req->query_len > 0) { snprintf(url, sizeof(url), "https://%s%s/?%.*s", http->server_host, req->endpoint, req->query_len, req->query); } else { snprintf(url, sizeof(url), "https://%s%s/", http->server_host, req->endpoint); } snprintf(auth, sizeof(auth), "Authorization: Token %s", http->api_token); snprintf(device_token, sizeof(device_token), "Device-Token: %s", http->device_token); snprintf(device_refresh, sizeof(device_refresh), "Device-Refresh: %s", http->device_refresh); curl_easy_reset(req->curl); curl_easy_setopt(req->curl, CURLOPT_URL, url); curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, auth); curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, device_token); curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, device_refresh); curl_easy_setopt(req->curl, CURLOPT_USERAGENT, "Spike-Connectivity-Agent/1.0"); if (req->method == HTTP_PUT) { curl_easy_setopt(req->curl, CURLOPT_CUSTOMREQUEST, "PUT"); } else if (req->method == HTTP_DELETE) { curl_easy_setopt(req->curl, CURLOPT_CUSTOMREQUEST, "DELETE"); } else if (req->method == HTTP_POST) { curl_easy_setopt(req->curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(req->curl, CURLOPT_HTTPHEADER, "Content-Type: application/json"); } CURLcode rc = curl_easy_perform(req->curl); if (rc != CURLE_OK) { log_error(log, "curl_easy_perform returned failure code %d.", rc); return HTTP_RESULT_CURL_ERROR; } read_response_headers(req, "Message-Number: ", "App-Status-Code: "); return classify_http_response(req); } void disable_network_interface(Log *log, const char *ifname) { char path[128]; char cmd[256]; if (!valid_interface_name(ifname)) { log_error(log, "Cannot reconfigure network interface; no interface name supplied."); return; } snprintf(path, sizeof(path), "/tmp/network_interface.%s", ifname); if (!file_exists(path)) { log_info(log, "Skipping disablement of network interface %s since interface configuration file %s does not exist.", ifname, path); return; } snprintf(cmd, sizeof(cmd), "ifdown -i %s %s", path, ifname); log_info(log, "Executing command \"%s\".", cmd); int rc = system(cmd); if (rc != 0) { log_error(log, "The command \"%s\" failed (exit code %d).", cmd, rc); } } bool reconfigure_network_interface(Log *log, const NetworkInterfaceConfig *cfg) { char path[128]; char cmd[256]; FILE *fp; validate_network_interface_config(cfg); snprintf(path, sizeof(path), "/tmp/network_interface.%s", cfg->ifname); fp = fopen(path, "w"); if (fp == NULL) { log_error(log, "Failed to open network interface configuration file \"%s\".", path); return false; } if (cfg->dhcp) { fprintf(fp, "iface %s inet dhcp\n", cfg->ifname); } else { fprintf(fp, "iface %s inet static\n", cfg->ifname); fprintf(fp, "address %s\n", cfg->address); fprintf(fp, "netmask %s\n", cfg->netmask); fprintf(fp, "gateway %s\n", cfg->gateway); if (cfg->secondary_dns) { fprintf(fp, "dns-nameservers %s %s\n", cfg->primary_dns, cfg->secondary_dns); } else { fprintf(fp, "dns-nameservers %s\n", cfg->primary_dns); } } fflush(fp); fclose(fp); if (cfg->store_only) { log_info(log, "Not actually reconfiguring network interface %s; store-only flag is set.", cfg->ifname); return true; } snprintf(cmd, sizeof(cmd), "ifup -i %s %s", path, cfg->ifname); log_info(log, "Executing command \"%s\".", cmd); return system(cmd) == 0; } void *agent_client_rx_thread(void *arg) { GameClient *gc = arg; pthread_setname_np(pthread_self(), "agent_client_rx"); log_info(gc->log, "Client receive thread started normally."); while (!gc->rx_shutdown) { fd_set readfds; FD_ZERO(&readfds); FD_SET(gc->socket_fd, &readfds); int rc = select(gc->socket_fd + 1, &readfds, NULL, NULL, NULL); if (rc < 0) { if (errno == EAGAIN || errno == EINTR) { continue; } gc->last_error = errno; break; } if (FD_ISSET(gc->socket_fd, &readfds)) { ssize_t n = recv(gc->socket_fd, gc->rxbuf, sizeof(gc->rxbuf), 0); if (n == 0) { log_info(gc->log, "Client receive thread saw client disconnect."); break; } if (n < 0) { gc->last_error = errno; break; } ParserStatus st = agent_parse_frame(&gc->parser, gc->rxbuf, n); if (st == PARSER_COMPLETE) { push_received_message(gc, &gc->parser.message); } else if (st == PARSER_ERROR) { log_error(gc->log, "Encountered error decoding message frame"); reset_stream(gc); } } } log_info(gc->log, "Client receive thread terminated."); return NULL; } void cloudwatch_put_log_events(CloudWatchLogger *cw, const char *message) { /* * Builds an AWS Signature Version 4 request: * service: logs * region: us-west-1 * target: Logs_20140328.PutLogEvents * URL: https://logs.us-west-1.amazonaws.com * * The binary imports SHA256_* and HMAC and contains strings for the * canonical request, signed headers, x-amz-date, and nextSequenceToken. */ char payload[MAX_CLOUDWATCH_PAYLOAD]; char authorization[MAX_HEADER]; build_put_log_events_json(payload, cw->log_group, cw->log_stream, cw->next_sequence_token, message); aws4_sign_request(authorization, cw->access_key, cw->secret_key, "us-west-1", "logs", payload); curl_easy_setopt(cw->curl, CURLOPT_URL, "https://logs.us-west-1.amazonaws.com"); curl_easy_setopt(cw->curl, CURLOPT_HTTPHEADER, "x-amz-target: Logs_20140328.PutLogEvents"); curl_easy_setopt(cw->curl, CURLOPT_HTTPHEADER, authorization); curl_easy_setopt(cw->curl, CURLOPT_POSTFIELDS, payload); if (curl_easy_perform(cw->curl) != CURLE_OK) { log_error(cw->log, "CLOUD LOG POST: curl_easy_perform() failed"); } }