This is the article on the 10th day of Yahoo! JAPAN 18 New Graduate Advent Calendar 2018 created by Yahoo! JAPAN 2018 new graduate volunteers.
I'm Takigadaira, a new graduate of Yahoo! JAPAN 2018. In this article, I will write hello world in C language.
The content of this article has been verified on CentOS 7 and on gcc version 4.8.5.
Generally, when writing hello world in C language, write the following code.
#include <stdio.h>
int main(void) {
printf("hello world");
return 0;
}
This code is compiled on x86_64 GNU / Linux with gcc-4.8.5 by the gcc -O2 -S -o hello.S hello.c
command as follows:
.file ""
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "hello world"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB11:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $.LC0, %edi
xorl %eax, %eax
call printf
xorl %eax, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
.section .note.GNU-stack,"",@progbits
The main important parts are:
movl $.LC0, %edi
xorl %eax, %eax
call printf
I have added the address $ .LC0
to the ʻedi register and called the
printf` function.
This is because the x86_64 System-V ABI determines that the first argument of a function is stored in the rdi
register. (ʻEdiregister is the lower 32 bits of
rdi` register)
I can't stand to end up writing an Advent calendar with all my might, so I would like to aim for a stronger ** hello world.
What is a "strong hello world"?
I wondered if it would be possible to use a string without explicitly stating hello world
, which would make it different and stronger.
And I wrote the following hello world in an internal article before.
#include <unistd.h>
#include <stdlib.h>
void (*a)(void);
char h = 0x00, e = 0x00, l = 0x00, o = 0x00, w = 0x00, r = 0x00, d = 0x00, sp = 0x00;
void search() {
while(1) {
char *b = (char *)a;
switch(*b) {
case 0x68:
h = *b;
break;
case 0x65:
e = *b;
break;
case 0x6c:
l = *b;
break;
case 0x6f:
o = *b;
break;
case 0x77:
w = *b;
break;
case 0x72:
r = *b;
break;
case 0x64:
d = *b;
break;
case 0x20:
sp = *b;
break;
default:
break;
}
if(h && e && l && o && w && r && d && sp) break;
a++;
};
}
int main() {
a = search;
a();
write(1, &h, 1);
write(1, &e, 1);
write(1, &l, 1);
write(1, &l, 1);
write(1, &o, 1);
write(1, &sp, 1);
write(1, &w, 1);
write(1, &o, 1);
write(1, &r, 1);
write(1, &l, 1);
write(1, &d, 1);
}
This is a program that searches for the required characters starting from the pointer of the search
function, which has become a machine language, and outputs hello world with the found characters.
Later this code was improved by my boss to something like the following.
#include <unistd.h>
char *a = "";
char h = 0x00, e = 0x00, l = 0x00, o = 0x00, w = 0x00, r = 0x00, d = 0x00, sp = 0x00;
int main() {
while(1) {
switch(*a) {
case 0x68:
h = *a;
break;
case 0x65:
e = *a;
break;
case 0x6c:
l = *a;
break;
case 0x6f:
o = *a;
break;
case 0x77:
w = *a;
break;
case 0x72:
r = *a;
break;
case 0x64:
d = *a;
break;
case 0x20:
sp = *a;
break;
default:
break;
}
if(h && e && l && o && w && r && d && sp) break;
a--;
};
write(1, &h, 1);
write(1, &e, 1);
write(1, &l, 1);
write(1, &l, 1);
write(1, &o, 1);
write(1, &sp, 1);
write(1, &w, 1);
write(1, &o, 1);
write(1, &r, 1);
write(1, &l, 1);
write(1, &d, 1);
}
By searching the search direction from the pointer ʻa` in the negative direction, we have succeeded in finding the required character without fail.
The position of the initialized pointer variable is placed after the actual program code, especially if no attributes such as linker script or pragma
are specified.
char *a = "";
int main(void) {
return *a;
}
If you save such code as test.c
and run gcc -o test test.c && objdump -D test
to check it,
In fact, the main
function is located in the .text
section, the ʻavariable is located in the
.data` section, and their addresses are
00000000004003e0 <main>:
Disassembly of section .data:
0000000000601020 <__data_start>:
...
0000000000601028 <a>:
601028: 70 05 jo 60102f <a+0x7>
60102a: 40 00 00 add %al,(%rax)
You can see that the address 0x400570
pointed to by the pointer ʻa is later than the
mainfunction as the address actually expanded into memory (as an example in my environment). Check this value with
readelf`
There are 31 section headers, starting at offset 0x1920:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
(Omission)
[13] .plt.got PROGBITS 00000000004003d0 000003d0
0000000000000008 0000000000000000 AX 0 0 8
[14] .text PROGBITS 00000000004003e0 000003e0
0000000000000172 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 0000000000400554 00000554
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 0000000000400560 00000560
0000000000000011 0000000000000000 A 0 0 8
[17] .eh_frame_hdr PROGBITS 0000000000400574 00000574
0000000000000034 0000000000000000 A 0 0 4
It points to the inside of .rodata
.
And since the ASCII code of the character actually required is used in the argument of the comparison instruction in the .text
section, the required character is surely aligned.
Also, looking at the section information, the sections from .rodata
to .text
are assigned.
Therefore, the program works without SEGV.
But this hello world doesn't look a little weak?
This is because each character of hello world
is actually included in the actual code.
To be more elaborate, for example, by adding or subtracting the difference of each character from the numerical value corresponding to the hexadecimal representation of an instruction.
You should be able to completely expel the characters hello world
to create a hello world.
So let's take a look at the x86_64 instruction list.
Consider which of these instructions is definitely included.
When using return 0;
at the end of the main function, it is obvious that xor% eax,% eax
is included.
--ABI assigns the return value of the function to % eax
-[Intel® 64 and IA-32 Architectures Optimization Reference Manual](https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization -manual.pdf) by
When assigning 0 to an arbitrary register, take the exclusive OR of the registers.
Is decided.
Therefore, 0x31
and 0xc0
are candidates.
We also know that the ret
instruction is called when the function finishes executing and returns to the caller.
So it's safe to assume that 0xc3
also exists.
There are many other candidates and techniques, but let's start with 0xc3
.
First, find the reference 0xc3.
char *a = "";
int main(void) {
while(1) {
if(*a == (char) 0xc3) break;
a--;
}
char b = *a;
//Processing after finding
return 0;
}
After finding it, we first need to output h
, so include the necessary header files.
After that, the difference between the ASCII code of 0xc3
and h
is calculated and assigned to the output variable as it is.
#include <unistd.h>
char *a = "";
int main(void) {
while(1) {
if(*a == (char) 0xc3) break;
a--;
}
char b = *a;
b = b + (char)-0x5b;
write(1, &b, 1);
return 0;
}
If you output other characters in this condition, the code will be as follows.
#include <unistd.h>
char *a = "";
char subs[] = {(char)-0x5b, (char)-0x3, (char)0x7, (char)0x0, (char)0x3, (char)-0x4f, (char)0x57, (char)-0x8, (char)0x3, (char)-0x6, (char)-0x8};
int main(void) {
while(1) {
if(*a == (char) 0xc3) break;
a--;
}
char b = *a;
for(int i=0; i < 11; i++) {
b = b + subs[i];
write(1, &b, 1);
}
return 0;
}
Since it is troublesome, I calculated the difference and left it to the for statement, but it became the above form.
The idea of generating a string at runtime is good, but it feels like it's still unsatisfactory.
So, this time, I will try to generate hello world
by rewriting the binary at runtime.
Let's do it step by step. When rewriting the binary at runtime, if the area that will be executed again is rewritten, problems may occur in future processing. Therefore
hello world
by rewriting the already executed 11-byte value from the function topwrite
system call with the address of the function top and offset 11 as arguments.I will output hello world by the method.
x86_64 On Linux, always start a function
pushq %rbp
movq %rsp, %rbp
(Reference: x86 calling convention) Looking at the x86_64 instruction list page earlier, the machine language is
Because the pushq
instruction is 0x50 + r
(r is the register number)
0x50 + 0x5 = 0x55
And next is movq
between 64-bit registers
0x48
--The upper 4 bits are fixed and 0b0100
--Lower 4 bits from the upper
--1 if the operand is 64-bit
--Extending the reg
bit of modR / M
--Extended SIB ʻindexfield --Extension of the fields
r / m in ModRM,
basein SIB, or
reg in opcode --
0x89 corresponding to
movq--From
% rsp to
% rbp, so
0xe5 --modR / M is ([Reference: Internal format of Jun's Homepage Assembly Proggraming instruction and addressing mode](https://www.mztn.org/lxasm64/amd05.html)) --
modis
0b11 because the execution address points to a register --
0b100because
reg is from
% rsp --
r / mis
% rbp, so
0b101--Therefore,
0b11100101 = 0xe5`Therefore, it becomes as follows.
55 (pushq %rbp)
48 89 e5 (movq %rsp, %rbp)
Using this, to output 11 bytes (for the number of characters in hello world) from the first address of the function, use this next instruction.
leaq -x(%rip), %rsi
As a matter of fact, it seems that you should put the address in the second argument of the write
system call. (x is the difference from the address of the function top from the program counter rip)
Refer to man syscall
for the correspondence between system call arguments and registers.
And the instruction leaq x (% rip),% rsi
is as follows in machine language.
48 8d 35 00 00 00 00
The last 4 bytes are the offset from the specified register (% rip this time).
Therefore, if you put about 11 bytes before the value of % rip
when this instruction is executed into% rsi
, it seems that you can put the pointer of the function top in the second argument.
That is, you need leaq -11 (% rip),% rsi
, and the resulting machine language you want is something like 48 8d 35 f5 ff ff ff
. (F5 ff ff ff
is a little endian representation of -11)
me too
0x48
--0x8d
corresponding to leaq
--35
to enter a fixed address from% rip
to % rsi
--Since modR / M is considered, it is rip relative addressing (Reference Intel® 64 and IA-32 Architectures Software Developer's Manual -32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4) Vol. 2A 2-12)
--mod
is 0b00
-- 0b110
because reg
is% rsi
-- r / m
is 0b101
--Therefore, 0b00110101 = 0x35
--Since the offset is -11
, f5 ff ff ff
You can confirm from that.
Then, specify the write
system call number, the output destination file descriptor (first argument), and the number of output bytes (third argument), and call the syscall instruction.
Expressed as an assembly, it looks like the following.
movq $1, %rax
movq $1, %rdi
movq $11, %rdx
syscall
And these instructions correspond to the following machine language. (Each line corresponds to the instruction on each line above)
48 c7 c0 01 00 00 00
48 c7 c7 01 00 00 00
48 c7 c2 0b 00 00 00
0f 05
These first three lines also correspond to the REX prefix 0x48
and the c7
corresponding to the immediate to register movq
and the last 4 bytes corresponding to the immediate value.
Considering modR / M for each target register
--% rax
is 0b11000000 = 0xc0
-- mod
is a register, so 0b11
--reg
does not require the value of reg
, and the opcode extension value is 0, so 0b000
-- r / m
is 0b000
from % rax
--% rdi
is 011000111 = 0xc7
--Similar to mod
and reg
, r / m
is 0b111
--% rdx
is 0b11000010 = 0xc2
--Similar to mod
and reg
, r / m
is 0b010
And syscall
is a double-byte instruction that corresponds to 0x0f 0x05
.
And at the end of the function, we need to return the value of the % rbp
register that was pushedq
back to % rbp
and back to the calling function.
popq %rbp
retq
You need to run. And these are
5d
c3
Corresponds to the machine language.
In each 1-byte instruction, poq% rbp
is 0x58 + r
and retq
is 0xc3
.
If you arrange the instructions so far
pushq %rbp
movq %rsp, %rbp
leaq -11(%rip), %rsi
movq $1, %rax
movq $1, %rdi
movq $11, %rdx
syscall
popq %rbp
retq
And the machine language is
55
48 89 e5
48 8d 35 f5 ff ff ff
48 c7 c0 01 00 00 00
48 c7 c7 01 00 00 00
48 c7 c2 0b 00 00 00
0f 05
5d
c3
It will be shaped like. If you make this a C string literal
char *code = "\x55\x48\x89\xe5\x48\x8d\x35\xf5\xff\xff\xff\x48\xc7\xc0\x01\x00\x00\x00\x48\xc7\xc7\x01\x00\x00\x00\x48\xc7\xc2\x0b\x00\x00\x00\x0f\x05\x5d\xc3";
It will be. And in C, even if it is a character string, it can be executed if it is cast to a function pointer (although it is undefined in the specification).
int main(void) {
char *code = "\x55\x48\x89\xe5\x48\x8d\x35\xf5\xff\xff\xff\x48\xc7\xc0\x01\x00\x00\x00\x48\xc7\xc7\x01\x00\x00\x00\x48\xc7\xc2\x0b\x00\x00\x00\x0f\x05\x5d\xc3";
void (*f)(void) = code;
f();
return 0;
}
If you compile and run code like this and pipe it to xxd
5548 89e5 488d 35f5 ffff ff
Is obtained, and it can be seen that it matches the machine language actually written.
In other words, if you write an instruction to adjust these 11 bytes after leaq -11 (% rip),% rsi
, it seems that you can generate the character hello world
.
So, adding the following assembly in the right place will solve it.
addl $19, (%rsi)
addl $29, 1(%rsi)
addl $-29, 2(%rsi)
addl $-121, 3(%rsi)
addl $39, 4(%rsi)
addl $-109, 5(%rsi)
addl $66, 6(%rsi)
addl $122, 7(%rsi)
addl $114, 8(%rsi)
addl $108, 9(%rsi)
addl $100, 10(%rsi)
And these instructions are in the following machine language:
83 06 13
83 46 01 1d
83 46 02 e3
83 46 03 87
83 46 04 27
83 46 05 93
83 46 06 42
83 46 07 7a
83 46 08 72
83 46 09 6c
83 46 0a 64
ʻAddl $ 19, (% rsi)also corresponds to
0x83`, mod R / M
--mod
is 0b00
because r / m
is the address of the register.
-- reg
is not used and the opcode extension is / 0
, so 0b000
-- r / m
is % rsi
, so 0b110
From 0x06
, then the immediate value is entered, and it becomes 0x13
.
Furthermore, when there is a 1-byte offset such as ʻaddl $ 29, 1 (% rsi) ,
0x83 of ʻaddl
is common.
--mod
is 0b01
because r / m
is a register + 8-bit offset
-- reg
is the same, 0b000
-- r / m
is also 0b110
From 0x46
, then 1 byte is the offset from% rsi
, and then 1 byte is the immediate value.
If you arrange the instructions so far properly
pushq %rbp
movq %rsp, %rbp
leaq -11(%rip), %rsi
addl $19, (%rsi)
addl $29, 1(%rsi)
addl $-29, 2(%rsi)
addl $-121, 3(%rsi)
addl $39, 4(%rsi)
addl $-109, 5(%rsi)
addl $66, 6(%rsi)
addl $122, 7(%rsi)
addl $114, 8(%rsi)
addl $108, 9(%rsi)
addl $100, 10(%rsi)
movq $1, %rax
movq $1, %rdi
movq $11, %rdx
syscall
popq %rbp
retq
As a machine language
55
48 89 e5
48 8d 35 f5 ff ff ff
83 06 13
83 46 01 1d
83 46 02 e3
83 46 03 87
83 46 04 27
83 46 05 93
83 46 06 42
83 46 07 7a
83 46 08 72
83 46 09 6c
83 46 0a 64
48 c7 c0 01 00 00 00
48 c7 c7 01 00 00 00
48 c7 c2 0b 00 00 00
0f 05
5d
c3
If you make this a string literal
char *code = "\x55\x48\x89\xe5\x48\x8d\x35\xf5\xff\xff\xff\x83\x06\x13\x83\x46\x01\x1d\x83\x46\x02\xe3\x83\x46\x03\x87\x83\x46\x04\x27\x83\x46\x05\x93\x83\x46\x06\x42\x83\x46\x07\x7a\x83\x46\x08\x72\x83\x46\x09\x6c\x83\x46\x0a\x64\x48\xc7\xc0\x01\x00\x00\x00\x48\xc7\xc7\x01\x00\x00\x00\x48\xc7\xc2\x0b\x00\x00\x00\x0f\x05\x5d\xc3";
It will be.
However, even if it is executed as it is, the area where the variables are stored is not rewritable, so we will devise a little.
#include <sys/mman.h>
#include <string.h>
const unsigned int CODE_LEN = 79;
int main()
{
void *output = mmap(NULL, CODE_LEN, (PROT_READ | PROT_WRITE | PROT_EXEC), (MAP_PRIVATE | MAP_ANONYMOUS), -1, 0);
char *code = "\x55\x48\x89\xe5\x48\x8d\x35\xf5\xff\xff\xff\x83\x06\x13\x83\x46\x01\x1d\x83\x46\x02\xe3\x83\x46\x03\x87\x83\x46\x04\x27\x83\x46\x05\x93\x83\x46\x06\x42\x83\x46\x07\x7a\x83\x46\x08\x72\x83\x46\x09\x6c\x83\x46\x0a\x64\x48\xc7\xc0\x01\x00\x00\x00\x48\xc7\xc7\x01\x00\x00\x00\x48\xc7\xc2\x0b\x00\x00\x00\x0f\x05\x5d\xc3";
memcpy(output, code, CODE_LEN);
void (*o)(void) = output;
o();
return 0;
}
Allocate an area where read / write can be executed with mmap
, write code
to it, and then execute it.
As a result, even if the instruction part is rewritten at the time of execution, it can be executed without a segmentation fault.
Execution example in wandbox is here. You have a solid hello world.
In general, many people are not good at handwriting machine language, so it is unlikely that people will have a turning point to understand machine language, but I hope this article will be a catalyst.
Let's write your own hello world and show off to your friends.
Recommended Posts