Reversing the AD Plugin UID algorithm

Background:
I’ll start by saying we have a rather large AD with over a million users. #humblebrag

I had a ticket escalated to me that was quite odd. A user had logged into their Mac, but a number of applications and command line tools were reporting a different user account.

After a bit of trouble shooting and I found that both of these users had the same UID on the Mac. So I started to dig into how this could be and how the AD Plugin in generates its UID. The post below is a result of that work.

I think it is pretty well known that the AD Plugin uses the first 32 Bits of the 128 Bit objectGUID in AD to determine the UID on the Mac.

But after a bit more digging it turns out its not quite that simple.

I’ll work with a few examples here and show you how the AD Plugin determines the UID that will be used and provide a script that will allow you to determine the UID of your users accounts in AD. From here you can check to see if you have any UID collisions.

First lets start with a user in AD.

If we inspect the users account record in AD with something like Apache directory studio, we can see the objectGUID which is a 128 bit hex string.

For example:
screen-shot-2016-11-29-at-4-50-30-pm

Here we can see an objectGUID with a value of:

6C703CF1-B5D1-41F8-880B-317728CBD4F5

Now the AD Plugin will read this value and take the first 32 Bits which is: 6C703CF1

It then converts this hex value to decimal. This can be achieved by using the following command:

echo "ibase=16; 6C703CF1" | bc

Which will return: 1819294961

Now you might say, well thats easy!

And I thought so too. But theres a slight issue with this. The UID on the Mac has a maximum decimal value as it is a 32 Bit integer. So the maximum number the UID can be is: 2147483647

In this example the hex value 6C703CF1 converts nicely into a 32 Bit integer (as in, its value is less than or equal to 2147483647) and so can be used as the Mac UID without any further work.

But lets look at another example:

screen-shot-2016-11-29-at-4-56-01-pm

Here we can see an objectGUID with a value of:

BEB08781-0DAF-4B12-9EB6-AF33CBA90876

Now if we do our conversion on this as we did before:

echo "ibase=16; BEB08781" | bc

We end up with a result of: 3199240065

Unfortunately this number is larger than the maximum 32 Bit integer allowed by the Mac UID.

So what do we do?

Turns out Apple use a table to convert the first character of these 32 Bit GUID’s into a number and then recalculate the UID based on this new 32 Bit GUID.

For example with the GUID above, BEB08781, we take the first character B and replace it with the number 3 to end up with: 3EB08781

Now when we do the conversion:

echo "ibase=16; 3EB08781" | bc

We get a value of: 1051756417

Which fits perfectly into our 32 Bit integer Mac UID.

The table of conversion looks like this:


case $FIRST_CHAR in
A)
NUMBER=2 ;;
B)
NUMBER=3 ;;
C)
NUMBER=4 ;;
D)
NUMBER=5 ;;
E)
NUMBER=6 ;;
F)
NUMBER=7 ;;
9)
NUMBER=1 ;;
8)
NUMBER=0 ;;
*)
esac

Building a script
So now we know this, how do we build a script to do this conversion for us?

Interacting with records in AD is usually done with `ldapsearch` and thats how i work with all my AD queries from my machine. It allows me to target specific OU’s and is generally easier to work with than dscl for me, and I don’t need to have my machine bound to AD for it to work.

So first lets start with a basic ldapsearch to get the users objectGUID.


#!/bin/bash
## Start by loading up our ldap query variables
SVC_ACCOUNT_NAME="Username"
SVC_ACCOUNT_PASS="Password"
DOMAIN="my.domain"
LDAP_SERVER="dc.my.domain:389"
SEARCH_BASE="OU=Users,DC=My,DC=Domain"
ldapsearch -LLL -H ldap://$LDAP_SERVER -E pr=1000/noprompt -o ldif-wrap=no -x -D ${SVC_ACCOUNT_NAME}@$DOMAIN -w ${SVC_ACCOUNT_PASS} -b "${SEARCH_BASE}" \
-s sub -a always "(objectClass=user)" "sAMAccountName" "objectGUID"

view raw

ldapsearch.sh

hosted with ❤ by GitHub

This should return an output like this:

dn: CN=Smith\, John,OU=Accounts,OU=My Users,OU=My House,OU=My Room,DC=My,DC=Domain
objectGUID:: TE0kyRyv8UCppPeXes5JTg==
sAMAccountName: john.smith

Now the objectGUID here does not match what we see in AD with Apache Directory Studio, and that is because it is encoded in base64 as denoted by the double colons "::" after the objectGUID label.

So to convert this into something we can work with we need to decode it from base64 and then hex dump it.

So to achieve that we use the following function:


#!/bin/bash
DECODE_BASE64(){
# This function takes the encoded output from ldapsearch and decodes it
# It then needs to be "hex-dumped" in order to get it into regular text
# So that we can work with it
OBJECT_ID="$1"
BASE64_DECODED=$(echo $OBJECT_ID | base64 -D)
G=($(echo ${BASE64_DECODED} | hexdump -e '16/1 " %02X"'))
OBJECTGUID="${G[3]}${G[2]}${G[1]}${G[0]}-${G[5]}${G[4]}-${G[7]}${G[6]}-${G[8]}${G[9]}-${G[10]}${G[11]}${G[12]}${G[13]}${G[14]}${G[15]}"
}
# Set our objectGUID that we get from ldapsearch to a variable
TO_BE_DECODED="TE0kyRyv8UCppPeXes5JTg=="
# Send this to our decode function
DECODE_BASE64 $TO_BE_DECODED
# Now echo the result
echo $OBJECTGUID
exit 0

This then converts our objectGUID from ldapsearch into:

C9244D4C-AF1C-40F1-A9A4-F7977ACE494E

By now we should have all the bits we need to run a script to
1. Pull the objectGUID from AD using ldapsearch
2. Convert that objectGUID from Base64 into text
3. Convert the first 32 Bit from hex to decimal
4. Decide if that decimal value is larger than the maximum for a 32 Bit integer
5. If it is larger, we then know what number to replace the first character of that objectGUID with
6. Recalculate that new objectGUID into decimal to determine the UID the AD Plugin will set for that user on the Mac.

Completed script
With the script below you can target a user account DN in the search base and it will return that users DN and ObjectGUID in clear text and also the UID that will be used on a Mac when that user logs in.


#!/bin/bash
#
# Author: Calum Hunter
# Date: 28/11/2016
# Version: 1.0
# Purpose: To generate a Mac UID from the objectGUID attribute
# (GeneratedUID) in AD.
# This uses the same method that the Apple
# AD Plugin uses
#
## Start by loading up our ldap query variables
SVC_ACCOUNT_NAME="Username"
SVC_ACCOUNT_PASS="Password"
DOMAIN="my.domain"
LDAP_SERVER="dc.my.domain:389"
SEARCH_BASE="CN=John\, Smith,OU=Users,DC=MY,DC=DOMAIN"
DECODE_BASE64(){
# This function takes the encoded output from ldapsearch and decodes it
# It then needs to be "hex-dumped" in order to get it into regular text
# So that we can work with it
OBJECT_ID="$1"
BASE64_DECODED=$(echo $OBJECT_ID | base64 -D)
G=($(echo ${BASE64_DECODED} | hexdump -e '16/1 " %02X"'))
OBJECTGUID="${G[3]}${G[2]}${G[1]}${G[0]}-${G[5]}${G[4]}-${G[7]}${G[6]}-${G[8]}${G[9]}-${G[10]}${G[11]}${G[12]}${G[13]}${G[14]}${G[15]}"
}
# Search LDAP for our user account
RESULT=$(ldapsearch -LLL -H ldap://$LDAP_SERVER -o ldif-wrap=no -x -D ${SVC_ACCOUNT_NAME}@$DOMAIN -w ${SVC_ACCOUNT_PASS} -b "${SEARCH_BASE}" \
-s sub -a always "(objectClass=user)" "objectGUID")
# Get our user DN and objectGUID from the result above.
USER_DN=$(echo "$RESULT" | grep "dn:")
USER_GUID_BASE64=$(echo "$RESULT" | awk -F "::" '/objectGUID/ {print $2}')
# Get our GeneratedUID from LDAPSEARCH by decoding and hex dumping it
DECODE_BASE64 "$USER_GUID_BASE64"
# Now lets get the first 32 bits of our GUID
GUID_32=${OBJECTGUID:0:8}
# Now convert this to decimal
GUID_32_DEC=$(echo "ibase=16; $GUID_32" | bc)
# Check if this is greater than the largest decimal figure allowed for a mac UID (32Bit Integer)
if [ $GUID_32_DEC -gt 2147483647 ]; then
# Get the first character of our 32bit GUID
FIRST_CHAR=${GUID_32:0:1}
# Use the below table to replace the first character with number it represents. ie: A=2
case $FIRST_CHAR in
A)
NUMBER=2 ;;
B)
NUMBER=3 ;;
C)
NUMBER=4 ;;
D)
NUMBER=5 ;;
E)
NUMBER=6 ;;
F)
NUMBER=7 ;;
9)
NUMBER=1 ;;
8)
NUMBER=0 ;;
*)
esac
# Now lets replace the first character with our new number
A=$(echo $GUID_32 | cut -c2-)
NEW_32_GUID="${NUMBER}${A}"
GUID_32_DEC=$(echo "ibase=16; $NEW_32_GUID" | bc)
fi
# Echo our output
echo "User: $(echo $USER_DN | awk -F "dn:" '{print $2}')"
echo "ObjectGUID: $OBJECTGUID"
echo "Mac UID: $GUID_32_DEC"

view raw

convert.sh

hosted with ❤ by GitHub

Bonus points
For bonus points, you might want to target a container of Users, say OU=Users and then iterate through that container outputting the UID’s for those users so you can then check for duplicates.

So here is an ugly bash script that does just that.


#!/bin/bash
#
# Author: Calum Hunter
# Date: 28/11/2016
# Version: 1.0
# Purpose: To generate a Mac UID from the objectGUID attribute
# (GeneratedUID) in AD.
# This uses the same method that the Apple
# AD Plugin uses
#
## Start by loading up our ldap query variables
SVC_ACCOUNT_NAME="Username"
SVC_ACCOUNT_PASS="Password"
DOMAIN="my.domain"
LDAP_SERVER="dc.my.domain:389"
SEARCH_BASE="OU=Users,DC=My,DC=Domain"
DECODE_BASE64(){
# This function takes the encoded output from ldapsearch and decodes it
# It then needs to be "hex-dumped" in order to get it into regular text
# So that we can work with it
OBJECT_ID="$1"
BASE64_DECODED=$(echo $OBJECT_ID | base64 -D)
G=($(echo ${BASE64_DECODED} | hexdump -e '16/1 " %02X"'))
OBJECTGUID="${G[3]}${G[2]}${G[1]}${G[0]}-${G[5]}${G[4]}-${G[7]}${G[6]}-${G[8]}${G[9]}-${G[10]}${G[11]}${G[12]}${G[13]}${G[14]}${G[15]}"
}
# Search LDAP for our user account
RESULT=$(ldapsearch -LLL -H ldap://$LDAP_SERVER -E pr=1000/noprompt -o ldif-wrap=no -x -D ${SVC_ACCOUNT_NAME}@$DOMAIN -w ${SVC_ACCOUNT_PASS} -b "${SEARCH_BASE}" \
-s sub -a always "(objectClass=user)" "sAMAccountName" "objectGUID")
i=1
s=1
declare -a RESULT_ARRAY
while IFS= read -r line; do
# If we find an empty line, then we increase the counter (i),
# set the flag (s) to one, and skip to the next line
[[ $line == "" ]] && ((i++)) && s=1 && continue
# If the flag (s) is zero, then we are not in a new line of the block
# so we set the value of the array to be the previous value concatenated
# with the current line
[[ $s == 0 ]] && RESULT_ARRAY[$i]="${RESULT_ARRAY[$i]}
$line" || {
# Otherwise we are in the first line of the block, so we set the value
# of the array to the current line, and then we reset the flag (s) to zero
RESULT_ARRAY[$i]="$line"
s=0;
}
done <<< "$RESULT"
for USER in "${RESULT_ARRAY[@]}"; do
USER_DN=$(echo "$USER" | grep "dn:")
USER_GUID_BASE64=$(echo "$USER" | awk -F "::" '/objectGUID/ {print $2}')
# Get our GeneratedUID from LDAPSEARCH by decoding and hex dumping it
DECODE_BASE64 "$USER_GUID_BASE64"
# Now lets get the first 32 bits of our GUID
GUID_32=${OBJECTGUID:0:8}
# Now convert this to decimal
GUID_32_DEC=$(echo "ibase=16; $GUID_32" | bc)
if [ $GUID_32_DEC -gt 2147483647 ]; then
# Get the first character of our 32bit GUID
FIRST_CHAR=${GUID_32:0:1}
# Use the below table to replace the first character with number it represents. ie: A=2
case $FIRST_CHAR in
A)
NUMBER=2 ;;
B)
NUMBER=3 ;;
C)
NUMBER=4 ;;
D)
NUMBER=5 ;;
E)
NUMBER=6 ;;
F)
NUMBER=7 ;;
9)
NUMBER=1 ;;
8)
NUMBER=0 ;;
*)
esac
# Now lets replace the first character with our new number
A=$(echo $GUID_32 | cut -c2-)
NEW_32_GUID="${NUMBER}${A}"
GUID_32_DEC=$(echo "ibase=16; $NEW_32_GUID" | bc)
fi
# Echo our output
USERNAME=$(echo $USER_DN | awk -F "dn:" '{print $2}')
echo "$USERNAME,$GUID_32_DEC"
echo "$USERNAME,$GUID_32_DEC" >> users_with_UID.csv
done

view raw

ad.sh

hosted with ❤ by GitHub

5 comments

  1. I ran into a similar issue, though likely very different root cause. My environment had recently moved from using a 3rd party utility to bind, to instead using MDM profiles to initiate as dsconfigad bind. While the resulting mobile accounts had the same name, they had differing Unique Identifiers, as apparently the two bind systems used different algorithms to build the UID. The solution was relatively simple, to just read the UniqueID from the DSCL record of the user after they had migrated, and then use that in a find command to globally migrate file ownership.

    Like

    1. In a small environment perhaps yes. Ours is extremely large, 2,000+ RODC’s alone.
      2+ million user accounts.
      Changing the schema for such an environment is not something that is done easily
      Are Unix services even still available in Win2012?

      Like

  2. No need for a conversion table. Just use the bitwise ‘and’ operator to mask out the high bit:

    GUID_32_DEC=$(echo $(($(echo “ibase=16; $USER_GUID_BASE64” | bc) & 2147483647)))

    Like

Leave a comment