Spoiling security - now automated!
Why should we do something manual if we can do it with Ansible?
If you read my previous newsletter, you know that you can create fixes that look like “normal” AIX security patches. But they do nothing. The problem with them is as old as the world itself. You can do it manually if you do it once. The failure risk is too high if you do it more than once. I am preparing for the worst-case scenario when I must create many of them. That’s why I automate with Ansible.
We know which information we need from an existing fix and we understand how to build fixes. Let’s start with extracting information from an existing fix.
The shell AWK script
As you probably already heard:
There is a shell script behind every Ansible playbook
This time there will be an AWK script behind the playbook. Why? Because I love AWK! It is easier (for me) to parse emgr
output using AWK.
If you have never used AWK, read some good books on it. Like this one. (This is not an affiliate link but the best book on AWK). This is maybe the oldest UNIX programming language and it was specifically developed to parse and manipulate text files. The easiest form of AWK program is:
awk '{ print $1 }' somefile
In this case, AWK reads every line from somefile
and prints the first field from the line. The fields are usually separated by spaces or tabs. Lines are known as records separated by newline symbols in AWK. You can change it every time but we don’t need it for our small emgr
parsing tool.
Fix label
First, we don’t want to parse every line in our emgr
output, but only specific lines. The generic form of AWK program is:
/regexp/ { action }
If you don’t write a regular expression, like we did in the first example, the action applies to each line of the input file. If you write some regular expression, the action applies only to the lines where the regular expression matches.
/^LABEL: / { label=$NF }
The code above finds the line which starts with LABEL: (/^LABEL: /
), extracts the last field from the line and assigns it to the variable label
. You don’t need to define any variables in AWK. The first time you use some variable name it is created. NF
is a special AWK variable and points always to the last field in the record. In our case - the last word in the line.
This is the line from the emgr
output:
LABEL: 3013sa
After parsing the line, the variable label
will contain the value 3013sa
. All other lines in the output will be ignored.
Fix abstract
We need not only the label but the abstract too. The abstract field can contain many words and we need to assign them to our abstract
variable.
/^ABSTRACT: / {
abstract = $2
for (i=3; i<=NF; i++) {
abstract = abstract" "$i
}
}
Similar to label we start by searching for ABSTRACT:
. It indicates the line where the abstract is written. The first word in the line is the word ABSTRACT:
and we skip it. The second word starts the real abstract line. So we initialize our variable abstract
with the second word. After that, we count all words till the end (the variable NF
- number of fields) and add the words to the end of our variable abstract
.
If it was hard take your time! The next part will be more complex, I promise! ;-)
Fix description, pre-requisites and aparref
We need also the values for the fix description, prereq, and aparref files. These are multiline values. We must analyze several lines like below:
+-----------------------------------------------------------------------------+
Efix Description
+-----------------------------------------------------------------------------+
ifix for CVE-2024-4603, CVE-2024-4741 and CVE-2024-5535
+-----------------------------------------------------------------------------+
Displaying Configuration File "PREREQ"
+-----------------------------------------------------------------------------+
openssl.base 3.0.13.1000 3.0.13.1000
+-----------------------------------------------------------------------------+
Displaying Configuration File "APARREF"
+-----------------------------------------------------------------------------+
NONE
+-----------------------------------------------------------------------------+
Don’t panic! We first write a function.
function getvalue() {
getline; getline;
val = ""
while ($0 !~ /^$/ && $0 !~ /^\+--/) {
if (val == "") {
val = $0
} else {
val = val"\n"$0
}
getline
}
return val
}
The function getvalue()
works with the same input file and as the first it skips two lines (getline
). Why? If you look at the output above, we have some “marker” strings like “Efix Description” and the next line is a simple line without any meaningful information. These both lines we don’t want to analyze, because they don’t have any useful information for us. We skip them.
Next, we initialize our return variable val
. As I already wrote, there is no need to initialize variables but sometimes I want to be sure what it has.
Now we are ready to analyze the input. We check each line if it is empty or starts with +—
. The empty line means the end of the section and the line with +—
means the beginning of the next section. If we find one of them we know that we finished our analysis and must return some value to the caller. The variable $0
is a special AWK variable and contains the whole line (or record).
Our “analysis” is pretty simple. If our return variable val
doesn’t contain any values, we assign the whole line to the variable val
. Otherwise, we add the line to the value of the variable val
.
That’s it! No magic, no witchery. Simply read everything that you need and add it to a variable. When you finish, give the variable back.
The rest of the AWK program will be then very easy:
/Efix Description/ { descr = getvalue() }
/Displaying Configuration File "PREREQ"/ { prereq = getvalue() }
/Displaying Configuration File "APARREF"/ { aparref = getvalue() }
Search for a marker line, read everything that you find, and assign it to a variable.
Printing the values
But we want to use the extracted values in our Ansible playbook, don’t we? We must output the found values. There are two special AWK blocks - BEGIN
and END
. The BEGIN
block executes before reading the input file and the END
block executes after the input file is processed. The END
block is the usual place to print all values that were found:
END {
print label
print abstract
print descr
print prereq
print aparref
}
After I wrote the AWK script and the playbook and tested everything, it works for me well. But I found one logical mistake in both the script and the playbook. I don’t tell you where it is. Try to find it too! And if you find it, write it down in the comments and how would you fix it.
The playbook
We have our emgr
parser. It is time to use it, and write the playbook to produce our own fixes.
In the playbook I use only one input variable - fix
. The variable contains the name of the fix we want to forge, like 3013sa.240722.epkg.Z
.
First, we need some temporary place to create the new fix:
- name: Create temporary directory
ansible.builtin.tempfile:
path: /tmp
prefix: emgr.
state: directory
register: epkg_build_dir
We create a temporary directory inside /tmp
with the prefix emgr
. If the playbook fails for some reason, you can find the directory by issuing ls -ld /tmp/emgr.*
or you can print the name of it in the playbook itself:
- name: Display directory name
ansible.builtin.debug:
var: epkg_build_dir.path
Parse emgr output
We copy our AWK script into the directory:
- name: Prepare fix parse script
ansible.builtin.copy:
src: ./efix_out.awk
dest: "{{ epkg_build_dir.path }}/efix_out.awk"
owner: root
group: system
mode: "0700"
My script is called efix_out.awk
and I will use it as an executable script. If you want to do the same, you must add the first (she-bang) line into the script:
#!/usr/bin/awk -f
Now we can parse the output of emgr
command:
- name: Get fix information
ansible.builtin.shell:
cmd: "/usr/sbin/emgr -d -v3 -e {{ fix }} | {{ epkg_build_dir.path }}/efix_out.awk"
register: efix_out
If your fix is in some other directory, write the path to it. You may want to copy it to the temporary directory first and execute the command there.
After we parsed the command output, it is saved in the variable efix_out
. You can check it by using Ansible’s debug
module:
- name: Display information
ansible.builtin.debug:
var: efix_out.stdout_lines
Write temporary files for epkg
Now the easiest part of the playbook. We write everything we’ve found into different files.
- name: Write aparref file
ansible.builtin.copy:
content: "{{ efix_out.stdout_lines.4 }}\n"
dest: "{{ epkg_build_dir.path }}/aparref"
owner: root
group: system
mode: "0644"
- name: Write prereq file
ansible.builtin.copy:
content: "{{ efix_out.stdout_lines.3 }}\n"
dest: "{{ epkg_build_dir.path }}/prereq"
owner: root
group: system
mode: "0644"
- name: Write description file
ansible.builtin.copy:
content: "{{ efix_out.stdout_lines.2 }}\n"
dest: "{{ epkg_build_dir.path }}/desc"
owner: root
group: system
mode: "0644"
We also need a file we “patch”. Similar to what I did last time, I create a simple text file with the name of the fix:
- name: Write patch file
ansible.builtin.copy:
content: "Patch {{ efix_out.stdout_lines.0 }} is installed\n"
dest: "{{ epkg_build_dir.path }}/{{ efix_out.stdout_lines.0 }}.txt"
owner: root
group: system
mode: "0644"
The last piece of the information we need is the control file:
- name: Prepare control file
ansible.builtin.template:
src: control.j2
dest: "{{ epkg_build_dir.path }}/control"
owner: root
group: system
mode: "0644"
In this case, I use template
module and we need a template file, I called control.j2
:
ABSTRACT={{ efix_out.stdout_lines.1 }}
PRE_INSTALL=.
POST_INSTALL=.
PRE_REMOVE=.
POST_REMOVE=.
REBOOT=no
PREREQ=prereq
APARREF=aparref
DESCRIPTION=desc
EFIX_FILES=1
LKU_CAPABLE=yes
EFIX_FILE:
EFIX_FILE_NUM=1
SHIP_FILE={{ efix_out.stdout_lines.0 }}.txt
TARGET_FILE=/tmp/{{ efix_out.stdout_lines.0 }}.txt
TYPE=1
INSTALLER=9
AR_MEM=.
ACL=root:system:0400
Create the new fix
We prepared everything and we are ready to build our new fix!
- name: Build the fix
ansible.builtin.command:
cmd: "/usr/sbin/epkg -e control -w ./tmp {{ efix_out.stdout_lines.0 }}"
chdir: "{{ epkg_build_dir.path }}"
After we built it, the fix got some name and we must find and fetch it to our system back:
- name: Find efix
ansible.builtin.find:
paths: "{{ epkg_build_dir.path }}/tmp/{{ efix_out.stdout_lines.0 }}"
patterns: "*.epkg.Z"
register: efix
- name: Fetch our new fix
ansible.builtin.fetch:
src: "{{ efix.files.0.path }}"
dest: "./"
flat: true
fail_on_missing: true
Don’t forget to clean up your work!
- name: Remove temporary directory
ansible.builtin.file:
path: "{{ epkg_build_dir.path }}"
state: absent
Special offer for readers of the Power DevOps Newsletter!
Have you ever wondered how I manage thousands of IBM Power LPARs while simultaneously crafting this newsletter? It might sound like a daunting task, but it’s not just a dream! Imagine the possibility of juggling your responsibilities effortlessly and having more time to focus on what you love. If this resonates with you, don’t miss out on my exclusive offer just for Power DevOps Newsletter readers! Let’s unlock your potential together!
Last chapter
That’s it! Now I can take any IBM-provided fix and create a fix with the same label which doesn’t do anything. I must only set the fix
variable correct:
# ansible-playbook -i localhost, -c local forge_fix.yml -e fix=1122400a.240722.epkg.Z
But before we finish with it, let’s talk a little bit about responsibility. YOUR RESPONSIBILITY as a system administrator, SRE, developer, or employee of your company. If you have a hammer or axe, you can use it for good or for bad. A similar is here. You have a tool and you can use the tool to help your company to be good in audits. But there is nothing that could stop you from using the tool to harm your company. Only your brain and understanding WHY you do it will help you to make the right decision.
Can you find out if someone else installed a forged fix on your AIX server? Yes, you can.
Each AIX fix has VUID
field. It contains the machine ID number, where the fix was built, and the timestamp of the fix. You can see the VUID
fields for installed fixes by using emgr -l -v2
. The VUID
field of IBM’s fix will be different from the VUID
of your own (or 3rd party) fix with the same name.
Can the field be forged too? Yes, it can be. But it is not the topic of this newsletter and I don’t plan to write about it.
Have fun spoiling your security guys!
Andrey
P.S. The full text of the AWK script and the playbook will be available for paid subscribers.
Hi, I am Andrey Klyachkin, IBM Champion and IBM AIX Community Advocate. It means I don’t work for IBM. Over the last 20 years, I worked with many different IBM Power customers all over the world both on-premise and in the cloud. I specialize in automating IBM Power infrastructures making them even more robust and agile. I co-authored several IBM Redbooks and IBM Power certifications. I am an active Red Hat Certified Engineer and Instructor.
Follow me on LinkedIn, Twitter, and YouTube.
Meet me at events like IBM TechXchange, Common Europe Congress, and GSE Germany’s IBM Power Working group sessions.