Sign in to follow this  

Here a very very simple c/c++ code obfuscation tool

This topic is 3311 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I've been now looking for some time for a c/c++ code obfuscation tool that allows to easily obfuscate only part of a whole project (only specific files). There are a few of those tools, but seem quite complicated to use and some of them require my code to be tagged (e.g. "do not obfuscate this"-tag). So I decided yesterday to write a quick, dirty and simple obfuscation tool. It works perfectly for me and does exactly what I needed. I put 3-4 hours into this and am not willing to spend more time on this since it works for me, but maybe someone is willing to slightly modify it/improve it or test it on their code? It works as following: when the application starts, it searches for a configuration file called "conf.txt": The first line should contain the filename of a file that contains names (variable names, function names, etc.) that should be obfuscated, the other lines should contain the filenames of files to be obfuscated (e.g. header and c or cpp). It then created a directory "modified" containing the obfuscated code. It might require 2-3 iterations until you have what you want (at each step add more names or remove the ones that conflict with the rest of the code). Obfuscation is only light but good enough for me. Here an output example:
#include "stdafx.h"
#include "IKGraphJoint.h"
#include "GCSDefs.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
CIKGraphJoint::CIKGraphJoint(bool iililiiilillllli,float iliilliliiiliiii,float lilllilllililiil,float liiillillliiiiil,float iilillililililii,float iiliiilililllill)
{illilililllilili=NULL;iiiilliiiliiliil=NULL;illlllllllliiili=lilliiiillilliil;if (iililiiilillllli)liiiilllilllliii=iliiilillilillli;else
liiiilllilllliii=iiiilllliillilll;parameter=iliilliliiiliiii;iiiiiillilllilll=lilllilllililiil;iiilliillllillll=liiillillliiiiil;iillllilillillli=iiliiilililllill;
ilililliilllllii=iilillililililii;iiliiiiiililiili=NULL;iiiliiiiiilillll=false;}CIKGraphJoint::CIKGraphJoint(C4Vector& lliilililiililil,float liiillillliiiiil,float iiliiilililllill)
{illilililllilili=NULL;iiiilliiiliiliil=NULL;illlllllllliiili=lilliiiillilliil;liiiilllilllliii=iiilllillliiilii;ilililliilllllii=0.0f;parameter=0.0f;iiiiiillilllilll=-piVal;
iiilliillllillll=liiillillliiiiil;iillllilillillli=iiliiilililllill;iiililiilllliiii=lliilililiililil;iiliiiiiililiili=NULL;iiiliiiiiilillll=false;}CIKGraphJoint::~CIKGraphJoint()
{}C7Vector CIKGraphJoint::illliiiiilllllil(){C7Vector retVal;retVal.setIdentity();if (liiiilllilllliii==iiilllillliiilii)retVal.Q=iiililiilllliiii;else if (liiiilllilllliii==iliiilillilillli)
retVal.Q.setAngleAndAxis(parameter,C3Vector(0.0f,0.0f,1.0f));else if (liiiilllilllliii==iiiilllliillilll)retVal.X(2)=parameter;return(retVal);}CIKGraphObject* CIKGraphJoint::iiilliilliilllii()
{return(illilililllilili);}CIKGraphObject* CIKGraphJoint::lililllllillllil(){return(iiiilliiiliiliil);}CIKGraphNode* CIKGraphJoint::lllilllliilllili(int pos){if (iiiilliiiliiliil->lllililliiiillli==-1)
{if (pos==0)return(iiiilliiiliiliil);pos--;}if (illilililllilili->lllililliiiillli==-1){if (pos==0)return(illilililllilili);pos--;}return(NULL);}CIKGraphNode* CIKGraphJoint::iiillliiillilili(int pos,bool illliillllliilli)
{if (pos==0)return(iiiilliiiliiliil);if (pos==1)return(illilililllilili);return(NULL);}int CIKGraphJoint::illiililiillllli(){int retVal=0;if (iiiilliiiliiliil->lllililliiiillli==-1)
retVal++;if (illilililllilili->lllililliiiillli==-1)retVal++;return(retVal);}CIKGraphNode* CIKGraphJoint::lllllillillilill(int theID){if (iiiilliiiliiliil->lllililliiiillli==theID)
return(iiiilliiiliiliil);if (illilililllilili->lllililliiiillli==theID)return(illilililllilili);return(NULL);}CIKGraphNode* CIKGraphJoint::lilllliilliiliil(){int smallest=999999;
CIKGraphObject* retVal=NULL;if (iiiilliiiliiliil->lllililliiiillli!=-1){if (iiiilliiiliiliil->lllililliiiillli<smallest){smallest=iiiilliiiliiliil->lllililliiiillli;
retVal=iiiilliiiliiliil;}}if (illilililllilili->lllililliiiillli!=-1){if (illilililllilili->lllililliiiillli<smallest){smallest=illilililllilili->lllililliiiillli;
retVal=illilililllilili;}}return(retVal);}int CIKGraphJoint::illiilliiillllll(){return(2);}void CIKGraphJoint::liillliiiiiiilil(CIKGraphJoint* it,float iililllllillliii,float iliiillllllillll)
{if (it!=NULL){if ( (it->liiiilllilllliii!=iiilllillliiilii)&&(liiiilllilllliii!=iiilllillliiilii) ){iiliiiiiililiili=it;lililillilliilil=iililllllillliii;iilllilllliiiill=iliiillllllillll;
}}else
iiliiiiiililiili=NULL;}

And here the code:
#include "stdafx.h"
#include <vector>

char* ctrlChars=";:,.=-+!()<>|*/ []&#%^{}~";
char* appendableChars=";:,.=-+!()<>|*/[]&%^{}~";
char* nonAppendableChars="# ";

std::vector<std::string> variables;
std::vector<std::string> obfuscatedVariables;

bool randBinary()
{
	return((rand()/(float)RAND_MAX)>0.5f);
}

int getVariableIndex(std::string var)
{
	for (int i=0;i<int(variables.size());i++)
	{
		if (variables[i].compare(var)==0)
			return(i);
	}
	return(-1);
}

int getObfuscatedVariableIndex(std::string var)
{
	for (int i=0;i<int(obfuscatedVariables.size());i++)
	{
		if (obfuscatedVariables[i].compare(var)==0)
			return(i);
	}
	return(-1);
}

std::string generateObfuscatedVariable()
{
	std::string retVal;
	while (true)
	{
		retVal="";
		for (int i=0;i<16;i++)
		{
			if (randBinary())
				retVal+='i';
			else
				retVal+='l';
		}
		if (getObfuscatedVariableIndex(retVal)==-1)
			break;
	}
	return(retVal);
}

void addVariable(std::string var,std::string obfuscatedVar)
{
	variables.push_back(var);
	obfuscatedVariables.push_back(obfuscatedVar);
}

bool removeSpacesAtBeginningAndEnd(std::string& line)
{
	while (line.length()!=0)
	{
		if (line[0]==' ')
			line.erase(line.begin());
		else
			break;
	}
	while (line.length()!=0)
	{
		if (line[line.length()-1]==' ')
			line.erase(line.begin()+line.length()-1);
		else
			break;
	}
	return(line.length()!=0);
}

void removeComments(std::string& line)
{
	for (int i=0;i<int(line.length())-1;i++)
	{
		if ( (line[i]=='/')&&(line[i+1]=='/') )
		{
			line.erase(line.begin()+i,line.end());
			return;
		}
	}
}

bool getLine(std::string& line,CArchive& ar,DWORD archiveLength,DWORD& actualPosition)
{
	BYTE oneByte;
	BOOL returnCarrier=false;
	line="";
	while (actualPosition<archiveLength)
	{
		ar >> oneByte;
		actualPosition++;
		if (oneByte==(BYTE)13)
			returnCarrier=true;
		if ((oneByte==(BYTE)10) )
			return(true);
		if (oneByte!=(BYTE)13)
			line.insert(line.end(),(char)oneByte);
	}
	if (line.length()!=0) 
		return(true);
	return(false);
}

bool removeFirstWord(std::string& line,std::string& theWord)
{
	theWord="";
	int s=int(line.find(' '));
	if (s==-1)
	{
		theWord=line;
		line="";
	}
	else
	{
		theWord.assign(line.begin(),line.begin()+s);
		line.erase(line.begin(),line.begin()+s+1);
	}
	return(theWord.length()!=0);
}

bool isNumber(char a)
{
	if ( (a>='0')&&(a<='9') )
		return(true);
	return(false);
}

bool isValidCharForVariable(char a,int index)
{
	if (isNumber(a))
		return(index!=0);
	if ( ( (a>='a')&&(a<='z') )||( (a>='A')&&(a<='Z') ) )
		return(true);
	if (a=='_')
		return(true);
	return(false);
}

bool checkIfVariable(std::string var)
{
	for (int i=0;i<int(var.length());i++)
	{
		char a=var[i];
		if (!isValidCharForVariable(a,i))
			return(false);
	}
	return(var.length()!=0);
}

int readVariables(CArchive& ar,DWORD archiveLength,std::vector<std::string> vars)
{
	DWORD currentPosition=0;
	std::string line;
	while (getLine(line,ar,archiveLength,currentPosition))
	{
		removeComments(line);
		removeSpacesAtBeginningAndEnd(line);
		std::string theVar;
		if (removeFirstWord(line,theVar))
		{
			if (checkIfVariable(theVar))
			{
				if (getVariableIndex(theVar)==-1)
				{
					std::string obfuscatedVar(generateObfuscatedVariable());
					addVariable(theVar,obfuscatedVar);
				}
			}
			else
				printf("ERROR: %s is not a valid variable name!\n",theVar.c_str());
		}
	}
	return(int(variables.size()));
}

char getControlChar(std::string& line,bool testOnly)
{
	char retVal=0;
	int l=int(line.length());
	if (l==0)
		return(retVal);
	int i=0;
	while (ctrlChars[i]!=0)
	{
		if (ctrlChars[i]==line[0])
		{
			if (!testOnly)
				line.erase(line.begin());
			return(ctrlChars[i]);
		}
		i++;
	}
	if (retVal==0)
	{
		if (line[0]==9)
		{
			retVal=line[0];
			if (!testOnly)
				line.erase(line.begin());
		}
	}
	return(retVal);
}

bool isAppendableChar(char c)
{
	int i=0;
	while (appendableChars[i]!=0)
	{
		if (appendableChars[i]==c)
			return(true);
		i++;
	}
	return(false);
}

bool isNonAppendableChar(char c)
{
	int i=0;
	while (nonAppendableChars[i]!=0)
	{
		if (nonAppendableChars[i]==c)
			return(true);
		i++;
	}
	return(false);
}

std::string getString(std::string& line,bool testOnly)
{
	std::string retVal="";
	int l=int(line.length());
	if (l==0)
		return(retVal);
	int previousCnt=0;
	bool opened=false;
	if (line[0]=='"')
	{
		for (int i=0;i<l;i++)
		{
			if (line[i]=='"')
			{
				opened=!opened;
				retVal+='"';
			}
			else
			{
				if (!opened)
					break;
				else
					retVal+=line[i];
			}
		}
	}
	if ( (retVal.length()!=0)&&(!testOnly) )
		line.erase(line.begin(),line.begin()+retVal.length());
	return(retVal);
}

std::string getNumberString(std::string& line,bool testOnly)
{
	std::string retVal="";
	std::string lineC(line);
	int l=int(lineC.length());
	if (l==0)
		return(retVal);
	bool fAdded=false;
	while (lineC.length()!=0)
	{
		if ( (lineC[0]<'0')||(lineC[0]>'9') )
		{
			if ( (lineC[0]!='f')||(retVal.length()==0)||fAdded )
			{
				if (!testOnly)
					line=lineC;
				return(retVal);
			}
			fAdded=true;
		}
		retVal+=lineC[0];
		lineC.erase(lineC.begin());
	}
	if (!testOnly)
		line=lineC;
	return(retVal);
}

std::string getVariableString(std::string& line)
{
	std::string retVal="";
	int l=int(line.length());
	if (l==0)
		return(retVal);
	while (line.length()!=0)
	{
		if (getControlChar(line,true)!=0)
			return(retVal);
		if (getString(line,true).length()!=0)
			return(retVal);
		if (getNumberString(line,true).length()!=0)
			return(retVal);
		retVal+=line[0];
		line.erase(line.begin());
	}
	return(retVal);
}

bool getCommentBracket(std::string& line,bool openComment)
{
	if (line.length()>1)
	{
		if ((line[0]=='/')&&(line[1]=='*'))
		{
			if (openComment)
				line.erase(line.begin(),line.begin()+2);
			return(openComment);
		}
		if ((line[0]=='*')&&(line[1]=='/'))
		{
			if (!openComment)
				line.erase(line.begin(),line.begin()+2);
			return(!openComment);
		}
	}
	return(false);
}

bool isCommentSlashSlash(std::string& line)
{
	if (line.length()>1)
	{
		if ((line[0]=='/')&&(line[1]=='/'))
			return(true);
	}
	return(false);
}

void writeLine(CArchive& ar,std::string line)
{
	for (int i=0;i<int(line.length());i++)
		ar << line[i];
	ar << char(13);
	ar << char(10);
}

int replaceVariables(std::string newFileName,CArchive& ar,DWORD archiveLength,std::vector<std::string> vars)
{
	DWORD currentPosition=0;
	std::string line;
	int retVal=0;

	CFile file(newFileName.c_str(),CFile::modeCreate|CFile::shareExclusive);
	file.Close();
	CFile myFile(newFileName.c_str(),CFile::modeWrite|CFile::shareExclusive);
	myFile.SetLength(0);
	CArchive arW(&myFile,CArchive::store);
	std::string multiLine;
	bool openComment=true;
	while (getLine(line,ar,archiveLength,currentPosition))
	{
		removeSpacesAtBeginningAndEnd(line);
		std::string newLine;
		while (line.length()!=0)
		{
			if (getCommentBracket(line,openComment))
				openComment=!openComment;
			else
			{
				if (!openComment)
				{ // we are in a comment section
					if (line.length()!=0)
						line.erase(line.begin());				
				}
				else
				{
					if (!isCommentSlashSlash(line))
					{
						char ctrlChar=getControlChar(line,false);
						if (ctrlChar!=0)
						{
							if (ctrlChar==9)
								ctrlChar=char(' ');
							if (ctrlChar!=0)
								newLine+=ctrlChar;
						}
						else
						{
							std::string str(getString(line,false));
							if (str.length()!=0)
								newLine+=str;
							else
							{
								str=getNumberString(line,false);
								if (str.length()!=0)
									newLine+=str;
								else
								{
									// Here we probably have a variable:
									std::string var(getVariableString(line));
									if (checkIfVariable(var))
									{
										int ind=getVariableIndex(var);
										if (ind!=-1)
										{
											var=obfuscatedVariables[ind];
											retVal++;
										}
									}
									else
										printf("THIS SHOULD NOT HAPPEN!!! %s\n",var.c_str());
									newLine+=var;
								}
							}
						}
					}
					else
						line="";
				}
			}
		}
		removeSpacesAtBeginningAndEnd(newLine);
		if (newLine.length()!=0)
		{
			bool append=false;
			if (multiLine.length()!=0)
			{
				if ( (isAppendableChar(multiLine[multiLine.length()-1]))&&(multiLine.length()<160)&&(!isNonAppendableChar(newLine[0])) )
					append=true;
			}
			else
				append=!isNonAppendableChar(newLine[0]);
			if (!append)
			{
				if (multiLine.length()!=0)
					writeLine(arW,multiLine);
				if (isNonAppendableChar(newLine[0]))
				{
					writeLine(arW,newLine);
					multiLine="";
				}
				else
					multiLine=newLine;
			}
			else
				multiLine.append(newLine);
		}
	}
	if (multiLine.length()!=0)
		writeLine(arW,multiLine);
	arW.Close();
	myFile.Close();
	return(retVal);
}

bool doesFileExist(const char* filenameAndPath)
{
	WIN32_FIND_DATA fData;
	HANDLE h=FindFirstFile(filenameAndPath,&fData);
	if (h==INVALID_HANDLE_VALUE)
		return(false);
	FindClose(h);
	return(true);
}


int main(int argc, char* argv[])
{
	srand(1);
	std::string cDir;
	{
		char curDirAndFile[1024];
		GetModuleFileName(NULL,curDirAndFile,1020);
		char drive[_MAX_DRIVE];
		char dir[_MAX_DIR];
		char name[_MAX_FNAME];
		char ext[_MAX_EXT];
		_splitpath(curDirAndFile,drive,dir,name,ext);
		CString tmp=drive;
		tmp+=dir;
		cDir=tmp.GetBuffer(0);
	}

	std::string tmp(cDir);
	tmp.append("conf.txt");
	if (!doesFileExist(tmp.c_str()))
	{
		printf("ERROR: 'conf.txt' doesn't exist!\n");
		return(0);
	}
	std::vector<std::string> fileNames;
	std::string variableDefinitionFileName;
	TRY
	{
		CFile file(tmp.c_str(),CFile::modeRead|CFile::shareDenyNone);
		CArchive archive(&file,CArchive::load);
		DWORD archiveLength=DWORD(file.GetLength());
		DWORD currentPosition=0;
		std::string line;
		int cnt=0;
		while (getLine(line,archive,archiveLength,currentPosition))
		{
			removeComments(line);
			removeSpacesAtBeginningAndEnd(line);
			std::string tmp2(cDir);
			tmp2.append(line);
			if (cnt==0)
				variableDefinitionFileName=tmp2;
			else
				fileNames.push_back(line);
			cnt++;
		}		
		archive.Close();
		file.Close();
	}
	CATCH(CFileException,e)
	{
		printf("ERROR: An error occured while reading '%s'!\n",tmp.c_str());
		return(0);
	}
	END_CATCH
	if (!doesFileExist(variableDefinitionFileName.c_str()))
	{
		printf("ERROR: File '%s' doesn't exist'\n",variableDefinitionFileName.c_str());
		return(0);
	}
	else
	{
		TRY
		{
			CFile file(variableDefinitionFileName.c_str(),CFile::modeRead|CFile::shareDenyNone);
			CArchive archive(&file,CArchive::load);
			DWORD archiveLength=DWORD(file.GetLength());
			int result=readVariables(archive,archiveLength,variables);
			archive.Close();
			file.Close();
			if (result==0)
			{
				printf("ERROR: No variables could be imported!\n");
				return(0);
			}
			else
				printf("STATUS: %i variables were imported!\n",result);
		}
		CATCH(CFileException,e)
		{
			printf("ERROR: An error occurred while reading '%s'!\n",variableDefinitionFileName.c_str());
			return(0);
		}
		END_CATCH
	}
	std::string newDir(cDir);
	newDir.append("modified\\");
	CreateDirectory(newDir.c_str(),NULL);
	for (int i=0;i<int(fileNames.size());i++)
	{
		std::string oldF(cDir);
		oldF.append(fileNames[i]);
		if (!doesFileExist(oldF.c_str()))
			printf("ERROR: File '%s' doesn't exist'\n",fileNames[i]);
		else
		{
			TRY
			{
				CFile file(oldF.c_str(),CFile::modeRead|CFile::shareDenyNone);
				CArchive archive(&file,CArchive::load);
				DWORD archiveLength=DWORD(file.GetLength());
				std::string outputFileName=newDir;
				outputFileName.append(fileNames[i]);
				int result=replaceVariables(outputFileName.c_str(),archive,archiveLength,variables);
				archive.Close();
				file.Close();
				printf("STATUS: %i words were replaced in '%s'\n",result,fileNames[i].c_str());
			}
			CATCH(CFileException,e)
			{
				printf("ERROR: An error occurred while reading '%s'!\n",fileNames[i].c_str());
				return(0);
			}
			END_CATCH
		}
	}
	printf("STATUS: Finished!!\n");
	Sleep(10000);
	return 0;
}


Share this post


Link to post
Share on other sites
Not useless! I am distributing my source code to a company which is not supposed to modify/understand parts of my code. I obfuscate that code (mainly proprietary geometric calculation algorithms). The reason why I don't distribute the proprietary code in a compiled form? That would require too much effort and time on my side to separate it from the rest of the code.

Share this post


Link to post
Share on other sites
If you want to prevent a company from infringing your copyright use legal, not technical means. The later has been thoroughly and repeatedly proven to be ineffectual.

Share this post


Link to post
Share on other sites
I can only imagine how effective a glorified find and replace will be at preventing people from reading your code...

I will also point out that changing variable names is useless. Decompilers cannot reconstruct variable and function names from assembly and just name them things like "var1...". Decompilers also spit out far more twisted/half functional code (that doesn't compile) than a simple find and replace will produce.

Any attempt at source obfuscation is beyond useless, and yours is on the simplistic side of things.

Share this post


Link to post
Share on other sites
Telastyn, there are some expensive code obfuscation packages out there for some reason. And about the legal means, would you trust this if dealing with a Chinese company? (sorry, I don't want to sound disrespectful torwards Chinese citizens, I just think their legal system needs a bit more adjustment).
Additionally, I also took care of the legal side, obfuscation is just an additional mean!

[Edited by - floatingwoods on November 17, 2008 9:16:03 PM]

Share this post


Link to post
Share on other sites
r691175002, I understand that every code can be reverse-engineered! But you can make the task a little bit more complicated. I am also talking about code that is initially very complicated and difficult to understand, even for an expert (please have a look at a demo of my software at : http://kr.youtube.com/watch?v=_hSG1N9wE80)

Share this post


Link to post
Share on other sites
Quote:
Original post by floatingwoods
Telastyn, there are some expensive code obfuscation packages out there for some reason.


They are byte code obfuscators. The reason there is simple - byte code contains adequate information to fully reconstruct sources.

They are not source code obfuscators. These are already present when developing in C++. They're called compiler and linker.

Source code obfuscators are generally known as 'junior programmers'.

Quote:
And about the legal means, would you trust this if dealing with an extenral (fixed for PC) company?


I would just provide them with a .lib or .dll. Kinda like C++ run-time is delivered with MSVC. 4 versions for each linker setting. And the way closed-source libraries have always been delivered.

Quote:
That would require too much effort and time on my side to separate it from the rest of the code.


Um, it's part of build process... Same thing that you do every time you hit compile to test your code, or run unit tests. You just need to copy a few extra files. There is no extra work involved, if you have even the most basic build system in place - if you use MSVC you don't even need that, just change from executable to library.

No other way to put this without being blunt, but this type of obfuscation is same as improving cryptography by doing ROT-13 twice. It completely misses and fails to understand the basic problem, and even fails to acknowledge the basics of C++ compilation process. It's a useless solution to a non-existent problem.

Share this post


Link to post
Share on other sites
Quote:
Original post by Antheus
Quote:
And about the legal means, would you trust this if dealing with an external company?
I would just provide them with a .lib or .dll. Kinda like C++ run-time is delivered with MSVC. 4 versions for each linker setting. And the way closed-source libraries have always been delivered.
What about software that is largely made up of template code?

e.g.
* if boost was a commercial product... (Forget that boost code is already unreadable! :P)
* One of my personal libraries is probably a 50/50 split between compliable-code and template "meta-code"...

Yes I'd probably just get legal protection, but if you don't trust the other party (but still insist on doing business), then obfuscation is much cheaper than a lawsuit...

Share this post


Link to post
Share on other sites
Quote:
Original post by floatingwoods
Telastyn, there are some expensive code obfuscation packages out there for some reason.


There's also copper bracelets that make you invulnerable. Things for sale exist solely because people will buy them.

Share this post


Link to post
Share on other sites
Quote:
Original post by Hodgman
Quote:
Original post by Antheus
Quote:
And about the legal means, would you trust this if dealing with an external company?
I would just provide them with a .lib or .dll. Kinda like C++ run-time is delivered with MSVC. 4 versions for each linker setting. And the way closed-source libraries have always been delivered.
What about software that is largely made up of template code?

e.g.
* if boost was a commercial product... (Forget that boost code is already unreadable! :P)
* One of my personal libraries is probably a 50/50 split between compliable-code and template "meta-code"...

Yes I'd probably just get legal protection, but if you don't trust the other party (but still insist on doing business), then obfuscation is much cheaper than a lawsuit...

Compiler errors from templated code are already obscure enough to figure out. I'd hate to try and use the templates from the obfuscated source, it'd probably make the whole thing unusable.

Share this post


Link to post
Share on other sites

This topic is 3311 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this