Welcome to this next part of the reverse engineering with radare2 course :) Today we are going to walk through some simple data structures such as variables, understand how basic conditional code structures work on the inside and we will also learn how to debug with radare2.
Let’s start with this simple example. The following example-program, declares four variables, regarding to the numeric variables the first one represents an int, second one represents a float and the third one a double then we also have a char ‘a’. At the end those values are added and the result is printed. We will see how the first variable gets directly stored in a (general purpose) register, the second one gets stored in memory too but in another place and the third one works the same with the exception that it needs 2 times the space of the second (double)
#include <stdio.h>
int main() {
char ab ='a';
int a = 3;
float b = 4.5;
double c = 5.25;
float sum;
sum = a+b+c;
printf("The sum of a, b, and c is %f.", sum);
return 0;
}
As always, we can compile the code by using gcc, no mistery.
We open the binary and then we analyze its content, aaa should work fine.
-- sudo make me a pancake
[0x08048310]> aaa
[Cannot analyze at 0x08048300g with sym. and entry0 (aa)
[x] Analyze all flags starting with sym. and entry0 (aa)
[Cannot analyze at 0x08048300ac)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x08048310]>
As we compiled the binary using GCC, we can identify some “typical” functions related to the initialization of the program.
[0x08048310]> afl
0x08048310 1 33 entry0
0x080482f0 1 6 sym.imp.__libc_start_main
0x08048350 4 43 sym.deregister_tm_clones
0x08048380 4 53 sym.register_tm_clones
0x080483c0 3 30 entry.fini0
0x080483e0 4 43 -> 40 entry.init0
0x080484f0 1 2 sym.__libc_csu_fini
0x08048340 1 4 sym.__x86.get_pc_thunk.bx
0x080484f4 1 20 sym._fini
0x08048490 4 93 sym.__libc_csu_init
0x0804840b 1 123 main
0x080482e0 1 6 sym.imp.printf
0x080482ac 3 35 sym._init
[0x08048310]>
The only interesting function here is the “main” one as it clearly belongs to the “main” function of the program. From here we can also identify that “printf” is used in the program.
[0x08048310]> sf main
[0x0804840b]> pdf
; DATA XREF from entry0 @ 0x8048327
┌ 123: int main (int32_t arg_4h, char **argv, char **envp);
│ ; var char var_1dh @ ebp-0x1d
│ ; var int32_t var_1ch @ ebp-0x1c
│ ; var int32_t var_4h @ ebp-0x4
│ ; arg int32_t arg_4h @ esp+0x4c
│ 0x0804840b 8d4c2404 lea ecx, [arg_4h]
│ 0x0804840f 83e4f0 and esp, 0xfffffff0
│ 0x08048412 ff71fc push dword [ecx - 4]
│ 0x08048415 55 push ebp
│ 0x08048416 89e5 mov ebp, esp
│ 0x08048418 51 push ecx
│ 0x08048419 83ec34 sub esp, 0x34
│ 0x0804841c c645e361 mov byte [var_1dh], 0x61 ; 'a' ; 97
│ 0x08048420 c745e4030000. mov dword [var_1ch], 3
│ 0x08048427 d9054c850408 fld dword [0x804854c]
│ 0x0804842d d95de8 fstp dword [ebp - 0x18]
│ 0x08048430 dd0550850408 fld qword [0x8048550]
│ 0x08048436 dd5df0 fstp qword [ebp - 0x10]
│ 0x08048439 d9ee fldz
│ 0x0804843b d95dec fstp dword [ebp - 0x14]
│ 0x0804843e db45e4 fild dword [var_1ch]
│ 0x08048441 d845e8 fadd dword [ebp - 0x18]
│ 0x08048444 dc45f0 fadd qword [ebp - 0x10]
│ 0x08048447 d95dec fstp dword [ebp - 0x14]
│ 0x0804844a d945ec fld dword [ebp - 0x14]
│ 0x0804844d 83ec04 sub esp, 4
│ 0x08048450 8d6424f8 lea esp, [esp - 8]
│ 0x08048454 dd1c24 fstp qword [esp]
│ 0x08048457 6810850408 push str.The_sum_of_a__b__and_c_is__f. ; 0x8048510 ; "The sum of a, b, and c is %f." ; const char *format
│ 0x0804845c e87ffeffff call sym.imp.printf ; int printf(const char *format)
│ 0x08048461 83c410 add esp, 0x10
│ 0x08048464 0fbe45e3 movsx eax, byte [var_1dh]
│ 0x08048468 83ec08 sub esp, 8
│ 0x0804846b 50 push eax
│ 0x0804846c 682e850408 push str.The_value_of_char_ab_is__c. ; 0x804852e ; "The value of char ab is %c." ; const char *format
│ 0x08048471 e86afeffff call sym.imp.printf ; int printf(const char *format)
│ 0x08048476 83c410 add esp, 0x10
│ 0x08048479 b800000000 mov eax, 0
│ 0x0804847e 8b4dfc mov ecx, dword [var_4h]
│ 0x08048481 c9 leave
│ 0x08048482 8d61fc lea esp, [ecx - 4]
└ 0x08048485 c3 ret
[0x0804840b]>
As we look through the main code of the program we see some new instructions here, like fstp, fild, fadd and so forth by pure deduction we could state that those may be related to “floating point operations” as we use float and double here. We can also identify how the print function is called, we see some parameters being pushed onto the stack.
As we are dealing with variables here, one of the things we may want to do is to see how radare2 identifies variables and maybe give them a nice name. We can doo this by using afv.
[0x0804840b]> afv
arg int32_t arg_4h @ esp+0x4c
var char var_1dh @ ebp-0x1d
var int32_t var_1ch @ ebp-0x1c
var int32_t var_4h @ ebp-0x4
[0x0804840b]>
As we clearly identify a char with radare2 (var char var_1…) we can rename that variable to char
[0x0804840b]> afvn char1 var_1dh
[0x0804840b]> afvn
char1
var_1ch
var_4h
arg_4h
[0x0804840b]>
That char variable is interesting. When using char variables, what really happens internally is that those chars are hex encoded. In hex, ‘a’ corresponds to 61 in the ascii table. In our example we can simply see how the program uses mov to move the 0x61 byte to the position of the variable.
0x0804841c c645e361 mov byte [var_1dh], 0x61 ; 'a' ; 97
0x08048420 c745e4030000. mov dword [var_1ch], 3
Now that we have the char variable identified, and we should already be able to identify the int variable as well let’s look at how the program deals with floating point variables.
The best way to inspect that is by running the program in debug mode. In radare2 we can open a program in debug mode by using the -d option.
In debug mode we can use commands such as “db memaddress” to set a breakpoint, “dc” to continue the execution flow to that/those breakpoint(s) and “dt” to run the current instruction and move to the next one right after.
In our program we can set some interesting points before the fldz, flstp and such.
[0x0804840b]> db 0x08048427
[0x0804840b]> db 0x0804842d
[0x0804840b]> db 0x08048430
[0x0804840b]> dc
hit breakpoint at: 8048427
[0x08048427]>
After we hit our first breakpoint we move to:
│ 0x08048420 c745e4030000. mov dword [var_1ch], 3
│ 0x08048427 d9054c850408 fld dword [0x804854c]
│ 0x0804842d d95de8 fstp dword [ebp - 0x18]
We can identify that the 3 value has been moved to var_1ch and then some strange instruction is executed “fld dword”. The fld instruction loads a 32 bit, 64 bit, or 80 bit floating point value onto the stack. This instruction converts 32 and 64 bit operand to an 80 bit extended precision value before pushing the value onto the floating point stack. So if we inspect what value is fld picking up for loading we will see something like:
[0x08048427]> px 32 @ 0x804854c
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x0804854c 0000 9040 0000 0000 0000 1540 011b 033b ...@.......@...;
0x0804855c 2800 0000 0400 0000 78fd ffff 4400 0000 (.......x...D...
[0x08048427]>
” 0000 9040 0000 0000 0000 “.We can now try to inspect the contents of that position in memory which clearly corresponds to a variable
[0x08048430]> px @ ebp-0x18
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbfe7a7e0 0000 9040 a4a8 e7bf aca8 e7bf b184 0408 ...@............
0xbfe7a7f0 dc93 f6b7 10a8 e7bf 0000 0000 37f6 dcb7 ............7...
0xbfe7a800 0090 f6b7 0090 f6b7 0000 0000 37f6 dcb7 ............7...
We can make the output a little bit more nice and human readable (for our case) by adding w and thus running pxw when w comes from word (two bytes)
[0x08048430]> pxw @ ebp-0x18
0xbfe7a7e0 0x40900000 0xbfe7a8a4 0xbfe7a8ac 0x080484b1 ...@............
0xbfe7a7f0 0xb7f693dc 0xbfe7a810 0x00000000 0xb7dcf637 ............7...
“0x40900000” must be the value. But that value gives us few information, at least in this format. As we suspect that this number represents a floating point encoded number, we can try to use the “rax2” tool to read it.
[0xbfe7a7f0]> rax2 Fx40900000
4.500000f
And we can clearly see how this number corresponds to the value of our first floating point variable.
We can do this exact same thing with the second variable, I will leave it up to you, you will find the value of 5.25, but as its a “double” value, instead of one word, we will have a size of 32.
So finally, we can see how those parameters are pusshed to the stack. The string “The sum of…” is pushed directly to the stack by just doing “push” and the “sum” variable is inserted in to the stack by using the fstp instruction. The FST instruction copies the value in the ST(0) register to the destination operand, which can be a memory location or another register in the FPU register stack. When storing the value in memory, the value is converted to single- or double-real format.
| 0x08048447 d95dec fstp dword [ebp - 0x14]
│ 0x0804844a d945ec fld dword [ebp - 0x14]
│ 0x0804844d 83ec04 sub esp, 4
│ 0x08048450 8d6424f8 lea esp, [esp - 8]
│ 0x08048454 dd1c24 fstp qword [esp]
│ 0x08048457 6810850408 push str.The_sum_of_a__b__and_c_is__f. ; 0x8048510 ; "The sum of a, b, and c is %f." ; const char *format
│ 0x0804845c e87ffeffff call sym.imp.printf ; int printf(const char *format)
We see how the “sum” value comes from ebp-0x14 as a result of the previous operation:
[0x08048444]> pxw @ ebp-0x14
0xbf8e0ba4 0x00000000 0x00000000 0x40150000 0xb7f573dc ...........@.s..
And it clearly corresponds to the actual result:
[0x0804844d]> rax2 Fx414c0000
12.750000f (as float)
And then it appears on the stack along with the address that points to the string to be printed
[0x0804845c]> pxw @ esp
0xbf8e0b70 0x08048510 0x00000000 0x40298000 0xb7f711b0 ..........)@....
0xbf8e0b80 0x00008000 0xb7f57000 0xb7f55244 0xb7dbd0ec .....p..DR......
0xbf8e0b90 0x00000001 0x00000000 0x61dd3a50 0x00000003 ........P:.a....
*note that little endian is used here and 0x4029800000000000 = 12.75 as double = sum
We will start with the following piece of code. It will declare a variable as a signed integer, then it will read the user input and store it in that variable. After that it will check whether if is a positive or negative int then print a message.
#include <stdio.h>
int main() {
signed int number;
printf("Enter an integer: ");
scanf("%d", &number);
if (number > 0) {
printf("You entered %d.\n", number);
}
else{
printf("You entered a negative number %d.\n", number);
}
printf("The if statement is easy.");
return 0;
We start as usual by running the program with r2 -d analyzing it and switching to the main function and running pdf to peek inside it:
[0x080484bb]> pdf
; DATA XREF from entry0 @ 0x80483d7
┌ 159: int main (int argc, char **argv, char **envp);
│ ; var int32_t var_10h @ ebp-0x10
│ ; var int32_t var_ch @ ebp-0xc
│ ; var int32_t var_4h @ ebp-0x4
│ ; arg int32_t arg_4h @ esp+0x34
│ 0x080484bb 8d4c2404 lea ecx, [arg_4h]
│ 0x080484bf 83e4f0 and esp, 0xfffffff0
│ 0x080484c2 ff71fc push dword [ecx - 4]
│ 0x080484c5 55 push ebp
│ 0x080484c6 89e5 mov ebp, esp
│ 0x080484c8 51 push ecx
│ 0x080484c9 83ec14 sub esp, 0x14
│ 0x080484cc 65a114000000 mov eax, dword gs:[0x14]
│ 0x080484d2 8945f4 mov dword [var_ch], eax
│ 0x080484d5 31c0 xor eax, eax
│ 0x080484d7 83ec0c sub esp, 0xc
│ 0x080484da 68e0850408 push str.Enter_an_integer: ; 0x80485e0 ; "Enter an integer: "
│ 0x080484df e88cfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x080484e4 83c410 add esp, 0x10
│ 0x080484e7 83ec08 sub esp, 8
│ 0x080484ea 8d45f0 lea eax, [var_10h]
│ 0x080484ed 50 push eax
│ 0x080484ee 68f3850408 push 0x80485f3
│ 0x080484f3 e8a8feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x080484f8 83c410 add esp, 0x10
│ 0x080484fb 8b45f0 mov eax, dword [var_10h]
│ 0x080484fe 85c0 test eax, eax
│ ┌─< 0x08048500 7e16 jle 0x8048518
│ │ 0x08048502 8b45f0 mov eax, dword [var_10h]
│ │ 0x08048505 83ec08 sub esp, 8
│ │ 0x08048508 50 push eax
│ │ 0x08048509 68f6850408 push str.You_entered__d. ; 0x80485f6 ; "You entered %d.\n"
│ │ 0x0804850e e85dfeffff call sym.imp.printf ; int printf(const char *format)
│ │ 0x08048513 83c410 add esp, 0x10
│ ┌──< 0x08048516 eb14 jmp 0x804852c
│ │└─> 0x08048518 8b45f0 mov eax, dword [var_10h]
│ │ 0x0804851b 83ec08 sub esp, 8
│ │ 0x0804851e 50 push eax
│ │ 0x0804851f 6808860408 push str.You_entered_a_negative_number__d. ; 0x8048608 ; "You entered a negative number %d.\n"
│ │ 0x08048524 e847feffff call sym.imp.printf ; int printf(const char *format)
│ │ 0x08048529 83c410 add esp, 0x10
│ │ ; CODE XREF from main @ 0x8048516
│ └──> 0x0804852c 83ec0c sub esp, 0xc
│ 0x0804852f 682b860408 push str.The_if_statement_is_easy. ; 0x804862b ; "The if statement is easy."
│ 0x08048534 e837feffff call sym.imp.printf ; int printf(const char *format)
│ 0x08048539 83c410 add esp, 0x10
│ 0x0804853c b800000000 mov eax, 0
│ 0x08048541 8b55f4 mov edx, dword [var_ch]
│ 0x08048544 653315140000. xor edx, dword gs:[0x14]
│ ┌─< 0x0804854b 7405 je 0x8048552
│ │ 0x0804854d e82efeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x08048552 8b4dfc mov ecx, dword [var_4h]
│ 0x08048555 c9 leave
│ 0x08048556 8d61fc lea esp, [ecx - 4]
└ 0x08048559 c3 ret
So in here, we can start seeing some arrows moving up and down the code. Those arrows indicate the execution flow that the program may follow depending on certain conditions, as we already have the code, we can guess that those flow directions relate to the IF-ELSE part of the code somehow. We also see some variables being declared on top of the code.
The next thing that may get our attention is the call to the SCANF function. Let’s examine it, by setting some breakpoints with “db”:
│ 0x080484df e88cfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x080484e4 83c410 add esp, 0x10
│ 0x080484e7 83ec08 sub esp, 8
│ 0x080484ea b 8d45f0 lea eax, [input]
│ 0x080484ed 50 push eax
│ 0x080484ee 68f3850408 push 0x80485f3
│ 0x080484f3 b e8a8feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x080484f8 b 83c410 add esp, 0x10
│ 0x080484fb 8b45f0 mov eax, dword [input]
│ 0x080484fe 85c0 test eax, eax
So after the PRINTF call, some stack adjustment is done and then we see that the address of one of the variables we saw on top of the code is loaded into eax (one can name it something like “input” with afvn for pure logical reasons), then eax is pushed to the stack for being passed as a parameter (we can guess) to the SCANF function, a memory address is pushed to the stack as well, in this case we can see that this address contains the string (“please enter…:”).
If we move after the first breakpoint, we can inspect the registers and see how the address is loaded:
[0x080484ed]> dr
eax = 0xbfa9a688
ebx = 0x00000000
ecx = 0x0910b01a
edx = 0xb7f0c870
esi = 0xb7f0b000
edi = 0xb7f0b000
esp = 0xbfa9a678
ebp = 0xbfa9a698
eip = 0x080484ed
eflags = 0x00000296
oeax = 0xffffffff
Then we can move to the next breakpoint and inspect the stack, we will see how those two parameters are appear there:
[0x080484ed]> dc
hit breakpoint at: 80484f3
[0x080484f3]> pxr @ esp
0xbfa9a670 0x080485f3 .... @esp (/home/lab/c_examples/bin/ifelse) (.rodata) program R X 'and eax, 0x6f590064' 'ifelse' (%d)
0xbfa9a674 0xbfa9a688 .... ([stack]) stack R W 0xbfa9a74c --> ([stack]) stack R W 0xbfa9c288 --> ([stack]) stack R W 0x5f474458 (XDG_VTNR=7) --> ascii ('X')
0xbfa9a678 0xb7d87a50 Pz.. (/lib/i386-linux-gnu/libc-2.23.so) library R X 'add ebx, 0x1835b0' 'libc-2.23.so'
0xbfa9a67c 0x080485ab .... (/home/lab/c_examples/bin/ifelse) (.text) sym.__libc_csu_init program R X 'add edi, 1' 'ifelse'
0xbfa9a680 0x00000001 .... 1 (.comment)
0xbfa9a684 0xbfa9a744 D... ([stack]) stack R W 0xbfa9c27f --> ([stack]) stack R W 0x66692f2e (./ifelse) --> ascii ('.')
Then if we hit the next breakpoint one more time, the program will ask for a value and will then hit the last breakpoint:
[0x080484f3]> dc
Enter an integer: 10
hit breakpoint at: 80484f8
[0x080484f8]>
If we inspect the registers, we will see how EAX is set with the value 0x00000001, that means the SCANF call executed properly without any major problem.
[0x080484f8]> dr
eax = 0x00000001
ebx = 0x00000000
ecx = 0x00000001
edx = 0xb7f0c87c
esi = 0xb7f0b000
edi = 0xb7f0b000
esp = 0xbfa9a670
ebp = 0xbfa9a698
eip = 0x080484f8
eflags = 0x00000246
oeax = 0xffffffff
[0x080484f8]>
So at that point we can now examine the address of the “input” variable and we will see how the number (10 in our case) is stored there in hex format.
[0x080484f8]> pxw @ 0xbfa9a688
0xbfa9a688 0x0000000a 0x630f3300 0xb7f0b3dc 0xbfa9a6b0 .....3.c........
We can even make sure of it by using rax2 to convert it to base10.
[0x080484f8]> rax2 0xa
10
At that point of the program, we have our input value properly stored, after that, the program will have to check whether that value is positive or negative and move the execution to one place or another depending on that.
So we can set another breakpoint right before “the first arrow” related to the execution flow. As arrows may not always appear (that always will depend on the disasm/framework you are using), another way to detect “switch points” on the execution flow is to look for JLE, JE and instructions like that.
[0x080484f8]>
│ 0x080484f8 b 83c410 add esp, 0x10
│ 0x080484fb 8b45f0 mov eax, dword [input]
│ 0x080484fe 85c0 test eax, eax
│ ┌─< 0x08048500 7e16 jle 0x8048518
│ │ 0x08048502 8b45f0 mov eax, dword [input]
│ │ 0x08048505 83ec08 sub esp, 8
So we basically see that the “10”base10 value is loaded into EAX and then a test instruction is executed:
After the test instruction is used, the flag registers status is the following:
[0x08048500]> dr 1
cf = 0x00000000
pf = 0x00000001
af = 0x00000000
zf = 0x00000000
sf = 0x00000000
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
And basically, the TEST instruction sets ZF(Zero flag) and SF(Sign flag) based on a logical AND between the operands, and clears OF(Overflow flag). So in this case those flags will be set to zero. Then the instruction JLE means JUMP (to the memory address that is indicated right after) If less than and “Less than” is defined as: ZF=1 or SF != OF
So we can see that if we reload the program and enter a negative number the SF flag will get activated:
[0x080484bb]> db 0x08048500
[0x080484bb]> dc
Enter an integer: -20
hit breakpoint at: 8048500
[0x08048500]> dr 1
cf = 0x00000000
pf = 0x00000000
af = 0x00000000
zf = 0x00000000
sf = 0x00000001
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x08048500]>
And one more time, as we enter a positive number the SF flag will not be activated.
[0x080484bb]> db 0x08048500
[0x080484bb]> dc
Enter an integer: 20
hit breakpoint at: 8048500
[0x08048500]> dr 1
cf = 0x00000000
pf = 0x00000001
af = 0x00000000
zf = 0x00000000
sf = 0x00000000
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x08048500]>
Then after that, the program will basically jump to a memory address that will print “positive” or to another one printing “negative” then move or jump (with jmp) to the end of the program and thus the program will end.
The 64 bit is prettyy the same, can you note the differences? Basically registers(esi, rdi..) are being used to pass parameters instead of the stack! Also, memory addresses are 64 bits.
[0x7f584b7b9090]> sf main
[0x561302d2871a]> pdf
;-- main:
/ (fcn) main 161
| main ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; DATA XREF from 0x561302d2862d (entry0)
| 0x561302d2871a 55 push rbp
| 0x561302d2871b 4889e5 mov rbp, rsp
| 0x561302d2871e 4883ec10 sub rsp, 0x10
| 0x561302d28722 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x561302d2872b 488945f8 mov qword [local_8h], rax
| 0x561302d2872f 31c0 xor eax, eax
| 0x561302d28731 488d3d100100. lea rdi, qword str.Enter_an_integer: ; 0x561302d28848 ; "Enter an integer: "
| 0x561302d28738 b800000000 mov eax, 0
| 0x561302d2873d e89efeffff call sym.imp.printf ; int printf(const char *format)
| 0x561302d28742 488d45f4 lea rax, qword [local_ch]
| 0x561302d28746 4889c6 mov rsi, rax
| 0x561302d28749 488d3d0b0100. lea rdi, qword [0x561302d2885b] ; "%d"
| 0x561302d28750 b800000000 mov eax, 0
| 0x561302d28755 e896feffff call sym.imp.__isoc99_scanf
| 0x561302d2875a 8b45f4 mov eax, dword [local_ch]
| 0x561302d2875d 85c0 test eax, eax
| ,=< 0x561302d2875f 7e18 jle 0x561302d28779
| | 0x561302d28761 8b45f4 mov eax, dword [local_ch]
| | 0x561302d28764 89c6 mov esi, eax
| | 0x561302d28766 488d3df10000. lea rdi, qword str.You_entered__d. ; 0x561302d2885e ; "You entered %d.\n"
| | 0x561302d2876d b800000000 mov eax, 0
| | 0x561302d28772 e869feffff call sym.imp.printf ; int printf(const char *format)
| ,==< 0x561302d28777 eb16 jmp 0x561302d2878f
| |`-> 0x561302d28779 8b45f4 mov eax, dword [local_ch]
| | 0x561302d2877c 89c6 mov esi, eax
| | 0x561302d2877e 488d3deb0000. lea rdi, qword str.You_entered_a_negative_number__d. ; 0x561302d28870 ; "You entered a negative number %d.\n"
| | 0x561302d28785 b800000000 mov eax, 0
| | 0x561302d2878a e851feffff call sym.imp.printf ; int printf(const char *format)
| | ; JMP XREF from 0x561302d28777 (main)
| `--> 0x561302d2878f 488d3dfd0000. lea rdi, qword str.The_if_statement_is_easy. ; 0x561302d28893 ; "The if statement is easy."
| 0x561302d28796 b800000000 mov eax, 0
| 0x561302d2879b e840feffff call sym.imp.printf ; int printf(const char *format)
| 0x561302d287a0 b800000000 mov eax, 0
| 0x561302d287a5 488b55f8 mov rdx, qword [local_8h]
| 0x561302d287a9 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x561302d287b2 7405 je 0x561302d287b9
| | 0x561302d287b4 e817feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `-> 0x561302d287b9 c9 leave
\ 0x561302d287ba c3 ret
[0x561302d2871a]>
See you in the next one :)