Published

[PoP:Rev] Reverse Engineering x86 ELF: 0x4

How loops look in disassembly, through a program that prints the length of its input.

reversing

3 min read

New Concepts Covered

  • Loops

The string length program

In this example, we will be analyzing a program that takes in user input and prints the length of the input.

Source Code

C
#include <stdio.h>

int main()
{
	char input[32];

	/* This is just an example, don't actually do this.
	 * it is vulnerable to buffer overflow. */

	scanf("%s", input);

	int i = 0;
	for(char * c = input; *c != '\0'; c++) {
		i++;
	}

	printf("Your input is %d characters long\n", i);

	return 0;
}

Disassembly

Text
Dump of assembler code for function main:
   0x0804846b <+0>:	push   ebp
   0x0804846c <+1>:	mov    ebp,esp
   0x0804846e <+3>:	sub    esp,0x28
   0x08048471 <+6>:	lea    eax,[ebp-0x28]
   0x08048474 <+9>:	push   eax
   0x08048475 <+10>:	push   0x8048540
   0x0804847a <+15>:	call   0x8048350 <__isoc99_scanf@plt>
   0x0804847f <+20>:	add    esp,0x8
   0x08048482 <+23>:	mov    DWORD PTR [ebp-0x4],0x0
   0x08048489 <+30>:	lea    eax,[ebp-0x28]
   0x0804848c <+33>:	mov    DWORD PTR [ebp-0x8],eax
   0x0804848f <+36>:	jmp    0x8048499 <main+46>
   0x08048491 <+38>:	add    DWORD PTR [ebp-0x4],0x1
   0x08048495 <+42>:	add    DWORD PTR [ebp-0x8],0x1
   0x08048499 <+46>:	mov    eax,DWORD PTR [ebp-0x8]
   0x0804849c <+49>:	movzx  eax,BYTE PTR [eax]
   0x0804849f <+52>:	test   al,al
   0x080484a1 <+54>:	jne    0x8048491 <main+38>
   0x080484a3 <+56>:	push   DWORD PTR [ebp-0x4]
   0x080484a6 <+59>:	push   0x8048544
   0x080484ab <+64>:	call   0x8048330 <printf@plt>
   0x080484b0 <+69>:	add    esp,0x8
   0x080484b3 <+72>:	mov    eax,0x0
   0x080484b8 <+77>:	leave
   0x080484b9 <+78>:	ret
End of assembler dump.

IDA Pro Graph View

Main Graph View

Recovering input and loop structure

As usual, let’s start off by translating the first few lines of disassembly.

Text
0x0804846b <+0>:	push   ebp
0x0804846c <+1>:	mov    ebp,esp
0x0804846e <+3>:	sub    esp,0x28
0x08048471 <+6>:	lea    eax,[ebp-0x28]
0x08048474 <+9>:	push   eax
0x08048475 <+10>:	push   0x8048540
0x0804847a <+15>:	call   0x8048350 <__isoc99_scanf@plt>
0x0804847f <+20>:	add    esp,0x8
C
int main()
{
	/* You can actually infer the size of x by looking at
	the next two lines of disassembly. You see offsets 0x8
	and 0x4 being used for variables. So you can guess that
	char x[] is of size 0x28 - 0x8 = 0x20 = 32 */

	char x[32];

	scanf("%s", x);
}

Loops

Now if we refer to the graph view given earlier, we will observe that then next portion of code seems to be setting up a loop. We can observe this from the dark blue arrow that loops back from the bottom left block.

Conceptually, there are three important features of a loop: initialization, terminating condition, and increment. This is seen from how a for loop is constructed.

Text
for (<initialization>; <terminating condition>; <increment>){}

As such, I look out for these three features when I reverse engineer code with loops.

Analyzing Loops

initialization

Text
0x08048482 <+23>:	mov    DWORD PTR [ebp-0x4],0x0
0x08048489 <+30>:	lea    eax,[ebp-0x28]
0x0804848c <+33>:	mov    DWORD PTR [ebp-0x8],eax
0x0804848f <+36>:	jmp    0x8048499 <main+46>

The above snippet of code initializes the variables used in the loop. With reference to what we reversed so far, it sets ebp-0x4 to 0 and sets ebp-0x8 to the address of x.

C
int y;
char * z;
for (y = 0, z = x; <terminating condition>; <increment>) {}

terminating condition

IDA Pro shows the jump instruction as jnz instead of jne, they are actually the same thing. We’ll follow the vesion from gdb (jne) for this section.

Text
0x08048499 <+46>:	mov    eax,DWORD PTR [ebp-0x8]
0x0804849c <+49>:	movzx  eax,BYTE PTR [eax]
0x0804849f <+52>:	test   al,al
0x080484a1 <+54>:	jne    0x8048491 <main+38>

The above moves the value at ebp-0x8 to eax and dereferences it as a BYTE PTR (i.e. char *), to get its value. After which, a test al, al instruction is executed, followed by a jne instruction. This is a common way of testing if a register contains a zero value. As such, we can derive the following terminating condition.

Please read the entry for the test instruction in the intel manual for a more detailed explanation

C
int y;
char * z;
for (y = 0, z = x; *z != '\0'; <increment>) {}
main's frame and the string-length loop's pointer walkMemory strip, addresses increasing to the right, 15 units per byte. The 32-byte array x starts at ebp-0x28, then the char pointer z at ebp-0x8, y at ebp-0x4 and the saved ebp at ebp. With the example input hello, z holds the address of x[5], the zero byte, and y is 5; digits 0 to 4 under x[0] to x[4] give y while z pointed there. Step 1 loads z from ebp-0x8; step 2 reads the byte at that address.example input: hellox[32] · 0x20 byteszyhello&x[5]5savedebp12012340x28 - 0x8 = 0x20ebp-0x28ebp-0x8ebp-0x4ebp1mov eax, [ebp-0x8]→ eax = z, an address2movzx eax, BYTE PTR [eax]→ al = *z = 0x00
Figure 1. x starts at ebp-0x28 and the next variable at ebp-0x8, a 0x20-byte gap, so the reconstruction declares char x[32]. z holds an address; the loop reads the byte at that address and stops when it is 0.

increment

Text
0x08048491 <+38>:	add    DWORD PTR [ebp-0x4],0x1
0x08048495 <+42>:	add    DWORD PTR [ebp-0x8],0x1

The above code just increments y and z by 1.

C
int y;
char * z;
for (y = 0, z = x; *z != '\0'; y++, z++) {}

At this stage, we would have the following code reversed.

C
int main()
{
	char x[32];

	scanf("%s", x);

	int y;
	char * z;
	for (y = 0, z = x; *z != '\0'; y++, z++) {}
}

Reconstructing the final output

We can now reverse the rest of the disassembly.

Text
0x080484a3 <+56>:	push   DWORD PTR [ebp-0x4]
0x080484a6 <+59>:	push   0x8048544
0x080484ab <+64>:	call   0x8048330 <printf@plt>
0x080484b0 <+69>:	add    esp,0x8
0x080484b3 <+72>:	mov    eax,0x0
0x080484b8 <+77>:	leave
0x080484b9 <+78>:	ret
C
int main()
{
	char x[32];

	scanf("%s", x);

	int y;
	char * z;
	for (y = 0, z = x; *z != '\0'; y++, z++) {}

	printf("Your input is %d characters long\n", y);

	return 0;
}