Bitwise operators in PowerShell and why you should know them

2024-01-17

For what bitwise operators are? I will show you this here by writing a simple subnet calculator in PowerShell. But you can use bitwise operator for much more.

What are bitwise-operators

With bitwise operators, it is possible to manipulate single bits in a bitfield. Bitwise operators are useful if you have to work with status flags. You can use it:

  1. if you would like to know if a specific flag is set
  2. or toggle the status of a specific flag
  3. or set a specific flag
  4. to negate all bits in a value
  5. to move bits to a higher or lower position

Bitwise operators work relatively low-level.
But also you can use bitwise operators to build a subnet-calculator.

Operator name Opearator Functionality
Binary AND -band This operator performs a bitwise AND operation on two numbers. It sets each bit to 1 if both bits are 1.
Binary OR -bor This operator performs a bitwise OR operation on two numbers. It sets each bit to 1 if either of the corresponding bits is 1.
Binary Exclusive OR -bxor This operator performs a bitwise XOR (exclusive OR) operation on two numbers. It sets each bit to 1 if only one of the corresponding bits is 1.
shift right -shr This operator shifts the bits of a number to the right by a specified number of positions.
shift left -shl This operator shifts the bits of a number to the left by a specified number of positions.
binary NOT -bnot This operator performs a bitwise NOT operation on a number. It flips each bit from 0 to 1 and vice versa.
Ok, now we should remember how the binary system represents numbers.

Suppose we have 6 bits for storing numbers. The highest value we can store is 2⁶ -1 = 63d.

This is because every number is a power of 2. So the position of the bit minus 1 is the corresponding power of 2.

The bits are counted from right to left.

Number of bit 6 5 4 3 2 1 Value
Worth of bit 32 16 8 4 2 1
Calculation of value 2⁵ 2⁴ 2⁰
1 1 1 1 1 1 63d

Binary AND

32 16 8 4 2 1 Decimal Value
0 0 1 1 1 0 14
-BAND
0 1 0 0 1 1 19
=
0 0 0 0 1 0 2
The result is 2d because the only bit that is set in both values to 1 is the 2nd one or the 2¹ bit.

With binary AND you can also easily determine if a number is an odd number or an even number.

Mostly this is done with the modulo operator % aka integer division with a reminder.

For example:

6 : 2 = 3 R 0 so 6 is an even number.

5 : 2 = 2 R 1 so 5 is an odd number.

$number = 3
if(($number % 2) -ne 0) {
write-output "Number $number is a odd number"
} else {
write-output "Number $number is a even number"
}

With binary AND you check if the first bit, 2⁰ is set, if so then the number is an odd number.

$number=3
if($number -band 1){
write-output "Number $number is a odd number"
} else {
write-output "Number $number is a even number"
}

Binary OR

32 16 8 4 2 1 Decimal Value
0 0 1 1 1 0 14
-BOR
0 1 0 0 1 1 19
=
0 1 1 1 1 1 31

As you can see the binary OR "merge" the two values but it is NOT the same as an addition

14 -bor 19

The binary OR is mostly used to add flags together. For example, let's say we would like to open a file for Read and Write and we would allow other processes to read the file but not to write.

The flag for read-write mode is 0x00000002h = 000010b = 2d

The flag for deny write to other processes is 0x00000020h = 100000b = 32d

To get these two flags together you do a binary OR

0x00000002 -bor 0x00000020

The result is 0x00000022h = 100010b = 34d

Binary XOR

32 16 8 4 2 1 Decimal Value
0 0 1 1 1 0 14
-BXOR
0 1 0 0 1 1 19
=
0 1 1 1 0 1 29

In PowerShell, this looks as follows:

14 -bxor 19

A funny usage of the binary XOR is to switch the value of two variables without a temporary variable. Ok in PowerShell you do not need a temporary variable for content switching you normally do this, this way:

$a = 14
$b = 19
$a,$b = $b,$a

but you can do it also this way if the value is an integer:

$a = 14
$b = 19
$a = $a -bxor $b
$b = $a -bxor $b
$a = $a -bxor $b

The binary XOR is used in RAID Systems or encryption algorithms.

(Binary) shift left (by two bits)

32 16 8 4 2 1 Decimal Value
0 0 1 1 1 0 14
-SHL 2
=
1 1 1 0 0 0 56
14 -shl 2

(Binary) shift right (by two bits)

32 16 8 4 2 1 Decimal Value
1 1 1 0 0 0 56
-SHR 2
=
0 0 1 1 1 0 14
56 -shr 2

Binary NOT

32 16 8 4 2 1 Decimal Value
-BNOT
0 0 1 1 1 0 14
=
1 1 0 0 0 1 49
32 16 8 4 2 1 Decimal Value
-BNOT
1 1 0 0 0 1 49
=
0 0 1 1 1 0 14
Demonstrating the -bnot in PowerShell with only 6 bits is a little tricky. Therefore I will explain how it works.

We have to set the 6 lowest bits of a 32-bit unsigned integer to 1. This represents our "mask" This is calculated as follows: 2⁶ = 64d = 1000000b so we can see the 7th bit is set to 1 it represents 64 but we need the 6 bits on the right side set to 1 so we have to subtract 1. 2⁶ -1 = 63d = 111111b

In PowerShell, we have multiple ways to do that. The easiest is to use the .NET System.Math library and do exactly what we have done above:

$mask=[uint32]([math]::pow(2,6)-1)

An other way is to get the highest possible 32-bit (unsigned) integer either by using the .NET System.Math library or the .NET System.UInt32 static property MaxValue.

[uint32]([math]::pow(2,32)-1)
4294967295

[uint32]::MaxValue
4294967295

but now further steps are necessary to get the right result. One is to do a shift right by 26 bits (32-6).

4294967295 -shr 26
63

An other is to do a shift left by 6 bits and then do a binary not

4294967295 -shl 6
4294967232
-bnot [uint32]4294967232
63

Now we have our "mask" and can use it to get the right value if we assume that we have a 6-bit binary value representing 14d.

(-bnot 14) -band $mask
49
(-bnot 49) -band $mask
14

the binary AND is necessary to "drop" the unneeded 26 bits (bits 7 to 32) which have also the value 1 after the binary not.

The binary NOT on 14 yields either the integer -15 (signed) or the integer 4294967281 (unsigned). If we now do a binary AND with the result and 63, only the bits that remain at 1 in the result and 63 remain at 1

The easy IPv4 subnet calculator.

Now we have enough information to build up our easy subnet calculator in PowerShell, using binary operators. Trust me it sounds much more complicated than it is. It's more complicated to do this with math calculations I've seen in a script.

What is an IPv4 address?

For a computer, an IPv4 address is an unsigned 32-bit integer, nothing else. For better human readability and because computers of the area when IPv4 was developed do not have 32-bit integers the IPv4 address is split into 4 8-bit unsigned bytes. each byte can store a value between 0 and 255.

But nowadays this problem does not exist anymore.

Get the IP address of the first connected NIC

To make it easy for us, we will get the first IPv4 address of the first connected NIC

$FirstIP=((Get-NetAdapter | Where-Object {$_.Status -eq 'Up'})[0] | Get-NetIPAddress)[0]

in my case, the following is stored in $FirstIP :

IPAddress : 172.26.1.105
InterfaceIndex : 10
InterfaceAlias : Wi-Fi
AddressFamily : IPv4
Type : Unicast
PrefixLength : 24
PrefixOrigin : Dhcp
SuffixOrigin : Dhcp
AddressState : Preferred
ValidLifetime : 29.21:16:38
PreferredLifetime : 29.21:16:38
SkipAsSource : False
Store : ActiveStore

There are only 2 values of interest: IPAddress, PrefixLength

[ipaddress]$IpAddress = $FirstIP.IPAddress
[byte]$CIDR = $FirstIP.PrefixLength

In $IpAddress is stored the following information:

Address : 1761680044
AddressFamily : InterNetwork
ScopeId :
IsIPv6Multicast : False
IsIPv6LinkLocal : False
IsIPv6SiteLocal : False
IsIPv6Teredo : False
IsIPv4MappedToIPv6 : False
IPAddressToString : 172.26.1.105
the only value of interest is: __Address__

therefore we store it in a variable of type unsigned integer.

[uint32]$IPAddressAsInt = $IpAddress.Address

now remember, an IPv4 address has 4 address bytes these are stored all together in a 32-bit unsigned integer. Ok you can get the address bytes by calling the method GetAddressBytes()

$IpAddress.GetAddressBytes()

but for this use case, this does not bring any advantage to us. So we go further with the 32-bit unsigned integer.

In the next step, we check if the value of $IPAddressAsInt is what we expect. we do this with a binary AND and a mask of 255

$IPAddressAsInt -band 255
172

ok, the good thing is that 172 is part of our IP address the bad one is that it is on the wrong "side" of the integer. So we have to clarify what's going on.

Because IP addresses are used on different systems and they all must be able to understand each other, they all must use a standardized storage format for IP addresses. Therefore IP addresses are stored on all systems in the so-called network byte order, which is also known as Big-Endian.

We need the integer in Little-Endian format so we must switch the position of the 1st and 4th byte and the position of the 2nd and 3rd byte. this sounds complicated but it is not.

For a better understanding, I will explain what we do:

First, we do a binary AND between $IPAddressAsInt and 255 then we do a 24-bit shift-left to the result

Next, we do an 8-bit shift-left to 255 and do a binary AND with $IPAddressAsInt then follow an 8-bit shift-left to the result.

Next, we do a 16-bit shift-left to 255 and do a binary AND with $IPAddressAsInt then follow an 8-bit shift-right to the result.

Finally, we do a 24-bit shift-left to 255 and do a binary AND with $IPAddressAsInt then follow a 24-bit shift-right to the result.

all 4 steps are concatenated with binary OR and the final result is stored in $IPAddressAsInt.

$IPAddressAsInt = ($IPAddressAsInt -band 255) -shl 24 -bor ($IPAddressAsInt -band (255 -shl 8)) -shl 8 -bor
($IPAddressAsInt -band (255 -shl 16)) -shr 8 -bor ($IPAddressAsInt -band (255 -shl 24)) -shr 24

you can easily check the result by explicitly casting it to [ipaddress]. The result is our IP address in the "wrong" order.

[ipaddress]$IPAddressAsInt
Address : 2887385449
AddressFamily : InterNetwork
ScopeId :
IsIPv6Multicast : False
IsIPv6LinkLocal : False
IsIPv6SiteLocal : False
IsIPv6Teredo : False
IsIPv4MappedToIPv6 : False
IPAddressToString : 105.1.26.172

Converting PrefixLength / CIDR to subnet mask as an integer

To get the subnet mask from the number of network bits we have two easy possibilities

  1. Calculate the value for the number of set network bits: 2²⁴ - 1 and do an 8-bit shift-left
  2. calculate the value of the host bits 2⁸ -1 and then negate the result with a binary NOT
# Method 1
$SubnetMask = [uint32]([math]::pow(2,$CIDR) - 1) -shl (32 - $CIDR)

# Method 2
$SubnetMask = -bnot ([uint32][math]::pow(2,(32 - $CIDR)) - 1)

to make life easier at this point we also get the host mask aka subnet wildcard mask or inverted subnet mask.

I think you all can guess how we can do that...

$HostMask = [uint32][math]::pow(2,(32 - $CIDR)) - 1

with this information, we can start our work.

Getting the network address, first IP address, last IP address, and the broadcast address

Now it is time to get the network address. We get our network address by doing a binary AND with the IP address and the subnet mask.

$NetworkAddress = $IPAddressAsInt -band $SubnetMask

For the broadcast address, we do a binary OR with the network address and the host mask.

$BroadcastAddress = $NetworkAddress -bor $HostMask

Now getting the first and the last IP address is simple math. For the first IP address, we simply add 1 to the network address and for the last IP address, we simply subtract 1 from the broadcast address.

$FirstIpAddress = $NetworkAddress + 1
$LastIpAddress = $BroadcastAddress - 1

Getting the maximum available IP addresses

To get the number of maximum usable IP addresses you simply subtract 1 from the host mask.

If you now want to know if an IP address, is in the subnet you simply check if the value of this IP address is greater or equal to the first IP address, and lower or equal to the last IP address.

Required functions

So it is time to build functions with the stuff from above.

Switch byte order
function switchByteOrder{
param(
[Parameter(Mandatory=$true)]
[uint32]$inputInteger
)
return ($inputInteger -band 255) -shl 24 -bor ($inputInteger -band (255 -shl 8)) -shl 8 -bor ($inputInteger -band (255
-shl 16)) -shr 8 -bor ($inputInteger -band (255 -shl 24)) -shr 24
}
Get IP address as integer
function Get-IPAddressAsInteger{
param(
[ipaddress]$IPAddress
)
return (switchByteOrder -inputInteger $IPAddress.Address)
}
Get subnet mask from network bits
function Get-SubnetMaskFromNtworkBits{
param(
[Parameter(Mandatory=$true)]
[ValidateRange(0,32)]
[byte]$CIDR
)
return -bnot ([uint32][math]::pow(2,(32 - $CIDR)) - 1)
}
Get host mask from network bits
function Get-HostMaskFromNtworkBits{
param(
[Parameter(Mandatory=$true)]
[ValidateRange(0,32)]
[byte]$CIDR
)
return ([uint32][math]::pow(2,(32 - $CIDR)) - 1)
}
Get network info from IP address and network bits
function Get-NetInfoFromIPasIntAndCIDR {
param(
[Parameter(Mandatory = $true)]
[uint32]$IpAddressAsInteger,
[Parameter(Mandatory = $true)]
[ValidateRange(0, 32)]
[byte]$CIDR
)
[uint32]$NetAddr = $IpAddressAsInteger -band (Get-SubnetMaskFromNtworkBits -CIDR $CIDR)
[uint32]$BCastAddr = $NetAddr -bor (Get-HostMaskFromNtworkBits -CIDR $CIDR)
[uint32]$SubnetMask = (Get-SubnetMaskFromNtworkBits -CIDR $CIDR)
[uint32]$HostMask = (Get-HostMaskFromNtworkBits -CIDR $CIDR)

return (New-Object psobject -Property ([ordered]@{
NetworkAddress = $NetAddr;
BroadcastAddress = $BCastAddr;
FirstIpAddress = $NetAddr + 1;
LastIpAddress = $BCastAddr - 1;
MaxHosts = $HostMask - 1;
SubnetMask = $SubnetMask;
HostMask = $HostMask
}))
}
Get IP address from IP address as integer
function Convert-IntIPAddressAsIPAddress{
param(
[Parameter(Mandatory=$true)]
[uint32]$IPAsInteger
)
return [ipaddress](switchByteOrder -inputInteger $IPAsInteger)
}
Get network bits from the network mask as a string
function Get-NetMaskBitsFromDottedMask {
param(
[ipaddress]$mask
)
$count=0
[uint32]$UintIP = (switchByteOrder -inputInteger $mask.Address)
for($i=0;$i -lt 32;$i++){
if($UintIP -band 1){
$count++
} else {
if($count -ne 0){
throw "Invalid SubnetMask"
}
}
$UintIP=$UintIP -shr 1
}
return $count
}

Some other helpful functions

Convert IP address as integer to string
function Convert-IntIPAddressToString{
param(
[Parameter(Mandatory=$true)]
[uint32]$IPAsInteger
)
return ,$($($IPAsInteger -shr 24 -band 255).ToString()+"."+$($IPAsInteger -shr 16 -band
255).ToString()+"."+$($IPAsInteger -shr 8 -band 255).ToString()+"."+$($IPAsInteger -band 255).ToString())
}
Convert an unsigned integer to a string in binary format
function Convert-UInt32ToBinaryStr {
param(
[uint32]$Integer
)
$(for($i=31;$i -ge 0 ;$i--){
if( $Integer -band (1 -shl $i) ){
1
} else {
0
}
}) -join '|'

}

Of course, you can do this also with [convert]::ToString($integer,2) but this method has the "problem", that leading zeros are cut off.

This does not matter mostly but comparing two binary displayed numbers is much easier if all bits are displayed.

Time to get all together

[CmdletBinding(DefaultParameterSetName = 'SubnetMaskAsCIDR')]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[Alias('Address', 'NetworkAddress')]
[ipaddress]$IPAddress,
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "SubnetMaskAsCIDR")]
[ValidateRange(0, 32)]
[Alias('PrefixLength')]
[byte]$CIDR,
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = "SubnetMaskAsIP")]
[Alias('SubnetMask')]
[ipaddress]$Mask,
[Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)]
[ipaddress]$IPAddressToCheck

)
#region functions
function switchByteOrder {
param(
[Parameter(Mandatory = $true)]
[uint32]$inputInteger
)
return ($inputInteger -band 255) -shl 24 -bor ($inputInteger -band (255 -shl 8)) -shl 8 -bor ($inputInteger -band (255
-shl 16)) -shr 8 -bor ($inputInteger -band (255 -shl 24)) -shr 24
}

function Get-IPAddressAsInteger {
param(
[ipaddress]$IPAddress
)
return (switchByteOrder -inputInteger $IPAddress.Address)
}

function Get-SubnetMaskFromNtworkBits {
param(
[Parameter(Mandatory = $true)]
[ValidateRange(0, 32)]
[byte]$CIDR
)
return -bnot ([uint32][math]::pow(2, (32 - $CIDR)) - 1)
}

function Get-HostMaskFromNtworkBits {
param(
[Parameter(Mandatory = $true)]
[ValidateRange(0, 32)]
[byte]$CIDR
)
return ([uint32][math]::pow(2, (32 - $CIDR)) - 1)
}
function Get-NetInfoFromIPasIntAndCIDR {
param(
[Parameter(Mandatory = $true)]
[uint32]$IpAddressAsInteger,
[Parameter(Mandatory = $true)]
[ValidateRange(0, 32)]
[byte]$CIDR
)
[uint32]$NetAddr = $IpAddressAsInteger -band (Get-SubnetMaskFromNtworkBits -CIDR $CIDR)
[uint32]$BCastAddr = $NetAddr -bor (Get-HostMaskFromNtworkBits -CIDR $CIDR)
[uint32]$SubnetMask = (Get-SubnetMaskFromNtworkBits -CIDR $CIDR)
[uint32]$HostMask = (Get-HostMaskFromNtworkBits -CIDR $CIDR)

return (New-Object psobject -Property ([ordered]@{
NetworkAddress = $NetAddr;
BroadcastAddress = $BCastAddr;
FirstIpAddress = $NetAddr + 1;
LastIpAddress = $BCastAddr - 1;
MaxHosts = $HostMask -1 ;
SubnetMask = $SubnetMask;
HostMask = $HostMask
}))
}

function Get-IntIPAddressAsIPAddress {
param(
[Parameter(Mandatory = $true)]
[uint32]$IPAsInteger
)
return [ipaddress](switchByteOrder -inputInteger $IPAsInteger)
}

function Get-NetMaskBitsFromDottedMask {
param(
[ipaddress]$mask
)
$count = 0
[uint32]$UintIP = (switchByteOrder -inputInteger $mask.Address)
for ($i = 0; $i -lt 32; $i++) {
if ($UintIP -band 1) {
$count++
}
else {
if ($count -ne 0) {
throw "Invalid SubnetMask"
}
}
$UintIP = $UintIP -shr 1
}
return $count
}

#endregion functions

if ($PSCmdlet.ParameterSetName -eq 'SubnetMaskAsIP') {
$CIDR = Get-NetMaskBitsFromDottedMask -mask $Mask
}

$IpAddressAsInteger = Get-IPAddressAsInteger -IPAddress $IPAddress

$NetWorkInfo = Get-NetInfoFromIPasIntAndCIDR -CIDR $CIDR -IpAddressAsInteger $IpAddressAsInteger

$NetworkInfoToReturn = New-Object psobject -Property $([ordered]@{
NetworkAddress = (Get-IntIPAddressAsIPAddress -IPAsInteger $NetWorkInfo.NetworkAddress);
BroadcastAddress = (Get-IntIPAddressAsIPAddress -IPAsInteger $NetWorkInfo.BroadcastAddress);
FirstIpAddress = (Get-IntIPAddressAsIPAddress -IPAsInteger $NetWorkInfo.FirstIpAddress);
LastIpAddress = (Get-IntIPAddressAsIPAddress -IPAsInteger $NetWorkInfo.LastIpAddress);
MaxUsableHosts = $NetWorkInfo.MaxHosts
SubnetMask = (Get-IntIPAddressAsIPAddress -IPAsInteger $NetWorkInfo.SubnetMask);
WildcardMask = (Get-IntIPAddressAsIPAddress -IPAsInteger $NetWorkInfo.HostMask);
CIDR = $CIDR
IpIsInHostRange = $null;
IPAdressToCheck = $null
})

if ($PSBoundParameters.ContainsKey("IPAddressToCheck")) {
$IpAddressToCheckAsInteger = Get-IPAddressAsInteger -IPAddress $IPAddressToCheck
$NetworkInfoToReturn.IPAdressToCheck = $IPAddressToCheck
if ($IpAddressToCheckAsInteger -ge $($NetWorkInfo.FirstIpAddress) -and $IpAddressToCheckAsInteger -le
$($NetWorkInfo.LastIpAddress)) {
$IpIsInHostRange = $true
}
else {
$IpIsInHostRange = $false
}
$NetworkInfoToReturn.IpIsInHostRange = $IpIsInHostRange
}

return $NetworkInfoToReturn

Some examples

PS C:\Users\DLR> D:\Daniel\SubnetCalcps1.ps1 -IPAddress 10.12.28.123 -CIDR 24

NetworkAddress : 10.12.28.0
BroadcastAddress : 10.12.28.255
FirstIpAddress : 10.12.28.1
LastIpAddress : 10.12.28.254
MaxUsableHosts : 254
SubnetMask : 255.255.255.0
WildcardMask : 0.0.0.255
CIDR : 24
IpIsInHostRange :
IPAdressToCheck :

PS C:\Users\DLR> D:\Daniel\SubnetCalcps1.ps1 -IPAddress 10.12.28.123 -CIDR 29

NetworkAddress : 10.12.28.120
BroadcastAddress : 10.12.28.127
FirstIpAddress : 10.12.28.121
LastIpAddress : 10.12.28.126
MaxUsableHosts : 6
SubnetMask : 255.255.255.248
WildcardMask : 0.0.0.7
CIDR : 29
IpIsInHostRange :
IPAdressToCheck :

.\SubnetCalcps1.ps1 -IPAddress 10.12.28.123 -CIDR 27 -IPAddressToCheck 10.12.28.100

NetworkAddress : 10.12.28.96
BroadcastAddress : 10.12.28.127
FirstIpAddress : 10.12.28.97
LastIpAddress : 10.12.28.126
MaxUsableHosts : 30
SubnetMask : 255.255.255.224
WildcardMask : 0.0.0.31
CIDR : 27
IpIsInHostRange : True
IPAdressToCheck : 10.12.28.100

.\SubnetCalcps1.ps1 -IPAddress 10.12.28.123 -CIDR 28 -IPAddressToCheck 10.12.28.100

NetworkAddress : 10.12.28.112
BroadcastAddress : 10.12.28.127
FirstIpAddress : 10.12.28.113
LastIpAddress : 10.12.28.126
MaxUsableHosts : 14
SubnetMask : 255.255.255.240
WildcardMask : 0.0.0.15
CIDR : 28
IpIsInHostRange : False
IPAdressToCheck : 10.12.28.100

.\SubnetCalcps1.ps1 -IPAddress 10.12.28.123 -CIDR 24 -IPAddressToCheck 10.12.28.100

NetworkAddress : 10.12.28.0
BroadcastAddress : 10.12.28.255
FirstIpAddress : 10.12.28.1
LastIpAddress : 10.12.28.254
MaxUsableHosts : 254
SubnetMask : 255.255.255.0
WildcardMask : 0.0.0.255
CIDR : 24
IpIsInHostRange : True
IPAdressToCheck : 10.12.28.100


Now I think you have an idea of what you can do with binary operators. But as always everything here is provided without warranty.

And yes I have not done error handling in this script but I will be sure you can do this on your own