Post

ArgvQuote - When passing argument is more complicated than you may assume

ArgvQuote - When passing argument is more complicated than you may assume

ArgvQuote - When passing argument is more complicated than you may assume

When I researched the ways to exploit CreateProcess API, I encountered a post that posed a problem of arguments convention in Windows Programming. That post was difficult to read as a newbie in this field, but later, I found it very intriguing. I decided to give it a try by writing the whole ArgvQuote- a func that resolves the problem - from scratch.

Now I will dig in how every thing work behind the func.


The problem

If you want to call a function, let’s say you want to read a file name poet.txt in notepad.exe, you have to specify the (path) file name in the lpCommandLine variable in CreateProcess as well as the name of the module you want to run. In this case, it will be notepad.exe poet.txt.

Sounds simple, right? Now, if you want to pass in an argument that has whitespace inside it, the instinct will probably tell you that you need the a couple of quotation marks to wrap the argument. Something like this: notepad.exe "the poet.txt". At this point, Windows, or specifically CommandLineToArgvW API, is not confused yet. It still considers the poet.txt as a single argument.

What if the argument itself contains quotation marks? The case here is "funny" poet.txt. You may think we can address it with notepad.exe ""funny" poet.txt". However, Windows is not as smart as you think. In other words, Windows does not work as we - human beings - think.

From the argument string ““funny” poet.txt”, Windows will break down into these arguments:

  • ""funny": Windows resolves the first two quote marks. Then, because funny" is right next to “”, we take it to the argument also.

  • poet.txt": After ""funny" is a whitespace, so it marks the end of that argument. No whitespace exists in the rest of the string, so it will be the second argument.

In terms of Windows Programming, if you only write " without the escaping character ‘', it is considered the quotation mark for beginning and ending of the argument, not the quotation mark serves as a part of the file name itself.

  • NOTE: The escaping char here is different from how C/C++ compiler understands it since we assume the argument is passed via Powershell. Escaping char is only needed in Windows Programming if the string has ‘”’.

This issue goes further when you pass in the path to the file. Suppose you want Notepad to read the .txt file with the path C:\Users\"Secret" Workspace of Mine\"funny" poet.txt. Here is the arguments:

  • C:\Users"Secret: ‘"’ becomes " (normal quote).

The next char from here is a quote mark without escaping char. So this is the end of Arg1.

  • ` Workspace of Mine”funny`: Another incident case of escaping quote mark.

Right next to funny is an unescaped quote mark. This marks the end of Arg2.

  • ` poet.txt`: The rest of the string.

Solution

To deal with the problem, we need a function that formats the argument string into a Windows-accepted string.

Parameter

  1. const std::string& Argument and std::string& CommandLine: Of course we need these ‘cause we are pushing the argument string to the Command Line.

  2. bool Force: Force the func to add quote marks wrapper

You would like to Force if:

  • Your argument string is empty "": Windows automatically skips this argument, which means it is not passed to the CommandLine. Sometimes you do not want to pass any arg, right?

  • You simply want to ensure everything is wrapped by quotation marks. Mayber you want to escape bad cases as much as possible, who knows how crazy an argument string can be?

Prerequisite

The module name (path) and the arg string must be separate by a whitespace. We need to push back a whitespace in CommandLine first.

What to do: Append the whitespace before doing anything.

1
CommandLine.push_back(' ');

First case

Suppose the Arg string does not contain whitespace, special chars [\t, \n, \v] (in case you want to pass arg string in source code, not from Console), and not forced.

What to do: Append the whole string.

1
2
3
4
if (!Force && !Argument.empty() &&
        Argument.find_first_of(" \t\n\v\"") == size_t(-1)) {
        CommandLine.append(Argument);
    }

Second case

This is the case when the arg string does not satisfy all conditions from the first case. Regardless of what case it is, we still wrap the arg string with double quotation marks anyway.

1
CommandLine.push_back('"')

Now we need to iterate the whole arg string. The goal here is to count how many consecutive backslashes before reaching the end of the string or meeting any other chars.

1
2
3
4
5
6
7
8
9
10
11
12
13
        int cnt = Argument.size();

        for (size_t i = 0; i < cnt; ++i) {
            int backslashesCount = 0;  // Count consecutive backslashes

            while (i != cnt && Argument[i] == '\\') {
                ++backslashesCount;
                ++i;
            }

        //=====Sub-cases=====

        } //Concluding step

Sub-case 1: Reach the end

It means the end of the string is either nothing or backslash(es).

In this case, we need to wrap quote marks, right? The neat part here is that if you pass the same number of backslashes to CommandLine just like the Arg string, it will escape the closing quote mark. As the result, your arg is messed up.

What to do: Escape all backslashes, making it the part of the arg.

1
2
3
if (i == cnt) {
    CommandLine.append(backslashesCount * 2, '\\'); //Pass double the amount so that two backslashes can escape into one real backslash
}

Sub-case 2: The next char is a genuine quote mark

We need to escape this quote mark because it is a part of the arg string.

Furthermore, we also need the backslashes to be escaped also. Imagine if the arg string contains something like this:

C:\Users\"Real.txt"

The \" in the middle will become a genuine ", causing the arg to be C:\Users"Real.txt.

What to do: Escape all backslashes and the quote mark.

1
2
3
4
else if (Argument[i] == '\"') {
    CommandLine.append(backslashesCount * 2 + 1, '\\');
    CommandLine.push_back('\"');
}

Sub-case 3

Now the backslash does not intervene any quote marks. We pass the same amount of backslash(es) to the CommandLine.

1
2
3
4
else {
    CommandLine.append(backslashesCount, '\\')
    CommandLine.push_back(Argument[i]);
}

Concluding step

We add the last quote mark to wrap the arg.

1
CommandLine.push_back('"');

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <string>

void ArgvQuote(const std::string& Argument, std::string& CommandLine,
               bool Force) {
    CommandLine.push_back(' ');
    // If (not Force) and (Argument is not Empty) and (Argument does not contain
    // space, tab, newline, quotationmark): Pass the Argument to the CommandLine
    // directly
    //
    if (!Force && !Argument.empty() &&
        Argument.find_first_of(" \t\n\v\"") == size_t(-1)) {
        CommandLine.append(Argument);
    }

    else {
        CommandLine.push_back('"');  // Push back the first quotation mark

        int cnt = Argument.size();

        for (size_t i = 0; i < cnt; ++i) {
            int backslashesCount = 0;  // Count consecutive backslashes

            while (i != cnt && Argument[i] == '\\') {
                ++backslashesCount;
                ++i;
            }

            // i == cnt means the last char of Argument is backslash
            // Solution: Append double the amount of backslash in Argument
            if (i == cnt) {
                CommandLine.append(backslashesCount * 2, '\\');
            }

            // If Arg[i] == '\"' ==> Need to escape the backslashes as well as
            // the quotation mark right behind the backslashes
            //  The number of bonus backslashes = The num of orginal backslashes +
            //  1 (for the mark)
            else if (Argument[i] == '\"') {
                CommandLine.append(backslashesCount * 2 + 1, '\\');
                CommandLine.push_back('\"');
            }

            // If not satisfy anything above, it means the backslashes are at the
            // middle of the Argument Add the same number of backslashes
            // NOTE: Do not misunderstand with how C/C++ resolves backslashes with a char
            // at next position. This is .exe file, C/C++ compiler has no work
            // here.
            else {
                CommandLine.append(backslashesCount, '\\');
                CommandLine.push_back(Argument[i]);
            }
        }

        CommandLine.push_back('"');
    }
}
This post is licensed under CC BY 4.0 by the author.