C黑魔法

whoami

  •   fallnight

  •   資工二甲

  •   113屆  儲備會長

  •   特選仔

  •   女婕思

outline

  • C黑魔法是啥

  • 小試身手 - 判斷奇偶

  • ​運算子的優先順序

  • for-loop 的各種寫法

  • 區域 & 全域變數

  • Random (亂數)

  • C的安全問題

C黑魔法是什麼?

C黑魔法是什麼?

  • 你平時寫程式不會注意的小細節

  • 遇到特定的測資就會出錯

  • 可能沒學過的酷酷的寫法

  • 遇到沒寫完善的code就可以鑽它的漏洞做壞壞的事(x

判斷奇偶

寫個可以判斷輸入的整數是奇數還是偶數的程式吧

if(n % 2 == 1)
// 奇數
else
// 偶數
if(n % 2 != 0)
// 奇數
else
// 偶數

哪一個比較準確?

直接丟個負數給它試試吧  你會知道誰比較好的

結果竟然不一樣?!

#include<stdio.h>

int main()
{
	int n;
	printf("請輸入n: ");
	scanf("%d",&n);
	if(n % 2 != 0)
	{
		printf("%d是奇數\n",n);
	}
	else
	{
		printf("%d是偶數\n",n);
	}
	return 0;
}
#include<stdio.h>

int main()
{
	int n;
	printf("請輸入n: ");
	scanf("%d",&n);
	if(n % 2 == 1)
	{
		printf("%d是奇數\n",n);
	}
	else
	{
		printf("%d是偶數\n",n);
	}
	return 0;
}

再試試看這個

#include<stdio.h>

int main()
{
	printf(" 5 %%  2  =   %d\n" , 5 % 2);
	printf("-5 %%  2  =  %d\n" , -5 % 2);
	printf(" 5 %% -2  =   %d\n" , 5 % -2);
	return 0;
}

第三行輸出怪怪的??

#include<stdio.h>

int main()
{
	printf(" 5 %%  2  =   %d\n" , 5 % 2);
	printf("-5 %%  2  =  %d\n" , -5 % 2);
	printf(" 5 %% -2  =   %d\n" , 5 % -2);
	return 0;
}

不賣關子了
解答時間~

C怎麼定義取餘數 %

a = (a / b) * b + a % b
=> (a % b) = a - (a / b) * b

整數型態的 a 等於 (a / b) * b + a % b

移項得知
 a 除以 b 的餘數是 (a % b) = a - (a / b) * b

代入數字就清楚了

(a % b) = a - (a / b) * b

// a=-5, b=2
-5 % 2 = -5 - (-5 / 2) * 2
= -5 - (-2) * 2
= -5 - (-4)
= -5 + 4
= -1

// a= 5, b=-2
5 % -2 = 5 - (5 / -2) * (-2)
= 5 - (-2) * (-2)
= 5 - 4

= 1

if(n % 2 == 1)
// 奇數
else
// 偶數
if(n % 2 != 0)
// 奇數
else
// 偶數

所以比較準確的是?

if(n % 2 == 0)
// 偶數
else
// 奇數

這樣寫也可以💯

驗證環節

#include<stdio.h>

int main()
{
	int n;
	while(1)
	{
		printf("請輸入n: ");
		scanf("%d",&n);
		if(n % 2 == 0)
		{
			printf("%d是偶數\n",n);
		}
		else
		{
			printf("%d是奇數\n",n);
		}	
	}
	return 0;
}

運算子的優先順序

一樣先來個小試身手時間!

它會輸出什麼?

int a = -1, b = 1, c;
c = a+++b;
printf("a, b, c = %d, %d, %d\n", a, b, c);

解答時間 !   答案是...

int a = -1, b = 1, c;
c = a+++b;
printf("a, b, c = %d, %d, %d\n", a, b, c);

C的運算子優先順序

圖片來源: https://en.cppreference.com/w/c/language/operator_precedence

  • Operator Precedence

  • 如果遇到同一種運算子就由左往右

再看回頭看一次

int a = -1, b = 1, c;
c = a+++b;
printf("a, b, c = %d, %d, %d\n", a, b, c);

c = a+++b

=> c = (a++)+b

=> c = -1 + 1 = 0
=> a = a+1 = -1 + 1 = 0

=> b = 1

=> a = 0, b=1, c=0

它會輸出什麼?

#include <stdio.h>
int main()
{
    for(int i=1;0<i<10;i++){
    	printf("NISRA");
    }
    return 0;
}

無限迴圈?!

解答時間 !   原因是...

for(int i=1;0<i<10;i++){
    	printf("NISRA");
    }

0 < i < 10

=> (0 < i) < 10

因為 i 的初始值直接從大於 0 的 1 開始,所以 0 < i 永遠是True = 1

=> 1 < 10

所以迴圈的條件判斷永遠都是True,造成無限迴圈

改良方案

for(int i=1;0<i && i<10;i++){
    	printf("NISRA");
    }

0 < i < 10

=> 0 < i  &&  i < 10

//i = 10

=> True && False
=> False,迴圈結束

它會輸出什麼?

#include <stdio.h>
int main()
{
    int a = 10, b = 20, c = 30;
    if (c > b > a)
        printf("True\n");
    else
        printf("False\n");
}

解答時間 !   答案是...

#include <stdio.h>
int main()
{
    int a = 10, b = 20, c = 30;
    if (c > b > a)
        printf("True\n");
    else
        printf("False\n");
}

c > b > a

=> (c > b) > a

=> (30 > 20) > 10

=> (True) > 10 = 1 > 10

=> False

改良方案

int a = 10, b = 20, c = 30;
if (c > b > a)
	printf("True\n");
else
	printf("False\n");

c > b > a

=> c > b && b > a

=> 30 > 20 && 20 > 10

=> True && True

=> True

for-loop的各種寫法

又一樣先來個小試身手時間!

請寫個for迴圈,印出以下結果

複習一下,迴圈是如何運作的?

#include <stdio.h>
int main()
{
	for(int i = 9; i >= 0; i--){
		printf("%d\n",i);
	}
}
  1. 先宣告初始值 i = 9
  2. 給個條件判斷迴圈何時終止
  3. 告訴程式結束一次迴圈之後要如何改變 i

1

3

2

還可以怎麼寫?

就是一些沒什麼意義但看起來很厲害的寫法

for-loop的黑魔法

#include <stdio.h>
int main()
{
    for(int i = 10; 0 <= --i;){
        printf("%d\n",i);
    }
}

for-loop的黑魔法

#include <stdio.h>
int main()
{
    for(int i = 10; 0 <= --i;){
        printf("%d\n",i);
    }
}
  1. 先宣告初始值 i = 10
  2. --i   ->   i 會先做 i = i - 1
  3. 最後進行條件判斷 0 <= i

1

3

2

for-loop的黑魔法

#include <stdio.h>
int main()
{
    for(int i = 10;i-->0;){
        printf("%d\n",i);
    }
}

for-loop的黑魔法

#include <stdio.h>
int main()
{
    for(int i = 10;i-->0;){
        printf("%d\n",i);
    }
}
  1. 先宣告初始值 i = 10
  2. i-->0 其實就是   i--   >   0
    先取 i 的值做條件判斷,再做 i = i - 1

1

2

for-loop的黑魔法

#include <stdio.h>
int main()
{
    for(int i = 10; 0 <= ~~ --i;){
        printf("%d\n",i);
    }
}

for-loop的黑魔法

#include <stdio.h>
int main()
{
    for(int i = 10; 0 <= ~~ --i;){
        printf("%d\n",i);
    }
}
  1. 先宣告初始值 i = 10
  2. ~ 是取反運算符,會把每一個 bit 取反,例如:  1010 取反變成 0101
    所以  0  <=  ~~  --i  跟  0  <=  --i  是一樣的意思

1

2

如果要寫出這樣的結果呢?

可以這樣寫

#include <stdio.h>
int main()
{
    for (int i = 10, j = 0; i > 0 && j < 10; i--, j++){
        printf("%d %d\n",i,j);
    }
}
  1. 先宣告兩個變數的初始值 i = 10, j = 0
  2. i、j的條件用&&相連,也就是只有i、j同時符合條件,迴圈才能繼續
  3. i 做 i--,j 做 j++,用逗號相連就可以一次做多個動作

1

3

2

珍惜生命,不要亂寫噁心別人

By 歷屆學長姐的留言

區域變數? 全域變數?

又又一樣先來個小試身手時間!

x會回傳多少?

int x=0;
int getNum(){
    int x=1214;
    {
    	return x;
    }
}

變數的可視範圍

int x=0;
int getNum(){
    int x=1214;
    {
    	return x;
    }
}
  • 由下往上、由內到外,遇到的第一個

  • 所以 x 會回傳1214

如果想跳過區域變數呢?

int x=0;
int getNum(){
    int x=1214;
    {
    	extern int x;
    	return x;
    }
}

x會回傳多少?

int main(){
	{
    	int x=0;
	}
	printf("%d\n",x);
}

報錯了?

int main(){
	{
    	int x=0;
	}
	printf("%d\n",x);
}
  • 跟 printf 相同的{ }內沒有 x 被宣告,所以出現錯誤

隨機亂數真的隨機嗎?

終於沒有小試身手了(x

Random ?

  • 產生亂數的函式

  • 但亂數並不是隨機產生

  • 而是由一個亂數種子,透過複雜的數學演算法算出很像亂數的數值

#include <stdio.h>
#include <stdlib.h> // C 的亂數標頭檔
/* #include <cstdlib> 
如果是C++則是引入這個標頭檔 */

int main(){

  int x = rand(); //產生亂數

  printf("x = %d\n",x);
  return 0;
}

多輸出幾次試試

#include <stdio.h>
#include <stdlib.h>

int main(){
    for(int i=0;i<10;i++){
        printf("%d\n",rand());
    }
}

每次執行結果都一樣??

Random怎麼生成亂數的?

  • 基於一個亂數種子,透過複雜的數學演算法算出很像亂數的數值

  • 因為沒有特別去設定這次的亂數種子是什麼,所以執行結果都一樣

#include <stdio.h>
#include <stdlib.h>

int main(){
    for(int i=0;i<10;i++){
        printf("%d\n",rand());
    }
}

怎麼設定亂數種子?

#include <stdio.h>
#include <stdlib.h>
#include <time.h> // C 的時間相關的標頭檔
/* #include <ctime> 
如果是C++則是引入這個標頭檔 */

int main(){

	srand(time(NULL)); //初始化時間種子
    
    printf("%d\n",rand());
    return 0;
}
  • 利用srand()函式設定

  • 可以設定成以當下時間為種子去做亂數計算

C的安全問題

交作業都來不及了誰還會想到安全問題

這有什麼問題嗎?

#include <stdio.h>
#include <string.h>

int main()
{
    char input[10];
    printf("Give me some input: ");
    scanf("%s", input);

    printf("%s\n", input);

    return 0;
}

input沒有限制輸入長度

#include <stdio.h>
#include <string.h>

int main()
{
    char input[10];
    printf("Give me some input: ");
    scanf("%s", input);

    printf("%s\n", input);

    return 0;
}

限制? 怎麼加? 為什麼要加?

#include <stdio.h>
#include <string.h>

int main()
{
    char input[10];
    printf("Give me some input: ");
    scanf("%9s", input);

    printf("%s\n", input);

    return 0;
}
  • %s -> %9s

  • 如果沒有限制,輸入過大的值會覆蓋到其他變數原本的值 -> Buffer Overflow

為什麼長度限制是9 ?

  • input字元陣列的宣告長度是10

  • 但字串結尾要有結束符號 \0

  • 所以是 9 個字元 + 1 個結束符號 = 10個陣列元素

N I S R A B L A B \0

input[10]

9

結尾

Buffer Overflow

  • 緩衝區溢位

  • 輸入的內容超過預先留好的記憶體大小

圖片來源: https://www.cloudflare.com/zh-tw/learning/security/threats/buffer-overflow/

讓我們鬼轉進入組語的世界

記憶體怎麼儲存輸入的變數內容?

kernel
space
stack
data
heap
code

簡易的記憶體佈局(Memory Layout)的示意圖

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

高位址

低位址

Memory Layout

kernel
space
stack
data
heap
code

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

  • 程式碼區段(code section)
  • 又稱為 text section

高位址

低位址

Memory Layout

kernel
space
stack
data
heap
code

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

  • 存放著程式的全域變數
  • 已初始化 / 未初始化

高位址

低位址

Memory Layout

kernel
space
stack
data
heap
code

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

  • 動態分配的空間

    • C

      • malloc / free

    • C++

      • new / delete

高位址

低位址

Memory Layout

kernel
space
stack
data
heap
code

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

  • 區域變數

高位址

低位址

Memory Layout

kernel
space
stack
data
heap
code

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

  • 系統保留的空間

高位址

低位址

kernel
space
stack
data
heap
code

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

#include <stdio.h>

int global = 87; // data
int main()
{
	int a = 10; // stack
}

Memory Layout

高位址

低位址

Registers of x86

Register ?

  • 暫存器

  • 是一個臨時儲存區域

  • 用於儲存指令、變數和變數的值、計算後的結果

  • 方便CPU需要時可以快速存取,不用每一次都從記憶體找

x86 ?

  • x86是intel發明的指令集,我們的電腦處理器就會去執行x86的這些指令

Registers of x86

  • EIP

    • Instruction Pointer

    • 下一個執行的 instruction 之位址

  • ESP

    • Stack Pointer

    • 儲存 Stack 頭位址

  • EBP

    • Base Pointer

    • 儲存 Stack 基底(base)位址

Memory Layout & Register of x86

kernel
space
stack
data
heap
code

系統保留

 

區域變數

 

 

動態分配


程式全域變數

 

程式碼

EBP

ESP

EIP

舉個栗子

#include<stdio.h>

int main()
{
    char a[4];
    scanf("%4s",&a);
    printf("%s",a);
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP ????
EBP-0x4 ????
EBP-0x8 ????
EBP-0xC ????
EBP-0x10 ????

EIP

舉個栗子

#include<stdio.h>

int main()
{
    char a[4];
    scanf("%4s",&a);
    printf("%s",a);
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 ????
EBP-0x8 ????
EBP-0xC ????
EBP-0x10 ????

EIP

舉個栗子

#include<stdio.h>

int main()
{
    char a[4];
    scanf("%4s",&a);
    printf("%s",a);
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 ????
EBP-0x8 ????
EBP-0xC ????
EBP-0x10 ????

EIP

a [ ]

輸入 abcd

#include<stdio.h>

int main()
{
    char a[4];
    scanf("%4s",&a);
    printf("%s",a);
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 dcba
EBP-0x8 ????
EBP-0xC ????
EBP-0x10 ????

EIP

a [ ]

輸出 abcd

#include<stdio.h>

int main()
{
    char a[4];
    scanf("%4s",&a);
    printf("%s",a);
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 dcba
EBP-0x8 ????
EBP-0xC ????
EBP-0x10 ????

EIP

Lab Time

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}

在不改動程式碼的前提下

輸入 input

印出 "Yes you pass it!"

~解答時間~

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP ????
EBP-0x4 ????
EBP-0x8 ????
EBP-0xC ????
EBP-0x10 ????

EIP

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 ????
EBP-0x8 ????
EBP-0xC ????
EBP-0x10 ????

EIP

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 0x00000041
EBP-0x8 0x5253494E
EBP-0xC ????
EBP-0x10 ????

EIP

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 \x00\x00\x00A
EBP-0x8 RSIN
EBP-0xC ????
EBP-0x10 ????

EIP

為什麼是A RSIN

Endian

  • 位元組存放順序(byte ordering)

  • 資料在記憶體中放的順序

    e.g.       0x12345678

  • little-endian                                                \x78 \x56 \x34 \x12

  • big-endian                                                   \x12 \x34 \x56 \x78

低位址

高位址

LSB

MSB

為什麼反過來 ?  為什麼一次只放4個 ?

  • x86 是 little-endian

  • 一個字元等於 1 byte

  • 1 byte = 8 bits

  • Register 是 32-bit = 4 byte

  • NISRA 存在EBP內,所以一行只能容納 4 個字元
               A

       R S I N

低位址

高位址

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 \x00\x00\x00A
EBP-0x8 RSIN
EBP-0xC ????
EBP-0x10 ????

EIP

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 \x00\x00\x00n
EBP-0x8 imda
EBP-0xC bbbb
EBP-0x10 aaaa

EIP

如果輸入aaaabbbbadmin

覆蓋到 pwd[]

#include <stdio.h>
#include <string.h>

int main()
{
    char pwd[8] = "NISRA";
    char input[8];
    printf("Give me some input: ");
    scanf("%s", input);
    if (strcmp(pwd, "admin") == 0)
    	printf("Yes you pass it!\n\n");
    else
    	printf("No, keep trying.\n\n");
    return 0;
}
index value
EBP+0x8 ...
EBP+0x4  ...
EBP EBP
EBP-0x4 \x00\x00\x00n
EBP-0x8 imda
EBP-0xC bbbb
EBP-0x10 aaaa

EIP

input [ ]

pwd [ ]

參考資料

  • https://blog.gtwang.org/programming/c-cpp-rand-random-number-generation-tutorial-examples/#google_vignette

  • https://hackmd.io/@Ben1102/B1gfGLT3u

  • https://ithelp.ithome.com.tw/articles/10188599

  • https://hackmd.io/@mushding/assembly_language

  • https://www.eagletek.com.tw/post/register-in-cpu?srsltid=AfmBOooF2MVABCyP1Rfk334LJCFAkoybJTZpVatjJceDhXFcchuolNQb

  • https://www.ithome.com.tw/tech/56880

Made with Slides.com