-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FEATURE] AntiDebug Software Breakpoints detection #43
Conversation
hi @cecio ! I can understand some false positives in case if accidentally, a comparison with 0xCC byte is done for other purposes than anti-debug, but here the byte labeled wasn't even 0xCC, which suggests that something is wrong with the implementation itself. |
hey @hasherezade No worries at all, no hurry on this :-) Mmmmmhh....yeah, I have a couple of ideas about what is going on and may be this need to be adjusted. The comparison you pointed out could trigger the alarm if in RAX we have 0xCC, but it in this case it's pointless because comparing with an immediate, so it's for sure a false positive. But may be it's also something else. Let me run some more tests and come back with more details and a more robust solution. |
It seems to me that anyways, we should just check if the second argument, to which the first argument is compared, is 0xCC, rather than checking all the arguments. This is how the compiled snippet from the Checkpoint's example looks like in the compiled version: So the whole checking function can be shorten to: VOID AntiDbg::WatchCompareSoftBrk(const CONTEXT* ctxt, ADDRINT Address, ADDRINT insArg)
{
PinLocker locker;
const WatchedType wType = isWatchedAddress(Address);
if (wType == WatchedType::NOT_WATCHED) return;
INS ins;
ins.q_set(insArg);
if (INS_OperandCount(ins) < 2) {
return;
}
bool isSet = false;
const UINT32 opIdx = 1;
if (INS_OperandIsImmediate(ins, opIdx))
{
UINT8 val = 0;
if ((val = (INS_OperandImmediate(ins, opIdx) & 0xFF)) == 0xCC)
{
isSet = true;
}
}
if (isSet) {
LogAntiDbg(wType, Address, "Software Breakpoint comparison",
"https://anti-debug.checkpoint.com/techniques/process-memory.html#anti-step-over");
}
} Please try to trace my my demo application antianalysis_demos (I added that testcase: https://github.com/hasherezade/antianalysis_demos/blob/master/main.cpp#L62), and have a look at the output. What also distinguish such cases, is that, the same comparison is called over and over, at the same offset (because usually the breakpoints are checked not at a single address, but through the whole function/code block). So we can also use the repetition as an indicator. And log it only once, i.e. at third attempt, in order not to slow down too much and spam the tracelog with repeated lines. |
You are right. I did also some tests on it, and actually the compiler always translate these kind of comparisons as immediate value (even if you set the 0xCC as a stack variable). In order to test the other cases I actually had to trick the compiler in producing some different code (like referenced values), with some ugly hacks. Thanks for your feedback! |
Yes, I think it is better to go with the immediate value, because other cases are too rare to be worth it. But in any case, only operand 1 should be checked. BTW, still I see some problem, that even if I specifically check if the immediate value is being used, it is not always reliable. In the case below, the comparison with RBP register was done, and Pin for some reason returned Edit: I filtered out such cases by checking the operand size. Still it doesn't answer the question why in the first place Pin identified it as Immediate, while it was a register. Yet, the check for the operand size is a reasonable constraint here. This is how I check it now: if (INS_OperandIsImmediate(ins, opIdx) && INS_OperandSize(ins, opIdx) == sizeof(UINT8)) And, just a sidenote - having this antidebug technique covered in TinyTracer isn't a very high priority. Due to the fact that Pin doesn't set breakpoints during tracing, this technique with 0xCC checks isn't able to disrupt Pin anyways. Listing them is only to provide an information about what problems may occur during debugging of such application. |
@cecio - and this is my reworked version, please have a look: /* ==================================================================== */
// Callback function to be executed when a compare is executed
/* ==================================================================== */
std::map<ADDRINT, size_t> cmpOccurrences;
VOID AntiDbg::WatchCompareSoftBrk(const CONTEXT* ctxt, ADDRINT Address, ADDRINT insArg)
{
PinLocker locker;
const WatchedType wType = isWatchedAddress(Address);
if (wType == WatchedType::NOT_WATCHED) return;
INS ins;
ins.q_set(insArg);
if (!ins.is_valid() || INS_OperandCount(ins) < 2) {
return;
}
bool isSet = false;
const UINT32 opIdx = 1;
if (INS_OperandIsImmediate(ins, opIdx) && INS_OperandSize(ins, opIdx) == sizeof(UINT8))
{
UINT8 val = 0;
if ((val = (INS_OperandImmediate(ins, opIdx) & 0xFF)) == 0xCC)
{
cmpOccurrences[Address]++;
if (cmpOccurrences[Address] == 3) isSet = true;
}
}
if (isSet) {
LogAntiDbg(wType, Address, "Software Breakpoint comparison",
"https://anti-debug.checkpoint.com/techniques/process-memory.html#anti-step-over");
}
} From what I checked it works fine. It tags what it's supposed to, and only once, not generating too much noise. Fragment:
I need to doublecheck it with the latest version of Al-Khaser now... |
Looks great! I'll run some tests with Al-Kasher as well (I'll do it this evening, now I need to leave the keyboard for a while :-)). I have a doubt about the incorrect immediate check done by PIN you noticed: may be this has something to do with the INS passed as parameter: I did it to avoid to clutter the |
@cecio - I checked with Al-Khaser, and at first I thought is was a bug, because although the implementation of this technique is there: https://github.com/LordNoteworthy/al-khaser/blob/master/al-khaser/AntiDebug/SoftwareBreakpoints.cpp - the tracer wasn't detecting it. However, upon a closer look I realized that this check is in fact a dead code, and never executed, so there is nothing to detect! I added a log line: BOOL SoftwareBreakpoints()
{
//NOTE this check might not work on x64 because of alignment 0xCC bytes
size_t sSizeToCheck = (size_t)(Myfunction_Adresss_Next)-(size_t)(My_Critical_Function);
PUCHAR Critical_Function = (PUCHAR)My_Critical_Function;
printf("Checking breakpoints in the area of size: %x\n", sSizeToCheck);
for (size_t i = 0; i < sSizeToCheck; i++) {
if (Critical_Function[i] == 0xCC) // Adding another level of indirection : 0xCC xor 0x55 = 0x99
return TRUE;
}
return FALSE;
} And I see that the function to be checked is too short, and due to inlining or some optimization, the area that was supposed to be check is of size 0: I made a modification in Al-Khaser to ensure that the check will not be optimized out: https://gist.github.com/hasherezade/38377585dc6f0b1bbdb80bd47e046261 And now the check is detected: At the same time, the earlier false positives are not detected, so all seems good. |
@cecio - if you can give me a permission, I will push my reworked branch to your repository. Then you can test it, and if all is good, we will merge to the main repo. (I think this will be cleaner than me creating additional forks). |
@hasherezade sure! |
… comparison of the 2nd operand. Only immediate.
thank you @cecio ! I pushed my branch as: https://github.com/cecio/tiny_tracer/tree/cc_fixed |
BTW, the function that I changed in Al-Khaser will cause it to report Software Breakpoints being set ( ...and anyways, there is the INT3 padding added between this function and the next function that is used as the area terminator, and the implementation of this technique does not account for this, checking the area from the beginning of the first function, till the beginning of the other, not stopping on RET: size_t sSizeToCheck = (size_t)(Myfunction_Adresss_Next)-(size_t)(My_Critical_Function); But anyways, this is more about Al-Khaser, and not directly related to TinyTracer. The point is, TinyTracer detected the comparison where it was supposed to detect it. |
[BUGFIX] In Software Debug checks: reworked to remove FPs, check only…
@hasherezade your implementation works great. All the test I ran worked fine. I merged your branch on the master. Thanks a lot! |
@cecio - I am happy to hear! thank you for testing and for the merge. |
Thanks to you @hasherezade ! |
@cecio - I am still checking what caused that if (isSet) {
std::stringstream ss;
ss << INS_Disassemble(ins);
ss << " : Software Breakpoint comparison";
LogAntiDbg(wType, Address, ss.str().c_str(),
"https://anti-debug.checkpoint.com/techniques/process-memory.html#anti-step-over");
} The output for the incorrectly identified instruction is indeed incorrect and weird:
The disasm for the correct check looks better, but also the register is different
So it seems that way of passing the Instruction (via |
@hasherezade Yes, that was what I was suspecting as well. I was trying to reproduce the problem. TBH we can avoid it: I used this trick to avoid to put too many code in |
@cecio - what do you propose to use instead of
while in case of the line where the correct check was made, the address is as the actual:
Used code: if (isSet) {
ADDRINT addr = INS_Address(ins);
ADDRINT rva = addr_to_rva(addr);
std::stringstream ss;
ss << "VA = " << std::hex << addr;
ss << " : ";
ss << "RVA = " << std::hex << rva;
ss << " : ";
ss << INS_Disassemble(ins);
ss << " : ";
ss << INS_Mnemonic(ins);
ss << " : Software Breakpoint comparison";
LogAntiDbg(wType, Address, ss.str().c_str(),
"https://anti-debug.checkpoint.com/techniques/process-memory.html#anti-step-over");
} So checking for those incorrect addressed can be used to filter out those invalid instructions. But still, it is not a perfect solution. And the correct code for those offsets is missing. |
@cecio - I tried doing it another ways round, and it seems to work: if (m_Settings.antidebug >= ANTIDEBUG_DEEP) {
// Check all comparison for 0xCC byte (anti stepinto/stepover checks)
if (INS_Opcode(ins) == XED_ICLASS_CMP && INS_OperandCount(ins) >= 2 && INS_OperandIsImmediate(ins, 1)) {
INS_InsertCall(
ins,
IPOINT_BEFORE, (AFUNPTR)AntiDbg::WatchCompareSoftBrk,
IARG_FAST_ANALYSIS_CALL,
IARG_INST_PTR,
IARG_ADDRINT, INS_OperandImmediate(ins, 1),
IARG_END);
}
} Is it what you meant? |
@hasherezade yes exactly. Even because I think it could be a bit complicated. Looking at this post looks like that INS at instrumentation time is not guaranteed to be kept at analysis time, especially if other instrumentation routines are called (which explain why I have different results in different tests I'm doing) But, at this point I don't know if it make sense. I think that the checks at Instrumentation level are clean enough (and effective as well). |
I saw that you left the size check in the instrumentation function
If I try to run your demo (64 bit compiled) In this case the immediate is Did you see cases where this check is still required? |
@cecio - I was thinking about removing this check, but then I decided to leave it, because anyways searching for breakpoints usually happens byte by byte. We cannot expect that breakpoints will be set as multiple in a row. This is the binary that I used for the test: https://ci.appveyor.com/project/hasherezade/antianalysis-demos/build/job/1341hhwduqgffy0u/artifacts Can you share with me the binary that you used? I will have a look what this check really is. You can drop it here... |
@hasherezade I need to sleep, it was pretty clear it couldn't be that check also looking at the instruction So, I think it works like a charm then. Sorry.... |
@cecio - no worries! It is good you asked and we clarified this. |
@hasherezade yeah great! :-) Hope you as well! |
I added a new detection for the AntiDebug feature, and in particular the Softfware Breakpoints detection.
Basically I tried to go through https://anti-debug.checkpoint.com/techniques/process-memory.html#breakpoints and noticed that most of the detection can be summarized in the checks for the presence of byte 0xCC somewhere.
So I implemented a check for this byte for comparison statements (it should work for immediate, register and referenced memory compare).
I put this under AntiDebug lelvel 2 (Deep), since it could slow down and may be lead to some false positives.
Let me know if you think that any rework is needed.
Thanks a lot!