פענוח מזהי מפרסמים עבור רשתות מודעות

רשתות מודעות שמשתמשות ב תגי JavaScript למילוי מודעות דרך Authorized Buyers יכולות לקבל מזהי מפרסמים גם במכשירי Android וגם במכשירי iOS. המידע נשלח באמצעות המאקרו %%EXTRA_TAG_DATA%% או %%ADVERTISING_IDENTIFIER%% בתג JavaScript שמנוהל על ידי Authorized Buyers. בהמשך הקטע הזה נעסוק בחילוץ %%EXTRA_TAG_DATA%%, אבל ניתן לעיין במאמר רימרקטינג באמצעות IDFA או מזהה פרסום כדי לקבל פרטים על מאגר המידע הזמני של %%ADVERTISING_IDENTIFIER%%המאגר המוצפן MobileAdvertisingId, שאפשר לפענח במקביל.

ציר הזמן

  1. רשת המודעות מעדכנת את תגי JavaScript בתוך האפליקציה דרך ממשק המשתמש של Authorized Buyers, ומוסיפה את פקודת המאקרו %%EXTRA_TAG_DATA%% כפי שמוסבר בהמשך.
  2. בזמן הצגת המודעה, האפליקציה מבקשת מודעה מ-Authorized Buyers באמצעות Google Mobile Ads SDK, תוך העברה מאובטחת של מזהה המפרסם.
  3. האפליקציה מקבלת בחזרה את תג ה-JavaScript, ופקודת המאקרו %%EXTRA_TAG_DATA%% ממולאת במאגר הנתונים הזמני של הפרוטוקול של רשת המודעות, שמכיל את המזהה הזה.
  4. האפליקציה מפעילה את התג הזה, ומבצעת קריאה לרשת המודעות עבור המודעה הזוכה.
  5. כדי להשתמש במידע הזה (לייצר ממנו הכנסות), רשת המודעות צריכה לעבד את מאגר הפרוטוקול:
    1. מפענחים את המחרוזת websafe בחזרה ל-bytestring באמצעות WebSafeBase64.
    2. פענח אותו באמצעות הסכמה המתוארת בהמשך.
    3. מבצעים פעולת deserialize של האב ומשיגים את מזהה המפרסם מ-ExtraTagData.advertising_id או מ-ExtraTagData.hashed_idfa.

יחסי תלות

  1. את המקודד WebSafeBase64.
  2. ספריית הצפנה שתומכת ב-SHA-1 HMAC, כמו Openssl.
  3. מהדר של מאגר אחסון לפרוטוקולים של Google.

פענוח מחרוזת websafe

מכיוון שהמידע שנשלח באמצעות המאקרו %%EXTRA_TAG_DATA%% חייב להישלח דרך כתובת URL, השרתים של Google מקודדים אותו באמצעות base64 שבטוח לשימוש באינטרנט (RFC 3548).

לכן, לפני שתנסו לבצע פענוח, תצטרכו לפענח את תווי ה-ASCII בחזרה למחרוזת בייט (bytestring). הקוד לדוגמה C++ שבהמשך מבוסס על BIO_f_base64() של Project OpenSSL(), והוא חלק מקוד הפענוח לדוגמה של Google.

string AddPadding(const string& b64_string) {
  if (b64_string.size() % 4 == 3) {
    return b64_string + "=";
  } else if (b64_string.size() % 4 == 2) {
    return b64_string + "==";
  }
  return b64_string;
}

// Adapted from http://www.openssl.org/docs/man1.1.0/crypto/BIO_f_base64.html
// Takes a web safe base64 encoded string (RFC 3548) and decodes it.
// Normally, web safe base64 strings have padding '=' replaced with '.',
// but we will not pad the ciphertext. We add padding here because
// openssl has trouble with unpadded strings.
string B64Decode(const string& encoded) {
  string padded = AddPadding(encoded);
  // convert from web safe -> normal base64.
  int32 index = -1;
  while ((index = padded.find_first_of('-', index + 1)) != string::npos) {
    padded[index] = '+';
  }
  index = -1;
  while ((index = padded.find_first_of('_', index + 1)) != string::npos) {
    padded[index] = '/';
  }

  // base64 decode using openssl library.
  const int32 kOutputBufferSize = 256;
  char output[kOutputBufferSize];

  BIO* b64 = BIO_new(BIO_f_base64());
  BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
  BIO* bio = BIO_new_mem_buf(const_cast(padded.data()),
                             padded.length());
  bio = BIO_push(b64, bio);
  int32 out_length = BIO_read(bio, output, kOutputBufferSize);
  BIO_free_all(bio);
  return string(output, out_length);
}

המבנה של bytestring מוצפן

לאחר שמפענחים את תווי ASCII בחזרה ל-bytestring, תוכלו להתחיל לפענח אותו. ה-bytestring המוצפן מכיל 3 קטעים:

  • initialization_vector: 16 בייט.
  • ciphertext: סדרה של קטעים של 20 בייטים.
  • integrity_signature: 4 בייט.
{initialization_vector (16 bytes)}{ciphertext (20-byte sections)}{integrity_signature (4 bytes)}

המערך הבייטים של ciphertext מחולק למספר קטעים בגודל 20 בייטים, למעט שהקטע האחרון יכול להכיל בין 1 ל-20 בייטים, כולל. לכל קטע של הקובץ byte_array המקורי, ה-ciphertext התואם לו בגודל 20 בייטים נוצר באופן הבא:

<byte_array <xor> HMAC(encryption_key, initialization_vector || counter_bytes)>

כאשר || הוא שרשור.

הגדרות

משתנה פרטים
initialization_vector 16 בייטים – ייחודי לחשיפה.
encryption_key 32 בייטים – סופק בעת הגדרת החשבון.
integrity_key 32 בייטים – סופק בעת הגדרת החשבון.
byte_array אובייקט ExtraTagData בסדרה, בקטעים של 20 בייטים.
counter_bytes ערך הבייטים שמציג את המספר הסידורי של הקטע מופיע בהמשך.
final_message מערך בייטים כולל שנשלח באמצעות המאקרו %%EXTRA_TAG_DATA%% (בלי הקידוד WebSafeBase64).
אופרטורים פרטים
hmac(key, data) SHA-1 HMAC, באמצעות key כדי להצפין את data.
a || b המחרוזת a משורשרת עם המחרוזת b.

חשבו Count_bytes

counter_bytes מסמן את הסדר של כל קטע של 20 בייטים ב-ciphertext. שימו לב שהקטע האחרון עשוי להכיל בין 1 ל-20 בייט, כולל. כדי למלא את counter_bytes בערך הנכון בזמן הרצת הפונקציה hmac(), סופרים את הקטעים של 20 בייטים (כולל השאר) ומשתמשים בטבלת העזר הבאה:

מספר קטע ערך של counter_bytes
0 ללא
1 ... 256 בייט אחד. הערך גדל מ-0 ל-255 ברצף.
257 ... 512 2 בייט. הערך של הבייט הראשון הוא 0, כלומר הערך של הבייט השני מתפתח ברציפות מ-0 ל-255.
513 ... 768 3 בייט. הערך של שני הבייטים הראשונים הוא 0, כלומר, הערך של כל בייטים ברצף מ-0 ל-255.

חזרה למעלה

סכמת הצפנה

סכימת ההצפנה מבוססת על אותה הסכמה שמשמשת לפענוח אות הטירגוט ההיפר-מקומי.

  1. סידור טורי: המופע של האובייקט ExtraTagData, כפי שמוגדר במאגר הפרוטוקולים, עובר סריאליזציה תחילה דרך SerializeAsString() למערך בייטים.

  2. הצפנה: המערך הבייטים מוצפן באמצעות סכמת הצפנה מותאמת אישית, שנועדה לצמצם את תקורת הגודל תוך הקפדה על אבטחה מספקת. בסכימת ההצפנה נעשה שימוש באלגוריתם HMAC מורשה כדי ליצור פנקס סודי על סמך הערך initialization_vector, שהוא ייחודי לאירוע החשיפה.

פסאודו-קוד של הצפנה

byte_array = SerializeAsString(ExtraTagData object)
pad = hmac(encryption_key, initialization_vector ||
      counter_bytes )  // for each 20-byte section of byte_array
ciphertext = pad <xor> byte_array // for each 20-byte section of byte_array
integrity_signature = hmac(integrity_key, byte_array ||
                      initialization_vector)  // first 4 bytes
final_message = initialization_vector || ciphertext || integrity_signature

סכמת פענוח

קוד הפענוח צריך: 1) לפענח את מאגר הנתונים הזמני של הפרוטוקול באמצעות מפתח ההצפנה, וגם 2) לאמת את ביטים של התקינות באמצעות מפתח התקינות. המפתחות יסופקו לכם במהלך הגדרת החשבון. אין הגבלות על מבנה ההטמעה. בדרך כלל, אתם אמורים להיות מסוגלים לקחת את הקוד לדוגמה ולהתאים אותו לצרכים שלכם.

  1. יוצרים את ה-pad: HMAC(encryption_key, initialization_vector || counter_bytes)
  2. XOR: לוקחים את התוצאה הזו ואת <xor> עם המידע מוצפן כדי להפוך את ההצפנה.
  3. אימות: חתימת התקינות מעבירה 4 בייטים של HMAC(integrity_key, byte_array || initialization_vector)

פסאודו-קוד של פענוח

// split up according to length rules
(initialization_vector, ciphertext, integrity_signature) = final_message

// for each 20-byte section of ciphertext
pad = hmac(encryption_key, initialization_vector || counter_bytes)

// for each 20-byte section of ciphertext
byte_array = ciphertext <xor> pad

confirmation_signature = hmac(integrity_key, byte_array ||
                         initialization_vector)
success = (confirmation_signature == integrity_signature)

קוד C++ לדוגמה

כאן מופיעה פונקציית מפתח מתוך הקוד המלא לדוגמה לפענוח.

bool DecryptByteArray(
    const string& ciphertext, const string& encryption_key,
    const string& integrity_key, string* cleartext) {
  // Step 1. find the length of initialization vector and clear text.
  const int cleartext_length =
     ciphertext.size() - kInitializationVectorSize - kSignatureSize;
  if (cleartext_length < 0) {
    // The length cannot be correct.
    return false;
  }

  string iv(ciphertext, 0, kInitializationVectorSize);

  // Step 2. recover clear text
  cleartext->resize(cleartext_length, '\0');
  const char* ciphertext_begin = string_as_array(ciphertext) + iv.size();
  const char* const ciphertext_end = ciphertext_begin + cleartext->size();
  string::iterator cleartext_begin = cleartext->begin();

  bool add_iv_counter_byte = true;
  while (ciphertext_begin < ciphertext_end) {
    uint32 pad_size = kHashOutputSize;
    uchar encryption_pad[kHashOutputSize];

    if (!HMAC(EVP_sha1(), string_as_array(encryption_key),
              encryption_key.length(), (uchar*)string_as_array(iv),
              iv.size(), encryption_pad, &pad_size)) {
      printf("Error: encryption HMAC failed.\n");
      return false;
    }

    for (int i = 0;
         i < kBlockSize && ciphertext_begin < ciphertext_end;
         ++i, ++cleartext_begin, ++ciphertext_begin) {
      *cleartext_begin = *ciphertext_begin ^ encryption_pad[i];
    }

    if (!add_iv_counter_byte) {
      char& last_byte = *iv.rbegin();
      ++last_byte;
      if (last_byte == '\0') {
        add_iv_counter_byte = true;
      }
    }

    if (add_iv_counter_byte) {
      add_iv_counter_byte = false;
      iv.push_back('\0');
    }
  }

קבלת נתונים ממאגר הנתונים הזמני של פרוטוקול רשת המודעות

אחרי פענוח ופענוח של הנתונים שהועברו ב-%%EXTRA_TAG_DATA%%, אפשר לבצע פעולת deserialize של מאגר הנתונים הזמני ולקבל את מזהה המפרסם לטירגוט.

אם אתם לא מכירים את מאגרי הפרוטוקולים, כדאי להתחיל בתיעוד שלנו.

הגדרה

מאגר הפרוטוקולים של רשתות המודעות שלנו מוגדר כך:

message ExtraTagData {
  // advertising_id can be Apple's identifier for advertising (IDFA)
  // or Android's advertising identifier. When the advertising_id is an IDFA,
  // it is the plaintext returned by iOS's [ASIdentifierManager
  // advertisingIdentifier]. For hashed_idfa, the plaintext is the MD5 hash of
  // the IDFA.  Only one of the two fields will be available, depending on the
  // version of the SDK making the request.  Later SDKs provide unhashed values.
  optional bytes advertising_id = 1;
  optional bytes hashed_idfa = 2;
}

צריך לבצע פעולת deserial שלו באמצעות ParseFromString(), כפי שמתואר בתיעוד של מאגר הנתונים הזמני של C++.

לפרטים על השדות 'advertising_id' ו-'hashed_idfa' ב-Android, קראו את המאמר פענוח מזהה פרסום ומלאי שטחי פרסום באפליקציות לנייד לפי IDFA.

ספריית Java

במקום להטמיע את האלגוריתמים של הקריפטו כדי לקודד ולפענח את מזהי המפרסם לרשתות של מודעות, אפשר להשתמש ב- DoubleClickCrypto.java. למידע נוסף קראו את המאמר קריפטוגרפיה.